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

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1,5 +1,5 @@
// 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 { fileURLToPath as fileURLToPath2 } from "node:url";
@ -267,6 +267,15 @@ function mcpContentText(content, fallback = "") {
// src/netx/skills-cache.ts
import { createHash } from "node:crypto";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@ -293,6 +302,7 @@ function normalizeApiUrl(raw) {
}
// src/netx/skills-cache.ts
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join(homedir(), ".dsh");
@ -305,9 +315,46 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash("sha256").update(base).digest("hex").slice(0, 16);
}
function skillsCacheDir(apiUrl) {
return join(dshHome(), "netxops-mcp-skills", skillsCacheKey(apiUrl));
function cacheParent() {
return join(dshHome(), "netxops-mcp-skills");
}
function skillsCacheDir(apiUrl) {
return join(cacheParent(), skillsCacheKey(apiUrl));
}
function tokenFingerprint(token) {
const raw = token.trim();
return raw ? createHash("sha256").update(raw).digest("hex").slice(0, 16) : "";
}
function readMeta(root) {
try {
const meta = JSON.parse(readFileSync(join(root, ".meta.json"), "utf8"));
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : null;
} catch {
return null;
}
}
function usableSkillsCacheDir(apiUrl, token) {
if (!apiUrl.trim())
return "";
const root = skillsCacheDir(apiUrl);
const pulledWith = readMeta(root)?.tokenFp;
if (pulledWith && pulledWith !== tokenFingerprint(token))
return "";
return root;
}
function cachedUnavailableTools(root) {
const raw = readMeta(root)?.unavailableTools;
const out = {};
if (!raw || typeof raw !== "object")
return out;
for (const group of CAPABILITY_GROUP_IDS) {
const names = raw[group];
if (Array.isArray(names))
out[group] = names.filter((n) => typeof n === "string");
}
return out;
}
var summaryMemo = new Map;
// src/netx/mcp-store.ts
var STORE_KEY = Symbol.for("dsh-netxops.mcp-catalog-store");
@ -512,12 +559,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -527,7 +574,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -546,6 +593,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -559,7 +607,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join2(root, SKILL_DIR_BY_GROUP[groupId]));
}
const 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) {
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
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";
// 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";
var EMPTY_CONTENT = {
hasRegions: false,
@ -624,7 +676,7 @@ function resolveKbPaths(snapshot) {
const paths = { skills: "_skills", ...snapshot.paths };
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
const absolute = resolve(snapshot.realRoot, rel);
return { key, relative: rel, absolute, exists: existsSync(absolute) };
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
});
}
function renderKbPathTable(snapshot) {
@ -640,7 +692,7 @@ function renderKbPathTable(snapshot) {
}
// 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";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
@ -749,7 +801,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
}
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
try {
if (existsSync2(checked.absolutePath)) {
if (existsSync3(checked.absolutePath)) {
const realFile = realpathSync(checked.absolutePath);
const realLocal = realpathSync(localRoot);
const realRel = relative(realLocal, realFile);
@ -760,7 +812,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
throw new Error(`path not under writable roots: ${realRel}`);
}
if (!statSync2(realFile).isFile()) {
if (!statSync3(realFile).isFile()) {
throw new Error(`not a file: ${realFile}`);
}
return {
@ -813,7 +865,7 @@ function readKbIdentity(snapshot) {
const dir = join5(localRoot, "identity");
let names;
try {
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
names = readdirSync3(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
} catch {
return [];
}
@ -825,9 +877,9 @@ function readKbIdentity(snapshot) {
const full = join5(dir, name);
let text;
try {
if (!statSync3(full).isFile())
if (!statSync4(full).isFile())
continue;
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
text = readFileSync3(full, "utf8").replace(/^\uFEFF/, "").trim();
} catch {
continue;
}
@ -950,13 +1002,13 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
// src/netx/kb-local-ops.ts
import {
existsSync as existsSync3,
mkdirSync,
readdirSync as readdirSync3,
readFileSync as readFileSync3,
rmSync,
statSync as statSync4,
writeFileSync
existsSync as existsSync4,
mkdirSync as mkdirSync2,
readdirSync as readdirSync4,
readFileSync as readFileSync4,
rmSync as rmSync2,
statSync as statSync5,
writeFileSync as writeFileSync2
} from "node:fs";
import { dirname as dirname3, join as join6 } from "node:path";
function requireLocalRoot(snapshot) {
@ -1004,12 +1056,12 @@ function createMemory(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = memoryFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1023,12 +1075,12 @@ function createDraft(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = draftFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
@ -1040,12 +1092,12 @@ function createSuggestion(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = suggestionFileName({ date: args.date, slug: args.slug });
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
if (existsSync3(target.absolutePath) && !args.overwrite) {
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1059,12 +1111,12 @@ function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
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}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync3(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
mkdirSync2(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync2(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
@ -1077,13 +1129,13 @@ function createRef(snapshot, args) {
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
`) ? args.body : `${args.body}
`;
writeFileSync(target.absolutePath, body, "utf8");
writeFileSync2(target.absolutePath, body, "utf8");
return {
ok: true,
action: "updated",
@ -1094,10 +1146,10 @@ function updateLocalFile(snapshot, args) {
function deleteLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
if (!existsSync3(target.absolutePath)) {
if (!existsSync4(target.absolutePath)) {
throw new Error(`file not found: ${target.relativePath}`);
}
rmSync(target.absolutePath, { force: false });
rmSync2(target.absolutePath, { force: false });
return {
ok: true,
action: "deleted",
@ -1108,7 +1160,7 @@ function deleteLocalFile(snapshot, args) {
function walkFiles(dir, baseLocal, out) {
let entries;
try {
entries = readdirSync3(dir);
entries = readdirSync4(dir);
} catch {
return;
}
@ -1118,7 +1170,7 @@ function walkFiles(dir, baseLocal, out) {
const full = join6(dir, name);
let st;
try {
st = statSync4(full);
st = statSync5(full);
} catch {
continue;
}
@ -1152,7 +1204,7 @@ function listLocalFiles(snapshot, args = {}) {
}
// src/netx/kb-read-ops.ts
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
import { readdirSync as readdirSync5, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
import { extname, join as join7, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
var SKIP_DIRS = new Set([".git", "node_modules"]);
@ -1197,14 +1249,14 @@ function resolveKbReadPath(snapshot, pathArg) {
}
function readKbFile(snapshot, args) {
const target = resolveKbReadPath(snapshot, args.path);
const st = statSync5(target.absolutePath);
const st = statSync6(target.absolutePath);
if (st.isDirectory()) {
const entries = readdirSync4(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
const entries = readdirSync5(target.absolutePath, { withFileTypes: true }).filter((entry) => !SKIP_DIRS.has(entry.name)).map((entry) => {
if (entry.isDirectory())
return { name: entry.name, type: "dir" };
let size;
try {
size = statSync5(join7(target.absolutePath, entry.name)).size;
size = statSync6(join7(target.absolutePath, entry.name)).size;
} catch {}
return { name: entry.name, type: "file", size };
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
@ -1213,7 +1265,7 @@ function readKbFile(snapshot, args) {
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
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(`
`);
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);
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
return hit;
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
const text = readFileSync5(path, "utf8").replace(/^\uFEFF/, "");
if (hit)
cache.chars -= hit.text.length;
if (cache.chars + text.length > CACHE_MAX_CHARS) {
@ -1273,7 +1325,7 @@ function cachedText(path, mtimeMs, size) {
function walkTextFiles(dir, out) {
let entries;
try {
entries = readdirSync4(dir, { withFileTypes: true });
entries = readdirSync5(dir, { withFileTypes: true });
} catch {
return;
}
@ -1288,7 +1340,7 @@ function walkTextFiles(dir, out) {
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
continue;
try {
const st = statSync5(full);
const st = statSync6(full);
if (st.size <= SEARCH_MAX_FILE_BYTES)
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
} catch {}
@ -1318,7 +1370,7 @@ function searchKb(snapshot, args) {
const files = [];
const seen = new Set;
for (const dir of dirs) {
const st = statSync5(dir);
const st = statSync6(dir);
const batch = [];
if (st.isFile())
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
@ -1432,7 +1484,7 @@ function toLosslessJson(value) {
}
// 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 { dirname as dirname4, join as join8 } from "node:path";
var KEY = Symbol.for("dsh-netxops.mount-inventory");
@ -1459,8 +1511,8 @@ function getToolMountStatus() {
function persist() {
try {
const path = join8(process.env.DSH_HOME?.trim() || join8(homedir2(), ".dsh"), "netxops-mount-status.json");
mkdirSync2(dirname4(path), { recursive: true });
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
mkdirSync3(dirname4(path), { recursive: true });
writeFileSync3(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
`, "utf8");
} catch {}
}
@ -1811,7 +1863,7 @@ async function registerKbPackSkills(ctx, snapshot, options) {
}
// 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 { fileURLToPath } from "node:url";
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
@ -1916,7 +1968,7 @@ function parametersFromInputSchema(schema) {
function packageVersion() {
try {
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";
} catch {
return "0";
@ -2048,7 +2100,7 @@ function registerNetxTools(ctx, connection, options) {
function packageVersion2() {
try {
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";
} catch {
return "0";

View file

@ -1070,6 +1070,40 @@ function serializeDisabledTools(names) {
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
var jsx_runtime = require("react/jsx-runtime");
function phaseLocaleKey(phase) {
@ -1641,6 +1675,10 @@ function NetxopsCard(props) {
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", {
className: "dsh-nx-fieldGrid",
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
function createSnapshotStore(init) {
let state = init;
@ -3228,6 +3244,7 @@ var en = {
sectionExport: "Session export",
apiUrl: "API URL",
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.",
lang: "API / alarm language",
thinkingLanguage: "Thinking language",
@ -3324,6 +3341,7 @@ var zh = {
sectionExport: "会话导出",
apiUrl: "API 地址",
apiUrlHint: "http://127.0.0.1:8890",
apiUrlInsecure: "连接的是远程主机且用的是明文 HTTP:token 和发给模型的 skill 都不加密,请改用 https://。",
apiUrlInvalid: "请输入不含账号密码的 HTTP 或 HTTPS 地址。",
lang: "API / 告警语言",
thinkingLanguage: "思考语言",

View file

@ -1398,6 +1398,7 @@ import {
} from "node:fs";
import { homedir as homedir3 } from "node:os";
import { dirname as dirname2, join as join3 } from "node:path";
var STALE_CACHE_MS = 30 * 24 * 60 * 60 * 1000;
function dshHome() {
const fromEnv = process.env.DSH_HOME?.trim();
return fromEnv || join3(homedir3(), ".dsh");
@ -1410,8 +1411,65 @@ function skillsCacheKey(apiUrl) {
const base = normalizeSkillsApiUrl(apiUrl);
return createHash2("sha256").update(base).digest("hex").slice(0, 16);
}
function cacheParent() {
return join3(dshHome(), "netxops-mcp-skills");
}
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) {
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("/");
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
}
function replaceSkillsCache(apiUrl, files) {
function replaceSkillsCache(apiUrl, files, options = {}) {
const root = skillsCacheDir(apiUrl);
rmSync(root, { recursive: true, force: true });
mkdirSync2(root, { recursive: true });
let written = 0;
const unavailableTools = {};
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];
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath))
continue;
@ -1445,10 +1508,14 @@ function replaceSkillsCache(apiUrl, files) {
apiUrl: normalized,
mcpUrl: normalized ? `${normalized}/mcp` : "",
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)}
`, "utf8");
summaryMemo.delete(root);
pruneStaleCaches(root);
return root;
}
function countFiles(dir) {
@ -1459,17 +1526,17 @@ function countFiles(dir) {
}
return n;
}
function listCachedSkills(apiUrl) {
const root = skillsCacheDir(apiUrl);
var summaryMemo = new Map;
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 = [];
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) {
const groupDir = join3(root, SKILL_DIR_BY_GROUP[group]);
if (!existsSync3(groupDir))
@ -1483,7 +1550,15 @@ function listCachedSkills(apiUrl) {
}
} 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
@ -1614,12 +1689,12 @@ function disabledFootnote(names, lang) {
const en = (lang ?? "").trim().toLowerCase().startsWith("en");
return en ? `
## Disabled tools
The following tools are disabled: ${list}. Do not call them.
## Unavailable tools
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) {
@ -1629,7 +1704,7 @@ function resolveSkillsRoot(options) {
const connection = getNetxConnection();
if (!connection?.apiUrl?.trim())
return "";
return skillsCacheDir(connection.apiUrl);
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? "");
}
async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions) {
const options = signalOrOptions instanceof AbortSignal || signalOrOptions === undefined ? { signal: signalOrOptions } : signalOrOptions;
@ -1648,6 +1723,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
}
return () => {};
}
const unavailable = cachedUnavailableTools(root);
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId))
@ -1661,7 +1737,11 @@ async function registerGroupSkills(ctx, groupIds, providerLabel, signalOrOptions
if (skills.length === 0) {
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) {
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 { 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
function disabledSet(disabledTools) {
const out = new Set;
@ -4720,7 +4794,14 @@ function parseSkillResource(resource, text) {
}
if (!relPath || relPath.includes(".."))
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) {
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities))
@ -5086,7 +5167,7 @@ async function refreshMcpCatalog(input) {
if (gen !== store5().generation)
return store5().state;
if (fetched.skills !== undefined) {
replaceSkillsCache(input.apiUrl, fetched.skills);
replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token });
} else {
console.warn("netxops: mcp skills pull failed, keeping cache — %s", fetched.skillsError);
}
@ -5513,7 +5594,8 @@ function installPresetPublication(ctx) {
// src/index.ts
function mcpCatalogPayload(catalog) {
const cached = listCachedSkills(getNetxConnection()?.apiUrl ?? "");
const connection = getNetxConnection();
const cached = listCachedSkills(connection?.apiUrl ?? "", connection?.token ?? "");
return {
phase: catalog.phase,
mcpUrl: catalog.mcpUrl,

View file

@ -23,6 +23,7 @@ import { ensureStyles } from './styles.ts'
import { sessionsExportReasonLocaleKey } from './sessions-export-view.ts'
import { parsePresetIds } from '../netx/capability-groups.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 {} from '@deepseek-ai/dsh-client-ui-settings/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) }}
onReset={() => { props.resetField('apiUrl') }}
/>
{isInsecureRemoteApiUrl(state.apiUrl.text)
? <p className="dsh-nx-invalid">{t('apiUrlInsecure')}</p>
: null}
<div className="dsh-nx-fieldGrid">
<SelectField
id="netxops-lang"

View file

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

View file

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

View file

@ -12,7 +12,8 @@ import {
type NetxCapabilityGroupId,
} from './capability-groups.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 {
name: string
@ -181,8 +182,8 @@ function disabledFootnote(names: readonly string[], lang?: string): string {
const list = names.join(', ')
const en = (lang ?? '').trim().toLowerCase().startsWith('en')
return en
? `\n\n## Disabled tools\nThe following tools are disabled: ${list}. Do not call them.\n`
: `\n\n## 已禁用工具\n以下工具已禁用:${list},不要调用。\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以下工具已禁用或当前 token 无权使用:${list},不要调用,涉及它们的步骤请跳过。\n`
}
function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string {
@ -191,7 +192,7 @@ function resolveSkillsRoot(options: RegisterGroupSkillsOptions): string {
}
const connection = getNetxConnection()
if (!connection?.apiUrl?.trim()) return ''
return skillsCacheDir(connection.apiUrl)
return usableSkillsCacheDir(connection.apiUrl, connection.token ?? '')
}
/**
@ -236,6 +237,7 @@ export async function registerGroupSkills(
}
return () => {}
}
const unavailable = cachedUnavailableTools(root)
try {
for (const groupId of CAPABILITY_GROUP_IDS) {
if (!enabled.has(groupId)) continue
@ -252,7 +254,11 @@ export async function registerGroupSkills(
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) {
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`

View file

@ -32,3 +32,19 @@ export function normalizeApiUrl(raw: string): NormalizeApiUrlResult {
const search = parsed.search && parsed.search !== '?' ? parsed.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 (!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 {

View file

@ -125,7 +125,7 @@ export async function refreshMcpCatalog(input: RefreshMcpCatalogInput): Promise<
if (fetched.skills !== undefined) {
// Includes "no resources capability" (empty list) → wipe stale files.
replaceSkillsCache(input.apiUrl, fetched.skills)
replaceSkillsCache(input.apiUrl, fetched.skills, { token: input.token })
} else {
// Transient resources failure: keep the last good skill cache.
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.
*
* 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'
@ -29,8 +30,23 @@ export interface CachedSkillFile {
/** Posix-relative path inside the skill directory (e.g. `SKILL.md`). */
relPath: 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 {
const fromEnv = process.env.DSH_HOME?.trim()
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)
}
function cacheParent(): string {
return join(dshHome(), 'netxops-mcp-skills')
}
/** Absolute cache root for one netx API URL (may not exist yet). */
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 {
@ -72,12 +154,21 @@ function isSafeRelPath(relPath: string): boolean {
* Call only after a successful tools/list when skills were resolved
* (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)
rmSync(root, { recursive: true, force: true })
mkdirSync(root, { recursive: true })
let written = 0
const unavailableTools: Partial<Record<NetxCapabilityGroupId, string[]>> = {}
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]
if (!groupDir || !isSafeSegment(file.skill) || !isSafeRelPath(file.relPath)) continue
const rel = file.relPath.replace(/\\/g, '/')
@ -88,13 +179,17 @@ export function replaceSkillsCache(apiUrl: string, files: readonly CachedSkillFi
written += 1
}
const normalized = normalizeSkillsApiUrl(apiUrl)
const meta = {
const meta: SkillsCacheMeta = {
apiUrl: normalized,
mcpUrl: normalized ? `${normalized}/mcp` : '',
listedAt: new Date().toISOString(),
fileCount: written,
...(options.token !== undefined ? { tokenFp: tokenFingerprint(options.token) } : {}),
unavailableTools,
}
writeFileSync(join(root, '.meta.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8')
summaryMemo.delete(root)
pruneStaleCaches(root)
return root
}
@ -125,18 +220,20 @@ function countFiles(dir: string): number {
return n
}
/** Skills currently in the cache for `apiUrl` (what the plugin will mount). */
export function listCachedSkills(apiUrl: string): { listedAt: string | null; skills: CachedSkillSummary[] } {
const root = skillsCacheDir(apiUrl)
type CachedSkillsListing = { listedAt: string | null; skills: CachedSkillSummary[] }
/** 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[] = []
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) {
const groupDir = join(root, SKILL_DIR_BY_GROUP[group])
if (!existsSync(groupDir)) continue
@ -150,5 +247,7 @@ export function listCachedSkills(apiUrl: string): { listedAt: string | null; ski
// 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',
})
assert.ok(registered.length >= 1)
assert.match(registered[0].content, /以下工具已禁用:netx__execManagedNe,不要调用/)
assert.match(registered[0].content, /以下工具已禁用或当前 token 无权使用:netx__execManagedNe,不要调用/)
dispose()
registered.length = 0
@ -71,6 +71,6 @@ test('registerGroupSkills appends disabled footnote; skips disabled groups', asy
disabledByGroup: { ops: ['netx__execManagedNe'] },
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()
})

View file

@ -5,7 +5,7 @@
import assert from 'node:assert/strict'
import test from 'node:test'
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 { 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 {
hasSkillsCache,
usableSkillsCacheDir,
listCachedSkills,
replaceSkillsCache,
skillsCacheDir,
@ -48,6 +49,7 @@ function skillResources(files) {
group: f.group,
skill: f.skill,
entry: f.relPath === 'SKILL.md',
...(f.unavailableTools ? { unavailableTools: f.unavailableTools } : {}),
},
},
text: f.text,
@ -365,3 +367,55 @@ test('listCachedSkills summarizes cached skills per group for the Plugins card',
])
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)
})