Skip to content
Draft
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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "nemo-relay",
"interface": {
"displayName": "NeMo Relay"
},
"plugins": [
{
"name": "nemo-relay-plugin",
"source": {
"source": "local",
"path": "./integrations/coding-agents/codex"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Coding"
}
]
}
16 changes: 16 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "nemo-relay",
"description": "NeMo Relay plugins for coding-agent observability.",
"owner": {
"name": "NVIDIA Corporation and Affiliates",
"email": "noreply@nvidia.com"
},
"plugins": [
{
"name": "nemo-relay-plugin",
"description": "Forward Claude Code lifecycle hooks to a local NeMo Relay sidecar.",
"source": "./integrations/coding-agents/claude-code",
"category": "development"
}
]
}
4 changes: 4 additions & 0 deletions .github/ci-path-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ dependencies:
- 'uv.lock'

docs:
- '.agents/plugins/marketplace.json'
- '.claude-plugin/marketplace.json'
- 'CONTRIBUTING.md'
- 'README.md'
- 'docs/**'
Expand All @@ -129,6 +131,8 @@ docs:
- 'scripts/docs/**'

rust:
- '.agents/plugins/marketplace.json'
- '.claude-plugin/marketplace.json'
- 'Cargo.lock'
- 'Cargo.toml'
- 'crates/**/Cargo.toml'
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ jobs:
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Download CLI binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down
15 changes: 11 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The release pipeline publishes these package surfaces from a tag push:
| crates.io | `nemo-relay`, `nemo-relay-adaptive`, `nemo-relay-ffi`, `nemo-relay-cli` |
| PyPI | `nemo-relay` |
| npm | `nemo-relay-node`, `nemo-relay-openclaw`, `nemo-relay-wasm` |
| GitHub Releases | CLI binaries and `SHA256SUMS` |
| Fern | The documentation site |

Go remains source-first. There is no separate Go package-manager publication
Expand Down Expand Up @@ -159,7 +160,6 @@ The helper updates:
4. [`integrations/openclaw/package.json`](integrations/openclaw/package.json)
and the `integrations/openclaw` entry in the root
[`package-lock.json`](package-lock.json) to the same release version.

Review docs and snippets that mention explicit versions, including:

- [`README.md`](README.md)
Expand Down Expand Up @@ -236,6 +236,8 @@ The release pipeline then:
- `package-openclaw` packs the npm OpenClaw plugin package.
- `package-python` builds platform wheels.
- `package-wasm` packs the npm WebAssembly package.
- The CLI release-asset job uploads each platform `nemo-relay` binary and
includes those binaries in `SHA256SUMS`.
4. Publishes packages from the top-level workflow after the reusable packaging
jobs complete:
- `publish-rust` stamps Cargo workspace versions from the release tag, then
Expand All @@ -249,6 +251,9 @@ The release pipeline then:
- Stable tags publish to the npm `latest` dist-tag
- Prerelease tags such as `0.1.0-rc.1` publish to the npm `next`
dist-tag so they do not become the default upgrade target
- The GitHub Release entry remains a draft until a maintainer publishes it.
End-user `nemo-relay install ...` commands require the CLI binary to be
installed and available on `PATH`.

The workflow boundary is split intentionally:

Expand All @@ -275,9 +280,9 @@ NVIDIA Artifactory publication for the same tag:
npm trusted publishing has its own registry-side constraints:

- Each npm package can only have one trusted publisher configured at a time.
- Because this repository publishes `nemo-relay-node`, `nemo-relay-openclaw`, and
`nemo-relay-wasm`, configure trusted publishers for all three packages before
pushing a release tag.
- Because this repository publishes `nemo-relay-node`, `nemo-relay-openclaw`,
and `nemo-relay-wasm`, configure trusted publishers for all three packages
before pushing a release tag.
- npm trusted publishing currently supports GitHub-hosted runners, not
self-hosted runners.

Expand All @@ -292,6 +297,8 @@ After the tag pipeline succeeds, publish or finalize the GitHub Release entry
for that tag.

- Keep complete release notes in GitHub Releases.
- Publish the draft release before announcing `nemo-relay install` commands for
the new version.
- Do not copy those notes into `CHANGELOG.md` or duplicate the full release
history in the docs site.
- If you use GitHub-generated notes, review them before publishing. The
Expand Down
49 changes: 48 additions & 1 deletion crates/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use serde::Deserialize;
use serde_json::Value;

use crate::error::CliError;
use crate::plugin_shim::PluginShimCommand;

