feat(skills): pull group skills from netx /mcp resources, drop bundled copy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 23:14:39 +08:00
parent 277a61894d
commit fa82f530e9
31 changed files with 2074 additions and 2027 deletions

View file

@ -1,7 +1,7 @@
// src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname6, join as join10 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";
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");
@ -123,7 +123,6 @@ function disabledPublicNames(catalog, enabledGroups, disabledTools, readOnlyOnly
// 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) {
@ -266,6 +265,50 @@ function mcpContentText(content, fallback = "") {
`) || fallback;
}
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
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
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 skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
}
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
var EMPTY = Object.freeze({
@ -297,34 +340,51 @@ function watchMcpCatalog(listener) {
}
// src/netx/group-skills.ts
import { existsSync } from "node:fs";
import { readdir, readFile, stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
function opsSkillsRoot() {
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
if (envRoot && existsSync(envRoot))
return envRoot;
const here = dirname(fileURLToPath(import.meta.url));
const checkout = [join(here, "..", ".git"), join(here, "..", "..", ".git")].some(existsSync);
const siblingCandidates = [
join(here, "..", "..", "..", "netx", "skills"),
join(here, "..", "..", "netx", "skills")
];
for (const candidate of checkout ? siblingCandidates : []) {
if (existsSync(candidate))
return candidate;
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;
}
const packaged = [
join(here, "..", "presets", "netxops", "skills"),
join(here, "..", "..", "presets", "netxops", "skills")
];
for (const candidate of packaged) {
if (existsSync(candidate))
return candidate;
}
return packaged[0];
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, `
@ -371,7 +431,7 @@ function stripQuotes(value) {
return value;
}
async function loadSkillBundle(dir) {
const skillPath = join(dir, "SKILL.md");
const skillPath = join2(dir, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
@ -394,7 +454,7 @@ async function loadSkillBundle(dir) {
};
}
async function loadGroupSkills(skillsRoot, groupId) {
const groupDir = join(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
const groupDir = join2(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
let entries;
try {
entries = await readdir(groupDir);
@ -404,7 +464,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
const skills = [];
const skipped = [];
for (const entry of entries) {
const full = join(groupDir, entry);
const full = join2(groupDir, entry);
let isDir = false;
try {
isDir = (await stat(full)).isDirectory();
@ -413,7 +473,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
}
if (!isDir)
continue;
const skillPath = join(full, "SKILL.md");
const skillPath = join2(full, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
@ -460,6 +520,15 @@ The following tools are disabled: ${list}. Do not call them.
以下工具已禁用:${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 skillsCacheDir(connection.apiUrl);
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
const signal = options.signal;
@ -467,10 +536,16 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const root = opsSkillsRoot();
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 () => {};
}
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -482,7 +557,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
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, join(root, SKILL_DIR_BY_GROUP[groupId]));
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
for (const skill of skills) {
@ -519,11 +594,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
// src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
import { join as join4 } from "node:path";
import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = {
hasRegions: false,
hasTheory: false,
@ -549,7 +624,7 @@ function resolveKbPaths(snapshot) {
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) };
return { key, relative: rel, absolute, exists: existsSync(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -565,8 +640,8 @@ function renderKbPathTable(snapshot) {
}
// src/netx/kb-local-path.ts
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
"drafts",
@ -674,7 +749,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync3(checked.absolutePath)) {
if (existsSync2(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -702,16 +777,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
return checked;
}
function memoryDir(bucket) {
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
return join4("memories", MEMORY_BUCKET_DIR[bucket]);
}
function suggestionDir(kind) {
return join3("suggestions", kind);
return join4("suggestions", kind);
}
function draftDir(domain) {
const d = domain?.trim();
if (!d)
return "drafts";
return join3("drafts", sanitizeSlug(d, "misc"));
return join4("drafts", sanitizeSlug(d, "misc"));
}
function refDir(area, device) {
if (!REF_AREAS.includes(area)) {
@ -722,9 +797,9 @@ function refDir(area, device) {
if (!host) {
throw new Error("refs/devices requires device (host_name)");
}
return join3("refs", "devices", host);
return join4("refs", "devices", host);
}
return join3("refs", area);
return join4("refs", area);
}
// src/netx/kb-context-skill.ts
@ -735,7 +810,7 @@ function readKbIdentity(snapshot) {
const localRoot = resolveKbLocalRoot(snapshot);
if (!localRoot)
return [];
const dir = join4(localRoot, "identity");
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
@ -747,7 +822,7 @@ function readKbIdentity(snapshot) {
for (const name of names) {
if (budget <= 0)
break;
const full = join4(dir, name);
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
@ -875,7 +950,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync4,
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
@ -883,7 +958,7 @@ import {
statSync as statSync4,
writeFileSync
} from "node:fs";
import { dirname as dirname3, join as join5 } from "node:path";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
const root = resolveKbLocalRoot(snapshot);
if (!root) {
@ -929,11 +1004,11 @@ 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) {
if (existsSync3(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
@ -948,11 +1023,11 @@ 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) {
if (existsSync3(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
@ -965,11 +1040,11 @@ 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) {
if (existsSync3(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
@ -984,11 +1059,11 @@ 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) {
if (existsSync3(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
@ -1002,7 +1077,7 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync4(target.absolutePath)) {
if (!existsSync3(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
@ -1019,7 +1094,7 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync4(target.absolutePath)) {
if (!existsSync3(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
@ -1040,7 +1115,7 @@ function walkFiles(dir, baseLocal, out) {
for (const name of entries) {
if (name === "." || name === "..")
continue;
const full = join5(dir, name);
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
@ -1065,7 +1140,7 @@ function listLocalFiles(snapshot, args = {}) {
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
const entries = [];
for (const r of roots) {
walkFiles(join5(localRoot, r), localRoot, entries);
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);
@ -1078,7 +1153,7 @@ function listLocalFiles(snapshot, args = {}) {
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
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;
@ -1129,7 +1204,7 @@ function readKbFile(snapshot, args) {
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join6(target.absolutePath, entry.name)).size;
size = statSync5(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);
@ -1205,7 +1280,7 @@ function walkTextFiles(dir, out) {
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name))
continue;
const full = join6(dir, entry.name);
const full = join7(dir, entry.name);
if (entry.isDirectory()) {
walkTextFiles(full, out);
continue;
@ -1312,18 +1387,18 @@ function searchKb(snapshot, args) {
}
// src/netx/kb-runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
function store2() {
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
function store3() {
const root = globalThis;
let current = root[STORE_KEY2];
let current = root[STORE_KEY3];
if (current === undefined) {
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
root[STORE_KEY2] = current;
root[STORE_KEY3] = current;
}
return current;
}
function getKbContext() {
const snap = store2().snapshot;
const snap = store3().snapshot;
return {
...snap,
content: { ...snap.content },
@ -1331,7 +1406,7 @@ function getKbContext() {
};
}
function watchKbContext(listener) {
const state = store2();
const state = store3();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
@ -1358,15 +1433,15 @@ function toLosslessJson(value) {
// src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
import { homedir } from "node:os";
import { dirname as dirname4, join as join7 } from "node:path";
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 store3() {
function store4() {
const root = globalThis;
return root[KEY] ??= { mounts: new Map, contexts: new WeakMap, sequence: 0 };
}
function getToolMountId(ctx, source) {
const state = store3();
const state = store4();
let id = state.contexts.get(ctx);
if (id === undefined) {
id = ++state.sequence;
@ -1375,7 +1450,7 @@ function getToolMountId(ctx, source) {
return `${source}:${id}`;
}
function getToolMountStatuses() {
return [...store3().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
return [...store4().mounts.values()].map((row) => ({ ...row, groups: [...row.groups], names: [...row.names] }));
}
function getToolMountStatus() {
const rows = getToolMountStatuses();
@ -1383,18 +1458,18 @@ function getToolMountStatus() {
}
function persist() {
try {
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
function publishToolMountStatus(next) {
store3().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
store4().mounts.set(next.id ?? next.source ?? next.plane, { ...next, groups: [...next.groups], names: [...next.names] });
persist();
}
function removeToolMountStatus(id) {
store3().mounts.delete(id);
store4().mounts.delete(id);
persist();
}
@ -1591,13 +1666,13 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
// src/netx/kb-pack-skills.ts
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
import { join as join8 } from "node:path";
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 join8(snapshot.realRoot, rel);
return join9(snapshot.realRoot, rel);
}
function kbPackSkillFooter(snapshot) {
return [
@ -1619,7 +1694,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
const rel = snapshot.paths.localSkills?.trim();
if (!rel)
return null;
return join8(snapshot.realRoot, rel);
return join9(snapshot.realRoot, rel);
}
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
let entries;
@ -1638,7 +1713,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
for (const entry of entries) {
if (options.signal?.aborted)
break;
const full = join8(root, entry);
const full = join9(root, entry);
let isDir = false;
try {
isDir = (await stat2(full)).isDirectory();
@ -1735,73 +1810,10 @@ async function registerKbPackSkills(ctx, snapshot, options) {
};
}
// 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/runtime.ts
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
function store4() {
const root = globalThis;
let current = root[STORE_KEY3];
if (current === undefined) {
current = { connection: undefined, listeners: new Set };
root[STORE_KEY3] = current;
}
return current;
}
function getNetxConnection() {
return store4().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 = store4();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
};
}
// src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs";
import { dirname as dirname5, join as join9 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
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
@ -1903,8 +1915,8 @@ function parametersFromInputSchema(schema) {
// src/netx/tools.ts
function packageVersion() {
try {
const here = dirname5(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";
@ -2035,8 +2047,8 @@ function registerNetxTools(ctx, connection, options) {
// src/netx/group-tools-plugin.ts
function packageVersion2() {
try {
const here = dirname6(fileURLToPath3(import.meta.url));
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";