feat(skills): merge unavailable tools into footnote, bind cache to token, prune stale caches, warn on remote plain HTTP

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 23:37:16 +08:00
parent 056f2d4ea3
commit 05bfd97c6e
19 changed files with 1100 additions and 496 deletions

View file

@ -2,7 +2,7 @@
"format": 1, "format": 1,
"builds": { "builds": {
"lib/agent-tools-biz-monitor.js": { "lib/agent-tools-biz-monitor.js": {
"output": "11fd312eeb902daab190770bf9465fcbd18ff97e48ca2d4e86144cffa6691790", "output": "b9ab81312e2443d43b283116ac7f981666f5673d9d550141f264a99702ea7398",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -11,9 +11,9 @@
"src/agent-tools-biz-monitor.ts": "201bd447198d5ee7cb1aa25031a3f6054fe12a8a221edd4915752696cca1537d", "src/agent-tools-biz-monitor.ts": "201bd447198d5ee7cb1aa25031a3f6054fe12a8a221edd4915752696cca1537d",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -24,18 +24,18 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/agent-tools-common.js": { "lib/agent-tools-common.js": {
"output": "9e40a55c9c823b76c8060aa5f328e89b1411091f1a23841e63505deb2de3e316", "output": "6cf374696df26b8519e4e43b33dc00991e6ad5dbb1eae442c446d68849a1d440",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -44,9 +44,9 @@
"src/agent-tools-common.ts": "cf463e940bc2042d7462502de93d45380550da54f1965fa684d72f71b16bc41b", "src/agent-tools-common.ts": "cf463e940bc2042d7462502de93d45380550da54f1965fa684d72f71b16bc41b",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -57,18 +57,18 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/agent-tools-nms.js": { "lib/agent-tools-nms.js": {
"output": "126ef9bc22cbe2814442efd68a0ff83ce29fdac7f9baed9602a7a186ef29db0f", "output": "f7fd09a4fb4fe79218c22567ffb806319271c47ad9de5fbcac3748562adbda60",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -77,9 +77,9 @@
"src/agent-tools-nms.ts": "6505e51bfb2948756440b00724b19d907d410d34ef1dcbf08019774c0f9d7ef5", "src/agent-tools-nms.ts": "6505e51bfb2948756440b00724b19d907d410d34ef1dcbf08019774c0f9d7ef5",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -90,18 +90,18 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/agent-tools-ops.js": { "lib/agent-tools-ops.js": {
"output": "56fd1c68147c6d48cca50535f67ce3807dab5f35db4ca9d675fe8db731d9f1d2", "output": "59218d9c32d97a92cf90c4a6c6d5f184f04162a93aada26d408e7d90debc93ca",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -110,9 +110,9 @@
"src/agent-tools-ops.ts": "2f0331106de53f3dcdd8d61ce2d9da363f0a9b854f1dd7d98e27997c82d98a77", "src/agent-tools-ops.ts": "2f0331106de53f3dcdd8d61ce2d9da363f0a9b854f1dd7d98e27997c82d98a77",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -123,18 +123,18 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/agent-tools-topology.js": { "lib/agent-tools-topology.js": {
"output": "7b683d38bbab4764684d88e6f0989177e60789e6b8923438b1c860c75d54fc26", "output": "b04b4ab6ed44cbb233f0d8c37d26d24d5336432d1ca8b950970a69aafdefdd5b",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -143,9 +143,9 @@
"src/agent-tools-topology.ts": "22bf5f1c71a5b1d33bd3a894bca2c6e4c2fc72072c8a9d7bbc00e214dfafa9f8", "src/agent-tools-topology.ts": "22bf5f1c71a5b1d33bd3a894bca2c6e4c2fc72072c8a9d7bbc00e214dfafa9f8",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -156,18 +156,18 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/agent-tools.js": { "lib/agent-tools.js": {
"output": "e831d2c6a671a9b4bcc2449a0e84772446848372925567704cdb0590a2bb7071", "output": "89f59175ba51d39a0fbd48e825e83470eadd84ef5378c8c29c51ab0cde3845d6",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
@ -176,9 +176,9 @@
"src/agent-tools.ts": "b7e4f8476ba9c5f2754d859074c94129642d18883da293b80e310c09336bc5c5", "src/agent-tools.ts": "b7e4f8476ba9c5f2754d859074c94129642d18883da293b80e310c09336bc5c5",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045", "src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463", "src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
@ -189,31 +189,31 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139" "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139"
} }
}, },
"lib/client.js": { "lib/client.js": {
"output": "76d73a9e8354e55c27d60c90d17206b7d9eb096829a64cbfc15ab1152c7db49b", "output": "bec095ff7f28d3eb97152486275c93badc43fe687377c5d0e3267b355add2e2d",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1", "scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
"src/client/NetxopsCard.tsx": "51d85f150fd13fc5377de29a4e80b992e860514e0700884218f9f9c7bdcd4cc1", "src/client/NetxopsCard.tsx": "d4c4e795949496f5f913914c23c338bdbc81ea2b721a55d835815f2eae13f667",
"src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31", "src/client/alarm-push-status-view.ts": "9ad84e4a691a6c22de4d85c45047251716929c63cada362205db98428a810a31",
"src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9", "src/client/card-form.ts": "00ec10c453fffae8beac4a35a13299514c120c913100a6e4536e7a00d072b8d9",
"src/client/controller.ts": "d3353feda6f6b004dd094b1fc90d743003b55ce5a9d0b46c8fb9c1eb33925cc1", "src/client/controller.ts": "d3353feda6f6b004dd094b1fc90d743003b55ce5a9d0b46c8fb9c1eb33925cc1",
"src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117", "src/client/im-delivery-catalog.ts": "faf18d4a8fdcf30033b6d905f156c6fc953d214f8d734417f3bed783994d6117",
"src/client/index.ts": "1f73ae8e8c02e13b47c5679869359444485b4621eca20a15c632289f22e5d760", "src/client/index.ts": "1f73ae8e8c02e13b47c5679869359444485b4621eca20a15c632289f22e5d760",
"src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10", "src/client/kb-status-view.ts": "458d1dfd9c0f93e6cdd06780c0ad813de54c3aa952fcdb63758a616ab43ffb10",
"src/client/locales.ts": "b9aa9cac558119c3a81e773dc3b49378f0c37fb2220696a484a92bf19aabb5d6", "src/client/locales.ts": "35106f934baaad22cf5e7afdebe415ef198af29f4831ad9dfa7ff2d0235cf4d9",
"src/client/mcp-catalog-view.ts": "3066ba1fd56b19e099e1f58ba9699a4d17de9f5733e27a4f4da0c4f58809a545", "src/client/mcp-catalog-view.ts": "3066ba1fd56b19e099e1f58ba9699a4d17de9f5733e27a4f4da0c4f58809a545",
"src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd", "src/client/sessions-export-view.ts": "3397b081318c23d9d816596566e19101df7234390a698a07481c7c1a019a66fd",
"src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb", "src/client/settings-form.ts": "0318749c9afd049982a60eee7b2ca4a0a2cdcd4f574f9c87616f2ccbe39779eb",
@ -222,7 +222,7 @@
"src/client/tools-status-view.ts": "4ff296641164124748356a24e0d38ec88f09ee9826bf1a2ab51159026a2b2ca8", "src/client/tools-status-view.ts": "4ff296641164124748356a24e0d38ec88f09ee9826bf1a2ab51159026a2b2ca8",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917", "src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02", "src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
@ -231,14 +231,14 @@
} }
}, },
"lib/index.js": { "lib/index.js": {
"output": "515aafc5a26d8680cedbe970751b5f565e098c8f691014fb0f9a1fe9edd6f82f", "output": "ffdddb5bd035a1ef3f268efd6590e472f32d99ad9105da056d9d00852c6adf17",
"inputs": { "inputs": {
"package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9", "package.json": "5132d4dd66628c59e1b495b5a75fc6c29a30c220a6c112059b4c3b5983d3cbb9",
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3", "pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8", "scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9", "scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
"src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc", "src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc",
"src/index.ts": "f583927e67045d83362930d8c416d540c1d8b08b18b2ad78e2a3d72ea03df559", "src/index.ts": "86d6ab02fb50b59de89e8ae84bb8c65978d715968f4c721f779e86b60d23a13e",
"src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62", "src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62",
"src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf", "src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf",
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f", "src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
@ -246,10 +246,10 @@
"src/netx/alarm-session.ts": "0df97efc3debb262573c19ade84d9d50584d58b21fa2c5ae5badd382915f18e3", "src/netx/alarm-session.ts": "0df97efc3debb262573c19ade84d9d50584d58b21fa2c5ae5badd382915f18e3",
"src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51", "src/netx/capability-groups.ts": "3b7ac48ec3bbef91f324c24156af783d741dc5aeeb4db95d52eeafadbcbbed51",
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117", "src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
"src/netx/group-skills.ts": "84a35fe639b3670fd4e95e3e5801a78020551655fd66007a9f151e2f8619e9ac", "src/netx/group-skills.ts": "3fa78686f4c035f056f3cb5ed2f0411ad52c043958b859ebfcc254a625554e3a",
"src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67", "src/netx/group-tools-plugin.ts": "647e5258b46648705585cd5d3765556f391810434c6c7f04f3df9e2f44d8ba67",
"src/netx/heal-profile-entry.ts": "18c2b635f727bf41d38c7c38fcc0ce0c5391c9d98b2d662db354f1908defb6be", "src/netx/heal-profile-entry.ts": "18c2b635f727bf41d38c7c38fcc0ce0c5391c9d98b2d662db354f1908defb6be",
"src/netx/http.ts": "aa0552d6087593924f448fc8efaadda92f8803147190741f5abc3030cf32c439", "src/netx/http.ts": "982ca3e5da8fe36debd5a0c561578c8f2c5e2a9cc0527d7dbacdf4e5a4aa9edd",
"src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082", "src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082",
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917", "src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9", "src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
@ -262,16 +262,16 @@
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86", "src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b", "src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
"src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d", "src/netx/mcp-catalog.ts": "b79962da8f74f04595ed89b166b851d83cac41e8338b80b7629da2021a2c281d",
"src/netx/mcp-remote.ts": "4fb64d62c3947463d7ea174edb4ceed2a8bba090ad5bf8ede50654c423d7a91f", "src/netx/mcp-remote.ts": "89576a02729f50b7f03ef5f689a5e9c8380fa6ad4e44c46e1c846eceadd04571",
"src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f", "src/netx/mcp-schema.ts": "2bb2eccd001101bd97389578c627931e89fbab4e057dab500e8b7b8bd655366f",
"src/netx/mcp-store.ts": "002a5d432d4532ea0ed07f00801eea9ca8cb84d6ebcb347311967e13007fde32", "src/netx/mcp-store.ts": "ea91f2171b93dc46c4d72cd811bbb03b16060db83abe21b6f71c68bbfb32d6ff",
"src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833", "src/netx/mcp-types.ts": "4b30d6e8fba328b0e116c43a1a90d2b686ef4b9fc648b521063876426e9c2833",
"src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e", "src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e",
"src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214", "src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214",
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967", "src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
"src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386", "src/netx/runtime.ts": "238555c92c3972ee9de8ec062b14d6a86961ec4c64609565ba1164b1a460d386",
"src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6", "src/netx/session-export.ts": "8e6e9a3094045901563366fac3d6dc74d9bfd78bbff853358d6500955d3dcff6",
"src/netx/skills-cache.ts": "10957061256cec6b381c5604745d905146abe5da1b418055414166879bb90094", "src/netx/skills-cache.ts": "58383b578d7d3c5376016351b3f36466ac74faad810f835ea5af9733a729ef7f",
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf", "src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
"src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139", "src/netx/tools.ts": "88c925aba50a7d6b3fc800986789955fd77c7aca8387c399b1ef58edbf7b9139",
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5" "src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1,5 +1,5 @@
// src/netx/group-tools-plugin.ts // src/netx/group-tools-plugin.ts
import { readFileSync as readFileSync6 } from "node:fs"; import { readFileSync as readFileSync7 } from "node:fs";
import { dirname as dirname6, join as join11 } from "node:path"; import { dirname as dirname6, join as join11 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
} }
// src/netx/skills-cache.ts // src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh"); return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16); return createHash("sha256").update(base).digest("hex").slice(0, 16);
} }
function skillsCacheDir(apiUrl) { function cacheParent() {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); 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 // src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store"); var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -593,11 +645,11 @@ Use only tools present in this session. Dual-unit/orbit/polish recipes require s
} }
// src/netx/kb-context-skill.ts // src/netx/kb-context-skill.ts
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs"; import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
import { join as join5 } from "node:path"; import { join as join5 } from "node:path";
// src/netx/kb-manifest.ts // src/netx/kb-manifest.ts
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; 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"; import { basename, dirname as dirname2, join as join3, resolve } from "node:path";
var EMPTY_CONTENT = { var EMPTY_CONTENT = {
hasRegions: false, hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths }; const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => { return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel); const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) }; return { key, relative: rel, absolute, exists: existsSync2(absolute) };
}); });
} }
function renderKbPathTable(snapshot) { function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
} }
// src/netx/kb-local-path.ts // src/netx/kb-local-path.ts
import { existsSync as existsSync2, realpathSync, statSync as statSync2 } from "node:fs"; import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
import { join as join4, relative, resolve as resolve2, sep } from "node:path"; import { join as join4, relative, resolve as resolve2, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories", "memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
} }
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try { try {
if (existsSync2(checked.absolutePath)) { if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath); const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot); const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile); const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) { if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`); throw new Error(`path not under writable roots: ${realRel}`);
} }
if (!statSync2(realFile).isFile()) { if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`); throw new Error(`not a file: ${realFile}`);
} }
return { return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity"); const dir = join5(localRoot, "identity");
let names; let names;
try { 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 { } catch {
return []; return [];
} }
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name); const full = join5(dir, name);
let text; let text;
try { try {
if (!statSync3(full).isFile()) if (!statSync4(full).isFile())
continue; continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim(); text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch { } catch {
continue; continue;
} }
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts // src/netx/kb-local-ops.ts
import { import {
existsSync as existsSync3, existsSync as existsSync4,
mkdirSync, mkdirSync as mkdirSync2,
readdirSync as readdirSync3, readdirSync as readdirSync4,
readFileSync as readFileSync3, readFileSync as readFileSync4,
rmSync, rmSync as rmSync2,
statSync as statSync4, statSync as statSync5,
writeFileSync writeFileSync as writeFileSync2
} from "node:fs"; } from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path"; import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) { function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug }); const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug }); const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8"); writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return { return {
ok: true, ok: true,
action: existed ? "updated" : "created", action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug }); const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug); const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync3(target.absolutePath) && !args.overwrite) { if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
} }
mkdirSync(dirname3(target.absolutePath), { recursive: true }); mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath); const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(` writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`, "utf8"); `, "utf8");
return { return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) { function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body} `) ? args.body : `${args.body}
`; `;
writeFileSync(target.absolutePath, body, "utf8"); writeFileSync2(target.absolutePath, body, "utf8");
return { return {
ok: true, ok: true,
action: "updated", action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot); const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path); const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) { if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`); throw new Error(`file not found: ${target.relativePath}`);
} }
rmSync(target.absolutePath, { force: false }); rmSync2(target.absolutePath, { force: false });
return { return {
ok: true, ok: true,
action: "deleted", action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) { function walkFiles(dir, baseLocal, out) {
let entries; let entries;
try { try {
entries = readdirSync3(dir); entries = readdirSync4(dir);
} catch { } catch {
return; return;
} }
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name); const full = join6(dir, name);
let st; let st;
try { try {
st = statSync4(full); st = statSync5(full);
} catch { } catch {
continue; continue;
} }
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
} }
// src/netx/kb-read-ops.ts // src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs"; 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"; 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 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 SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
} }
function readKbFile(snapshot, args) { function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path); const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath); const st = statSync6(target.absolutePath);
if (st.isDirectory()) { 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()) if (entry.isDirectory())
return { name: entry.name, type: "dir" }; return { name: entry.name, type: "dir" };
let size; let size;
try { try {
size = statSync5(join7(target.absolutePath, entry.name)).size; size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {} } catch {}
return { name: entry.name, type: "file", size }; return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1); }).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) { if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
throw new Error(`not a text KB file: ${target.relativePath}`); throw new Error(`not a text KB file: ${target.relativePath}`);
} }
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, ` const lines = readFileSync5(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
`).split(` `).split(`
`); `);
const start = Math.max(1, Math.floor(args.offset ?? 1)); const start = Math.max(1, Math.floor(args.offset ?? 1));
@ -1258,7 +1310,7 @@ function cachedText(path, mtimeMs, size) {
const hit = cache.files.get(path); const hit = cache.files.get(path);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit; return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, ""); const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit) if (hit)
cache.chars -= hit.text.length; cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) { if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) { function walkTextFiles(dir, out) {
let entries; let entries;
try { try {
entries = readdirSync4(dir, { withFileTypes: true }); entries = readdirSync5(dir, { withFileTypes: true });
} catch { } catch {
return; return;
} }
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue; continue;
try { try {
const st = statSync5(full); const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES) if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size }); out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {} } catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = []; const files = [];
const seen = new Set; const seen = new Set;
for (const dir of dirs) { for (const dir of dirs) {
const st = statSync5(dir); const st = statSync6(dir);
const batch = []; const batch = [];
if (st.isFile()) if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size }); batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
} }
// src/netx/tool-mount-status.ts // src/netx/tool-mount-status.ts
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs"; import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
import { homedir as homedir2 } from "node:os"; import { homedir as homedir2 } from "node:os";
import { dirname as dirname4, join as join8 } from "node:path"; import { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory"); var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() { function persist() {
try { try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".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 }); mkdirSync3(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)} writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8"); `, "utf8");
} catch {} } catch {}
} }
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
} }
// src/netx/tools.ts // src/netx/tools.ts
import { readFileSync as readFileSync5 } from "node:fs"; import { readFileSync as readFileSync6 } from "node:fs";
import { dirname as dirname5, join as join10 } from "node:path"; import { dirname as dirname5, join as join10 } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() { function packageVersion() {
try { try {
const here = dirname5(fileURLToPath(import.meta.url)); const here = dirname5(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync5(join10(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() { function packageVersion2() {
try { try {
const here = dirname6(fileURLToPath2(import.meta.url)); const here = dirname6(fileURLToPath2(import.meta.url));
const pkg = JSON.parse(readFileSync6(join11(here, "..", "package.json"), "utf8")); const pkg = JSON.parse(readFileSync7(join11(here, "..", "package.json"), "utf8"));
return typeof pkg.version === "string" ? pkg.version : "0"; return typeof pkg.version === "string" ? pkg.version : "0";
} catch { } catch {
return "0"; return "0";

View file

@ -1070,6 +1070,40 @@ function serializeDisabledTools(names) {
return JSON.stringify([...new Set(names.map((n) => n.trim()).filter(Boolean))].sort()); return JSON.stringify([...new Set(names.map((n) => n.trim()).filter(Boolean))].sort());
} }
// 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}` };
}
function isInsecureRemoteApiUrl(raw) {
let url;
try {
url = new URL(raw.trim());
} catch {
return false;
}
if (url.protocol !== "http:")
return false;
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
return !(host === "localhost" || host.endsWith(".localhost") || host === "::1" || /^127\./.test(host));
}
// src/client/NetxopsCard.tsx // src/client/NetxopsCard.tsx
var jsx_runtime = require("react/jsx-runtime"); var jsx_runtime = require("react/jsx-runtime");
function phaseLocaleKey(phase) { function phaseLocaleKey(phase) {
@ -1641,6 +1675,10 @@ function NetxopsCard(props) {
props.resetField("apiUrl"); props.resetField("apiUrl");
} }
}), }),
isInsecureRemoteApiUrl(state.apiUrl.text) ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-invalid",
children: t("apiUrlInsecure")
}) : null,
/* @__PURE__ */ jsx_runtime.jsxs("div", { /* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-fieldGrid", className: "dsh-nx-fieldGrid",
children: [ children: [
@ -2161,28 +2199,6 @@ function NetxopsCard(props) {
}); });
} }
// 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/client/snapshot-store.ts // src/client/snapshot-store.ts
function createSnapshotStore(init) { function createSnapshotStore(init) {
let state = init; let state = init;
@ -3228,6 +3244,7 @@ var en = {
sectionExport: "Session export", sectionExport: "Session export",
apiUrl: "API URL", apiUrl: "API URL",
apiUrlHint: "http://127.0.0.1:8890", apiUrlHint: "http://127.0.0.1:8890",
apiUrlInsecure: "Plain HTTP to a remote host: the token and the skills sent to the model are not encrypted. Use https://.",
apiUrlInvalid: "Enter an HTTP or HTTPS URL without credentials.", apiUrlInvalid: "Enter an HTTP or HTTPS URL without credentials.",
lang: "API / alarm language", lang: "API / alarm language",
thinkingLanguage: "Thinking language", thinkingLanguage: "Thinking language",
@ -3324,6 +3341,7 @@ var zh = {
sectionExport: "会话导出", sectionExport: "会话导出",
apiUrl: "API 地址", apiUrl: "API 地址",
apiUrlHint: "http://127.0.0.1:8890", apiUrlHint: "http://127.0.0.1:8890",
apiUrlInsecure: "连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。",
apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。", apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。",
lang: "API / 告警语言", lang: "API / 告警语言",
thinkingLanguage: "思考语言", thinkingLanguage: "思考语言",

View file

@ -1398,6 +1398,7 @@ import {
} from "node:fs"; } from "node:fs";
import { homedir as homedir3 } from "node:os"; import { homedir as homedir3 } from "node:os";
import { dirname as dirname2, join as join3 } from "node:path"; import { dirname as dirname2, join as join3 } from "node:path";
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() { function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim(); const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join3(homedir3(), ".dsh"); return fromEnv || join3(homedir3(), ".dsh");
@ -1410,8 +1411,65 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl); const base = normalizeSkillsApiUrl(apiUrl);
return createHash2("sha256").update(base).digest("hex").slice(0, 16); return createHash2("sha256").update(base).digest("hex").slice(0, 16);
} }
function cacheParent() {
return join3(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) { function skillsCacheDir(apiUrl) {
return join3(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl)); return join3(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash2("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync3(join3(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;
}
function pruneStaleCaches(keep) {
const parent = cacheParent();
let entries;
try {
entries = readdirSync(parent);
} catch {
return;
}
const now = Date.now();
for (const name of entries) {
const dir = join3(parent, name);
if (dir === keep)
continue;
try {
const listedAt = Date.parse(readMeta(dir)?.listedAt ?? "");
const age = now - (Number.isFinite(listedAt) ? listedAt : statSync2(dir).mtimeMs);
if (age > STALE_CACHE_MS)
rmSync(dir, { recursive: true, force: true });
} catch {}
}
} }
function isSafeSegment(value) { function isSafeSegment(value) {
return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00"); return Boolean(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\") && !value.includes("\x00");
@ -1422,12 +1480,17 @@ function isSafeRelPath(relPath) {
const parts = relPath.replace(/\\/g, "/").split("/"); const parts = relPath.replace(/\\/g, "/").split("/");
return parts.every((part) => part.length > 0 && part !== "." && part !== ".."); return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
} }
function replaceSkillsCache(apiUrl, files) { function replaceSkillsCache(apiUrl, files, options = {}) {
const root = skillsCacheDir(apiUrl); const root = skillsCacheDir(apiUrl);
rmSync(root, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true });
mkdirSync2(root, { recursive: true }); mkdirSync2(root, { recursive: true });
let written = 0; let written = 0;
const unavailableTools = {};
for (const file of files) { for (const file of files) {
if (file.unavailableTools?.length && SKILL_DIR_BY_GROUP[file.group]) {
const merged = new Set([...unavailableTools[file.group] ?? [], ...file.unavailableTools]);
unavailableTools[file.group] = [...merged].sort();
}
const groupDir = SKILL_DIR_BY_GROUP[file.group]; const groupDir = SKILL_DIR_BY_GROUP[file.group];
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath))
continue; continue;
@ -1445,10 +1508,14 @@ function replaceSkillsCache(apiUrl, files) {
apiUrl: normalized, apiUrl: normalized,
mcpUrl: normalized ? `${normalized}/mcp` : "", mcpUrl: normalized ? `${normalized}/mcp` : "",
listedAt: new Date().toISOString(), listedAt: new Date().toISOString(),
fileCount: written fileCount: written,
...options.token !== undefined ? { tokenFp: tokenFingerprint(options.token) } : {},
unavailableTools
}; };
writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)} writeFileSync3(join3(root, ".meta.json"), `${JSON.stringify(meta, null, 2)}
`, "utf8"); `, "utf8");
summaryMemo.delete(root);
pruneStaleCaches(root);
return root; return root;
} }
function countFiles(dir) { function countFiles(dir) {
@ -1459,17 +1526,17 @@ function countFiles(dir) {
} }
return n; return n;
} }
function listCachedSkills(apiUrl) { var summaryMemo = new Map;
const root = skillsCacheDir(apiUrl); function listCachedSkills(apiUrl, token = "") {
const root = usableSkillsCacheDir(apiUrl, token);
if (!root || !existsSync3(root))
return { listedAt: null, skills: [] };
const metaListedAt = readMeta(root)?.listedAt;
const listedAt = typeof metaListedAt === "string" ? metaListedAt : null;
const memo = summaryMemo.get(root);
if (memo && memo.listedAt === listedAt)
return memo;
const skills = []; const skills = [];
let listedAt = null;
if (!apiUrl.trim() || !existsSync3(root))
return { listedAt, skills };
try {
const meta = JSON.parse(readFileSync3(join3(root, ".meta.json"), "utf8"));
if (typeof meta.listedAt === "string")
listedAt = meta.listedAt;
} catch {}
for (const group of CAPABILITY_GROUP_IDS) { for (const group of CAPABILITY_GROUP_IDS) {
const groupDir = join3(root, SKILL_DIR_BY_GROUP[group]); const groupDir = join3(root, SKILL_DIR_BY_GROUP[group]);
if (!existsSync3(groupDir)) if (!existsSync3(groupDir))
@ -1483,7 +1550,15 @@ function listCachedSkills(apiUrl) {
} }
} catch {} } catch {}
} }
return { listedAt, skills }; const listing = { listedAt, skills };
summaryMemo.set(root, listing);
return listing;
}
// src/netx/mcp-types.ts
var NETX_TOOL_PREFIX = "netx__";
function publicToolName(serverName) {
return `${NETX_TOOL_PREFIX}${serverName}`;
} }
// src/netx/group-skills.ts // src/netx/group-skills.ts
@ -1614,12 +1689,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en"); const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? ` return en ? `
## Disabled tools ## Unavailable tools
The following tools are disabled: ${list}. Do not call them. The following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.
` : ` ` : `
## 已禁用工具 ## 不可用工具
以下工具已禁用:${list},不要调用。 以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。
`; `;
} }
function resolveSkillsRoot(options) { function resolveSkillsRoot(options) {
@ -1629,7 +1704,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection(); const connection = getNetxConnection();
if (!connection?.apiUrl?.trim()) if (!connection?.apiUrl?.trim())
return ""; return "";
return skillsCacheDir(connection.apiUrl); return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
} }
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) { async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions; const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -1648,6 +1723,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
} }
return () => {}; return () => {};
} }
const unavailable = cachedUnavailableTools(root);
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) if (!enabled.has(groupId))
@ -1661,7 +1737,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) { if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join4(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); const offNames = new Set([
...options.disabledByGroup?.[groupId] ?? [],
...(unavailable[groupId] ?? []).map(publicToolName)
]);
const footnote = disabledFootnote([...offNames].sort(), options.lang);
for (const skill of skills) { for (const skill of skills) {
const base = groupId === "topology" ? `${skill.content} const base = groupId === "topology" ? `${skill.content}
@ -4581,12 +4661,6 @@ import { dirname as dirname6, join as join12 } from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url"; import { fileURLToPath as fileURLToPath2 } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools"; import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
// src/netx/mcp-types.ts
var NETX_TOOL_PREFIX = "netx__";
function publicToolName(serverName) {
return `${NETX_TOOL_PREFIX}${serverName}`;
}
// src/netx/mcp-catalog.ts // src/netx/mcp-catalog.ts
function disabledSet(disabledTools) { function disabledSet(disabledTools) {
const out = new Set; const out = new Set;
@ -4720,7 +4794,14 @@ function parseSkillResource(resource, text) {
} }
if (!relPath || relPath.includes("..")) if (!relPath || relPath.includes(".."))
return null; return null;
return { group, skill, relPath, text }; const unavailableTools = Array.isArray(netx.unavailableTools) ? netx.unavailableTools.filter((n) => typeof n === "string" && n.length > 0) : [];
return {
group,
skill,
relPath,
text,
...netx.entry === true && unavailableTools.length > 0 ? { unavailableTools } : {}
};
} }
function resourcesCapability(capabilities) { function resourcesCapability(capabilities) {
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities)) if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities))
@ -5086,7 +5167,7 @@ async function refreshMcpCatalog(input) {
if (gen !== store5().generation) if (gen !== store5().generation)
return store5().state; return store5().state;
if (fetched.skills !== undefined) { if (fetched.skills !== undefined) {
replaceSkillsCache(input.apiUrl, fetched.skills); replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token });
} else { } else {
console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError); console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError);
} }
@ -5513,7 +5594,8 @@ function installPresetPublication(ctx) {
// src/index.ts // src/index.ts
function mcpCatalogPayload(catalog) { function mcpCatalogPayload(catalog) {
const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? ""); const connection = getNetxConnection();
const cached = listCachedSkills(connection?.apiUrl ?? "", connection?.token ?? "");
return { return {
phase: catalog.phase, phase: catalog.phase,
mcpUrl: catalog.mcpUrl, mcpUrl: catalog.mcpUrl,

View file

@ -23,6 +23,7 @@ import { ensureStyles } from './styles.ts'
import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts' import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts'
import { parsePresetIds } from '../netx/capability-groups.ts' import { parsePresetIds } from '../netx/capability-groups.ts'
import { parseDisabledTools } from '../netx/mcp-catalog.ts' import { parseDisabledTools } from '../netx/mcp-catalog.ts'
import { isInsecureRemoteApiUrl } from '../netx/http.ts'
import type { McpSkillRow, McpToolRow } from './mcp-catalog-view.ts' import type { McpSkillRow, McpToolRow } from './mcp-catalog-view.ts'
import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client' import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
@ -559,6 +560,9 @@ export function NetxopsCard(props: NetxopsCardProps) {
onEdit={(text) => { props.edit('apiUrl', text) }} onEdit={(text) => { props.edit('apiUrl', text) }}
onReset={() => { props.resetField('apiUrl') }} onReset={() => { props.resetField('apiUrl') }}
/> />
{isInsecureRemoteApiUrl(state.apiUrl.text)
? <p className="dsh-nx-invalid">{t('apiUrlInsecure')}</p>
: null}
<div className="dsh-nx-fieldGrid"> <div className="dsh-nx-fieldGrid">
<SelectField <SelectField
id="netxops-lang" id="netxops-lang"

View file

@ -24,6 +24,7 @@ export type NetxopsLocaleKey =
| 'sectionExport' | 'sectionExport'
| 'apiUrl' | 'apiUrl'
| 'apiUrlHint' | 'apiUrlHint'
| 'apiUrlInsecure'
| 'apiUrlInvalid' | 'apiUrlInvalid'
| 'lang' | 'lang'
| 'thinkingLanguage' | 'thinkingLanguage'
@ -120,6 +121,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
sectionExport: 'Session export', sectionExport: 'Session export',
apiUrl: 'API URL', apiUrl: 'API URL',
apiUrlHint: 'http://127.0.0.1:8890', apiUrlHint: 'http://127.0.0.1:8890',
apiUrlInsecure: 'Plain HTTP to a remote host: the token and the skills sent to the model are not encrypted. Use https://.',
apiUrlInvalid: 'Enter an HTTP or HTTPS URL without credentials.', apiUrlInvalid: 'Enter an HTTP or HTTPS URL without credentials.',
lang: 'API / alarm language', lang: 'API / alarm language',
thinkingLanguage: 'Thinking language', thinkingLanguage: 'Thinking language',
@ -217,6 +219,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
sectionExport: '会话导出', sectionExport: '会话导出',
apiUrl: 'API 地址', apiUrl: 'API 地址',
apiUrlHint: 'http://127.0.0.1:8890', apiUrlHint: 'http://127.0.0.1:8890',
apiUrlInsecure: '连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。',
apiUrlInvalid: '请输入不含账号密码的 HTTP 或 HTTPS 地址。', apiUrlInvalid: '请输入不含账号密码的 HTTP 或 HTTPS 地址。',
lang: 'API / 告警语言', lang: 'API / 告警语言',
thinkingLanguage: '思考语言', thinkingLanguage: '思考语言',

View file

@ -108,7 +108,8 @@ import {
/** Plugins-card catalog payload: tools from the MCP store + skills from the disk cache. */ /** Plugins-card catalog payload: tools from the MCP store + skills from the disk cache. */
function mcpCatalogPayload(catalog: ReturnType<typeof getMcpCatalogState>) { function mcpCatalogPayload(catalog: ReturnType<typeof getMcpCatalogState>) {
const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? '') const connection = getNetxConnection()
const cached = listCachedSkills(connection?.apiUrl ?? '', connection?.token ?? '')
return { return {
phase: catalog.phase, phase: catalog.phase,
mcpUrl: catalog.mcpUrl, mcpUrl: catalog.mcpUrl,

View file

@ -12,7 +12,8 @@ import {
type NetxCapabilityGroupId, type NetxCapabilityGroupId,
} from './capability-groups.ts' } from './capability-groups.ts'
import { getNetxConnection } from './runtime.ts' import { getNetxConnection } from './runtime.ts'
import { skillsCacheDir } from './skills-cache.ts' import { cachedUnavailableTools, usableSkillsCacheDir } from './skills-cache.ts'
import { publicToolName } from './mcp-types.ts'
interface ParsedSkill { interface ParsedSkill {
name: string name: string
@ -181,8 +182,8 @@ function disabledFootnote(names: readonly string[], lang?: string): string {
const list = names.join(', ') const list = names.join(', ')
const en = (lang ?? '').trim().toLowerCase().startsWith('en') const en = (lang ?? '').trim().toLowerCase().startsWith('en')
return en return en
? `\n\n## Disabled tools\nThe following tools are disabled: ${list}. Do not call them.\n` ? `\n\n## Unavailable tools\nThe following tools are disabled or not granted to this token: ${list}. Do not call them; skip steps that need them.\n`
: `\n\n## 已禁用工具\n以下工具已禁用:${list},不要调用。\n` : `\n\n## 不可用工具\n以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。\n`
} }
function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string { function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string {
@ -191,7 +192,7 @@ function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string {
} }
const connection = getNetxConnection() const connection = getNetxConnection()
if (!connection?.apiUrl?.trim()) return '' if (!connection?.apiUrl?.trim()) return ''
return skillsCacheDir(connection.apiUrl) return usableSkillsCacheDir(connection.apiUrl, connection.token ?? '')
} }
/** /**
@ -236,6 +237,7 @@ export async function registerGroupSkills(
} }
return () => {} return () => {}
} }
const unavailable = cachedUnavailableTools(root)
try { try {
for (const groupId of CAPABILITY_GROUP_IDS) { for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) continue if (!enabled.has(groupId)) continue
@ -252,7 +254,11 @@ export async function registerGroupSkills(
join(root, SKILL_DIR_BY_GROUP[groupId]), join(root, SKILL_DIR_BY_GROUP[groupId]),
) )
} }
const footnote = disabledFootnote(options.disabledByGroup?.[groupId] ?? [], options.lang) const offNames = new Set([
...(options.disabledByGroup?.[groupId] ?? []),
...(unavailable[groupId] ?? []).map(publicToolName),
])
const footnote = disabledFootnote([...offNames].sort(), options.lang)
for (const skill of skills) { for (const skill of skills) {
const base = groupId === 'topology' const base = groupId === 'topology'
? `${skill.content}\n\n## DeepSeek Harness REST capabilities\nUse only tools present in this session. Dual-unit/orbit/polish recipes require server-side topology tools. Use only tools present in this session.\n` ? `${skill.content}\n\n## DeepSeek Harness REST capabilities\nUse only tools present in this session. Dual-unit/orbit/polish recipes require server-side topology tools. Use only tools present in this session.\n`

View file

@ -32,3 +32,19 @@ export function normalizeApiUrl(raw: string): NormalizeApiUrlResult {
const search = parsed.search && parsed.search !== '?' ? parsed.search : '' const search = parsed.search && parsed.search !== '?' ? parsed.search : ''
return { ok: true, url: `${parsed.origin}${path}${search}` } return { ok: true, url: `${parsed.origin}${path}${search}` }
} }
/**
* True for plain `http://` to a non-loopback host: the token and the skills
* injected into the model's prompt would travel unencrypted.
*/
export function isInsecureRemoteApiUrl(raw: string): boolean {
let url: URL
try {
url = new URL(raw.trim())
} catch {
return false
}
if (url.protocol !== 'http:') return false
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
return !(host === 'localhost' || host.endsWith('.localhost') || host === '::1' || /^127\./.test(host))
}

View file

@ -110,7 +110,16 @@ function parseSkillResource(
if (parts.length >= 3) relPath = parts.slice(2).join('/') if (parts.length >= 3) relPath = parts.slice(2).join('/')
} }
if (!relPath || relPath.includes('..')) return null if (!relPath || relPath.includes('..')) return null
return { group, skill, relPath, text } const unavailableTools = Array.isArray(netx.unavailableTools)
? netx.unavailableTools.filter((n): n is string => typeof n === 'string' && n.length > 0)
: []
return {
group,
skill,
relPath,
text,
...(netx.entry === true && unavailableTools.length > 0 ? { unavailableTools } : {}),
}
} }
function resourcesCapability(capabilities: unknown): boolean { function resourcesCapability(capabilities: unknown): boolean {

View file

@ -125,7 +125,7 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<
if (fetched.skills !== undefined) { if (fetched.skills !== undefined) {
// Includes "no resources capability" (empty list) → wipe stale files. // Includes "no resources capability" (empty list) → wipe stale files.
replaceSkillsCache(input.apiUrl, fetched.skills) replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token })
} else { } else {
// Transient resources failure: keep the last good skill cache. // Transient resources failure: keep the last good skill cache.
console.warn('netxops: mcp skills pull failed, keeping cache — %s', fetched.skillsError) console.warn('netxops: mcp skills pull failed, keeping cache — %s', fetched.skillsError)

View file

@ -2,6 +2,7 @@
* On-disk cache of netx MCP skill resources, keyed by API URL. * On-disk cache of netx MCP skill resources, keyed by API URL.
* *
* Layout: `$DSH_HOME/netxops-mcp-skills/<key>/<groupDir>/<skill>/<relPath>` * Layout: `$DSH_HOME/netxops-mcp-skills/<key>/<groupDir>/<skill>/<relPath>`
* plus `.meta.json` (listedAt, token fingerprint, per-group unavailable tools).
*/ */
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
@ -29,8 +30,23 @@ export interface CachedSkillFile {
/** Posix-relative path inside the skill directory (e.g. `SKILL.md`). */ /** Posix-relative path inside the skill directory (e.g. `SKILL.md`). */
relPath: string relPath: string
text: string text: string
/** SKILL.md only: server tools of this group the token cannot call. */
unavailableTools?: readonly string[]
} }
interface SkillsCacheMeta {
apiUrl?: string
mcpUrl?: string
listedAt?: string
fileCount?: number
/** sha256 prefix of the token that pulled this cache. */
tokenFp?: string
unavailableTools?: Partial<Record<NetxCapabilityGroupId, string[]>>
}
/** Caches untouched for this long (other netx addresses) are pruned on refresh. */
const STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000
function dshHome(): string { function dshHome(): string {
const fromEnv = process.env.DSH_HOME?.trim() const fromEnv = process.env.DSH_HOME?.trim()
return fromEnv || join(homedir(), '.dsh') return fromEnv || join(homedir(), '.dsh')
@ -48,9 +64,75 @@ export function skillsCacheKey(apiUrl: string): string {
return createHash('sha256').update(base).digest('hex').slice(0, 16) return createHash('sha256').update(base).digest('hex').slice(0, 16)
} }
function cacheParent(): string {
return join(dshHome(), 'netxops-mcp-skills')
}
/** Absolute cache root for one netx API URL (may not exist yet). */ /** Absolute cache root for one netx API URL (may not exist yet). */
export function skillsCacheDir(apiUrl: string): string { export function skillsCacheDir(apiUrl: string): string {
return join(dshHome(), 'netxops-mcp-skills', skillsCacheKey(apiUrl)) return join(cacheParent(), skillsCacheKey(apiUrl))
}
/** Short fingerprint of a token (never the token itself). */
export function tokenFingerprint(token: string): string {
const raw = token.trim()
return raw ? createHash('sha256').update(raw).digest('hex').slice(0, 16) : ''
}
function readMeta(root: string): SkillsCacheMeta | null {
try {
const meta = JSON.parse(readFileSync(join(root, '.meta.json'), 'utf8')) as unknown
return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta as SkillsCacheMeta : null
} catch {
return null
}
}
/**
* Cache root usable for this connection, or '' when the cache was pulled with
* a different token (switching to a narrower token must not keep mounting the
* broader token's skills while netx is offline).
*/
export function usableSkillsCacheDir(apiUrl: string, token: string): string {
if (!apiUrl.trim()) return ''
const root = skillsCacheDir(apiUrl)
const pulledWith = readMeta(root)?.tokenFp
if (pulledWith && pulledWith !== tokenFingerprint(token)) return ''
return root
}
/** Per-group tools the pulling token could not call (from `.meta.json`). */
export function cachedUnavailableTools(root: string): Partial<Record<NetxCapabilityGroupId, string[]>> {
const raw = readMeta(root)?.unavailableTools
const out: Partial<Record<NetxCapabilityGroupId, string[]>> = {}
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): n is string => typeof n === 'string')
}
return out
}
function pruneStaleCaches(keep: string): void {
const parent = cacheParent()
let entries: string[]
try {
entries = readdirSync(parent)
} catch {
return
}
const now = Date.now()
for (const name of entries) {
const dir = join(parent, name)
if (dir === keep) continue
try {
const listedAt = Date.parse(readMeta(dir)?.listedAt ?? '')
const age = now - (Number.isFinite(listedAt) ? listedAt : statSync(dir).mtimeMs)
if (age > STALE_CACHE_MS) rmSync(dir, { recursive: true, force: true })
} catch {
// best effort
}
}
} }
function isSafeSegment(value: string): boolean { function isSafeSegment(value: string): boolean {
@ -72,12 +154,21 @@ function isSafeRelPath(relPath: string): boolean {
* Call only after a successful tools/list when skills were resolved * Call only after a successful tools/list when skills were resolved
* (including “no resources / empty list”). * (including “no resources / empty list”).
*/ */
export function replaceSkillsCache(apiUrl: string, files: readonly CachedSkillFile[]): string { export function replaceSkillsCache(
apiUrl: string,
files: readonly CachedSkillFile[],
options: { token?: string } = {},
): string {
const root = skillsCacheDir(apiUrl) const root = skillsCacheDir(apiUrl)
rmSync(root, { recursive: true, force: true }) rmSync(root, { recursive: true, force: true })
mkdirSync(root, { recursive: true }) mkdirSync(root, { recursive: true })
let written = 0 let written = 0
const unavailableTools: Partial<Record<NetxCapabilityGroupId, string[]>> = {}
for (const file of files) { for (const file of files) {
if (file.unavailableTools?.length && SKILL_DIR_BY_GROUP[file.group]) {
const merged = new Set([...(unavailableTools[file.group] ?? []), ...file.unavailableTools])
unavailableTools[file.group] = [...merged].sort()
}
const groupDir = SKILL_DIR_BY_GROUP[file.group] const groupDir = SKILL_DIR_BY_GROUP[file.group]
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue
const rel = file.relPath.replace(/\\/g, '/') const rel = file.relPath.replace(/\\/g, '/')
@ -88,13 +179,17 @@ export function replaceSkillsCache(apiUrl: string, files: readonly CachedSkillFi
written += 1 written += 1
} }
const normalized = normalizeSkillsApiUrl(apiUrl) const normalized = normalizeSkillsApiUrl(apiUrl)
const meta = { const meta: SkillsCacheMeta = {
apiUrl: normalized, apiUrl: normalized,
mcpUrl: normalized ? `${normalized}/mcp` : '', mcpUrl: normalized ? `${normalized}/mcp` : '',
listedAt: new Date().toISOString(), listedAt: new Date().toISOString(),
fileCount: written, fileCount: written,
...(options.token !== undefined ? { tokenFp: tokenFingerprint(options.token) } : {}),
unavailableTools,
} }
writeFileSync(join(root, '.meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8') writeFileSync(join(root, '.meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8')
summaryMemo.delete(root)
pruneStaleCaches(root)
return root return root
} }
@ -125,18 +220,20 @@ function countFiles(dir: string): number {
return n return n
} }
/** Skills currently in the cache for `apiUrl` (what the plugin will mount). */ type CachedSkillsListing = { listedAt: string | null; skills: CachedSkillSummary[] }
export function listCachedSkills(apiUrl: string): { listedAt: string | null; skills: CachedSkillSummary[] } {
const root = skillsCacheDir(apiUrl) /** root → listing; invalidated by replaceSkillsCache, keyed by .meta.json listedAt. */
const summaryMemo = new Map<string, CachedSkillsListing>()
/** Skills the plugin will mount for this connection (Plugins card polls this). */
export function listCachedSkills(apiUrl: string, token = ''): CachedSkillsListing {
const root = usableSkillsCacheDir(apiUrl, token)
if (!root || !existsSync(root)) return { listedAt: null, skills: [] }
const metaListedAt = readMeta(root)?.listedAt
const listedAt = typeof metaListedAt === 'string' ? metaListedAt : null
const memo = summaryMemo.get(root)
if (memo && memo.listedAt === listedAt) return memo
const skills: CachedSkillSummary[] = [] const skills: CachedSkillSummary[] = []
let listedAt: string | null = null
if (!apiUrl.trim() || !existsSync(root)) return { listedAt, skills }
try {
const meta = JSON.parse(readFileSync(join(root, '.meta.json'), 'utf8')) as { listedAt?: unknown }
if (typeof meta.listedAt === 'string') listedAt = meta.listedAt
} catch {
// no meta yet
}
for (const group of CAPABILITY_GROUP_IDS) { for (const group of CAPABILITY_GROUP_IDS) {
const groupDir = join(root, SKILL_DIR_BY_GROUP[group]) const groupDir = join(root, SKILL_DIR_BY_GROUP[group])
if (!existsSync(groupDir)) continue if (!existsSync(groupDir)) continue
@ -150,5 +247,7 @@ export function listCachedSkills(apiUrl: string): { listedAt: string | null; ski
// unreadable group dir → skip // unreadable group dir → skip
} }
} }
return { listedAt, skills } const listing = { listedAt, skills }
summaryMemo.set(root, listing)
return listing
} }

View file

@ -54,7 +54,7 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
lang: 'zh', lang: 'zh',
}) })
assert.ok(registered.length >= 1) assert.ok(registered.length >= 1)
assert.match(registered[0].content, /以下工具已禁用:netx__execManagedNe,不要调用/) assert.match(registered[0].content, /以下工具已禁用或当前 token 无权使用:netx__execManagedNe,不要调用/)
dispose() dispose()
registered.length = 0 registered.length = 0
@ -71,6 +71,6 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
disabledByGroup: { ops: ['netx__execManagedNe'] }, disabledByGroup: { ops: ['netx__execManagedNe'] },
lang: 'en', lang: 'en',
}) })
assert.match(registered[0].content, /The following tools are disabled: netx__execManagedNe/) assert.match(registered[0].content, /disabled or not granted to this token: netx__execManagedNe/)
dispose3() dispose3()
}) })