#[derive(Debug, Clone, Parser)]
#[command(name = "nemo-relay")]
Expand Down Expand Up @@ -79,6 +80,10 @@ pub(crate) enum Command {
Config(ConfigCommand),
/// Create or edit plugin configuration (writes `plugins.toml`)
Plugins(PluginsCommand),
/// Install coding-agent plugins from the local nemo-relay CLI.
Install(InstallCommand),
/// Uninstall coding-agent plugins installed by `nemo-relay install`.
Uninstall(UninstallCommand),
/// Diagnose env, agents, config, observability (optionally scoped to one agent)
Doctor(DoctorCommand),
/// List supported and locally-detected agents (use `--json` for machine output)
Expand All @@ -90,6 +95,9 @@ pub(crate) enum Command {
/// Internal: subprocess used by installed hooks to forward events. Not typed by humans.
#[command(hide = true)]
HookForward(HookForwardCommand),
/// Internal: plugin-local hook and sidecar supervisor. Not typed by humans.
#[command(hide = true)]
PluginShim(PluginShimCommand),
}

/// Args for `nemo-relay doctor`. `--json` is on this command (rather than as a global flag)
Expand All @@ -99,12 +107,42 @@ pub(crate) struct DoctorCommand {
/// Limit readiness checks to one supported agent.
#[arg(value_enum)]
pub(crate) agent: Option<CodingAgent>,
/// Diagnose an installed coding-agent plugin instead of the normal relay config.
#[arg(long, value_enum)]
pub(crate) plugin: Option<PluginHost>,
/// Plugin install state directory. Defaults to the platform data directory.
#[arg(long)]
pub(crate) install_dir: Option<PathBuf>,
/// Emit machine-readable JSON instead of the formatted human report. Versioned via
/// `schema_version`; stable shape for CI / evaluation harness consumption.
#[arg(long)]
#[arg(long, conflicts_with = "plugin")]
pub(crate) json: bool,
}

#[derive(Debug, Clone, Args)]
pub(crate) struct InstallCommand {
#[arg(value_enum)]
pub(crate) host: PluginHost,
#[arg(long)]
pub(crate) install_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) force: bool,
#[arg(long)]
pub(crate) dry_run: bool,
#[arg(long)]
pub(crate) skip_doctor: bool,
}

#[derive(Debug, Clone, Args)]
pub(crate) struct UninstallCommand {
#[arg(value_enum)]
pub(crate) host: PluginHost,
#[arg(long)]
pub(crate) install_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) dry_run: bool,
}

/// Args for `nemo-relay agents`. Shares the `--json` shape with `nemo-relay doctor`'s
/// `agents` field so the two outputs can be unified by downstream consumers.
#[derive(Debug, Clone, Args)]
Expand Down Expand Up @@ -289,6 +327,15 @@ pub(crate) enum CodingAgent {
Hermes,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub(crate) enum PluginHost {
Codex,
#[value(name = "claude-code", alias = "claude")]
ClaudeCode,
All,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub(crate) enum GatewayMode {
Expand Down
2 changes: 2 additions & 0 deletions crates/cli/src/gateway.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub(crate) async fn passthrough(
State(state): State<AppState>,
request: Request<Body>,
) -> Result<Response<Body>, CliError> {
state.touch();
let prepared = prepare_gateway_request(&state.config, request).await?;
let prep = state
.sessions
Expand Down Expand Up @@ -883,6 +884,7 @@ pub(crate) async fn models(
State(state): State<AppState>,
request: Request<Body>,
) -> Result<Response<Body>, CliError> {
state.touch();
let (parts, _body) = request.into_parts();
if parts.method != Method::GET {
return build_response(
Expand Down
13 changes: 12 additions & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ mod gateway;
mod installer;
mod launcher;
mod model;
mod plugin_install;
mod plugin_shim;
mod plugins;
mod server;
mod session;
Expand Down Expand Up @@ -54,6 +56,9 @@ async fn run() -> Result<ExitCode, error::CliError> {
installer::hook_forward(command).await?;
Ok(ExitCode::SUCCESS)
}
Some(Command::PluginShim(command)) => plugin_shim::run(command),
Some(Command::Install(command)) => plugin_install::install(command),
Some(Command::Uninstall(command)) => plugin_install::uninstall(command),
Some(Command::Run(command)) => launcher::run(command, Some(&cli.server)).await,
Some(Command::Claude(command)) => {
launcher::easy_path(CodingAgent::ClaudeCode, command, Some(&cli.server)).await
Expand Down Expand Up @@ -81,7 +86,13 @@ async fn run() -> Result<ExitCode, error::CliError> {
}
Ok(ExitCode::SUCCESS)
}
Some(Command::Doctor(command)) => doctor::run_doctor(command.agent, command.json).await,
Some(Command::Doctor(command)) => {
if let Some(plugin) = command.plugin {
plugin_install::doctor(plugin, command.install_dir, command.json)
} else {
doctor::run_doctor(command.agent, command.json).await
}
}
Some(Command::Agents(command)) => doctor::run_agents(command.json).await,
Some(Command::Completions(command)) => {
if command.install {
Expand Down
Loading