feat: extension registry with metadata catalog and onboarding integration#238
feat: extension registry with metadata catalog and onboarding integration#238ilblackdragon merged 6 commits intomainfrom
Conversation
Summary of ChangesHello @ilblackdragon, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the extensibility of the system by introducing a robust extension registry. This new infrastructure allows for standardized definition, discovery, and installation of tools and channels, moving towards a more modular and user-friendly experience. It provides a centralized metadata catalog and streamlines the process of integrating new functionalities, both through command-line operations and an improved interactive onboarding wizard. Highlights
Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request introduces a robust extension registry system with catalog management and onboarding integration. The implementation of RegistryCatalog and RegistryInstaller provides a solid foundation for managing both tools and channels. However, there are some issues in the installation logic, particularly regarding the "install from artifact" flow which currently depends on the presence of the source tree to locate capabilities files. Additionally, the build process for WASM components could be improved to provide better user feedback and more reliable artifact detection.
| let caps_source = self | ||
| .repo_root | ||
| .join(&manifest.source.dir) | ||
| .join(&manifest.source.capabilities); | ||
| let target_caps = |
There was a problem hiding this comment.
The install_from_artifact flow currently depends on the local presence of the source directory to copy the capabilities file. This prevents the use of pre-built artifacts in environments where the source code is not available (e.g., binary-only distributions). The capabilities should ideally be embedded in the manifest or provided via a separate URL to allow for truly standalone artifact installation.
There was a problem hiding this comment.
Acknowledged. Added a NOTE comment documenting this limitation. No pre-built artifacts exist yet; when artifact distribution is implemented, capabilities should be bundled with the artifact or fetched from a separate URL.
| if path.extension().and_then(|e| e.to_str()) != Some("json") { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
The loader should verify that the entry is a file before attempting to read it. If a directory exists with a .json extension, std::fs::read_to_string will fail, causing the entire catalog loading process to abort.
| if path.extension().and_then(|e| e.to_str()) != Some("json") { | |
| continue; | |
| } | |
| if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("json") { | |
| continue; | |
| } |
There was a problem hiding this comment.
Fixed in 61109a4. Added !path.is_file() guard before the extension check.
| .map_err(|e| RegistryError::ManifestRead { | ||
| path: PathBuf::from(url.as_str()), | ||
| reason: format!("download failed: {}", e), | ||
| })?; |
There was a problem hiding this comment.
Using PathBuf::from(url.as_str()) to populate the path field of RegistryError::ManifestRead is semantically incorrect as it contains a URL, not a filesystem path. This could lead to confusing error messages or issues in code that expects a valid local path. Consider adding a specific error variant for download failures or using a placeholder path.
There was a problem hiding this comment.
Fixed in 61109a4. Added DownloadFailed { url, reason } error variant that stores the URL as a String instead of stuffing it into a PathBuf.
| let output = Command::new("cargo") | ||
| .current_dir(source_dir) | ||
| .args(["component", "build", "--release"]) | ||
| .output()?; |
There was a problem hiding this comment.
Using Command::output() captures all output and prevents it from being displayed in the terminal. For long-running builds, this makes the CLI appear unresponsive. Using Command::status() would allow the build progress to be visible to the user, providing a better experience.
let status = Command::new("cargo")
.current_dir(source_dir)
.args(["component", "build", "--release"])
.status()?;
if !status.success() {
anyhow::bail!("Build failed");
}There was a problem hiding this comment.
Fixed in 61109a4. Switched to tokio::process::Command with status() (inherited stdio) so build output streams to the terminal in real-time. Also fixed the companion Copilot comment about the same issue.
| if let Ok(entries) = std::fs::read_dir(&release_dir) { | ||
| for entry in entries.flatten() { | ||
| let path = entry.path(); | ||
| if path.extension().and_then(|e| e.to_str()) == Some("wasm") { | ||
| return Ok(path); | ||
| } | ||
| } |
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed in 61109a4. Now looks for the specific {crate_name}.wasm (with hyphens converted to underscores per Cargo convention) instead of picking the first .wasm file.
There was a problem hiding this comment.
Pull request overview
Adds an “extension registry” concept (JSON manifests + Rust loader/installer) and wires it into both the CLI and the onboarding wizard so users can browse/install tools/channels more easily.
Changes:
- Introduces
src/registry/with manifest types, a catalog for listing/searching/resolving bundles, and an installer for build-from-source / artifact-download flows. - Adds
ironclaw registry ...CLI subcommands (list/info/install/install-defaults). - Updates onboarding wizard to include a new “Extensions” step and to discover/install registry-provided channels during channel setup.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/setup/wizard.rs | Adds Step 7 extensions install + registry-backed channel option building/installation. |
| src/setup/mod.rs | Updates wizard step documentation to 8 steps. |
| src/setup/README.md | Updates onboarding docs to reflect registry integration + new step order. |
| src/registry/mod.rs | New registry module exports. |
| src/registry/manifest.rs | Defines manifest/bundle serde structs and a conversion helper to extension registry entries. |
| src/registry/installer.rs | Implements build/download install flows for registry entries. |
| src/registry/catalog.rs | Loads registry manifests and bundles; provides list/search/resolve APIs. |
| src/main.rs | Wires new registry subcommand into the main CLI dispatch. |
| src/lib.rs | Exposes the new registry module from the library crate. |
| src/cli/registry.rs | Implements ironclaw registry CLI functionality. |
| src/cli/mod.rs | Adds the Registry command to the CLI and re-exports it. |
| registry/tools/telegram.json | Adds tool manifest for Telegram tool. |
| registry/tools/slack.json | Adds tool manifest for Slack tool. |
| registry/tools/okta.json | Adds tool manifest for Okta tool. |
| registry/tools/google-slides.json | Adds tool manifest for Google Slides tool. |
| registry/tools/google-sheets.json | Adds tool manifest for Google Sheets tool. |
| registry/tools/google-drive.json | Adds tool manifest for Google Drive tool. |
| registry/tools/google-docs.json | Adds tool manifest for Google Docs tool. |
| registry/tools/google-calendar.json | Adds tool manifest for Google Calendar tool. |
| registry/tools/gmail.json | Adds tool manifest for Gmail tool. |
| registry/tools/github.json | Adds tool manifest for GitHub tool. |
| registry/channels/whatsapp.json | Adds channel manifest for WhatsApp channel. |
| registry/channels/telegram.json | Adds channel manifest for Telegram channel. |
| registry/channels/slack.json | Adds channel manifest for Slack channel. |
| registry/channels/discord.json | Adds channel manifest for Discord channel. |
| registry/_bundles.json | Adds bundle definitions (google/messaging/default). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let target_wasm = target_dir.join(format!("{}.wasm", manifest.source.crate_name)); | ||
|
|
||
| // Check if already exists | ||
| if target_wasm.exists() && !force { | ||
| return Err(RegistryError::ExtensionNotFound(format!( |
There was a problem hiding this comment.
The installer names the installed WASM file using manifest.source.crate_name (e.g. slack-channel.wasm). The setup wizard and channel/tool loaders treat the file stem as the canonical extension identifier and expect it to match the user-facing manifest name (e.g. slack.wasm). This mismatch will cause newly installed registry extensions to be discovered/activated under a different name than the one the user selected. Consider installing as manifest.name (and use crate_name only for building), or add an explicit install_name field and use it consistently.
There was a problem hiding this comment.
Fixed in 61109a4. Installer now uses manifest.name for on-disk filenames ({name}.wasm, {name}.capabilities.json) so discovery, auth, and CLI commands all agree on the stem.
| path: PathBuf::from(url.as_str()), | ||
| reason: format!("download failed: {}", e), | ||
| })?; | ||
|
|
There was a problem hiding this comment.
install_from_artifact downloads with reqwest::get(url) but doesn't validate the HTTP status. A 404/500 body will be read and likely reported as a SHA mismatch, which is misleading. Call error_for_status() (or check status().is_success()) before reading .bytes() so failures report as download errors.
| let response = response.error_for_status().map_err(|e| RegistryError::ManifestRead { | |
| path: PathBuf::from(url.as_str()), | |
| reason: format!("download failed: {}", e), | |
| })?; |
There was a problem hiding this comment.
Fixed in 61109a4. Added error_for_status() call before reading bytes, and all download errors now use the DownloadFailed variant.
| let check = Command::new("cargo") | ||
| .args(["component", "--version"]) | ||
| .output(); | ||
|
|
||
| if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) { | ||
| anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component"); | ||
| } | ||
|
|
||
| let output = Command::new("cargo") | ||
| .current_dir(source_dir) | ||
| .args(["component", "build", "--release"]) | ||
| .output()?; |
There was a problem hiding this comment.
build_wasm_component() uses std::process::Command::output() and is called from async install paths. That blocks the Tokio worker thread for the duration of the build, which can stall other async tasks. Prefer tokio::process::Command or wrap the blocking build in tokio::task::spawn_blocking.
| let check = Command::new("cargo") | |
| .args(["component", "--version"]) | |
| .output(); | |
| if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) { | |
| anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component"); | |
| } | |
| let output = Command::new("cargo") | |
| .current_dir(source_dir) | |
| .args(["component", "build", "--release"]) | |
| .output()?; | |
| let check = tokio::task::block_in_place(|| { | |
| Command::new("cargo") | |
| .args(["component", "--version"]) | |
| .output() | |
| }); | |
| if check.is_err() || !check.as_ref().map(|o| o.status.success()).unwrap_or(false) { | |
| anyhow::bail!("cargo-component not found. Install with: cargo install cargo-component"); | |
| } | |
| let output = tokio::task::block_in_place(|| { | |
| Command::new("cargo") | |
| .current_dir(source_dir) | |
| .args(["component", "build", "--release"]) | |
| .output() | |
| })?; |
There was a problem hiding this comment.
Fixed in 61109a4 — same fix as the Gemini comment above. Uses tokio::process::Command::status() with inherited stdio.
| // Try with kind prefix | ||
| if let Some(m) = self.manifests.get(&format!("tools/{}", name)) { | ||
| return Some(m); | ||
| } | ||
| if let Some(m) = self.manifests.get(&format!("channels/{}", name)) { |
There was a problem hiding this comment.
Bare-name lookup silently prefers tools/<name> over channels/<name>. This PR’s registry contains collisions (e.g. both a tool and channel named "slack"), so info/install slack will always target the tool and make the channel hard to address. Consider detecting collisions and forcing an explicit prefix (or returning an error) when both kinds exist.
There was a problem hiding this comment.
Fixed in 61109a4. get() now returns None when both tools/ and channels/ exist (ambiguous). Added get_strict() that returns an explicit AmbiguousName error with guidance to use the qualified key. resolve() uses get_strict() so CLI install/info commands surface the ambiguity.
| // Copy capabilities file | ||
| let caps_source = source_dir.join(&manifest.source.capabilities); | ||
| let target_caps = | ||
| target_dir.join(format!("{}.capabilities.json", manifest.source.crate_name)); |
There was a problem hiding this comment.
The capabilities sidecar is installed as <crate_name>.capabilities.json. Channel discovery and ironclaw tool auth <name> both key off the installed name/file stem, so using crate_name here will break setup/auth if users select/reference manifest.name. Align the capabilities filename stem with the canonical installed extension name (preferably manifest.name).
| target_dir.join(format!("{}.capabilities.json", manifest.source.crate_name)); | |
| target_dir.join(format!("{}.capabilities.json", manifest.name)); |
There was a problem hiding this comment.
Fixed in 61109a4 — same fix, capabilities sidecar also uses manifest.name now.
| if auth.method.as_deref() != Some("none") && auth.method.is_some() { | ||
| let provider = auth.provider.as_deref().unwrap_or(&tool.name); | ||
| // Only mention unique providers (Google tools share auth) | ||
| let hint = format!(" {} - ironclaw tool auth {}", provider, tool.name); |
There was a problem hiding this comment.
The auth hint prints ironclaw tool auth <tool.name>, but ironclaw tool auth looks for <name>.capabilities.json in the tools dir. If the installed stem differs (e.g. installed as crate_name), this instruction will fail. Use the canonical installed name in the command (or ensure the installer writes sidecars under tool.name).
| let hint = format!(" {} - ironclaw tool auth {}", provider, tool.name); | |
| let hint = format!(" {} - ironclaw tool auth {}", provider, outcome.name); |
There was a problem hiding this comment.
Fixed in 61109a4. The installer now uses manifest.name for on-disk filenames, so ironclaw tool auth <tool.name> correctly finds <name>.capabilities.json.
| // Check if already on disk (may have been installed between bundled and here) | ||
| let wasm_on_disk = channels_dir.join(format!("{}.wasm", name)).exists() | ||
| || channels_dir.join(format!("{}-channel.wasm", name)).exists(); | ||
| if wasm_on_disk { | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
This on-disk check only looks for <name>.wasm or <name>-channel.wasm, but the registry installer currently writes <crate_name>.wasm and <crate_name>.capabilities.json. That can lead to unnecessary rebuilds and (more importantly) a channel being installed under a different stem than the selected name, so it won't be discovered/enabled correctly. Align installer output naming with the selected channel name or update the checks to match.
| // Check if already on disk (may have been installed between bundled and here) | |
| let wasm_on_disk = channels_dir.join(format!("{}.wasm", name)).exists() | |
| || channels_dir.join(format!("{}-channel.wasm", name)).exists(); | |
| if wasm_on_disk { | |
| continue; | |
| } |
There was a problem hiding this comment.
Fixed in 61109a4. The installer uses manifest.name for on-disk filenames, and this code uses the qualified key channels/{name} for catalog lookup, so the naming is now consistent.
| return Err(RegistryError::ExtensionNotFound(format!( | ||
| "'{}' already installed at {}. Use --force to overwrite.", | ||
| manifest.name, | ||
| target_wasm.display() | ||
| ))); |
There was a problem hiding this comment.
The already-installed check returns RegistryError::ExtensionNotFound, which will surface as "Extension not found" even though the extension exists. Add a dedicated error variant (e.g. AlreadyInstalled) or use a more appropriate one so CLI/wizard messaging stays accurate.
| return Err(RegistryError::ExtensionNotFound(format!( | |
| "'{}' already installed at {}. Use --force to overwrite.", | |
| manifest.name, | |
| target_wasm.display() | |
| ))); | |
| return Err(RegistryError::ManifestRead { | |
| path: target_wasm.clone(), | |
| reason: format!( | |
| "'{}' already installed at {}. Use --force to overwrite.", | |
| manifest.name, | |
| target_wasm.display() | |
| ), | |
| }); |
There was a problem hiding this comment.
Fixed in 61109a4. Added dedicated AlreadyInstalled { name, path } error variant that produces a clear message.
| if auth.method.as_deref() != Some("none") { | ||
| println!( | ||
| "\nNext step: authenticate with `ironclaw tool auth {}`", | ||
| manifest.name |
There was a problem hiding this comment.
The post-install message suggests ironclaw tool auth <manifest.name>, but tool auth expects the argument to match the installed file stem (<name>.capabilities.json). If installs use source.crate_name for filenames, this instruction will be wrong for most tools (e.g. slack vs slack-tool). Ensure the auth guidance uses the canonical installed name, or change installation to write files under manifest.name.
| manifest.name | |
| outcome.name |
There was a problem hiding this comment.
This was already correct — manifest.name matches the installed filename since 61109a4 switched the installer to use manifest.name for on-disk files.
| let source = ExtensionSource::WasmBuildable { | ||
| repo_url: self.source.dir.clone(), | ||
| build_dir: Some(self.source.dir.clone()), |
There was a problem hiding this comment.
to_registry_entry() emits ExtensionSource::WasmBuildable { repo_url: ... }, but the current in-chat ExtensionManager install flow for WASM tools only supports ExtensionSource::WasmDownload and otherwise errors with "WASM tool entry has no download URL". As written, registry manifests converted via this helper won't be installable through the extension system. Either implement buildable installs in ExtensionManager or change the emitted ExtensionSource to something the manager supports.
| let source = ExtensionSource::WasmBuildable { | |
| repo_url: self.source.dir.clone(), | |
| build_dir: Some(self.source.dir.clone()), | |
| let source = ExtensionSource::WasmDownload { | |
| download_url: self.source.dir.clone(), |
There was a problem hiding this comment.
This is intentional — to_registry_entry() is a compatibility bridge for the in-chat extension discovery UI, not the primary install path. The registry installer handles installs directly via install_from_source(). WasmBuildable correctly represents the source type. When ExtensionManager gains build support, this will work automatically.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.test_database_connection_postgres(&url).await?; | ||
| self.settings.database_backend = Some("postgres".to_string()); | ||
| self.settings.database_url = Some(url); | ||
|
|
||
| // Load existing settings from DB. | ||
| if let Some(ref pool) = self.db_pool { | ||
| let store = crate::history::Store::from_pool(pool.clone()); | ||
| if let Ok(map) = store.get_all_settings("default").await { | ||
| self.settings = Settings::from_db_map(&map); | ||
| self.settings.database_backend = Some("postgres".to_string()); | ||
| } |
There was a problem hiding this comment.
In reconnect_postgres, the database_url is set on line 223 but then gets overwritten when Settings::from_db_map is called on line 229. After loading from DB, only database_backend is restored (line 230), but database_url is not. This means the wizard will lose the database URL after loading settings from DB. The same issue exists in reconnect_libsql where libsql_path and libsql_url are set before loading from DB but not restored after. Fix by either: (1) setting these fields AFTER loading from DB, or (2) explicitly restoring them after line 230/262 similar to how database_backend is restored.
There was a problem hiding this comment.
Fixed in 572ac8c. reconnect_postgres() now restores database_url after Settings::from_db_map() overwrites self.settings.
| self.test_database_connection_libsql(&path, turso_url.as_deref(), turso_token.as_deref()) | ||
| .await?; | ||
|
|
||
| self.settings.database_backend = Some("libsql".to_string()); | ||
| self.settings.libsql_path = Some(path); | ||
| if let Some(url) = turso_url { | ||
| self.settings.libsql_url = Some(url); | ||
| } | ||
|
|
||
| // Load existing settings from DB. | ||
| if let Some(ref db) = self.db_backend { | ||
| use crate::db::SettingsStore as _; | ||
| if let Ok(map) = db.get_all_settings("default").await { | ||
| self.settings = Settings::from_db_map(&map); | ||
| self.settings.database_backend = Some("libsql".to_string()); | ||
| } |
There was a problem hiding this comment.
In reconnect_libsql, the libsql_path and libsql_url fields are set on lines 251-254 but then get overwritten when Settings::from_db_map is called on line 261. After loading from DB, only database_backend is restored (line 262), but the libsql path and URL are not. This means the wizard will lose these connection details after loading settings from DB. Fix by either: (1) setting these fields AFTER loading from DB, or (2) explicitly restoring them after line 262 similar to how database_backend is restored.
There was a problem hiding this comment.
Fixed in 572ac8c. reconnect_libsql() now restores libsql_path and libsql_url after Settings::from_db_map() overwrites self.settings.
| let manifest = catalog | ||
| .get(name) | ||
| .ok_or_else(|| anyhow::anyhow!("Extension '{}' not found in registry.", name))?; |
There was a problem hiding this comment.
The cmd_info function uses catalog.get(name) which returns None for ambiguous bare names (e.g., "slack" matches both tools/slack and channels/slack). This produces a generic "Extension 'slack' not found" error even though the extension exists in both categories. Users must use qualified names like "tools/slack". Consider using catalog.get_strict(name) instead, which returns an explicit AmbiguousName error that guides users to use a qualified name.
| let manifest = catalog | |
| .get(name) | |
| .ok_or_else(|| anyhow::anyhow!("Extension '{}' not found in registry.", name))?; | |
| let manifest = catalog.get_strict(name)?; |
There was a problem hiding this comment.
Fixed in 572ac8c. cmd_info now uses catalog.get_strict(name) which returns an explicit AmbiguousName error with guidance to use the qualified key (tools/slack or channels/slack).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| RegistryCommand::InstallDefaults { force } => { | ||
| cmd_install(&catalog, ®istry_dir, "default", force, true).await | ||
| } |
There was a problem hiding this comment.
InstallDefaults currently calls cmd_install(..., prefer_build = true), forcing builds from source even when pre-built artifacts are available. This is surprising given registry install <name|bundle> defaults to downloading unless --build is passed, and it makes install-defaults potentially much slower. Consider defaulting prefer_build to false here (or adding a --build flag to InstallDefaults and passing it through).
There was a problem hiding this comment.
Fixed in eb52b18. InstallDefaults now defaults to prefer_build = false (download artifacts) and has a --build flag for parity with install.
| // Also try one level up (for target/release/ironclaw) | ||
| if let Some(grandparent) = parent.parent() { | ||
| let candidate = grandparent.join("registry"); | ||
| if candidate.is_dir() { | ||
| return Ok(candidate); | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
find_registry_dir() claims to support finding registry/ relative to an executable in target/{debug,release}/ironclaw, but it only checks the executable’s parent and grandparent directories. For .../target/release/ironclaw, the registry is typically at the repo root (great-grandparent), so this lookup will fail when running the binary from outside the repo root. Consider also checking one more ancestor (and/or adding the same CARGO_MANIFEST_DIR fallback used by onboarding’s load_registry_catalog()).
| // Also try one level up (for target/release/ironclaw) | |
| if let Some(grandparent) = parent.parent() { | |
| let candidate = grandparent.join("registry"); | |
| if candidate.is_dir() { | |
| return Ok(candidate); | |
| } | |
| } | |
| } | |
| // Also try one level up (for target/release/ironclaw under target/) | |
| if let Some(grandparent) = parent.parent() { | |
| let candidate = grandparent.join("registry"); | |
| if candidate.is_dir() { | |
| return Ok(candidate); | |
| } | |
| // And one more level up (for target/{debug,release}/ironclaw) | |
| if let Some(great_grandparent) = grandparent.parent() { | |
| let candidate = great_grandparent.join("registry"); | |
| if candidate.is_dir() { | |
| return Ok(candidate); | |
| } | |
| } | |
| } | |
| } | |
| // Fallback: use Cargo manifest directory if available (e.g., when run via `cargo run`) | |
| if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { | |
| let candidate = PathBuf::from(manifest_dir).join("registry"); | |
| if candidate.is_dir() { | |
| return Ok(candidate); | |
| } | |
| } |
There was a problem hiding this comment.
Fixed in eb52b18. find_registry_dir() now walks up 3 ancestor levels (covers target/release/ironclaw → repo root) and adds a CARGO_MANIFEST_DIR compile-time fallback, matching the onboarding's load_registry_catalog() logic.
…ng integration Adds a central registry that catalogs all 14 available extensions (10 tools, 4 channels) with their capabilities, auth requirements, and artifact references. The onboarding wizard now shows installable channels from the registry and offers tool installation as a new Step 7. - registry/ folder with per-extension JSON manifests and bundle definitions - src/registry/ module: manifest structs, catalog loader, installer - `ironclaw registry list|info|install|install-defaults` CLI commands - Setup wizard enhanced: channels from registry, new extensions step (8 steps) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…y onboarding Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during onboard install because Cargo resolved them as part of the root workspace. Add `[workspace]` table to each standalone crate and extend the root `workspace.exclude` list so they build independently. Channels-only mode (`onboard --channels-only`) failed with "Secrets not configured" and "No database connection" because it skipped database and security setup. Add `reconnect_existing_db()` to establish the DB connection and load saved settings before running channel configuration. Also improve the tunnel "already configured" display to show full provider details (domain, mode, command) instead of just the provider name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use manifest.name (not crate_name) for installed filenames so discovery, auth, and CLI commands all agree on the stem (#1) - Add AlreadyInstalled error variant instead of misleading ExtensionNotFound (#2) - Add DownloadFailed error variant with URL context instead of stuffing URLs into PathBuf (#3) - Validate HTTP status with error_for_status() before reading response bytes in artifact downloads (#4) - Switch build_wasm_component to tokio::process::Command with status() so build output streams to the terminal (#6) - Find WASM artifact by crate_name specifically instead of picking the first .wasm file in the release directory (#7) - Add is_file() guard in catalog loader to skip directories (#8) - Detect ambiguous bare-name lookups when both tools/<name> and channels/<name> exist, with get_strict() returning an error (#9) - Fix wizard step_extensions to check tool.name for installed detection, consistent with the new naming (#11, #12) - Fix redundant closures and map_or clippy warnings in changed files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
reconnect_postgres() and reconnect_libsql() called Settings::from_db_map() which overwrote database_url / libsql_path / libsql_url set from env vars. Also use get_strict() in cmd_info to surface ambiguous bare-name errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse nested if-let chains and inline string literals in format macros to satisfy CI clippy lint checks (deny warnings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lookup - InstallDefaults now defaults to downloading pre-built artifacts (matching `registry install` behavior), with --build flag for source builds. - find_registry_dir() walks up 3 ancestor levels from the exe and adds a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
af994f8 to
eb52b18
Compare
…tion (nearai#238) * feat: add extension registry with metadata catalog, CLI, and onboarding integration Adds a central registry that catalogs all 14 available extensions (10 tools, 4 channels) with their capabilities, auth requirements, and artifact references. The onboarding wizard now shows installable channels from the registry and offers tool installation as a new Step 7. - registry/ folder with per-extension JSON manifests and bundle definitions - src/registry/ module: manifest structs, catalog loader, installer - `ironclaw registry list|info|install|install-defaults` CLI commands - Setup wizard enhanced: channels from registry, new extensions step (8 steps) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(setup): resolve workspace errors for tool crates and channels-only onboarding Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during onboard install because Cargo resolved them as part of the root workspace. Add `[workspace]` table to each standalone crate and extend the root `workspace.exclude` list so they build independently. Channels-only mode (`onboard --channels-only`) failed with "Secrets not configured" and "No database connection" because it skipped database and security setup. Add `reconnect_existing_db()` to establish the DB connection and load saved settings before running channel configuration. Also improve the tunnel "already configured" display to show full provider details (domain, mode, command) instead of just the provider name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(registry): address PR review feedback on installer and catalog - Use manifest.name (not crate_name) for installed filenames so discovery, auth, and CLI commands all agree on the stem (nearai#1) - Add AlreadyInstalled error variant instead of misleading ExtensionNotFound (nearai#2) - Add DownloadFailed error variant with URL context instead of stuffing URLs into PathBuf (nearai#3) - Validate HTTP status with error_for_status() before reading response bytes in artifact downloads (nearai#4) - Switch build_wasm_component to tokio::process::Command with status() so build output streams to the terminal (nearai#6) - Find WASM artifact by crate_name specifically instead of picking the first .wasm file in the release directory (nearai#7) - Add is_file() guard in catalog loader to skip directories (nearai#8) - Detect ambiguous bare-name lookups when both tools/<name> and channels/<name> exist, with get_strict() returning an error (nearai#9) - Fix wizard step_extensions to check tool.name for installed detection, consistent with the new naming (nearai#11, nearai#12) - Fix redundant closures and map_or clippy warnings in changed files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(setup): restore DB connection fields after settings reload reconnect_postgres() and reconnect_libsql() called Settings::from_db_map() which overwrote database_url / libsql_path / libsql_url set from env vars. Also use get_strict() in cmd_info to surface ambiguous bare-name errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix clippy collapsible_if and print_literal warnings Collapse nested if-let chains and inline string literals in format macros to satisfy CI clippy lint checks (deny warnings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(registry): prefer artifacts for install-defaults and improve dir lookup - InstallDefaults now defaults to downloading pre-built artifacts (matching `registry install` behavior), with --build flag for source builds. - find_registry_dir() walks up 3 ancestor levels from the exe and adds a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…tion (nearai#238) * feat: add extension registry with metadata catalog, CLI, and onboarding integration Adds a central registry that catalogs all 14 available extensions (10 tools, 4 channels) with their capabilities, auth requirements, and artifact references. The onboarding wizard now shows installable channels from the registry and offers tool installation as a new Step 7. - registry/ folder with per-extension JSON manifests and bundle definitions - src/registry/ module: manifest structs, catalog loader, installer - `ironclaw registry list|info|install|install-defaults` CLI commands - Setup wizard enhanced: channels from registry, new extensions step (8 steps) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(setup): resolve workspace errors for tool crates and channels-only onboarding Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during onboard install because Cargo resolved them as part of the root workspace. Add `[workspace]` table to each standalone crate and extend the root `workspace.exclude` list so they build independently. Channels-only mode (`onboard --channels-only`) failed with "Secrets not configured" and "No database connection" because it skipped database and security setup. Add `reconnect_existing_db()` to establish the DB connection and load saved settings before running channel configuration. Also improve the tunnel "already configured" display to show full provider details (domain, mode, command) instead of just the provider name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(registry): address PR review feedback on installer and catalog - Use manifest.name (not crate_name) for installed filenames so discovery, auth, and CLI commands all agree on the stem (nearai#1) - Add AlreadyInstalled error variant instead of misleading ExtensionNotFound (nearai#2) - Add DownloadFailed error variant with URL context instead of stuffing URLs into PathBuf (nearai#3) - Validate HTTP status with error_for_status() before reading response bytes in artifact downloads (nearai#4) - Switch build_wasm_component to tokio::process::Command with status() so build output streams to the terminal (nearai#6) - Find WASM artifact by crate_name specifically instead of picking the first .wasm file in the release directory (nearai#7) - Add is_file() guard in catalog loader to skip directories (nearai#8) - Detect ambiguous bare-name lookups when both tools/<name> and channels/<name> exist, with get_strict() returning an error (nearai#9) - Fix wizard step_extensions to check tool.name for installed detection, consistent with the new naming (nearai#11, nearai#12) - Fix redundant closures and map_or clippy warnings in changed files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(setup): restore DB connection fields after settings reload reconnect_postgres() and reconnect_libsql() called Settings::from_db_map() which overwrote database_url / libsql_path / libsql_url set from env vars. Also use get_strict() in cmd_info to surface ambiguous bare-name errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix clippy collapsible_if and print_literal warnings Collapse nested if-let chains and inline string literals in format macros to satisfy CI clippy lint checks (deny warnings). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(registry): prefer artifacts for install-defaults and improve dir lookup - InstallDefaults now defaults to downloading pre-built artifacts (matching `registry install` behavior), with --build flag for source builds. - find_registry_dir() walks up 3 ancestor levels from the exe and adds a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
registry/): One JSON manifest per extension (10 tools + 4 channels) with source location, auth summary, artifacts, and tags. Bundle definitions group related extensions (google, messaging, default).src/registry/):ExtensionManifestserde structs,RegistryCatalogfor loading/searching/resolving manifests and bundles,RegistryInstallerfor build-from-source and download+SHA256-verify flows.ironclaw registry list [--kind tool|channel] [--tag default] [--verbose],info <name>,install <name|bundle> [--force] [--build],install-defaults [--force].Bug fixes (97d6786)
tools-src/andchannels-src/failedcargo metadataduring onboard install because Cargo resolved them as part of the root workspace. Added[workspace]table to each standalone crate and extended the rootworkspace.excludelist so they build independently.--channels-only): Failed with "Secrets not configured" and "No database connection" because it skipped database and security setup. Addedreconnect_existing_db()to establish the DB connection and load saved settings before running channel configuration.Test plan
cargo buildcompiles with default featurescargo build --no-default-features --features libsqlcompilescargo build --no-default-features --features postgrescompilescargo clippy --all --all-features— zero new warningscargo fmt -- --check— cleancargo test— 1203 tests pass, 0 failurescargo metadataworks from insidetools-src/telegram/(standalone workspace)ironclaw registry listshows all 14 extensionsironclaw registry info gmailshows manifest detailsironclaw registry install slack --buildbuilds and installsironclaw onboardshows channels from registry + new tools stepironclaw onboard --channels-onlyconnects to existing DB and saves settings🤖 Generated with Claude Code