netxops/lib/agent-tools-nms.js

2307 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
// src/netx/config-values.ts
var volatileWrite = Symbol.for("cosmokit.volatile.write");
function readConfigValue(value) {
if (value !== null && typeof value === "object" && volatileWrite in value && "get" in value && typeof value.get === "function") {
return value.get();
}
return value;
}
function readConfigSnapshot(input) {
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, readConfigValue(value)]));
}
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false }),
bizMonitor: Object.freeze({ inPreset: true, public: false })
});
var SKILL_DIR_BY_GROUP = Object.freeze({
ops: "ops",
topology: "topology",
bizMonitor: "biz-monitor"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"ops",
"topology",
"bizMonitor"
]);
function kbEnabledForPlane(flags, plane, presetId = "netxops") {
if (flags)
flags = readConfigSnapshot(flags);
if (flags?.toolsEnabled === false)
return false;
if (!flags)
return plane === "preset";
if (plane === "preset") {
return !flags.groupKbPublic && (presetId === "netxops" ? flags.groupKbInPreset !== false : parsePresetIds(flags.groupKbPresets ?? "").includes(presetId));
}
return flags.groupKbPublic === true;
}
function groupsForPlane(groups, plane, only) {
const policy = groups ?? DEFAULT_CAPABILITY_GROUPS;
const enabled = CAPABILITY_GROUP_IDS.filter((id) => {
if (plane === "preset")
return policy[id].inPreset;
return policy[id].public;
});
if (!only || only.length === 0)
return enabled;
const allow = new Set(only);
return enabled.filter((id) => allow.has(id));
}
function parsePresetIds(value) {
const current = readConfigValue(value);
if (typeof current !== "string")
return [];
return [...new Set(current.split(/[\s,,]+/).filter(Boolean))];
}
function groupsForPreset(connection, presetId, only) {
if (connection.toolsEnabled === false)
return [];
const policy = connection.groups ?? DEFAULT_CAPABILITY_GROUPS;
return CAPABILITY_GROUP_IDS.filter((id) => (!only?.length || only.includes(id)) && !policy[id].public && (presetId === "netxops" ? policy[id].inPreset : policy[id].presets?.includes(presetId)));
}
// src/netx/mcp-types.ts
var NETX_TOOL_PREFIX = "netx__";
function publicToolName(serverName) {
return `${NETX_TOOL_PREFIX}${serverName}`;
}
// src/netx/mcp-catalog.ts
function disabledSet(disabledTools) {
const out = new Set;
for (const name of disabledTools ?? []) {
const trimmed = typeof name === "string" ? name.trim() : "";
if (trimmed)
out.add(trimmed);
}
return out;
}
function filterCatalogTools(catalog, enabledGroups, policy) {
const allowGroups = new Set(enabledGroups);
const disabled = disabledSet(policy.disabledTools);
const readOnlyOnly = policy.readOnlyOnly === true;
const out = [];
for (const tool of catalog) {
if (!tool.group || !allowGroups.has(tool.group))
continue;
if (disabled.has(tool.name))
continue;
if (readOnlyOnly && tool.readOnlyHint !== true)
continue;
out.push(tool);
}
return out;
}
function resolveEnabledTools(catalog, options) {
if (options.toolsEnabled === false)
return [];
const enabledGroups = options.forceGroups ?? (options.plane === "preset" ? groupsForPreset({ toolsEnabled: options.toolsEnabled, groups: options.groups }, options.presetId ?? "netxops", options.only) : groupsForPlane(options.groups, options.plane, options.only));
return filterCatalogTools(catalog, enabledGroups, options);
}
function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly) {
const allowGroups = new Set(enabledGroups);
const disabled = disabledSet(disabledTools);
const names = [];
for (const tool of catalog) {
if (!tool.group || !allowGroups.has(tool.group))
continue;
const off = disabled.has(tool.name) || readOnlyOnly === true && tool.readOnlyHint !== true;
if (off)
names.push(publicToolName(tool.name));
}
return names.sort();
}
// src/netx/mcp-remote.ts
var PROTOCOL_VERSION = "2025-03-26";
var CLIENT_INFO = { name: "dsh-netxops", version: "0.1.71" };
class McpRemoteError extends Error {
status;
constructor(message, status) {
super(message);
this.name = "McpRemoteError";
this.status = status;
}
}
function mcpEndpointUrl(apiUrl) {
const trimmed = apiUrl.trim().replace(/\/$/, "");
if (!trimmed)
return "";
return `${trimmed}/mcp`;
}
function parseMcpResponseBody(raw) {
const text = raw.replace(/^\uFEFF/, "").trim();
if (!text)
throw new McpRemoteError("empty /mcp response body");
if (text.startsWith("{") || text.startsWith("[")) {
try {
return JSON.parse(text);
} catch (error) {
throw new McpRemoteError(`mcp JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
if (text.includes("data:")) {
const dataLines = [];
for (const line of text.split(/\r?\n/)) {
if (line.startsWith("data:"))
dataLines.push(line.slice(5).trim());
}
for (let i = dataLines.length - 1;i >= 0; i -= 1) {
try {
return JSON.parse(dataLines[i]);
} catch {}
}
}
throw new McpRemoteError("mcp response was not JSON");
}
function asRpcResult(payload) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
throw new McpRemoteError("invalid JSON-RPC envelope from /mcp");
}
const row = payload;
if (row.error && typeof row.error === "object" && !Array.isArray(row.error)) {
const err = row.error;
const message = typeof err.message === "string" ? err.message : "JSON-RPC error";
throw new McpRemoteError(message);
}
if (!("result" in row)) {
throw new McpRemoteError("JSON-RPC response missing result");
}
return payload.result;
}
var nextId = 1;
async function postRpc(options, method, params, notification = false) {
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream"
};
const token = options.token.trim();
if (token)
headers.Authorization = `Bearer ${token}`;
const lang = options.lang?.trim();
if (lang) {
headers["Accept-Language"] = lang.toLowerCase().startsWith("en") ? "en" : "zh";
}
const body = notification ? { jsonrpc: "2.0", method, ...params ? { params } : {} } : { jsonrpc: "2.0", id: nextId++, method, ...params ? { params } : {} };
const bodyText = JSON.stringify(body);
const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? options.timeoutMs : 60000;
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
if (typeof fetchImpl !== "function") {
throw new McpRemoteError("fetch is not available");
}
const abort = new AbortController;
const timer = setTimeout(() => abort.abort(), timeoutMs);
const onOuterAbort = () => {
abort.abort();
};
options.signal?.addEventListener("abort", onOuterAbort, { once: true });
let response;
try {
response = await fetchImpl(options.mcpUrl, {
method: "POST",
headers,
body: bodyText,
signal: abort.signal
});
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
throw new McpRemoteError("mcp http timeout");
}
throw error;
} finally {
clearTimeout(timer);
options.signal?.removeEventListener("abort", onOuterAbort);
}
const responseText = await response.text();
if (response.status === 401 || response.status === 403) {
throw new McpRemoteError(`HTTP ${response.status} unauthorized`, response.status);
}
if (response.status < 200 || response.status >= 300) {
throw new McpRemoteError(`HTTP ${response.status}${responseText ? `: ${responseText.slice(0, 200)}` : ""}`, response.status);
}
if (notification) {
return;
}
return asRpcResult(parseMcpResponseBody(responseText));
}
async function callMcpTool(options, name, args) {
if (!options.mcpUrl)
throw new McpRemoteError("mcp url is empty");
await postRpc(options, "initialize", {
protocolVersion: PROTOCOL_VERSION,
capabilities: {},
clientInfo: CLIENT_INFO
});
await postRpc(options, "notifications/initialized", {}, true);
const result = await postRpc(options, "tools/call", {
name,
arguments: args
});
const content = Array.isArray(result.content) ? result.content : [];
return {
content,
...result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {},
...result.isError === true ? { isError: true } : {}
};
}
function mcpContentText(content, fallback = "") {
const parts = [];
for (const block of content) {
if (!block || typeof block !== "object" || Array.isArray(block))
continue;
const row = block;
if (row.type === "text" && typeof row.text === "string")
parts.push(row.text);
}
return parts.join(`
`) || fallback;
}
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
// src/netx/http.ts
function normalizeApiUrl(raw) {
const trimmed = typeof raw === "string" ? raw.trim() : "";
if (!trimmed)
return { ok: false, error: "empty" };
let parsed;
try {
parsed = new URL(trimmed);
} catch {
return { ok: false, error: "invalid_url" };
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return { ok: false, error: "unsupported_scheme" };
}
if (parsed.username || parsed.password) {
return { ok: false, error: "credentials_in_url" };
}
const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
const search = parsed.search && parsed.search !== "?" ? parsed.search : "";
return { ok: true, url: `${parsed.origin}${path}${search}` };
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
}
function normalizeSkillsApiUrl(apiUrl) {
const normalized = normalizeApiUrl(apiUrl);
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
}
function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
var EMPTY = Object.freeze({
phase: "idle",
mcpUrl: "",
tools: Object.freeze([]),
error: "",
listedAt: null,
fromCache: false
});
function store() {
const root = globalThis;
let current = root[STORE_KEY];
if (current === undefined) {
current = { state: EMPTY, listeners: new Set, generation: 0 };
root[STORE_KEY] = current;
}
return current;
}
function getMcpCatalogTools() {
return store().state.tools;
}
function watchMcpCatalog(listener) {
const state = store();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}
// src/netx/group-skills.ts
import { readdir, readFile, stat } from "node:fs/promises";
import { join as join2 } from "node:path";
// src/netx/runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
function store2() {
const root = globalThis;
let current = root[STORE_KEY2];
if (current === undefined) {
current = { connection: undefined, listeners: new Set };
root[STORE_KEY2] = current;
}
return current;
}
function getNetxConnection() {
return store2().connection;
}
function provisionalNetxConnection() {
const envUrl = process.env.NETX_API_URL?.trim() ?? "";
const normalized = envUrl ? normalizeApiUrl(envUrl) : normalizeApiUrl("http://127.0.0.1:8890");
return {
toolsEnabled: false,
apiUrl: normalized.ok ? normalized.url : "http://127.0.0.1:8890",
token: process.env.NETX_API_TOKEN?.trim() ?? "",
lang: typeof process.env.NETX_LANG === "string" && process.env.NETX_LANG.trim() ? process.env.NETX_LANG.trim() : "zh",
toolCallTimeoutMs: 120000,
groups: DEFAULT_CAPABILITY_GROUPS,
disabledTools: [],
readOnlyOnly: false,
groupKbInPreset: true,
groupKbPublic: false
};
}
function requireNetxConnection() {
return getNetxConnection() ?? provisionalNetxConnection();
}
function watchNetxConnection(listener) {
const state = store2();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}
// src/netx/group-skills.ts
function parseFrontmatter(raw) {
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
`).replace(/\r/g, `
`);
if (!text.startsWith("---"))
return;
const end = text.indexOf(`
---`, 3);
if (end < 0)
return;
const yaml = text.slice(3, end).replace(/^\n/, "");
const body = text.slice(end + 4).replace(/^\n/, "");
const data = {};
const lines = yaml.split(`
`);
for (let i = 0;i < lines.length; i += 1) {
const line = lines[i];
const nameMatch = /^name\s*:\s*(.+)\s*$/.exec(line);
if (nameMatch) {
data.name = stripQuotes(nameMatch[1].trim());
continue;
}
const descMatch = /^description\s*:\s*(.*)$/.exec(line);
if (!descMatch)
continue;
const head = descMatch[1].trim();
if (head === ">" || head === ">-" || head === "|" || head === "|-") {
const parts = [];
while (i + 1 < lines.length && /^[ \t]+/.test(lines[i + 1])) {
i += 1;
parts.push(lines[i].trim());
}
data.description = parts.filter(Boolean).join(" ");
continue;
}
data.description = stripQuotes(head);
}
return { data, body };
}
function stripQuotes(value) {
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
return value.slice(1, -1);
}
return value;
}
async function loadSkillBundle(dir) {
const skillPath = join2(dir, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
} catch {
return null;
}
const parsed = parseFrontmatter(raw);
if (!parsed)
return null;
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
if (!name || !description)
return null;
return {
name,
description,
content: parsed.body.trimStart(),
path: skillPath,
directory: dir
};
}
async function loadGroupSkills(skillsRoot, groupId) {
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
let entries;
try {
entries = await readdir(groupDir);
} catch {
return { skills: [], skipped: [{ directory: groupDir, reason: "missing group dir" }] };
}
const skills = [];
const skipped = [];
for (const entry of entries) {
const full = join2(groupDir, entry);
let isDir = false;
try {
isDir = (await stat(full)).isDirectory();
} catch {
continue;
}
if (!isDir)
continue;
const skillPath = join2(full, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
} catch {
skipped.push({ directory: full, reason: "no SKILL.md" });
continue;
}
const parsed = parseFrontmatter(raw);
if (!parsed) {
skipped.push({ directory: full, reason: "invalid frontmatter" });
continue;
}
const name = typeof parsed.data.name === "string" ? parsed.data.name.trim() : "";
const description = typeof parsed.data.description === "string" ? parsed.data.description.trim() : "";
if (!name || !description) {
skipped.push({
directory: full,
reason: !name ? "missing name" : "missing description"
});
continue;
}
skills.push({
name,
description,
content: parsed.body.trimStart(),
path: skillPath,
directory: full
});
}
return { skills, skipped };
}
function disabledFootnote(names, lang) {
if (names.length === 0)
return "";
const list = names.join(", ");
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Unavailable tools
The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : `
## 不可用工具
以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`;
}
function resolveSkillsRoot(options) {
if (typeof options.skillsRoot === "string" && options.skillsRoot.trim()) {
return options.skillsRoot.trim();
}
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
const signal = options.signal;
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const root = resolveSkillsRoot(options);
const disposers = [];
const enabled = new Set(groupIds);
const log = ctx.logger;
if (!root) {
if (enabled.size > 0) {
log?.warn?.("%s: no MCP skills cache (missing apiUrl)", providerLabel);
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
continue;
const { skills, skipped } = await loadGroupSkills(root, groupId);
if (signal?.aborted)
break;
for (const miss of skipped) {
log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason);
}
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content}
## DeepSeek Harness REST capabilities
Use only tools present in this session. Dual-unit/orbit/polish recipes require server-side topology tools. Use only tools present in this session.
` : skill.content;
disposers.push(skillsApi.register({
name: skill.name,
description: skill.description,
content: `${base}${footnote}`,
path: skill.path,
resourceBase: { kind: "directory", path: skill.directory },
provider: providerLabel,
source: "custom"
}));
}
}
if (signal?.aborted) {
for (const dispose of disposers.splice(0))
dispose();
}
} catch (error) {
for (const dispose of disposers.splice(0))
dispose();
throw error;
}
return () => {
for (const dispose of disposers)
dispose();
};
}
// src/netx/kb-context-skill.ts
import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = {
hasRegions: false,
hasTheory: false,
hasPacket: false,
hasCommon: false,
hasSkills: false
};
function unconfiguredKbSnapshot() {
return {
status: "unconfigured",
realRoot: "",
operatorName: "",
country: "",
version: "",
content: { ...EMPTY_CONTENT },
paths: {},
errorMessage: ""
};
}
function resolveKbPaths(snapshot) {
if (snapshot.status !== "configured")
return [];
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
const rows = resolveKbPaths(snapshot);
if (rows.length === 0)
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
return [
"| paths.* | Absolute path |",
"| --- | --- |",
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
].join(`
`);
}
// src/netx/kb-local-path.ts
import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
"drafts",
"suggestions",
"refs"
]);
var REF_AREAS = Object.freeze([
"inventory",
"devices",
"topology",
"business",
"commands",
"handbooks"
]);
var MEMORY_BUCKET_DIR = Object.freeze({
note: "日常笔记",
rca_review: "排障复盘",
ai_trace: "AI思维链"
});
function resolveKbLocalRoot(snapshot) {
if (snapshot.status !== "configured")
return null;
const rel = snapshot.paths.local?.trim();
if (!rel)
return null;
return resolve2(snapshot.realRoot, rel);
}
function sanitizeSlug(raw, fallback = "entry") {
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
return cleaned || fallback;
}
function todayIsoDate(now = new Date) {
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, "0");
const d = String(now.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
function toCompactDate(isoOrEmpty, now = new Date) {
const iso = (isoOrEmpty?.trim() || todayIsoDate(now)).replace(/-/g, "");
if (!/^\d{8}$/.test(iso)) {
throw new Error(`invalid date (want YYYY-MM-DD or YYYYMMDD): ${isoOrEmpty}`);
}
return iso;
}
function normalizeIsoDate(raw, now = new Date) {
const t = raw?.trim();
if (!t)
return todayIsoDate(now);
if (/^\d{4}-\d{2}-\d{2}$/.test(t))
return t;
if (/^\d{8}$/.test(t)) {
return `${t.slice(0, 4)}-${t.slice(4, 6)}-${t.slice(6, 8)}`;
}
throw new Error(`invalid date (want YYYY-MM-DD or YYYYMMDD): ${raw}`);
}
function memoryFileName(opts) {
const date = normalizeIsoDate(opts.date, opts.now);
return `${date}-${sanitizeSlug(opts.slug)}.md`;
}
function draftFileName(opts) {
const compact = toCompactDate(opts.date, opts.now);
const slug = sanitizeSlug(opts.slug);
const base = slug.toUpperCase().startsWith("DRAFT-") ? slug : `DRAFT-${compact}-${slug}`;
return base.toLowerCase().endsWith(".md") ? base : `${base}.md`;
}
function suggestionFileName(opts) {
const date = normalizeIsoDate(opts.date, opts.now);
return `${date}-${sanitizeSlug(opts.slug)}.md`;
}
function refFileName(slug) {
const base = sanitizeSlug(slug, "note");
return base.toLowerCase().endsWith(".md") ? base : `${base}.md`;
}
function resolveWritableLocalPath(localRoot, ...segments) {
if (segments.some((s) => s.includes("\x00"))) {
throw new Error("path segment contains NUL");
}
const absolutePath = resolve2(localRoot, ...segments);
const rel = relative(localRoot, absolutePath);
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
throw new Error(`path escapes local root: ${absolutePath}`);
}
const top = rel.split(/[/\\]/)[0];
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`writes only allowed under ${KB_LOCAL_WRITABLE_ROOTS.join("|")}/ (got ${top}/)`);
}
return {
absolutePath,
relativePath: rel.split(sep).join("/"),
writableRoot: top
};
}
function resolveExistingWritableFile(localRoot, pathArg) {
const trimmed = pathArg.trim();
if (!trimmed)
throw new Error("path is empty");
const candidate = resolve2(trimmed);
let absolutePath;
const localResolved = resolve2(localRoot);
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
if (underLocal) {
absolutePath = candidate;
} else {
absolutePath = resolve2(localRoot, trimmed);
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
if (!realRel || realRel.startsWith("..") || realRel.split(/[/\\]/).includes("..")) {
throw new Error(`path escapes local root after realpath: ${realFile}`);
}
const top = realRel.split(/[/\\]/)[0];
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
absolutePath: realFile,
relativePath: realRel.split(sep).join("/"),
writableRoot: top
};
}
} catch (error) {
if (error instanceof Error && /escapes|writable|not a file/.test(error.message)) {
throw error;
}
}
return checked;
}
function memoryDir(bucket) {
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
}
function suggestionDir(kind) {
return join4("suggestions", kind);
}
function draftDir(domain) {
const d = domain?.trim();
if (!d)
return "drafts";
return join4("drafts", sanitizeSlug(d, "misc"));
}
function refDir(area, device) {
if (!REF_AREAS.includes(area)) {
throw new Error(`invalid refs area: ${area}`);
}
if (area === "devices") {
const host = sanitizeSlug(device?.trim() || "", "");
if (!host) {
throw new Error("refs/devices requires device (host_name)");
}
return join4("refs", "devices", host);
}
return join4("refs", area);
}
// src/netx/kb-context-skill.ts
var SKILL_NAME = "kb-context";
var IDENTITY_FILE_MAX = 8000;
var IDENTITY_TOTAL_MAX = 24000;
function readKbIdentity(snapshot) {
const localRoot = resolveKbLocalRoot(snapshot);
if (!localRoot)
return [];
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
const out = [];
let budget = IDENTITY_TOTAL_MAX;
for (const name of names) {
if (budget <= 0)
break;
const full = join5(dir, name);
let text;
try {
if (!statSync4(full).isFile())
continue;
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
if (!text)
continue;
const cap = Math.min(IDENTITY_FILE_MAX, budget);
if (text.length > cap)
text = `${text.slice(0, cap)}
…(truncated — read \`${full}\` for the rest)`;
budget -= text.length;
out.push({ name, text });
}
return out;
}
function skillBody(snapshot) {
if (snapshot.status === "configured") {
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
const localRoot = resolveKbLocalRoot(snapshot);
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
const identity = readKbIdentity(snapshot);
const identityBlock = identity.length > 0 ? [
"",
"### Site identity (`identity/`, authoritative — follow over generic advice)",
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
] : [];
const localGuide = localRoot ? [
"",
"### Dual plane (mandatory)",
"",
"- **HQ pack = read-only** (formal RCA, theory, packet, `_common`, `_skills`, shared cmdLib).",
`- **Writable evolution core**: \`${localRoot}\` → \`refs/\` | \`memories/\` | \`drafts/\` | \`suggestions/\`.`,
"- **All KB writes via host tools** (workspace Write/bash cannot reach KB):",
" `netx__kbWriteRef` / `netx__kbWriteMemory` / `netx__kbWriteDraft` / `netx__kbWriteSuggestion` /",
" `netx__kbUpdateLocal` / `netx__kbDeleteLocal` / `netx__kbListLocal`.",
"",
"### refs — site product knowledge (evolve every task)",
"- On any NE: **read** `refs/devices/<host_name>/` first; if missing, prove with netx then `kbWriteRef` to create/update.",
"- `area=devices` requires `device=<host_name>` (never UUID); primary file slug=`PROFILE`.",
"- Also: `inventory` (ledger), `topology`, `business`, `commands` (site-only), `handbooks`.",
"- Device facts → refs; episodic experience → memories (do not dump ledgers into diaries).",
"",
"### memories — write immediately when valuable",
"- `bucket=note` | `rca_review` | `ai_trace` — short beats lost.",
"- Open cases → `kbWriteDraft`. HQ pack gaps → `kbWriteSuggestion`."
] : [
"",
"No local write tools until `paths.local` is present in MANIFEST."
];
return {
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
content: [
"## Knowledge base (configured)",
"",
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
"Do not invent another operator or country.",
"",
`| Field | Value |`,
`| --- | --- |`,
`| kbStatus | configured |`,
`| kbRoot | \`${snapshot.realRoot}\` |`,
localRow,
`| kbOperator | ${snapshot.operatorName} |`,
`| kbCountry | ${snapshot.country} |`,
`| kbVersion | ${snapshot.version} |`,
`| kbContent (on) | ${flags} |`,
"",
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
"",
renderKbPathTable(snapshot),
...identityBlock,
...localGuide,
"",
"### Reading the HQ pack",
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
"",
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
"",
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
"keep netx-ops for live netx evidence only."
].join(`
`)
};
}
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
return {
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
content: [
"## Knowledge base (unavailable)",
"",
`kbStatus=${snapshot.status}. ${reason}`,
"",
"**Do not invent an operator, country, or KB paths.**",
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
].join(`
`)
};
}
function registerKbContextSkill(ctx, snapshot) {
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const body = skillBody(snapshot);
return skillsApi.register({
name: SKILL_NAME,
description: body.description,
content: body.content,
provider: "netxops-kb",
source: "custom"
});
}
// src/netx/kb-local-tools.ts
import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
const root = resolveKbLocalRoot(snapshot);
if (!root) {
throw new Error("KB local root unavailable (configure kbRoot + MANIFEST paths.local)");
}
return root;
}
function ensureDraftBody(body) {
const trimmed = body.replace(/^\uFEFF/, "");
if (/^---\r?\n[\s\S]*?\r?\nstatus:\s*draft\b/m.test(trimmed) || /^---\r?\n[\s\S]*?\nstatus:\s*["']?draft["']?\s*$/m.test(trimmed)) {
return trimmed.endsWith(`
`) ? trimmed : `${trimmed}
`;
}
if (trimmed.startsWith("---")) {
const end = trimmed.indexOf(`
---`, 3);
if (end !== -1) {
const fm = trimmed.slice(0, end + 4);
const rest = trimmed.slice(end + 4);
if (/\nstatus:\s*/.test(fm))
return trimmed.endsWith(`
`) ? trimmed : `${trimmed}
`;
const injected = fm.replace(/^---\r?\n/, `---
status: draft
`);
const out = `${injected}${rest}`;
return out.endsWith(`
`) ? out : `${out}
`;
}
}
return `---
status: draft
---
${trimmed.endsWith(`
`) ? trimmed : `${trimmed}
`}`;
}
function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync4(dir);
} catch {
return;
}
for (const name of entries) {
if (name === "." || name === "..")
continue;
const full = join6(dir, name);
let st;
try {
st = statSync5(full);
} catch {
continue;
}
if (st.isDirectory()) {
walkFiles(full, baseLocal, out);
} else if (st.isFile() && name.toLowerCase().endsWith(".md")) {
const rel = full.slice(baseLocal.length).replace(/^[/\\]/, "").split(/[/\\]/).join("/");
out.push({
absolutePath: full,
relativePath: rel,
mtimeMs: st.mtimeMs,
size: st.size
});
}
}
}
function listLocalFiles(snapshot, args = {}) {
const localRoot = requireLocalRoot(snapshot);
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
const entries = [];
for (const r of roots) {
walkFiles(join6(localRoot, r), localRoot, entries);
}
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
return {
ok: true,
localRoot,
entries: entries.slice(0, limit)
};
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
var READ_DEFAULT_LINES = 400;
var READ_MAX_LINES = 2000;
var READ_MAX_CHARS = 80000;
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
var SEARCH_HITS_PER_FILE = 4;
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
function requireRoot(snapshot) {
if (snapshot.status !== "configured" || !snapshot.realRoot) {
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
}
return snapshot.realRoot;
}
function toPosix(rel) {
return rel.split(sep2).join("/");
}
function isInside(root, target) {
const rel = relative2(root, target);
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
}
function resolveKbReadPath(snapshot, pathArg) {
const root = requireRoot(snapshot);
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
if (trimmed.includes("\x00"))
throw new Error("path contains NUL");
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
if (!isInside(root, candidate))
throw new Error(`path escapes kbRoot: ${candidate}`);
let real;
try {
real = realpathSync2(candidate);
} catch {
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
}
const realRoot = realpathSync2(root);
if (!isInside(realRoot, real))
throw new Error(`path escapes kbRoot after realpath: ${real}`);
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
return { ok: true, kind: "directory", ...target, entries };
}
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`);
}
const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(`
`);
const start = Math.max(1, Math.floor(args.offset ?? 1));
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
let slice = lines.slice(start - 1, start - 1 + limit);
let text = slice.join(`
`);
let truncated = start - 1 + slice.length < lines.length;
if (text.length > READ_MAX_CHARS) {
let used = 0;
let keep = 0;
for (const line of slice) {
if (used + line.length + 1 > READ_MAX_CHARS)
break;
used += line.length + 1;
keep += 1;
}
slice = slice.slice(0, Math.max(keep, 1));
text = slice.join(`
`).slice(0, READ_MAX_CHARS);
truncated = true;
}
return {
ok: true,
kind: "file",
...target,
totalLines: lines.length,
startLine: start,
endLine: start - 1 + slice.length,
truncated,
text
};
}
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
function searchCache() {
const root = globalThis;
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
return root[CACHE_KEY];
}
function cachedText(path, mtimeMs, size) {
const cache = searchCache();
const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
cache.files.clear();
cache.chars = 0;
}
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
cache.files.set(path, entry);
cache.chars += text.length;
return entry;
}
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name))
continue;
const full = join7(dir, entry.name);
if (entry.isDirectory()) {
walkTextFiles(full, out);
continue;
}
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
}
}
function searchKb(snapshot, args) {
const root = requireRoot(snapshot);
const query = args.query.trim();
if (!query)
throw new Error("query is empty");
const scopeArg = args.scope?.trim() || "all";
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
let matchers;
if (args.regex) {
let re;
try {
re = new RegExp(query, "i");
} catch (error) {
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
}
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
} else {
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
}
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
else
walkTextFiles(dir, batch);
for (const file of batch) {
if (seen.has(file.path))
continue;
seen.add(file.path);
files.push(file);
}
}
const hits = [];
for (const file of files) {
let cached;
try {
cached = cachedText(file.path, file.mtimeMs, file.size);
} catch {
continue;
}
const rel = toPosix(relative2(root, file.path));
const matched = new Set;
let score = 0;
for (const m of matchers) {
if (m.test(rel.toLowerCase(), rel)) {
matched.add(m.term);
score += 3;
}
}
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
if (inBody.length === 0 && matched.size === 0)
continue;
const lines = [];
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
let lineHits = 0;
for (let i = 0;i < rows.length; i += 1) {
const row = rows[i];
const lower = row.toLowerCase();
let lineHit = false;
for (const m of inBody) {
if (m.test(lower, row)) {
matched.add(m.term);
lineHit = true;
}
}
if (!lineHit)
continue;
lineHits += 1;
if (lines.length < SEARCH_HITS_PER_FILE) {
const trimmed = row.trim();
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
}
}
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
hits.push({
relativePath: rel,
absolutePath: file.path,
score: Math.round(score * 10) / 10,
matchedTerms: [...matched],
lines
});
}
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
}
// src/netx/kb-runtime.ts
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
function store3() {
const root = globalThis;
let current = root[STORE_KEY3];
if (current === undefined) {
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
root[STORE_KEY3] = current;
}
return current;
}
function getKbContext() {
const snap = store3().snapshot;
return {
...snap,
content: { ...snap.content },
paths: { ...snap.paths }
};
}
function watchKbContext(listener) {
const state = store3();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}
// src/netx/json-safe.ts
function toLosslessJson(value) {
try {
const text = JSON.stringify(value);
if (text === undefined) {
return { ok: false, error: "tool_result_not_json", detail: "undefined_root" };
}
const parsed = JSON.parse(text);
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
return parsed;
}
return { ok: true, data: { value: parsed } };
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
return { ok: false, error: "tool_result_not_json", detail: detail.slice(0, 400) };
}
}
// src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
function store4() {
const root = globalThis;
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
}
function getToolMountId(ctx, source) {
const state = store4();
let id = state.contexts.get(ctx);
if (id === undefined) {
id = ++state.sequence;
state.contexts.set(ctx, id);
}
return `${source}:${id}`;
}
function getToolMountStatuses() {
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
}
function getToolMountStatus() {
const rows = getToolMountStatuses();
return rows.find((row) => row.plane === "host") ?? rows.at(-1);
}
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
function publishToolMountStatus(next) {
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
persist();
}
function removeToolMountStatus(id) {
store4().mounts.delete(id);
persist();
}
// src/netx/kb-local-tools.ts
var str = (description) => ({ type: "string", ...description ? { description } : {} });
var bool = (description) => ({ type: "boolean", ...description ? { description } : {} });
var num = (description) => ({ type: "number", ...description ? { description } : {} });
var reqStr = (description) => ({
type: "string",
required: true,
...description ? { description } : {}
});
function renderJson(_args, value) {
return [{ type: "text", text: JSON.stringify(value, null, 0) }];
}
var jsonOut = {
schema: { type: "object", additionalProperties: true },
render: renderJson
};
function liveSnapshot() {
return getKbContext();
}
function tool(name, description, parameters, execute) {
return defineTool({
name,
description,
parameters,
output: jsonOut,
timeoutMs: 30000,
isConcurrencySafe: () => false,
async execute(args) {
try {
return toLosslessJson(await execute(args));
} catch (error) {
throw new Error(error instanceof Error ? error.message : String(error));
}
}
});
}
function writeTools(localRoot) {
return [
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
bucket: {
type: "string",
required: true,
enum: ["note", "rca_review", "ai_trace"],
description: "Memory subdirectory"
},
slug: reqStr("Short filename stem (no path separators)"),
body: reqStr("Full markdown body"),
date: str("Optional YYYY-MM-DD (default today)"),
overwrite: bool("Replace if the target file already exists")
}, (args) => createMemory(liveSnapshot(), {
bucket: args.bucket,
slug: String(args.slug ?? ""),
body: String(args.body ?? ""),
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbWriteDraft", `Create (or overwrite) a DRAFT case under ${localRoot}/drafts/. ` + "Filename is forced to DRAFT-YYYYMMDD-…; body gets status: draft frontmatter if missing. " + "Set overwrite=true to replace. Not for formal RCA.", {
slug: reqStr("Case stem (DRAFT- prefix added if missing)"),
body: reqStr("Markdown body (RCA-ish draft)"),
domain: str("Optional fault-domain subfolder under drafts/"),
date: str("Optional event date YYYY-MM-DD or YYYYMMDD"),
overwrite: bool("Replace if the target file already exists")
}, (args) => createDraft(liveSnapshot(), {
slug: String(args.slug ?? ""),
body: String(args.body ?? ""),
domain: args.domain != null ? String(args.domain) : undefined,
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbWriteSuggestion", `Create (or overwrite) a suggestion under ${localRoot}/suggestions/{theory|improvement}/. ` + "Use for HQ pack/theory corrections or tool/process improvements.", {
kind: {
type: "string",
required: true,
enum: ["theory", "improvement"],
description: "theory = knowledge fix; improvement = tools/skills/process"
},
slug: reqStr("Short filename stem"),
body: reqStr("Full markdown body"),
date: str("Optional YYYY-MM-DD (default today)"),
overwrite: bool("Replace if the target file already exists")
}, (args) => createSuggestion(liveSnapshot(), {
kind: args.kind,
slug: String(args.slug ?? ""),
body: String(args.body ?? ""),
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbWriteRef", `Create (or overwrite) site product-knowledge under ${localRoot}/refs/. ` + "area=inventory|devices|topology|business|commands|handbooks. " + "For devices: require device=host_name; default slug PROFILE for the ops profile. " + "Use for NE ledger, per-device profiles, topology/business notes, site command books — not diaries.", {
area: {
type: "string",
required: true,
enum: ["inventory", "devices", "topology", "business", "commands", "handbooks"],
description: "refs/ subtree"
},
slug: reqStr("Filename stem (e.g. PROFILE, neighbors, ledger)"),
body: reqStr("Full markdown body"),
device: str("Required when area=devices — host_name (never UUID)"),
overwrite: bool("Replace if the target file already exists")
}, (args) => createRef(liveSnapshot(), {
area: args.area,
slug: String(args.slug ?? ""),
body: String(args.body ?? ""),
device: args.device != null ? String(args.device) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbUpdateLocal", `Replace the body of an existing file under memories|drafts|suggestions|refs ` + `(absolute path or path relative to ${localRoot}). ` + "Drafts keep status: draft.", {
path: reqStr("Absolute path or path relative to KB local root"),
body: reqStr("New full markdown body")
}, (args) => updateLocalFile(liveSnapshot(), {
path: String(args.path ?? ""),
body: String(args.body ?? "")
})),
tool("netx__kbDeleteLocal", `Delete one existing markdown under memories|drafts|suggestions|refs ` + `(absolute or relative to ${localRoot}).`, {
path: reqStr("Absolute path or path relative to KB local root")
}, (args) => deleteLocalFile(liveSnapshot(), {
path: String(args.path ?? "")
})),
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
root: {
type: "string",
enum: ["memories", "drafts", "suggestions", "refs", "all"],
description: "Subtree to list (default all writable)"
},
limit: num("Max entries 1–200 (default 50)")
}, (args) => listLocalFiles(liveSnapshot(), {
root: args.root ?? "all",
limit: typeof args.limit === "number" ? args.limit : undefined
}))
];
}
function readTools(snapshot) {
return [
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
regex: bool("Treat query as one case-insensitive JS regex"),
limit: num("Max files 1–100 (default 20)")
}, (args) => searchKb(liveSnapshot(), {
query: String(args.query ?? ""),
scope: args.scope != null ? String(args.scope) : undefined,
regex: args.regex === true,
limit: typeof args.limit === "number" ? args.limit : undefined
})),
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
offset: num("First line (1-based, default 1)"),
limit: num("Max lines 1–2000 (default 400)")
}, (args) => readKbFile(liveSnapshot(), {
path: String(args.path ?? ""),
offset: typeof args.offset === "number" ? args.offset : undefined,
limit: typeof args.limit === "number" ? args.limit : undefined
}))
];
}
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
if (snapshot.status !== "configured")
return () => {};
const localRoot = resolveKbLocalRoot(snapshot);
const toolsApi = ctx.tools;
if (!toolsApi || typeof toolsApi.register !== "function")
return () => {};
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
const disposers = [];
const source = options.source ?? "netxops-kb-local";
const id = getToolMountId(ctx, source);
const status = {
id,
source,
version: getToolMountStatus()?.version ?? "",
plane: options.plane ?? "preset",
groups: ["kb"],
at: new Date().toISOString()
};
try {
for (const entry of entries)
disposers.push(toolsApi.register(entry));
publishToolMountStatus({ ...status, names: entries.map((entry) => entry.name), count: entries.length });
} catch (error) {
for (const dispose of disposers.reverse())
dispose();
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
throw error;
}
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
return () => {
for (const dispose of disposers)
dispose();
removeToolMountStatus(id);
};
}
// src/netx/kb-pack-skills.ts
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
import { join as join9 } from "node:path";
function kbPackSkillsEnabled(snapshot) {
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
}
function resolveKbSkillsRoot(snapshot) {
const rel = snapshot.paths.skills?.trim() || "_skills";
return join9(snapshot.realRoot, rel);
}
function kbPackSkillFooter(snapshot) {
return [
"",
"",
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
"",
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
"",
renderKbPathTable(snapshot),
"",
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
""
].join(`
`);
}
function resolveKbLocalSkillsRoot(snapshot) {
const rel = snapshot.paths.localSkills?.trim();
if (!rel)
return null;
return join9(snapshot.realRoot, rel);
}
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
let entries;
try {
entries = await readdir2(root);
} catch {
if (options.logMissing) {
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
}
return [];
}
if (entries.length === 0)
return [];
const disposers = [];
try {
for (const entry of entries) {
if (options.signal?.aborted)
break;
const full = join9(root, entry);
let isDir = false;
try {
isDir = (await stat2(full)).isDirectory();
} catch {
continue;
}
if (!isDir)
continue;
let skill;
try {
skill = await loadSkillBundle(full);
} catch (error) {
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
continue;
}
if (!skill) {
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
continue;
}
if (options.signal?.aborted)
break;
if (options.seen.has(skill.name)) {
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
continue;
}
try {
disposers.push(skillsApi.register({
name: skill.name,
description: skill.description,
content: `${skill.content.trimEnd()}${options.footer}`,
path: skill.path,
resourceBase: { kind: "directory", path: skill.directory },
provider,
source: "custom"
}));
options.seen.add(skill.name);
} catch (error) {
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
}
}
} catch (error) {
for (const dispose of disposers.splice(0))
dispose();
throw error;
}
return disposers;
}
async function registerKbPackSkills(ctx, snapshot, options) {
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
return () => {};
}
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const provider = options.providerLabel ?? "netxops-kb-pack";
const disposers = [];
const skillsRoot = resolveKbSkillsRoot(snapshot);
const localRoot = resolveKbLocalSkillsRoot(snapshot);
const footer = kbPackSkillFooter(snapshot);
const seen = new Set;
try {
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
logMissing: true,
label: "pack",
signal: options.signal,
footer,
seen
}));
if (localRoot) {
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
logMissing: false,
label: "local",
signal: options.signal,
footer,
seen
}));
}
} catch (error) {
for (const dispose of disposers.splice(0))
dispose();
throw error;
}
if (options.signal?.aborted) {
for (const dispose of disposers.splice(0))
dispose();
}
if (disposers.length > 0) {
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
}
return () => {
for (const dispose of disposers)
dispose();
};
}
// src/netx/tools.ts
import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
// src/netx/mcp-schema.ts
var ANNOTATIONS = ["description", "title", "default", "examples"];
var SCALAR_TYPES = new Set(["string", "number", "integer", "boolean", "null"]);
var CONSTRAINTS = [
"minimum",
"maximum",
"exclusiveMinimum",
"exclusiveMaximum",
"multipleOf",
"minLength",
"maxLength",
"pattern",
"format",
"minItems",
"maxItems",
"uniqueItems"
];
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function withAnnotations(source, target) {
for (const key of ANNOTATIONS) {
if (Object.hasOwn(source, key))
target[key] = source[key];
}
const notes = CONSTRAINTS.filter((key) => Object.hasOwn(source, key)).map((key) => `${key}=${JSON.stringify(source[key])}`);
if (notes.length > 0) {
const base = typeof target.description === "string" ? target.description : "";
target.description = `${base}${base ? " " : ""}(${notes.join(", ")})`;
}
return target;
}
function variants(node) {
const list = Array.isArray(node.oneOf) ? node.oneOf : Array.isArray(node.anyOf) ? node.anyOf : undefined;
if (list)
return list.filter(isRecord).filter((branch) => branch.type !== "null");
if (Array.isArray(node.type)) {
const { type: _types, ...rest } = node;
return node.type.filter((t) => t !== "null").map((t) => ({ ...rest, type: t }));
}
return;
}
function toValue(node) {
if (!isRecord(node))
return { type: "json" };
const branches = variants(node);
if (branches) {
if (branches.length === 0)
return withAnnotations(node, { type: "json" });
if (branches.length === 1)
return withAnnotations(node, toValue({ ...branches[0] }));
return withAnnotations(node, { oneOf: branches.map(toValue) });
}
const type = node.type;
if (type === "object" || type === undefined && isRecord(node.properties)) {
const out = {
type: "object",
additionalProperties: typeof node.additionalProperties === "boolean" ? node.additionalProperties : true
};
if (isRecord(node.properties))
out.properties = toProperties(node.properties, node.required);
return withAnnotations(node, out);
}
if (type === "array") {
const out = { type: "array" };
if (Object.hasOwn(node, "items"))
out.items = toValue(node.items);
return withAnnotations(node, out);
}
if (typeof type === "string" && SCALAR_TYPES.has(type)) {
const out = { type };
if (Array.isArray(node.enum) && node.enum.length > 0)
out.enum = node.enum;
if (Object.hasOwn(node, "const"))
out.const = node.const;
return withAnnotations(node, out);
}
return withAnnotations(node, { type: "json" });
}
function toProperties(properties, required) {
const requiredSet = new Set(Array.isArray(required) ? required.filter((k) => typeof k === "string") : []);
const out = {};
for (const [key, value] of Object.entries(properties)) {
const converted = toValue(value);
if (requiredSet.has(key))
converted.required = true;
out[key] = converted;
}
return out;
}
function parametersFromInputSchema(schema) {
if (!isRecord(schema) || !isRecord(schema.properties))
return {};
return toProperties(schema.properties, schema.required);
}
// src/netx/tools.ts
function packageVersion() {
try {
const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";
}
}
function renderMcpResult(_args, value) {
if (value && typeof value === "object" && !Array.isArray(value)) {
const row = value;
if (typeof row.text === "string" && row.text.length > 0) {
return [{ type: "text", text: row.text }];
}
if (Array.isArray(row.content)) {
return [{ type: "text", text: mcpContentText(row.content, JSON.stringify(value)) }];
}
}
return [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value ?? null) }];
}
var mcpOut = {
schema: { type: "object", additionalProperties: true },
render: renderMcpResult
};
function defineRemoteTool(tool2, timeoutMs) {
const name = publicToolName(tool2.name);
return defineTool2({
name,
description: tool2.description || name,
parameters: parametersFromInputSchema(tool2.inputSchema),
output: mcpOut,
timeoutMs,
isConcurrencySafe: () => tool2.readOnlyHint === true,
async execute(args, exec) {
const live = getNetxConnection();
const apiUrl = live?.apiUrl ?? "";
const token = live?.token ?? "";
const lang = live?.lang ?? "zh";
const mcpUrl = mcpEndpointUrl(apiUrl);
if (!mcpUrl)
throw new Error("netx mcp: apiUrl empty");
const result = await callMcpTool({
mcpUrl,
token,
lang,
timeoutMs,
signal: exec.signal
}, tool2.name, typeof args === "object" && args !== null ? args : {});
const text = mcpContentText(result.content, "");
if (result.isError === true) {
throw new Error(text || `Tool "${tool2.name}" failed`);
}
return JSON.parse(JSON.stringify({
content: result.content,
...result.structuredContent !== undefined ? { structuredContent: result.structuredContent } : {},
...text ? { text } : {}
}));
}
});
}
function registerNetxTools(ctx, connection, options) {
const id = getToolMountId(ctx, options.source ?? options.plane);
const enabled = resolveEnabledTools(getMcpCatalogTools(), {
toolsEnabled: connection.toolsEnabled,
groups: connection.groups,
disabledTools: connection.disabledTools,
readOnlyOnly: connection.readOnlyOnly,
plane: options.plane,
forceGroups: options.forceGroups,
only: options.only,
presetId: options.presetId
});
const groups = [...new Set(enabled.map((tool2) => tool2.group).filter(Boolean))];
if (enabled.length === 0) {
publishToolMountStatus({
id,
version: packageVersion(),
plane: options.plane,
groups,
names: [],
count: 0,
at: new Date().toISOString(),
source: options.source ?? options.plane
});
return () => {
removeToolMountStatus(id);
};
}
const t = typeof connection.toolCallTimeoutMs === "number" && Number.isFinite(connection.toolCallTimeoutMs) && connection.toolCallTimeoutMs > 0 ? connection.toolCallTimeoutMs : 120000;
const disposers = [];
const names = [];
try {
for (const tool2 of enabled) {
const definition = defineRemoteTool(tool2, t);
disposers.push(ctx.tools.register(definition));
names.push(publicToolName(tool2.name));
}
} catch (error) {
for (const dispose of disposers.splice(0))
dispose();
publishToolMountStatus({
id,
version: packageVersion(),
plane: options.plane,
groups,
names: [],
count: 0,
at: new Date().toISOString(),
error: String(error),
source: options.source ?? options.plane
});
throw error;
}
publishToolMountStatus({
id,
version: packageVersion(),
plane: options.plane,
groups,
names,
count: names.length,
at: new Date().toISOString(),
source: options.source ?? options.plane
});
return () => {
for (const dispose of disposers)
dispose();
removeToolMountStatus(id);
};
}
// src/netx/group-tools-plugin.ts
function packageVersion2() {
try {
const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";
}
}
function disabledByGroupFor(groupIds, connection) {
const catalog = getMcpCatalogTools();
const out = {};
for (const groupId of groupIds) {
const names = disabledPublicNames(catalog, [groupId], connection.disabledTools, connection.readOnlyOnly);
if (names.length > 0)
out[groupId] = names;
}
return out;
}
function applyGroupToolsPlugin(ctx, options) {
let unregisterTools;
let unregisterKbLocal;
let unregisterSkills;
let skillGeneration = 0;
const presetId = () => {
if (options.presetId)
return options.presetId;
const registry = ctx.get("agentPresets");
return registry?.composedPreset?.(ctx) ?? "netxops";
};
const mountId = getToolMountId(ctx, options.name);
const resolveGroups = () => {
const connection = requireNetxConnection();
return groupsForPreset(connection, presetId(), options.only);
};
const remountTools = () => {
unregisterTools?.();
unregisterTools = undefined;
const connection = requireNetxConnection();
const enabled = resolveGroups();
try {
unregisterTools = registerNetxTools(ctx, connection, {
plane: "preset",
forceGroups: enabled,
source: options.name,
presetId: presetId()
});
ctx.logger.info("%s: groups=[%s] → %s tokenConfigured=%s live=%s catalog=%s", options.name, enabled.join(",") || "(none)", connection.apiUrl, connection.token.trim().length > 0, getNetxConnection() !== undefined, getMcpCatalogTools().length);
} catch (error) {
publishToolMountStatus({
id: mountId,
version: packageVersion2(),
plane: "preset",
groups: [...enabled],
names: [],
count: 0,
at: new Date().toISOString(),
error: String(error),
source: options.name
});
ctx.logger.error("%s: registerNetxTools failed: %s", options.name, error);
}
};
const remountKbLocal = () => {
unregisterKbLocal?.();
unregisterKbLocal = undefined;
const connection = requireNetxConnection();
if (options.only || !kbEnabledForPlane(connection, "preset", presetId()))
return;
unregisterKbLocal = registerKbLocalTools(ctx, getKbContext(), { plane: "preset", source: `${options.name}:kb-local` });
};
const stopToolWatch = watchNetxConnection(() => {
remountTools();
remountKbLocal();
});
const stopCatalogWatch = watchMcpCatalog(() => {
remountTools();
});
const stopKbToolWatch = watchKbContext(() => {
remountKbLocal();
});
remountTools();
remountKbLocal();
let pollTicks = 0;
const pollTimer = setInterval(() => {
pollTicks += 1;
if (getNetxConnection() !== undefined) {
remountTools();
remountKbLocal();
clearInterval(pollTimer);
return;
}
if (pollTicks >= 40)
clearInterval(pollTimer);
}, 500);
ctx.inject(["skills"], (skillsCtx) => {
let skillAbort = new AbortController;
let packAbort = new AbortController;
let unregisterKbSkill;
let unregisterKbPack;
let packGeneration = 0;
const remountSkills = () => {
const gen = ++skillGeneration;
skillAbort.abort();
skillAbort = new AbortController;
unregisterSkills?.();
unregisterSkills = undefined;
const connection = requireNetxConnection();
const enabled = resolveGroups();
registerGroupSkills(skillsCtx, enabled, options.name, {
signal: skillAbort.signal,
disabledByGroup: disabledByGroupFor(enabled, connection),
lang: connection.lang
}).then((dispose) => {
if (gen !== skillGeneration) {
dispose();
return;
}
unregisterSkills = dispose;
}).catch((error) => {
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
});
};
const remountKbSkill = () => {
unregisterKbSkill?.();
unregisterKbSkill = undefined;
const connection = requireNetxConnection();
if (options.only || !kbEnabledForPlane(connection, "preset", presetId()))
return;
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
};
const remountKbPack = () => {
const gen = ++packGeneration;
packAbort.abort();
packAbort = new AbortController;
unregisterKbPack?.();
unregisterKbPack = undefined;
const connection = requireNetxConnection();
const enabled = !options.only && kbEnabledForPlane(connection, "preset", presetId());
registerKbPackSkills(skillsCtx, getKbContext(), {
enabled,
providerLabel: `${options.name}-kb-pack`,
signal: packAbort.signal
}).then((dispose) => {
if (gen !== packGeneration) {
dispose();
return;
}
unregisterKbPack = dispose;
}).catch((error) => {
skillsCtx.logger.warn("%s: kb pack skill register failed: %s", options.name, error);
});
};
const stopSkillWatch = watchNetxConnection(() => {
remountSkills();
remountKbSkill();
remountKbPack();
});
const stopSkillCatalogWatch = watchMcpCatalog(() => {
remountSkills();
});
const stopKbWatch = watchKbContext(() => {
remountKbSkill();
remountKbPack();
});
remountSkills();
remountKbSkill();
remountKbPack();
skillsCtx.effect(() => () => {
skillGeneration += 1;
packGeneration += 1;
skillAbort.abort();
packAbort.abort();
stopSkillWatch();
stopSkillCatalogWatch();
stopKbWatch();
unregisterSkills?.();
unregisterSkills = undefined;
unregisterKbSkill?.();
unregisterKbSkill = undefined;
unregisterKbPack?.();
unregisterKbPack = undefined;
}, `${options.name}: dispose skills`);
});
ctx.effect(() => () => {
clearInterval(pollTimer);
stopToolWatch();
stopCatalogWatch();
stopKbToolWatch();
unregisterTools?.();
unregisterTools = undefined;
unregisterKbLocal?.();
unregisterKbLocal = undefined;
removeToolMountStatus(mountId);
}, `${options.name}: dispose tools`);
}
// src/agent-tools-nms.ts
var name = "netxops-tools-nms";
var inject = ["tools"];
function apply(ctx) {
applyGroupToolsPlugin(ctx, { name, mode: "forced", only: ["ops"] });
}
export {
name,
inject,
apply
};