From 5179eb74b1da29ca2f113b71686d1b6ac52a6b69 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 4 Sep 2026 23:09:58 +0800 Subject: [PATCH] Auto-install the Netx Ops agent preset on plugin activate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy presets into ~/.dsh/.agent-presets (not a Windows junction — DSH discovery skips those) so dsh plugin add alone surfaces Custom → Netx Ops with skills. Co-authored-by: Cursor --- README.md | 35 ++++++++--------- cordis.patch.yml | 2 +- docs/INSTALL.md | 56 ++++++++++++++-------------- lib/index.js | 44 +++++++++++++++++++++- package.json | 2 +- presets/netxops/agent.cordis.yml | 7 ++-- scripts/link-preset.ps1 | 20 +++++----- scripts/link-preset.sh | 26 ++++++------- src/index.ts | 64 ++++++++++++++++++++++++++++++-- 9 files changed, 174 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 48d150c..fb0506f 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,33 @@ # netxops — Netx Ops for DeepSeek Harness -Public **DeepSeek Harness** package: host bridge + **Plugins settings card** + ops agent preset. +Public **DeepSeek Harness** package: host bridge + Plugins card + **agent preset (skills included)**. - GitHub: https://github.com/hansjone/netxops -- Topic: [`dsh-plugin`](https://github.com/topics/dsh-plugin) - Package: `dsh-netxops` - Brand: **Netx Ops** -## What you get - -| Piece | Where | Purpose | -|-------|--------|---------| -| Host plugin | `src/index.ts` + `cordis.patch.yml` | settings `netxops` + credential `NETX_API_TOKEN` → `mcp__netx__*` | -| Plugins card | `src/client/` → `lib/client.js` | Settings → Plugins → fill URL / token in UI | -| Agent preset | `presets/netxops/` | Persona + UME / managed-NE skills | - -**No OS env required** for day-to-day use. See [docs/INSTALL.md](docs/INSTALL.md). - -**Not in v1:** topology canvas MCP, Excel report plugin. - -## Quick start +## Install (one command) ```powershell dsh plugin --profile web add github:hansjone/netxops dsh web -# Settings → Plugins → Netx Ops → token + API URL → Save ``` -Agent preset (skills): clone once and run `scripts/link-preset.ps1` (or `.sh`). +Then: -## Related +1. Settings → **Plugins** → **Netx Ops** → API URL / token +2. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate) +3. New session → choose **Netx Ops** -- Data plane: [hansjone/netx](https://github.com/hansjone/netx) -- Porting: [docs/PORTING.md](docs/PORTING.md) +Also need: running **netx API**, and `pip install` of `netx_mcp` (Python execution plane). Details: [docs/INSTALL.md](docs/INSTALL.md). + +## What is coupled + +| In the plugin | Outside (data / runtime) | +|---------------|---------------------------| +| MCP spawn + settings + credentials | netx HTTP API | +| Persona + UME / managed-NE skills | `python -m netx_mcp` on PATH | +| Agent preset auto-install to `~/.dsh/.agent-presets` | | ## License diff --git a/cordis.patch.yml b/cordis.patch.yml index 0ac53c0..590c287 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -12,4 +12,4 @@ pythonCommand: python tokenCredentialRef: NETX_API_TOKEN toolCallTimeoutMs: 120000 - failOnStartupError: false + installAgentPreset: true diff --git a/docs/INSTALL.md b/docs/INSTALL.md index cf5bd42..491065d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,52 +1,52 @@ # Install Netx Ops on DeepSeek Harness -## Prerequisites +## What `dsh plugin add` gives you -1. **DeepSeek Harness** (`dsh`) installed. -2. **netx API** reachable (default `http://127.0.0.1:8890`). -3. **Python 3.11+** with `netx_mcp` importable: +One install wires **all of**: + +| Piece | How you use it | +|-------|----------------| +| Host bridge | spawns `mcp__netx__*` via `python -m netx_mcp` | +| Plugins card | Settings → Plugins → **Netx Ops** (URL / lang / python) | +| Agent preset + skills | Settings → Agent presets → **Custom → Netx Ops** (copied into `~/.dsh/.agent-presets` on first boot) | + +You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback. + +## Still required outside the npm package + +1. **netx API** reachable (default `http://127.0.0.1:8890`) — the data plane. +2. **Python `netx_mcp`** on the same machine as `dsh`: ```powershell pip install "git+https://github.com/hansjone/netx.git#subdirectory=packages/netx-mcp" python -c "import netx_mcp; print('ok')" ``` -No OS env vars needed for URL / token. +v1 keeps the Python MCP as the execution plane (shared with OpenClaw / `python -m netx_mcp`). The DSH plugin owns orchestration, settings, persona, and skills — not a second MCP install in the agent preset. -## Install (GitHub — recommended) +## Install ```powershell dsh plugin --profile web add github:hansjone/netxops -dsh web +# or from DeepSeekHarness source: +# pnpm dsh plugin --profile web add github:hansjone/netxops +dsh web # or: pnpm dsh web ``` -Then: **Settings → Plugins → Netx Ops** → paste API token + URL → Save. - -That is the whole host install. You do **not** run any `link-dsh-peers` script for GitHub installs. - -### Agent preset (persona + skills) - -One-time (or after pull): - -```powershell -git clone https://github.com/hansjone/netxops.git -cd netxops -powershell -File .\scripts\link-preset.ps1 -``` - -Unix: `./scripts/link-preset.sh` - -New session → preset **Netx Ops**. +1. **Settings → Plugins → Netx Ops** → API URL (+ token if the field is enabled). + Token fallback: `scripts/set-netx-token.ps1` / `.sh`. +2. Restart or open Settings → **Agent presets** → Custom → **Netx Ops** should appear after the host plugin has activated once. +3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms. ## Verify -1. Settings → Plugins shows **Netx Ops**; token badge “Configured” after save. -2. Tools include `mcp__netx__queryUmeAlarms`. -3. Critical Top / single-host alarm queries work. +1. Plugins card **Netx Ops** visible. +2. Agent presets → Custom → **Netx Ops**. +3. Tools include `mcp__netx__queryUmeAlarms`. See [TOOL_MAP.md](TOOL_MAP.md). -## Path / local checkout (developers only) +## Path / local checkout (developers) ```powershell dsh plugin --profile web add D:\path\to\netxops diff --git a/lib/index.js b/lib/index.js index 0a06a33..9fbd188 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,8 @@ // src/index.ts +import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import z from "@deepseek-ai/schemastery"; import { credentialRef } from "@deepseek-ai/dsh-credentials"; import * as DshSettings from "@deepseek-ai/dsh-settings"; @@ -6,6 +10,7 @@ import * as McpClient from "@deepseek-ai/dsh-mcp-client"; var name = "netxops"; var inject = ["tools"]; var NETXOPS_SETTINGS_NAMESPACE = "netxops"; +var NETXOPS_PRESET_ID = "netxops"; var DEFAULT_TOKEN_REF = "NETX_API_TOKEN"; var Config2 = z.object({ apiUrl: z.string().default("http://127.0.0.1:8890"), @@ -13,8 +18,40 @@ var Config2 = z.object({ pythonCommand: z.string().default("python"), tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF), toolCallTimeoutMs: z.number().step(1).min(1000).default(120000), - failOnStartupError: z.boolean().default(false) + failOnStartupError: z.boolean().default(false), + installAgentPreset: z.boolean().default(true) }); +function packageRoot() { + return join(dirname(fileURLToPath(import.meta.url)), ".."); +} +function resolveDshHome() { + const fromEnv = process.env.DSH_HOME?.trim(); + if (fromEnv !== undefined && fromEnv.length > 0) + return fromEnv; + return join(homedir(), ".dsh"); +} +function ensureAgentPresetInstalled(logger) { + const src = join(packageRoot(), "presets", NETXOPS_PRESET_ID); + const composition = join(src, "agent.cordis.yml"); + if (!existsSync(composition)) { + logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src); + return; + } + const destParent = join(resolveDshHome(), ".agent-presets"); + const dest = join(destParent, NETXOPS_PRESET_ID); + try { + mkdirSync(destParent, { recursive: true }); + if (existsSync(dest)) { + rmSync(dest, { recursive: true, force: true }); + } + cpSync(src, dest, { recursive: true }); + writeFileSync(join(dest, ".dsh-netxops-managed"), `${new Date().toISOString()} +`, "utf8"); + logger.info("netxops: agent preset installed at %s", dest); + } catch (error) { + logger.error("netxops: failed to install agent preset: %s", error); + } +} async function resolveToken(ctx, refName) { const credentials = ctx.get("credentials"); if (credentials === undefined) @@ -37,6 +74,9 @@ function apply(ctx, config = Config2({})) { let mcpFiber; let remounting = Promise.resolve(); let generation = 0; + if (config.installAgentPreset) { + ensureAgentPresetInstalled(ctx.logger); + } const remount = () => { remounting = remounting.then(async () => { const gen = ++generation; @@ -102,8 +142,10 @@ function apply(ctx, config = Config2({})) { export { name, inject, + ensureAgentPresetInstalled, apply, NETXOPS_SETTINGS_NAMESPACE, + NETXOPS_PRESET_ID, DEFAULT_TOKEN_REF, Config2 as Config }; diff --git a/package.json b/package.json index f4c33de..5cdf525 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-netxops", - "version": "0.1.10", + "version": "0.1.11", "description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset", "license": "MIT", "type": "module", diff --git a/presets/netxops/agent.cordis.yml b/presets/netxops/agent.cordis.yml index d09b7d2..28b7dd4 100644 --- a/presets/netxops/agent.cordis.yml +++ b/presets/netxops/agent.cordis.yml @@ -63,6 +63,7 @@ - id: tool-skill name: '@deepseek-ai/dsh-tool-skill' -# netx MCP is mounted on the HOST by package `dsh-netxops` (cordis.patch.yml): -# Settings → Plugins / credentials NETX_API_TOKEN — not OS env, not this preset. -# Tools appear as mcp__netx__* for sessions that can see the host tool registry. +# netx MCP is mounted on the HOST by package `dsh-netxops` (cordis.patch.yml). +# The same package copies this preset into `$DSH_HOME/.agent-presets/netxops` +# on activate — no manual link-preset script for normal installs. +# Tools appear as mcp__netx__* on the host tool registry. diff --git a/scripts/link-preset.ps1 b/scripts/link-preset.ps1 index 8100c4a..deb6555 100644 --- a/scripts/link-preset.ps1 +++ b/scripts/link-preset.ps1 @@ -1,4 +1,6 @@ -# Link presets/netxops into the DSH user agent-presets root (Windows junction). +# Link / install presets/netxops into the DSH user agent-presets root. +# Prefer a real directory copy: DSH discovery skips Windows junctions +# (Dirent.isDirectory() is false for reparse points). $ErrorActionPreference = "Stop" $RepoRoot = Split-Path -Parent $PSScriptRoot $Source = Join-Path $RepoRoot "presets\netxops" @@ -6,18 +8,14 @@ $DshHome = if ($env:DSH_HOME) { $env:DSH_HOME } else { Join-Path $env:USERPROFIL $TargetParent = Join-Path $DshHome ".agent-presets" $Target = Join-Path $TargetParent "netxops" -if (-not (Test-Path $Source)) { +if (-not (Test-Path (Join-Path $Source "agent.cordis.yml"))) { throw "Missing preset dir: $Source" } New-Item -ItemType Directory -Force -Path $TargetParent | Out-Null if (Test-Path $Target) { - $item = Get-Item $Target -Force - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { - cmd /c "rmdir `"$Target`"" - } else { - throw "Target exists and is not a junction: $Target — remove or rename it first." - } + Remove-Item -Recurse -Force $Target } -cmd /c "mklink /J `"$Target`" `"$Source`"" -Write-Host "Linked $Target -> $Source" -Write-Host "Restart dsh / open a new session and select preset 'netxops' (Netx Ops)." +Copy-Item -Recurse -Force $Source $Target +Set-Content -Path (Join-Path $Target ".dsh-netxops-managed") -Value ((Get-Date).ToString("o")) +Write-Host "Installed $Target (copy from $Source)" +Write-Host "Open Settings → Agent presets → Custom → Netx Ops (or new session)." diff --git a/scripts/link-preset.sh b/scripts/link-preset.sh index 6bb1e57..ba38fe4 100644 --- a/scripts/link-preset.sh +++ b/scripts/link-preset.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash +# Install presets/netxops into $DSH_HOME/.agent-presets (copy, not symlink). set -euo pipefail -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SOURCE="$REPO_ROOT/presets/netxops" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SRC="$ROOT/presets/netxops" DSH_HOME="${DSH_HOME:-$HOME/.dsh}" -TARGET_PARENT="$DSH_HOME/.agent-presets" -TARGET="$TARGET_PARENT/netxops" +DEST_PARENT="$DSH_HOME/.agent-presets" +DEST="$DEST_PARENT/netxops" -if [[ ! -d "$SOURCE" ]]; then - echo "Missing preset dir: $SOURCE" >&2 +if [[ ! -f "$SRC/agent.cordis.yml" ]]; then + echo "Missing preset dir: $SRC" >&2 exit 1 fi -mkdir -p "$TARGET_PARENT" -if [[ -e "$TARGET" || -L "$TARGET" ]]; then - rm -rf "$TARGET" -fi -ln -s "$SOURCE" "$TARGET" -echo "Linked $TARGET -> $SOURCE" -echo "Restart dsh / open a new session and select preset 'netxops' (Netx Ops)." +mkdir -p "$DEST_PARENT" +rm -rf "$DEST" +cp -R "$SRC" "$DEST" +date -u +%Y-%m-%dT%H:%M:%SZ > "$DEST/.dsh-netxops-managed" +echo "Installed $DEST" +echo "Open Settings → Agent presets → Custom → Netx Ops" diff --git a/src/index.ts b/src/index.ts index d241200..13c8230 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,13 +2,17 @@ * Host-plane Netx Ops bridge: settings (apiUrl / lang / python) + credentials * (NETX_API_TOKEN) drive a dynamically mounted `@deepseek-ai/dsh-mcp-client`. * - * Configure in DSH Settings → Plugins → Netx Ops (settings card when client - * half is installed). Token is stored as credential `NETX_API_TOKEN` (same - * store as model API keys). Helper: `scripts/set-netx-token.ps1`. + * On activate, the agent preset + skills are copied into + * `$DSH_HOME/.agent-presets/netxops` so `dsh plugin add` alone is enough + * (Windows junctions are invisible to DSH's `Dirent.isDirectory()` scan). * * @module dsh-netxops */ +import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import type { Context, Fiber } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { credentialRef } from '@deepseek-ai/dsh-credentials' @@ -26,6 +30,9 @@ export const inject = ['tools'] /** Settings / composition namespace (Plugins page join key). */ export const NETXOPS_SETTINGS_NAMESPACE = 'netxops' +/** Agent preset id under `$DSH_HOME/.agent-presets/`. */ +export const NETXOPS_PRESET_ID = 'netxops' + /** Default credential reference for the netx API bearer token. */ export const DEFAULT_TOKEN_REF = 'NETX_API_TOKEN' @@ -43,6 +50,11 @@ export interface Config { toolCallTimeoutMs: number /** Fail activation when MCP cannot connect / sync tools. */ failOnStartupError: boolean + /** + * Copy bundled agent preset + skills into `$DSH_HOME/.agent-presets/netxops` + * on every activate (required for Settings → Agent presets). + */ + installAgentPreset: boolean } export const Config: z = z.object({ @@ -52,8 +64,49 @@ export const Config: z = z.object({ tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF), toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000), failOnStartupError: z.boolean().default(false), + installAgentPreset: z.boolean().default(true), }) +/** Package root (parent of `lib/` or `src/` depending on launch). */ +function packageRoot(): string { + return join(dirname(fileURLToPath(import.meta.url)), '..') +} + +/** Harness home used by agent-presets' user root. */ +function resolveDshHome(): string { + const fromEnv = process.env.DSH_HOME?.trim() + if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv + return join(homedir(), '.dsh') +} + +/** + * Install the bundled Netx Ops preset as a real directory under the user + * preset root. DSH discovery skips Windows junctions (`Dirent.isDirectory()` + * is false for reparse points), so a copy is required — not `mklink /J`. + * @param logger - cordis logger for non-fatal install failures. + */ +export function ensureAgentPresetInstalled(logger: Context['logger']): void { + const src = join(packageRoot(), 'presets', NETXOPS_PRESET_ID) + const composition = join(src, 'agent.cordis.yml') + if (!existsSync(composition)) { + logger.warn('netxops: bundled preset missing at %s — skip user-preset install', src) + return + } + const destParent = join(resolveDshHome(), '.agent-presets') + const dest = join(destParent, NETXOPS_PRESET_ID) + try { + mkdirSync(destParent, { recursive: true }) + if (existsSync(dest)) { + rmSync(dest, { recursive: true, force: true }) + } + cpSync(src, dest, { recursive: true }) + writeFileSync(join(dest, '.dsh-netxops-managed'), `${new Date().toISOString()}\n`, 'utf8') + logger.info('netxops: agent preset installed at %s', dest) + } catch (error) { + logger.error('netxops: failed to install agent preset: %s', error) + } +} + /** * Resolve bearer token from the credentials seam (or empty when unset). */ @@ -105,6 +158,10 @@ export function apply(ctx: Context, config: Config = Config({})): void { let remounting: Promise = Promise.resolve() let generation = 0 + if (config.installAgentPreset) { + ensureAgentPresetInstalled(ctx.logger) + } + const remount = (): void => { remounting = remounting.then(async () => { const gen = ++generation @@ -148,7 +205,6 @@ export function apply(ctx: Context, config: Config = Config({})): void { }) } - // Composition defaults first (works even when settings provider is absent). remount() installNetxopsSettings(ctx, config, {