View file

@ -5,7 +5,7 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import http from 'node:http' import http from 'node:http'
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os' import { tmpdir } from 'node:os'
import { join } from 'node:path' import { join } from 'node:path'
@ -14,6 +14,7 @@ import { refreshMcpCatalog, resetMcpCatalog } from '../src/netx/mcp-store.ts'
import { publishNetxConnection } from '../src/netx/runtime.ts' import { publishNetxConnection } from '../src/netx/runtime.ts'
import { import {
hasSkillsCache, hasSkillsCache,
usableSkillsCacheDir,
listCachedSkills, listCachedSkills,
replaceSkillsCache, replaceSkillsCache,
skillsCacheDir, skillsCacheDir,
@ -48,6 +49,7 @@ function skillResources(files) {
group: f.group, group: f.group,
skill: f.skill, skill: f.skill,
entry: f.relPath === 'SKILL.md', entry: f.relPath === 'SKILL.md',
...(f.unavailableTools ? { unavailableTools: f.unavailableTools } : {}),
}, },
}, },
text: f.text, text: f.text,
@ -365,3 +367,55 @@ test('listCachedSkills summarizes cached skills per group for the Plugins card',
]) ])
assert.deepEqual(listCachedSkills('').skills, []) assert.deepEqual(listCachedSkills('').skills, [])
}) })
test('tools the token cannot call are merged into the skill footnote', async (t) => {
withTempHome(t)
const resources = skillResources([{
group: 'ops', groupDir: 'ops', skill: 'netx-ops', relPath: 'SKILL.md',
text: OPS_SKILL, unavailableTools: ['execManagedNe'],
}])
const mock = await startMockMcp({ resources })
t.after(() => mock.server.close())
await refreshMcpCatalog({ apiUrl: mock.apiUrl, token: 'secret' })
publishNetxConnection({ apiUrl: mock.apiUrl, token: 'secret', lang: 'zh', toolCallTimeoutMs: 5_000 })
const { ctx, registered } = fakeSkillsCtx()
await registerGroupSkills(ctx, ['ops'], 'test', { disabledByGroup: { ops: ['netx__queryNmsAlarms'] } })
assert.match(registered[0].content, /netx__execManagedNe/)
assert.match(registered[0].content, /netx__queryNmsAlarms/)
assert.match(registered[0].content, /当前 token 无权使用/)
})
test('cache pulled with another token is not mounted', async (t) => {
withTempHome(t)
replaceSkillsCache(API, [{ group: 'ops', skill: 'netx-ops', relPath: 'SKILL.md', text: OPS_SKILL }], { token: 'wide' })
assert.ok(usableSkillsCacheDir(API, 'wide'))
assert.equal(usableSkillsCacheDir(API, 'narrow'), '')
assert.deepEqual(listCachedSkills(API, 'narrow').skills, [])
publishNetxConnection({ apiUrl: API, token: 'narrow', lang: 'zh', toolCallTimeoutMs: 5_000 })
const { ctx, registered } = fakeSkillsCtx()
await registerGroupSkills(ctx, ['ops'], 'test')
assert.equal(registered.length, 0)
})
test('refresh prunes caches of other netx addresses unused for 30 days', (t) => {
const home = withTempHome(t)
const stale = join(home, 'netxops-mcp-skills', 'deadbeefdeadbeef')
mkdirSync(stale, { recursive: true })
writeFileSync(join(stale, '.meta.json'), JSON.stringify({ listedAt: '2020-01-01T00:00:00.000Z' }))
const fresh = join(home, 'netxops-mcp-skills', 'feedfacefeedface')
mkdirSync(fresh, { recursive: true })
writeFileSync(join(fresh, '.meta.json'), JSON.stringify({ listedAt: new Date().toISOString() }))
replaceSkillsCache(API, [], { token: 'secret' })
assert.equal(existsSync(stale), false)
assert.equal(existsSync(fresh), true)
})
test('isInsecureRemoteApiUrl flags plain http only for non-loopback hosts', async () => {
const { isInsecureRemoteApiUrl } = await import('../src/netx/http.ts')
assert.equal(isInsecureRemoteApiUrl('http://127.0.0.1:8890'), false)
assert.equal(isInsecureRemoteApiUrl('http://localhost:8890'), false)
assert.equal(isInsecureRemoteApiUrl('http://[::1]:8890'), false)
assert.equal(isInsecureRemoteApiUrl('https://netx.example.com'), false)
assert.equal(isInsecureRemoteApiUrl('http://192.168.1.10:8890'), true)
assert.equal(isInsecureRemoteApiUrl('not a url'), false)
})