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

@ -3,8 +3,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
// src/index.ts
import { readFileSync as readFileSync9 } from "node:fs";
import { dirname as dirname8, join as join13 } from "node:path";
import { fileURLToPath as fileURLToPath5 } from "node:url";
import { dirname as dirname8, join as join14 } from "node:path";
import { fileURLToPath as fileURLToPath4 } from "node:url";
import z from "@deepseek-ai/schemastery";
import { credentialRef } from "@deepseek-ai/dsh-credentials";
import * as DshSettings from "@deepseek-ai/dsh-settings";
@ -1328,34 +1328,129 @@ function groupsForPreset(connection, presetId, only) {
}
// src/netx/group-skills.ts
import { existsSync as existsSync3 } from "node:fs";
import { readdir, readFile, stat } from "node:fs/promises";
import { dirname as dirname2, join as join3 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
function opsSkillsRoot() {
const envRoot = process.env.NETX_SKILLS_ROOT?.trim();
if (envRoot && existsSync3(envRoot))
return envRoot;
const here = dirname2(fileURLToPath2(import.meta.url));
const checkout = [join3(here, "..", ".git"), join3(here, "..", "..", ".git")].some(existsSync3);
const siblingCandidates = [
join3(here, "..", "..", "..", "netx", "skills"),
join3(here, "..", "..", "netx", "skills")
];
for (const candidate of checkout ? siblingCandidates : []) {
if (existsSync3(candidate))
return candidate;
import { join as join4 } 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 = [
join3(here, "..", "presets", "netxops", "skills"),
join3(here, "..", "..", "presets", "netxops", "skills")
];
for (const candidate of packaged) {
if (existsSync3(candidate))
return candidate;
}
return packaged[0];
return current;
}
function publishNetxConnection(next) {
const state = store2();
if (JSON.stringify(state.connection) === JSON.stringify(next))
return;
state.connection = next;
for (const listener of [...state.listeners]) {
try {
listener();
} catch (error) {
console.error("netxops: connection subscriber failed", error);
}
}
}
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/skills-cache.ts
import { createHash as createHash2 } from "node:crypto";
import {
existsSync as existsSync3,
mkdirSync as mkdirSync2,
readdirSync,
rmSync,
writeFileSync as writeFileSync3
} from "node:fs";
import { homedir as homedir3 } from "node:os";
import { dirname as dirname2, join as join3 } from "node:path";
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join3(homedir3(), ".dsh");
}
function normalizeSkillsApiUrl(apiUrl) {
const normalized = normalizeApiUrl(apiUrl);
return normalized.ok ? normalized.url : apiUrl.trim().replace(/\/$/, "");
}
function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash2("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join3(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
}
function isSafeSegment(value) {
return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00");
}
function isSafeRelPath(relPath) {
if (!relPath || relPath.startsWith("/") || relPath.includes("\x00"))
return false;
const parts = relPath.replace(/\\/g, "/").split("/");
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
}
function replaceSkillsCache(apiUrl, files) {
const root = skillsCacheDir(apiUrl);
rmSync(root, { recursive: true, force: true });
mkdirSync2(root, { recursive: true });
let written = 0;
for (const file of files) {
const groupDir = SKILL_DIR_BY_GROUP[file.group];
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath))
continue;
const rel = file.relPath.replace(/\\/g, "/");
const dest = join3(root, groupDir, file.skill, ...rel.split("/"));
mkdirSync2(dirname2(dest), { recursive: true });
const text = file.text.replace(/\r\n/g, `
`).replace(/\r/g, `
`);
writeFileSync3(dest, text, "utf8");
written += 1;
}
const normalized = normalizeSkillsApiUrl(apiUrl);
const meta = {
apiUrl: normalized,
mcpUrl: normalized ? `${normalized}/mcp` : "",
listedAt: new Date().toISOString(),
fileCount: written
};
writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)}
`, "utf8");
return root;
}
// src/netx/group-skills.ts
function parseFrontmatter(raw) {
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n/g, `
`).replace(/\r/g, `
@ -1402,7 +1497,7 @@ function stripQuotes(value) {
return value;
}
async function loadSkillBundle(dir) {
const skillPath = join3(dir, "SKILL.md");
const skillPath = join4(dir, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
@ -1425,7 +1520,7 @@ async function loadSkillBundle(dir) {
};
}
async function loadGroupSkills(skillsRoot, groupId) {
const groupDir = join3(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
const groupDir = join4(skillsRoot, SKILL_DIR_BY_GROUP[groupId]);
let entries;
try {
entries = await readdir(groupDir);
@ -1435,7 +1530,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
const skills = [];
const skipped = [];
for (const entry of entries) {
const full = join3(groupDir, entry);
const full = join4(groupDir, entry);
let isDir = false;
try {
isDir = (await stat(full)).isDirectory();
@ -1444,7 +1539,7 @@ async function loadGroupSkills(skillsRoot, groupId) {
}
if (!isDir)
continue;
const skillPath = join3(full, "SKILL.md");
const skillPath = join4(full, "SKILL.md");
let raw;
try {
raw = await readFile(skillPath, "utf8");
@ -1491,6 +1586,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;
@ -1498,10 +1602,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))
@ -1513,7 +1623,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, join3(root, SKILL_DIR_BY_GROUP[groupId]));
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join4(root, SKILL_DIR_BY_GROUP[groupId]));
}
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang);
for (const skill of skills) {
@ -1549,12 +1659,12 @@ 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 readFileSync4, statSync as statSync4 } from "node:fs";
import { join as join6 } from "node:path";
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync4 } from "node:fs";
import { join as join7 } from "node:path";
// src/netx/kb-manifest.ts
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
import { basename, dirname as dirname3, join as join4, resolve } from "node:path";
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
import { basename, dirname as dirname3, join as join5, resolve } from "node:path";
var EMPTY_CONTENT = {
hasRegions: false,
hasTheory: false,
@ -1607,7 +1717,7 @@ function findManifest(kbRoot, maxDepth = 3) {
if (!rootStat.isDirectory()) {
return { paths: [], error: `kbRoot is not a directory: ${root}` };
}
const direct = join4(root, "MANIFEST.json");
const direct = join5(root, "MANIFEST.json");
if (existsSync4(direct)) {
try {
if (statSync2(direct).isFile())
@ -1618,7 +1728,7 @@ function findManifest(kbRoot, maxDepth = 3) {
const walk = (dir, depth) => {
if (depth > maxDepth)
return;
const candidate = join4(dir, "MANIFEST.json");
const candidate = join5(dir, "MANIFEST.json");
if (existsSync4(candidate)) {
try {
if (statSync2(candidate).isFile())
@ -1629,14 +1739,14 @@ function findManifest(kbRoot, maxDepth = 3) {
return;
let entries;
try {
entries = readdirSync(dir);
entries = readdirSync2(dir);
} catch {
return;
}
for (const name of entries) {
if (name === "node_modules" || name === ".git")
continue;
const full = join4(dir, name);
const full = join5(dir, name);
let st;
try {
st = statSync2(full);
@ -1649,14 +1759,14 @@ function findManifest(kbRoot, maxDepth = 3) {
};
let topEntries;
try {
topEntries = readdirSync(root);
topEntries = readdirSync2(root);
} catch {
return { paths: [], error: `cannot read kbRoot: ${root}` };
}
for (const name of topEntries) {
if (name === "node_modules" || name === ".git")
continue;
const full = join4(root, name);
const full = join5(root, name);
let st;
try {
st = statSync2(full);
@ -1817,7 +1927,7 @@ function renderKbPathTable(snapshot) {
// src/netx/kb-local-path.ts
import { existsSync as existsSync5, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join5, relative, resolve as resolve2, sep } from "node:path";
import { join as join6, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
"drafts",
@ -1953,16 +2063,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
return checked;
}
function memoryDir(bucket) {
return join5("memories", MEMORY_BUCKET_DIR[bucket]);
return join6("memories", MEMORY_BUCKET_DIR[bucket]);
}
function suggestionDir(kind) {
return join5("suggestions", kind);
return join6("suggestions", kind);
}
function draftDir(domain) {
const d = domain?.trim();
if (!d)
return "drafts";
return join5("drafts", sanitizeSlug(d, "misc"));
return join6("drafts", sanitizeSlug(d, "misc"));
}
function refDir(area, device) {
if (!REF_AREAS.includes(area)) {
@ -1973,9 +2083,9 @@ function refDir(area, device) {
if (!host) {
throw new Error("refs/devices requires device (host_name)");
}
return join5("refs", "devices", host);
return join6("refs", "devices", host);
}
return join5("refs", area);
return join6("refs", area);
}
// src/netx/kb-context-skill.ts
@ -1986,10 +2096,10 @@ function readKbIdentity(snapshot) {
const localRoot = resolveKbLocalRoot(snapshot);
if (!localRoot)
return [];
const dir = join6(localRoot, "identity");
const dir = join7(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -1998,7 +2108,7 @@ function readKbIdentity(snapshot) {
for (const name of names) {
if (budget <= 0)
break;
const full = join6(dir, name);
const full = join7(dir, name);
let text;
try {
if (!statSync4(full).isFile())
@ -2127,14 +2237,14 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync6,
mkdirSync as mkdirSync2,
readdirSync as readdirSync3,
mkdirSync as mkdirSync3,
readdirSync as readdirSync4,
readFileSync as readFileSync5,
rmSync,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync3
writeFileSync as writeFileSync4
} from "node:fs";
import { dirname as dirname4, join as join7 } from "node:path";
import { dirname as dirname4, join as join8 } from "node:path";
function requireLocalRoot(snapshot) {
const root = resolveKbLocalRoot(snapshot);
if (!root) {
@ -2183,9 +2293,9 @@ function createMemory(snapshot, args) {
if (existsSync6(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
const existed = existsSync6(target.absolutePath);
writeFileSync3(target.absolutePath, args.body.endsWith(`
writeFileSync4(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -2202,9 +2312,9 @@ function createDraft(snapshot, args) {
if (existsSync6(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
const existed = existsSync6(target.absolutePath);
writeFileSync3(target.absolutePath, ensureDraftBody(args.body), "utf8");
writeFileSync4(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -2219,9 +2329,9 @@ function createSuggestion(snapshot, args) {
if (existsSync6(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
const existed = existsSync6(target.absolutePath);
writeFileSync3(target.absolutePath, args.body.endsWith(`
writeFileSync4(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -2238,9 +2348,9 @@ function createRef(snapshot, args) {
if (existsSync6(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync2(dirname4(target.absolutePath), { recursive: true });
mkdirSync3(dirname4(target.absolutePath), { recursive: true });
const existed = existsSync6(target.absolutePath);
writeFileSync3(target.absolutePath, args.body.endsWith(`
writeFileSync4(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -2259,7 +2369,7 @@ function updateLocalFile(snapshot, args) {
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync3(target.absolutePath, body, "utf8");
writeFileSync4(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -2273,7 +2383,7 @@ function deleteLocalFile(snapshot, args) {
if (!existsSync6(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -2284,14 +2394,14 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
for (const name of entries) {
if (name === "." || name === "..")
continue;
const full = join7(dir, name);
const full = join8(dir, name);
let st;
try {
st = statSync5(full);
@ -2316,7 +2426,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(join7(localRoot, r), localRoot, entries);
walkFiles(join8(localRoot, r), localRoot, entries);
}
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
@ -2328,8 +2438,8 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join8, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
import { readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join9, 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;
@ -2375,12 +2485,12 @@ function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
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(join8(target.absolutePath, entry.name)).size;
size = statSync6(join9(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);
@ -2449,14 +2559,14 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name))
continue;
const full = join8(dir, entry.name);
const full = join9(dir, entry.name);
if (entry.isDirectory()) {
walkTextFiles(full, out);
continue;
@ -2563,7 +2673,7 @@ function searchKb(snapshot, args) {
}
// src/netx/kb-runtime.ts
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
var STORE_KEY3 = Symbol.for("dsh-netxops.kb-store");
var ENV_KEYS = [
"KB_ROOT",
"KB_LOCAL",
@ -2574,17 +2684,17 @@ var ENV_KEYS = [
"KB_PATHS",
"KB_STATUS"
];
function store2() {
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 },
@ -2592,7 +2702,7 @@ function getKbContext() {
};
}
function publishKbContext(next, options = {}) {
const state = store2();
const state = store3();
if (!options.force && JSON.stringify(state.snapshot) === JSON.stringify(next))
return;
state.snapshot = {
@ -2609,7 +2719,7 @@ function publishKbContext(next, options = {}) {
}
}
function watchKbContext(listener) {
const state = store2();
const state = store3();
state.listeners.add(listener);
return () => {
state.listeners.delete(listener);
@ -2656,16 +2766,16 @@ function toLosslessJson(value) {
}
// src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
import { homedir as homedir3 } from "node:os";
import { dirname as dirname5, join as join9 } from "node:path";
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
import { homedir as homedir4 } from "node:os";
import { dirname as dirname5, join as join10 } 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;
@ -2674,7 +2784,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();
@ -2682,22 +2792,22 @@ function getToolMountStatus() {
}
function persist() {
try {
const path = join9(process.env.DSH_HOME?.trim() || join9(homedir3(), ".dsh"), "netxops-mount-status.json");
mkdirSync3(dirname5(path), { recursive: true });
writeFileSync4(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
const path = join10(process.env.DSH_HOME?.trim() || join10(homedir4(), ".dsh"), "netxops-mount-status.json");
mkdirSync4(dirname5(path), { recursive: true });
writeFileSync5(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();
}
function resetToolMountStatus() {
store3().mounts.clear();
store4().mounts.clear();
persist();
}
@ -2894,13 +3004,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 join10 } from "node:path";
import { join as join11 } 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 join10(snapshot.realRoot, rel);
return join11(snapshot.realRoot, rel);
}
function kbPackSkillFooter(snapshot) {
return [
@ -2922,7 +3032,7 @@ function resolveKbLocalSkillsRoot(snapshot) {
const rel = snapshot.paths.localSkills?.trim();
if (!rel)
return null;
return join10(snapshot.realRoot, rel);
return join11(snapshot.realRoot, rel);
}
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
let entries;
@ -2941,7 +3051,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
for (const entry of entries) {
if (options.signal?.aborted)
break;
const full = join10(root, entry);
const full = join11(root, entry);
let isDir = false;
try {
isDir = (await stat2(full)).isDirectory();
@ -3160,64 +3270,10 @@ function replyReminder(setting) {
].join(" ");
}
// 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 publishNetxConnection(next) {
const state = store4();
if (JSON.stringify(state.connection) === JSON.stringify(next))
return;
state.connection = next;
for (const listener of [...state.listeners]) {
try {
listener();
} catch (error) {
console.error("netxops: connection subscriber failed", error);
}
}
}
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/session-export.ts
import { homedir as homedir4, hostname as osHostname } from "node:os";
import { homedir as homedir5, hostname as osHostname } from "node:os";
// node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
// ../../netxops/node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
import { createRequire as createRequire3 } from "module";
var require2 = createRequire3("/");
var _a;
@ -4485,8 +4541,8 @@ function mountNetxopsWebRoute(ctx, channel, handler) {
// src/netx/tools.ts
import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";
import { dirname as dirname6, join as join12 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
// src/netx/mcp-types.ts
@ -4563,6 +4619,7 @@ 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" };
var SKILLS_URI_PREFIX = "netx://skills/";
class McpRemoteError extends Error {
status;
@ -4594,9 +4651,46 @@ function parseTool(raw) {
const readOnlyHint = annotations.readOnlyHint === true;
const meta = row._meta && typeof row._meta === "object" && !Array.isArray(row._meta) ? row._meta : {};
const netx = meta.netx && typeof meta.netx === "object" && !Array.isArray(meta.netx) ? meta.netx : {};
if (netx.group === "meta")
return null;
const group = isGroupId(netx.group) ? netx.group : undefined;
return { name, description, inputSchema, readOnlyHint, group };
}
function readMetaNetx(row) {
const meta = row._meta ?? row.meta;
if (!meta || typeof meta !== "object" || Array.isArray(meta))
return {};
const netx = meta.netx;
if (!netx || typeof netx !== "object" || Array.isArray(netx))
return {};
return netx;
}
function parseSkillResource(resource, text) {
if (!resource || typeof resource !== "object" || Array.isArray(resource))
return null;
const row = resource;
const uri = typeof row.uri === "string" ? row.uri.trim() : "";
const netx = readMetaNetx(row);
const group = isGroupId(netx.group) ? netx.group : undefined;
const skill = typeof netx.skill === "string" ? netx.skill.trim() : "";
if (!group || !skill)
return null;
let relPath = typeof row.name === "string" ? row.name.trim().replace(/\\/g, "/") : "";
if (!relPath && uri.startsWith(SKILLS_URI_PREFIX)) {
const rest = uri.slice(SKILLS_URI_PREFIX.length);
const parts = rest.split("/");
if (parts.length >= 3)
relPath = parts.slice(2).join("/");
}
if (!relPath || relPath.includes(".."))
return null;
return { group, skill, relPath, text };
}
function resourcesCapability(capabilities) {
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities))
return false;
return "resources" in capabilities;
}
function parseMcpResponseBody(raw) {
const text = raw.replace(/^\uFEFF/, "").trim();
if (!text)
@ -4692,24 +4786,57 @@ async function postRpc(options, method, params, notification = false) {
}
return asRpcResult(parseMcpResponseBody(responseText));
}
async function listMcpTools(options) {
async function fetchMcpCatalog(options) {
if (!options.mcpUrl)
throw new McpRemoteError("mcp url is empty");
await postRpc(options, "initialize", {
const init = await postRpc(options, "initialize", {
protocolVersion: PROTOCOL_VERSION,
capabilities: {},
clientInfo: CLIENT_INFO
});
await postRpc(options, "notifications/initialized", {}, true);
const result = await postRpc(options, "tools/list", {});
const tools = Array.isArray(result?.tools) ? result.tools : [];
const out = [];
for (const item of tools) {
const toolsResult = await postRpc(options, "tools/list", {});
const toolsRaw = Array.isArray(toolsResult?.tools) ? toolsResult.tools : [];
const tools = [];
for (const item of toolsRaw) {
const parsed = parseTool(item);
if (parsed)
out.push(parsed);
tools.push(parsed);
}
if (!resourcesCapability(init?.capabilities)) {
return { tools, skills: [] };
}
try {
const listed = await postRpc(options, "resources/list", {});
const resources = Array.isArray(listed?.resources) ? listed.resources : [];
const skills = [];
for (const resource of resources) {
if (!resource || typeof resource !== "object" || Array.isArray(resource))
continue;
const uri = typeof resource.uri === "string" ? resource.uri.trim() : "";
if (!uri.startsWith(SKILLS_URI_PREFIX))
continue;
const read = await postRpc(options, "resources/read", { uri });
const contents = Array.isArray(read?.contents) ? read.contents : [];
let text = "";
for (const block of contents) {
if (!block || typeof block !== "object" || Array.isArray(block))
continue;
const row = block;
if (typeof row.text === "string") {
text = row.text;
break;
}
}
const parsed = parseSkillResource(resource, text);
if (parsed)
skills.push(parsed);
}
return { tools, skills };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { tools, skillsError: message };
}
return out;
}
async function callMcpTool(options, name, args) {
if (!options.mcpUrl)
@ -4912,7 +5039,7 @@ async function refreshMcpCatalog(input) {
fromCache: previous.tools.length > 0
});
try {
const tools = await listMcpTools({
const fetched = await fetchMcpCatalog({
mcpUrl,
token: input.token,
lang: input.lang,
@ -4922,10 +5049,15 @@ async function refreshMcpCatalog(input) {
});
if (gen !== store5().generation)
return store5().state;
if (fetched.skills !== undefined) {
replaceSkillsCache(input.apiUrl, fetched.skills);
} else {
console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError);
}
const next = {
phase: "connected",
mcpUrl,
tools: Object.freeze([...tools]),
tools: Object.freeze([...fetched.tools]),
error: "",
listedAt: new Date().toISOString(),
fromCache: false
@ -4952,8 +5084,8 @@ async function refreshMcpCatalog(input) {
// src/netx/tools.ts
function packageVersion() {
try {
const here = dirname6(fileURLToPath3(import.meta.url));
const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync7(join12(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";
@ -5086,12 +5218,12 @@ import { createScope, scopeOf } from "@deepseek-ai/dsh-scope";
// src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync8 } from "node:fs";
import { dirname as dirname7, join as join12 } from "node:path";
import { fileURLToPath as fileURLToPath4 } from "node:url";
import { dirname as dirname7, join as join13 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";
function packageVersion2() {
try {
const here = dirname7(fileURLToPath4(import.meta.url));
const pkg = JSON.parse(readFileSync8(join12(here, "..", "package.json"), "utf8"));
const here = dirname7(fileURLToPath3(import.meta.url));
const pkg = JSON.parse(readFileSync8(join13(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";
@ -5417,8 +5549,8 @@ function resolveApiUrlForConnection(value) {
}
function packageVersion3() {
try {
const here = dirname8(fileURLToPath5(import.meta.url));
const pkg = JSON.parse(readFileSync9(join13(here, "..", "package.json"), "utf8"));
const here = dirname8(fileURLToPath4(import.meta.url));
const pkg = JSON.parse(readFileSync9(join14(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0";
} catch {
return "0";