mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-20 02:43:21 +08:00
Fix KB loading bugs and add jailed KB read/search tools (0.1.71).
- Strip UTF-8 BOM before parsing MANIFEST.json (PowerShell 5.1 output).
- kb.reload force-republishes so edited SKILL.md / new local_skills / identity apply.
- Duplicate or rejected pack skills are skipped instead of unregistering the whole pack.
- kb-context and every pack skill carry resolved absolute paths.* anchors; KB_PATHS env.
- Inline {paths.local}/identity/*.md into kb-context (capped).
- New netx__kbSearch / netx__kbRead host tools, realpath-jailed to kbRoot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b2166e5b6
commit
2def53e7c7
22 changed files with 4286 additions and 1013 deletions
|
|
@ -2,9 +2,9 @@
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"builds": {
|
"builds": {
|
||||||
"lib/agent-tools-biz-monitor.js": {
|
"lib/agent-tools-biz-monitor.js": {
|
||||||
"output": "2e870f4d26b8b9560ad4ebced0e8c6b02bc265bd677acae0e85e7f98de1e22ff",
|
"output": "8e72f3ef56c2700444531f0c0af778efbb4838b1f67d42e4fc8711aa575f544a",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -16,13 +16,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -30,9 +31,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/agent-tools-common.js": {
|
"lib/agent-tools-common.js": {
|
||||||
"output": "87742324e6d39db0e42a2a28866a97835aacfceb24d6105ff7a76bc9ec00af2d",
|
"output": "a4bd50da911bed64245ebee31a27f4f112729e7b35b99816671c3253e89719f0",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -44,13 +45,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -58,9 +60,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/agent-tools-nms.js": {
|
"lib/agent-tools-nms.js": {
|
||||||
"output": "5433fcebca463c2d3bde07f5cc948f572706153c41797e0d805c2a6c9fbff9a0",
|
"output": "b67bb855df12beb0ee24c1c3cd3fb0d42203c060568731784e9a14b545060214",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -72,13 +74,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -86,9 +89,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/agent-tools-ops.js": {
|
"lib/agent-tools-ops.js": {
|
||||||
"output": "9945eb9b4e400b5cab9de885f75d29cc43da53f7e129071b22d019c10ed8e698",
|
"output": "4515ae3096e1698f6e8b059c84eb4b91f0f3f62ca1831551484f581d29c28a80",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -100,13 +103,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -114,9 +118,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/agent-tools-topology.js": {
|
"lib/agent-tools-topology.js": {
|
||||||
"output": "44f4fba8d174bdcfc6b55a38d1fb1e32828f0ac8c3c87a14ff02a43693ca46ef",
|
"output": "54f0b223025fcc1618771cb1148f41547cb66ea296e238b9ad214fef28bd73ae",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -128,13 +132,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -142,9 +147,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/agent-tools.js": {
|
"lib/agent-tools.js": {
|
||||||
"output": "a1978da94dae7fc721c386a147a4ddbab9ec1a055ab86ccedc3565e0f96bced5",
|
"output": "16f2f1ec267929d2055546eebf980544db5758590a7e1b4073c2d4ae5843ddde",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -156,13 +161,14 @@
|
||||||
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
"src/netx/handlers.ts": "5b6f86c96ccd57f00183c98f9fa122101d485444cc73d04a5d1529cd8522d08d",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
"src/netx/runtime.ts": "eca10943a2ade5f0460f92a69a1d26abb77981ff56dcf00e6199b8586a818450",
|
||||||
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
"src/netx/tool-mount-status.ts": "783981774d717ca5e7bf7c0ec7273b40787ef12a6b40749c74543b30fa390faf",
|
||||||
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
"src/netx/tools.ts": "3027ee16ed8df3de020b07d0fa39d91b7c5676fbea6fcf074309175a0d853508",
|
||||||
|
|
@ -172,7 +178,7 @@
|
||||||
"lib/client.js": {
|
"lib/client.js": {
|
||||||
"output": "cd0b85687902aae42158838e000f7e9a6bef488abbcbf6c7885d0a87c40e00f8",
|
"output": "cd0b85687902aae42158838e000f7e9a6bef488abbcbf6c7885d0a87c40e00f8",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
|
"scripts/build-client.mjs": "bbb37a7d5024772f35824a4a3b5b9bc75fc670da391151e35a3811a677e23fd1",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
|
|
@ -193,19 +199,19 @@
|
||||||
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
"src/netx/config-values.ts": "7e8ca894d0b09f5e52686d6b293e49b46dd2cc13d714b0e0b103c3d07433e117",
|
||||||
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
"src/netx/http.ts": "1846030e34e03eeebd44f5ed87d92dcdab62429c4f3f740d6c2dcfafd1854f7d",
|
||||||
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"
|
"src/session-export-shared.ts": "e30d884ec22a0ea2e016c1479f62a8bc8d7ddcce5369a36647d411d07a8620d5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lib/index.js": {
|
"lib/index.js": {
|
||||||
"output": "514890b88808556213c851071faf0c73a4e5761a5eac73c9a2bd3376a08fb304",
|
"output": "982dd1a42b990fdd17467a5963b11494ac1b5f32ba54dffdeee414ed84df5908",
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"package.json": "3b8ffe892624815c6dab4313c01056fc2c2d573a3e4e6771a6ff63918d4a8dab",
|
"package.json": "1bb1fb8d615f49c69665a3a89b5912fa414a3b93aa3c541ce909ff57f97e100d",
|
||||||
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
"pnpm-lock.yaml": "f1209ac70e9c2294e0f300c0aad88b64aeecd6aa19dfc929c5766c7d033615a3",
|
||||||
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
"scripts/build-host.mjs": "1210c12b05a31f4ed64439fe5f08750cb75605b30327210cd97584b64aadf6a8",
|
||||||
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
"scripts/build-integrity.mjs": "18e9e32c452ef6217d771675fa8b763464a27ac0cf192d5fbe5cb1682380b0d9",
|
||||||
"src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc",
|
"src/agent-preset-install.ts": "1f397bb8e1c9a3f13cb7e4c442c3d13c42fce94fbc4eb1d90b85a0485bda48bc",
|
||||||
"src/index.ts": "a7c4a08590af7cfe2a9fa671fa5518e9b6de13d96f5360349e4d8a6ed2cb0517",
|
"src/index.ts": "dc869281bf04502e63abf69d7502afa834cfb2f6cbafb8e1a97e009c15844d61",
|
||||||
"src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62",
|
"src/netx/alarm-dispatch.ts": "9f476f1c620e40d3e2017554ec2fa8a1848f75d7e77e7c9ee5ee0464e970ba62",
|
||||||
"src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf",
|
"src/netx/alarm-im.ts": "a75b37e379500cbb90f1f4b81d448d45a97fd9e744e507608910f510480cffaf",
|
||||||
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
|
"src/netx/alarm-push-status.ts": "21f0efa33ea6f21e3ede35b4accbc6c0c4c78fea23631f1c5b80e6e9aef8b82f",
|
||||||
|
|
@ -221,13 +227,14 @@
|
||||||
"src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082",
|
"src/netx/i18n.ts": "9278e4adaa96cae413326f4dd8c796f2712d4749bcf92e5170967e38cee2d082",
|
||||||
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
"src/netx/im-targets.ts": "123613038dee8b14ce63ea46e88029287c1a8c527e6ba6cadab02f8c95b28917",
|
||||||
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
"src/netx/json-safe.ts": "5fe610d5ec96d31096733f7e828b4b2266c19692535a232a644bb49784ee24d9",
|
||||||
"src/netx/kb-context-skill.ts": "48f2b1f10c1615aa79d3965f97a4ffb27b7bb91ec9c2ec06681fbe7d9e0a70f0",
|
"src/netx/kb-context-skill.ts": "e0375006ba1e505fa6e5fd50f1748d7b43bb8c34809d10a0549e934006eb3045",
|
||||||
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
"src/netx/kb-local-ops.ts": "2c28ff25c8abcb68e2b197767a3eea0e056bcc3aa4ccdd6561d069a7a0da2463",
|
||||||
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
"src/netx/kb-local-path.ts": "343b8f653e3016f41396125c0a82b015ba325698d4b1642bf4459dd0365d153c",
|
||||||
"src/netx/kb-local-tools.ts": "c3ae8119e1a917498d945161bfe4965401fce5d167144d6500fba8205993511a",
|
"src/netx/kb-local-tools.ts": "5dc2f5c058231b96c130b67cfcf0458bda9804d12ed1746a4b3db371bfcc8881",
|
||||||
"src/netx/kb-manifest.ts": "17dcf4ede421485eabcefde63232c8d79e55e76c0374eae74405fb3a35328166",
|
"src/netx/kb-manifest.ts": "0389b1801aa2028b2a05044f5b4560273e9ea77f11145280d7620762df4cad02",
|
||||||
"src/netx/kb-pack-skills.ts": "0a2e43f04b2301707439764d29a60d60aeb07946a66dfc024f1b6aed8a3556d8",
|
"src/netx/kb-pack-skills.ts": "a843b91bf45ba7072b330945bd692eb4c36922958f8d8875b9a163c00fd096e5",
|
||||||
"src/netx/kb-runtime.ts": "71ea48399260ebb228633f26d77411d808c41b7c17946901582af387070f1a3d",
|
"src/netx/kb-read-ops.ts": "1359b370293b9f4afa51774ed729525d77756f1552cd46abf4d4d6c595eaeb86",
|
||||||
|
"src/netx/kb-runtime.ts": "ebf2fd36c407439f1a59344af18d9fe7275825925adbe08719595d06161d6d5b",
|
||||||
"src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e",
|
"src/netx/model-language.ts": "07df448b407086237c184a71bfc78e7cb27e50d7f8c499995d8b368b14780c9e",
|
||||||
"src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214",
|
"src/netx/netxops-web-rpc.ts": "c6981c91b0a300806d44f45708f07c5102ec40655e5d387394608e5472ad3214",
|
||||||
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
|
"src/netx/preset-publication.ts": "22e357cb58bf72cb0d269246f9e3e083269feb1706a9c4f152eb042330c9e967",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ dsh web
|
||||||
使用已经下载的发布包,例如:
|
使用已经下载的发布包,例如:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dsh plugin --profile web add "D:\Downloads\dsh-netxops-0.1.70.tgz"
|
dsh plugin --profile web add "D:\Downloads\dsh-netxops-0.1.71.tgz"
|
||||||
dsh web
|
dsh web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,15 @@ Authoritative packaging prose lives in the workspace contract notes (`插件` /
|
||||||
|
|
||||||
| Channel | Fields |
|
| Channel | Fields |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| `process.env` | `KB_ROOT`, `KB_LOCAL` (when `paths.local` set), `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT` (JSON of `has*`), `KB_STATUS` |
|
| `process.env` | `KB_ROOT`, `KB_LOCAL` (when `paths.local` set), `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT` (JSON of `has*`), `KB_PATHS` (JSON `{key: absolutePath}` of every `paths.*`), `KB_STATUS` |
|
||||||
| Skill `kb-context` | Same MANIFEST fields + `kbLocal` + dual-plane write rules (refs / memories / drafts / suggestions) |
|
| Skill `kb-context` | Same MANIFEST fields + `kbLocal` + **resolved absolute anchor table for every `paths.*`** + `{paths.local}/identity/*.md` inlined (README excluded; ≤8 KB/file, ≤24 KB total) + dual-plane write rules |
|
||||||
| Skills from pack | Registered when `configured` **and** `hasSkills` **and** the inPreset/public toggle for that plane |
|
| Skills from pack | Registered when `configured` **and** `hasSkills` **and** the inPreset/public toggle for that plane. Each pack skill gets the same resolved anchor table appended, so playbooks never need to re-read MANIFEST or compose `regions/<c>/<o>/…` |
|
||||||
|
| KB read tools | When `configured` + KB toggles: `netx__kbSearch` / `netx__kbRead`, jailed (realpath-checked) to `kbRoot` |
|
||||||
| Local FS tools | When `configured` + `paths.local` + same KB toggles: `netx__kbWriteRef` / `WriteMemory` / `WriteDraft` / `WriteSuggestion` / `UpdateLocal` / `DeleteLocal` / `ListLocal` — jail under `refs\|memories\|drafts\|suggestions`. HQ pack is read-only; evolve site product knowledge with `kbWriteRef` (per-`host_name` PROFILE) and capture experience with `kbWriteMemory` as it appears. |
|
| Local FS tools | When `configured` + `paths.local` + same KB toggles: `netx__kbWriteRef` / `WriteMemory` / `WriteDraft` / `WriteSuggestion` / `UpdateLocal` / `DeleteLocal` / `ListLocal` — jail under `refs\|memories\|drafts\|suggestions`. HQ pack is read-only; evolve site product knowledge with `kbWriteRef` (per-`host_name` PROFILE) and capture experience with `kbWriteMemory` as it appears. |
|
||||||
|
|
||||||
RPC (channel `/netxops`): `kb.status` (published snapshot), `kb.reload` (re-resolve live `kbRoot` + republish), `kb.resolve` with `{ path }` (preview unsaved paths).
|
RPC (channel `/netxops`): `kb.status` (published snapshot), `kb.reload` (re-resolve live `kbRoot` + **force** republish — remounts skills/tools even when MANIFEST is unchanged, so edited `SKILL.md`, new `local_skills/` or `identity/` files are picked up), `kb.resolve` with `{ path }` (preview unsaved paths).
|
||||||
|
|
||||||
|
MANIFEST may carry a UTF-8 BOM (PowerShell 5.1 `Out-File`/`Set-Content -Encoding UTF8`); it is stripped before parsing. A `local_skills/` bundle whose `name` duplicates a pack skill is skipped with a warning (pack wins) instead of aborting the whole pack registration.
|
||||||
|
|
||||||
## Pack skill registration
|
## Pack skill registration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,12 @@ Execution: HTTP `apiUrl` + Bearer `NETX_API_TOKEN`.
|
||||||
|
|
||||||
## knowledge base → `groupKb*` (not ops/topology)
|
## knowledge base → `groupKb*` (not ops/topology)
|
||||||
|
|
||||||
Registered when KB is configured, MANIFEST has `paths.local`, and the KB in-preset / public toggle is on. Host-side FS writes (bypass workspace sandbox). Jail: `{kbLocal}/refs|memories|drafts|suggestions`.
|
Registered when KB is configured and the KB in-preset / public toggle is on. Read tools are jailed to `kbRoot`; write tools additionally need MANIFEST `paths.local` and are jailed to `{kbLocal}/refs|memories|drafts|suggestions`. Host-side FS (bypass workspace sandbox).
|
||||||
|
|
||||||
| Tool | Role |
|
| Tool | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| `kbSearch` | Keyword (space-separated, case-insensitive) or regex search over the whole package or `scope` = MANIFEST paths key(s) / relative dir. Files ranked by distinct terms matched; ≤4 hit lines each. Stat-validated in-memory cache. |
|
||||||
|
| `kbRead` | Read one text file (offset/limit line paging) or list a directory. `path` = kbRoot-relative, absolute inside kbRoot, or a paths key (`neIndex`, `cmdLib`, `rca`, …). |
|
||||||
| `kbWriteRef` | Site product KB under `refs/{inventory\|devices\|topology\|business\|commands\|handbooks}/`. `devices` requires `device=host_name`; primary profile slug=`PROFILE`. |
|
| `kbWriteRef` | Site product KB under `refs/{inventory\|devices\|topology\|business\|commands\|handbooks}/`. `devices` requires `device=host_name`; primary profile slug=`PROFILE`. |
|
||||||
| `kbWriteMemory` | Diary under `memories/{日常笔记\|排障复盘\|AI思维链}/` — write as soon as reusable experience appears. |
|
| `kbWriteMemory` | Diary under `memories/{日常笔记\|排障复盘\|AI思维链}/` — write as soon as reusable experience appears. |
|
||||||
| `kbWriteDraft` | New `DRAFT-…` under `drafts/` (+ `status: draft`) |
|
| `kbWriteDraft` | New `DRAFT-…` under `drafts/` (+ `status: draft`) |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
import { readFileSync as readFileSync3 } from "node:fs";
|
import { readFileSync as readFileSync6 } from "node:fs";
|
||||||
import { dirname as dirname5, join as join7 } from "node:path";
|
import { dirname as dirname6, join as join10 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
||||||
|
|
||||||
// src/netx/config-values.ts
|
// src/netx/config-values.ts
|
||||||
|
|
@ -324,9 +324,56 @@ Use only tools present in this session. Native layout supports catalog and move_
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/netx/kb-context-skill.ts
|
||||||
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
|
||||||
|
import { join as join4 } from "node:path";
|
||||||
|
|
||||||
|
// src/netx/kb-manifest.ts
|
||||||
|
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { basename, dirname as dirname2, join as join2, resolve } from "node:path";
|
||||||
|
var EMPTY_CONTENT = {
|
||||||
|
hasRegions: false,
|
||||||
|
hasTheory: false,
|
||||||
|
hasPacket: false,
|
||||||
|
hasCommon: false,
|
||||||
|
hasSkills: false
|
||||||
|
};
|
||||||
|
function unconfiguredKbSnapshot() {
|
||||||
|
return {
|
||||||
|
status: "unconfigured",
|
||||||
|
realRoot: "",
|
||||||
|
operatorName: "",
|
||||||
|
country: "",
|
||||||
|
version: "",
|
||||||
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
|
errorMessage: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function resolveKbPaths(snapshot) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return [];
|
||||||
|
const paths = { skills: "_skills", ...snapshot.paths };
|
||||||
|
return Object.entries(paths).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "").map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel);
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync2(absolute) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function renderKbPathTable(snapshot) {
|
||||||
|
const rows = resolveKbPaths(snapshot);
|
||||||
|
if (rows.length === 0)
|
||||||
|
return "(MANIFEST has no paths — legacy package; ask HQ for a repack)";
|
||||||
|
return [
|
||||||
|
"| paths.* | Absolute path |",
|
||||||
|
"| --- | --- |",
|
||||||
|
...rows.map((row) => `| ${row.key} | \`${row.absolute}\`${row.exists ? "" : " (missing)"} |`)
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-local-path.ts
|
// src/netx/kb-local-path.ts
|
||||||
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
||||||
import { join as join2, relative, resolve, sep } from "node:path";
|
import { join as join3, relative, resolve as resolve2, sep } from "node:path";
|
||||||
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
|
||||||
"memories",
|
"memories",
|
||||||
"drafts",
|
"drafts",
|
||||||
|
|
@ -352,10 +399,7 @@ function resolveKbLocalRoot(snapshot) {
|
||||||
const rel = snapshot.paths.local?.trim();
|
const rel = snapshot.paths.local?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return resolve(snapshot.realRoot, rel);
|
return resolve2(snapshot.realRoot, rel);
|
||||||
}
|
|
||||||
function kbLocalToolsEnabled(snapshot) {
|
|
||||||
return resolveKbLocalRoot(snapshot) !== null;
|
|
||||||
}
|
}
|
||||||
function sanitizeSlug(raw, fallback = "entry") {
|
function sanitizeSlug(raw, fallback = "entry") {
|
||||||
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
const cleaned = raw.trim().replace(/[/\\]+/g, "-").replace(/\.\.+/g, "").replace(/[^\w.\u4e00-\u9fff-]+/gu, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
@ -407,7 +451,7 @@ function resolveWritableLocalPath(localRoot, ...segments) {
|
||||||
if (segments.some((s) => s.includes("\x00"))) {
|
if (segments.some((s) => s.includes("\x00"))) {
|
||||||
throw new Error("path segment contains NUL");
|
throw new Error("path segment contains NUL");
|
||||||
}
|
}
|
||||||
const absolutePath = resolve(localRoot, ...segments);
|
const absolutePath = resolve2(localRoot, ...segments);
|
||||||
const rel = relative(localRoot, absolutePath);
|
const rel = relative(localRoot, absolutePath);
|
||||||
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
if (!rel || rel.startsWith("..") || rel.split(/[/\\]/).includes("..")) {
|
||||||
throw new Error(`path escapes local root: ${absolutePath}`);
|
throw new Error(`path escapes local root: ${absolutePath}`);
|
||||||
|
|
@ -426,18 +470,18 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
const trimmed = pathArg.trim();
|
const trimmed = pathArg.trim();
|
||||||
if (!trimmed)
|
if (!trimmed)
|
||||||
throw new Error("path is empty");
|
throw new Error("path is empty");
|
||||||
const candidate = resolve(trimmed);
|
const candidate = resolve2(trimmed);
|
||||||
let absolutePath;
|
let absolutePath;
|
||||||
const localResolved = resolve(localRoot);
|
const localResolved = resolve2(localRoot);
|
||||||
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
const underLocal = candidate === localResolved || candidate.startsWith(localResolved + sep);
|
||||||
if (underLocal) {
|
if (underLocal) {
|
||||||
absolutePath = candidate;
|
absolutePath = candidate;
|
||||||
} else {
|
} else {
|
||||||
absolutePath = resolve(localRoot, trimmed);
|
absolutePath = resolve2(localRoot, trimmed);
|
||||||
}
|
}
|
||||||
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean));
|
||||||
try {
|
try {
|
||||||
if (existsSync2(checked.absolutePath)) {
|
if (existsSync3(checked.absolutePath)) {
|
||||||
const realFile = realpathSync(checked.absolutePath);
|
const realFile = realpathSync(checked.absolutePath);
|
||||||
const realLocal = realpathSync(localRoot);
|
const realLocal = realpathSync(localRoot);
|
||||||
const realRel = relative(realLocal, realFile);
|
const realRel = relative(realLocal, realFile);
|
||||||
|
|
@ -448,7 +492,7 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
if (!KB_LOCAL_WRITABLE_ROOTS.includes(top)) {
|
||||||
throw new Error(`path not under writable roots: ${realRel}`);
|
throw new Error(`path not under writable roots: ${realRel}`);
|
||||||
}
|
}
|
||||||
if (!statSync(realFile).isFile()) {
|
if (!statSync2(realFile).isFile()) {
|
||||||
throw new Error(`not a file: ${realFile}`);
|
throw new Error(`not a file: ${realFile}`);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -465,16 +509,16 @@ function resolveExistingWritableFile(localRoot, pathArg) {
|
||||||
return checked;
|
return checked;
|
||||||
}
|
}
|
||||||
function memoryDir(bucket) {
|
function memoryDir(bucket) {
|
||||||
return join2("memories", MEMORY_BUCKET_DIR[bucket]);
|
return join3("memories", MEMORY_BUCKET_DIR[bucket]);
|
||||||
}
|
}
|
||||||
function suggestionDir(kind) {
|
function suggestionDir(kind) {
|
||||||
return join2("suggestions", kind);
|
return join3("suggestions", kind);
|
||||||
}
|
}
|
||||||
function draftDir(domain) {
|
function draftDir(domain) {
|
||||||
const d = domain?.trim();
|
const d = domain?.trim();
|
||||||
if (!d)
|
if (!d)
|
||||||
return "drafts";
|
return "drafts";
|
||||||
return join2("drafts", sanitizeSlug(d, "misc"));
|
return join3("drafts", sanitizeSlug(d, "misc"));
|
||||||
}
|
}
|
||||||
function refDir(area, device) {
|
function refDir(area, device) {
|
||||||
if (!REF_AREAS.includes(area)) {
|
if (!REF_AREAS.includes(area)) {
|
||||||
|
|
@ -485,18 +529,63 @@ function refDir(area, device) {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw new Error("refs/devices requires device (host_name)");
|
throw new Error("refs/devices requires device (host_name)");
|
||||||
}
|
}
|
||||||
return join2("refs", "devices", host);
|
return join3("refs", "devices", host);
|
||||||
}
|
}
|
||||||
return join2("refs", area);
|
return join3("refs", area);
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-context-skill.ts
|
// src/netx/kb-context-skill.ts
|
||||||
var SKILL_NAME = "kb-context";
|
var SKILL_NAME = "kb-context";
|
||||||
|
var IDENTITY_FILE_MAX = 8000;
|
||||||
|
var IDENTITY_TOTAL_MAX = 24000;
|
||||||
|
function readKbIdentity(snapshot) {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
if (!localRoot)
|
||||||
|
return [];
|
||||||
|
const dir = join4(localRoot, "identity");
|
||||||
|
let names;
|
||||||
|
try {
|
||||||
|
names = readdirSync2(dir).filter((name) => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort();
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
let budget = IDENTITY_TOTAL_MAX;
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0)
|
||||||
|
break;
|
||||||
|
const full = join4(dir, name);
|
||||||
|
let text;
|
||||||
|
try {
|
||||||
|
if (!statSync3(full).isFile())
|
||||||
|
continue;
|
||||||
|
text = readFileSync2(full, "utf8").replace(/^\uFEFF/, "").trim();
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!text)
|
||||||
|
continue;
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget);
|
||||||
|
if (text.length > cap)
|
||||||
|
text = `${text.slice(0, cap)}
|
||||||
|
|
||||||
|
…(truncated — read \`${full}\` for the rest)`;
|
||||||
|
budget -= text.length;
|
||||||
|
out.push({ name, text });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function skillBody(snapshot) {
|
function skillBody(snapshot) {
|
||||||
if (snapshot.status === "configured") {
|
if (snapshot.status === "configured") {
|
||||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
|
||||||
|
const identity = readKbIdentity(snapshot);
|
||||||
|
const identityBlock = identity.length > 0 ? [
|
||||||
|
"",
|
||||||
|
"### Site identity (`identity/`, authoritative — follow over generic advice)",
|
||||||
|
...identity.flatMap(({ name, text }) => ["", `#### ${name}`, "", text])
|
||||||
|
] : [];
|
||||||
const localGuide = localRoot ? [
|
const localGuide = localRoot ? [
|
||||||
"",
|
"",
|
||||||
"### Dual plane (mandatory)",
|
"### Dual plane (mandatory)",
|
||||||
|
|
@ -521,7 +610,7 @@ function skillBody(snapshot) {
|
||||||
"No local write tools until `paths.local` is present in MANIFEST."
|
"No local write tools until `paths.local` is present in MANIFEST."
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
|
description: `Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
"## Knowledge base (configured)",
|
"## Knowledge base (configured)",
|
||||||
"",
|
"",
|
||||||
|
|
@ -537,13 +626,22 @@ function skillBody(snapshot) {
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
"",
|
||||||
|
"### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)",
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
"",
|
"",
|
||||||
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
|
"### Reading the HQ pack",
|
||||||
|
"- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).",
|
||||||
|
"- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).",
|
||||||
|
"- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.",
|
||||||
"",
|
"",
|
||||||
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
"Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.",
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
"",
|
||||||
"plus paths.localSkills when present — use those skills for KB triage;",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills",
|
||||||
|
"when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -584,15 +682,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
// src/netx/kb-local-ops.ts
|
// src/netx/kb-local-ops.ts
|
||||||
import {
|
import {
|
||||||
existsSync as existsSync3,
|
existsSync as existsSync4,
|
||||||
mkdirSync,
|
mkdirSync,
|
||||||
readdirSync,
|
readdirSync as readdirSync3,
|
||||||
readFileSync,
|
readFileSync as readFileSync3,
|
||||||
rmSync,
|
rmSync,
|
||||||
statSync as statSync2,
|
statSync as statSync4,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
import { dirname as dirname2, join as join3 } from "node:path";
|
import { dirname as dirname3, join as join5 } from "node:path";
|
||||||
function requireLocalRoot(snapshot) {
|
function requireLocalRoot(snapshot) {
|
||||||
const root = resolveKbLocalRoot(snapshot);
|
const root = resolveKbLocalRoot(snapshot);
|
||||||
if (!root) {
|
if (!root) {
|
||||||
|
|
@ -638,11 +736,11 @@ function createMemory(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = memoryFileName({ date: args.date, slug: args.slug });
|
const name = memoryFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -657,11 +755,11 @@ function createDraft(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = draftFileName({ date: args.date, slug: args.slug });
|
const name = draftFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
writeFileSync(target.absolutePath, ensureDraftBody(args.body), "utf8");
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
@ -674,11 +772,11 @@ function createSuggestion(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
const name = suggestionFileName({ date: args.date, slug: args.slug });
|
||||||
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -693,11 +791,11 @@ function createRef(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const name = refFileName(args.slug);
|
const name = refFileName(args.slug);
|
||||||
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
|
||||||
if (existsSync3(target.absolutePath) && !args.overwrite) {
|
if (existsSync4(target.absolutePath) && !args.overwrite) {
|
||||||
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
mkdirSync(dirname2(target.absolutePath), { recursive: true });
|
mkdirSync(dirname3(target.absolutePath), { recursive: true });
|
||||||
const existed = existsSync3(target.absolutePath);
|
const existed = existsSync4(target.absolutePath);
|
||||||
writeFileSync(target.absolutePath, args.body.endsWith(`
|
writeFileSync(target.absolutePath, args.body.endsWith(`
|
||||||
`) ? args.body : `${args.body}
|
`) ? args.body : `${args.body}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
|
|
@ -711,7 +809,7 @@ function createRef(snapshot, args) {
|
||||||
function updateLocalFile(snapshot, args) {
|
function updateLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(`
|
||||||
|
|
@ -728,7 +826,7 @@ function updateLocalFile(snapshot, args) {
|
||||||
function deleteLocalFile(snapshot, args) {
|
function deleteLocalFile(snapshot, args) {
|
||||||
const localRoot = requireLocalRoot(snapshot);
|
const localRoot = requireLocalRoot(snapshot);
|
||||||
const target = resolveExistingWritableFile(localRoot, args.path);
|
const target = resolveExistingWritableFile(localRoot, args.path);
|
||||||
if (!existsSync3(target.absolutePath)) {
|
if (!existsSync4(target.absolutePath)) {
|
||||||
throw new Error(`file not found: ${target.relativePath}`);
|
throw new Error(`file not found: ${target.relativePath}`);
|
||||||
}
|
}
|
||||||
rmSync(target.absolutePath, { force: false });
|
rmSync(target.absolutePath, { force: false });
|
||||||
|
|
@ -742,17 +840,17 @@ function deleteLocalFile(snapshot, args) {
|
||||||
function walkFiles(dir, baseLocal, out) {
|
function walkFiles(dir, baseLocal, out) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = readdirSync(dir);
|
entries = readdirSync3(dir);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const name of entries) {
|
for (const name of entries) {
|
||||||
if (name === "." || name === "..")
|
if (name === "." || name === "..")
|
||||||
continue;
|
continue;
|
||||||
const full = join3(dir, name);
|
const full = join5(dir, name);
|
||||||
let st;
|
let st;
|
||||||
try {
|
try {
|
||||||
st = statSync2(full);
|
st = statSync4(full);
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -774,7 +872,7 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS];
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const r of roots) {
|
for (const r of roots) {
|
||||||
walkFiles(join3(localRoot, r), localRoot, entries);
|
walkFiles(join5(localRoot, r), localRoot, entries);
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
||||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
const limit = Math.min(Math.max(args.limit ?? 50, 1), 200);
|
||||||
|
|
@ -785,26 +883,240 @@ function listLocalFiles(snapshot, args = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/kb-manifest.ts
|
// src/netx/kb-read-ops.ts
|
||||||
var EMPTY_CONTENT = {
|
import { readdirSync as readdirSync4, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
|
||||||
hasRegions: false,
|
import { extname, join as join6, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
||||||
hasTheory: false,
|
var TEXT_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".json", ".yml", ".yaml", ".csv", ".ps1", ".sh", ".cfg", ".conf", ".log"]);
|
||||||
hasPacket: false,
|
var SKIP_DIRS = new Set([".git", "node_modules"]);
|
||||||
hasCommon: false,
|
var READ_DEFAULT_LINES = 400;
|
||||||
hasSkills: false
|
var READ_MAX_LINES = 2000;
|
||||||
};
|
var READ_MAX_CHARS = 80000;
|
||||||
function unconfiguredKbSnapshot() {
|
var SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
||||||
|
var SEARCH_HITS_PER_FILE = 4;
|
||||||
|
var CACHE_MAX_CHARS = 64 * 1024 * 1024;
|
||||||
|
function requireRoot(snapshot) {
|
||||||
|
if (snapshot.status !== "configured" || !snapshot.realRoot) {
|
||||||
|
throw new Error("knowledge base not configured (set kbRoot to a folder with MANIFEST.json)");
|
||||||
|
}
|
||||||
|
return snapshot.realRoot;
|
||||||
|
}
|
||||||
|
function toPosix(rel) {
|
||||||
|
return rel.split(sep2).join("/");
|
||||||
|
}
|
||||||
|
function isInside(root, target) {
|
||||||
|
const rel = relative2(root, target);
|
||||||
|
return rel === "" || !rel.startsWith("..") && !rel.split(/[/\\]/).includes("..") && !/^[a-zA-Z]:/.test(rel);
|
||||||
|
}
|
||||||
|
function resolveKbReadPath(snapshot, pathArg) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, "");
|
||||||
|
if (trimmed.includes("\x00"))
|
||||||
|
throw new Error("path contains NUL");
|
||||||
|
const anchor = resolveKbPaths(snapshot).find((row) => row.key === trimmed);
|
||||||
|
const candidate = anchor ? anchor.absolute : !trimmed || trimmed === "." || trimmed === "/" ? root : resolve3(root, trimmed);
|
||||||
|
if (!isInside(root, candidate))
|
||||||
|
throw new Error(`path escapes kbRoot: ${candidate}`);
|
||||||
|
let real;
|
||||||
|
try {
|
||||||
|
real = realpathSync2(candidate);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative2(root, candidate)) || "."}`);
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync2(root);
|
||||||
|
if (!isInside(realRoot, real))
|
||||||
|
throw new Error(`path escapes kbRoot after realpath: ${real}`);
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative2(realRoot, real)) || "." };
|
||||||
|
}
|
||||||
|
function readKbFile(snapshot, args) {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path);
|
||||||
|
const st = statSync5(target.absolutePath);
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync4(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(join6(target.absolutePath, entry.name)).size;
|
||||||
|
} catch {}
|
||||||
|
return { name: entry.name, type: "file", size };
|
||||||
|
}).sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1);
|
||||||
|
return { ok: true, kind: "directory", ...target, entries };
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`);
|
||||||
|
}
|
||||||
|
const lines = readFileSync4(target.absolutePath, "utf8").replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
||||||
|
`).split(`
|
||||||
|
`);
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1));
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES);
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit);
|
||||||
|
let text = slice.join(`
|
||||||
|
`);
|
||||||
|
let truncated = start - 1 + slice.length < lines.length;
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0;
|
||||||
|
let keep = 0;
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS)
|
||||||
|
break;
|
||||||
|
used += line.length + 1;
|
||||||
|
keep += 1;
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1));
|
||||||
|
text = slice.join(`
|
||||||
|
`).slice(0, READ_MAX_CHARS);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status: "unconfigured",
|
ok: true,
|
||||||
realRoot: "",
|
kind: "file",
|
||||||
operatorName: "",
|
...target,
|
||||||
country: "",
|
totalLines: lines.length,
|
||||||
version: "",
|
startLine: start,
|
||||||
content: { ...EMPTY_CONTENT },
|
endLine: start - 1 + slice.length,
|
||||||
paths: {},
|
truncated,
|
||||||
errorMessage: ""
|
text
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
var CACHE_KEY = Symbol.for("dsh-netxops.kb-search-cache");
|
||||||
|
function searchCache() {
|
||||||
|
const root = globalThis;
|
||||||
|
root[CACHE_KEY] ??= { files: new Map, chars: 0 };
|
||||||
|
return root[CACHE_KEY];
|
||||||
|
}
|
||||||
|
function cachedText(path, mtimeMs, size) {
|
||||||
|
const cache = searchCache();
|
||||||
|
const hit = cache.files.get(path);
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size)
|
||||||
|
return hit;
|
||||||
|
const text = readFileSync4(path, "utf8").replace(/^\uFEFF/, "");
|
||||||
|
if (hit)
|
||||||
|
cache.chars -= hit.text.length;
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear();
|
||||||
|
cache.chars = 0;
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() };
|
||||||
|
cache.files.set(path, entry);
|
||||||
|
cache.chars += text.length;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
function walkTextFiles(dir, out) {
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = readdirSync4(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name))
|
||||||
|
continue;
|
||||||
|
const full = join6(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase()))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const st = statSync5(full);
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES)
|
||||||
|
out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function searchKb(snapshot, args) {
|
||||||
|
const root = requireRoot(snapshot);
|
||||||
|
const query = args.query.trim();
|
||||||
|
if (!query)
|
||||||
|
throw new Error("query is empty");
|
||||||
|
const scopeArg = args.scope?.trim() || "all";
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean);
|
||||||
|
const dirs = scopes.includes("all") ? [root] : scopes.map((scope) => resolveKbReadPath(snapshot, scope).absolutePath);
|
||||||
|
let matchers;
|
||||||
|
if (args.regex) {
|
||||||
|
let re;
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, "i");
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }];
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
|
||||||
|
matchers = terms.map((term) => ({ term, test: (lower) => lower.includes(term) }));
|
||||||
|
}
|
||||||
|
const files = [];
|
||||||
|
const seen = new Set;
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync5(dir);
|
||||||
|
const batch = [];
|
||||||
|
if (st.isFile())
|
||||||
|
batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size });
|
||||||
|
else
|
||||||
|
walkTextFiles(dir, batch);
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path))
|
||||||
|
continue;
|
||||||
|
seen.add(file.path);
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hits = [];
|
||||||
|
for (const file of files) {
|
||||||
|
let cached;
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative2(root, file.path));
|
||||||
|
const matched = new Set;
|
||||||
|
let score = 0;
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const inBody = matchers.filter((m) => m.test(cached.lower, cached.text));
|
||||||
|
if (inBody.length === 0 && matched.size === 0)
|
||||||
|
continue;
|
||||||
|
const lines = [];
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : [];
|
||||||
|
let lineHits = 0;
|
||||||
|
for (let i = 0;i < rows.length; i += 1) {
|
||||||
|
const row = rows[i];
|
||||||
|
const lower = row.toLowerCase();
|
||||||
|
let lineHit = false;
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term);
|
||||||
|
lineHit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit)
|
||||||
|
continue;
|
||||||
|
lineHits += 1;
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim();
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1;
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines
|
||||||
|
});
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath));
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100);
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) };
|
||||||
|
}
|
||||||
|
|
||||||
// src/netx/kb-runtime.ts
|
// src/netx/kb-runtime.ts
|
||||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||||
|
|
@ -862,7 +1174,7 @@ function omitUndefined(record) {
|
||||||
// src/netx/tool-mount-status.ts
|
// src/netx/tool-mount-status.ts
|
||||||
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { dirname as dirname3, join as join4 } from "node:path";
|
import { dirname as dirname4, join as join7 } from "node:path";
|
||||||
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
var KEY = Symbol.for("dsh-netxops.mount-inventory");
|
||||||
function store2() {
|
function store2() {
|
||||||
const root = globalThis;
|
const root = globalThis;
|
||||||
|
|
@ -886,8 +1198,8 @@ function getToolMountStatus() {
|
||||||
}
|
}
|
||||||
function persist() {
|
function persist() {
|
||||||
try {
|
try {
|
||||||
const path = join4(process.env.DSH_HOME?.trim() || join4(homedir(), ".dsh"), "netxops-mount-status.json");
|
const path = join7(process.env.DSH_HOME?.trim() || join7(homedir(), ".dsh"), "netxops-mount-status.json");
|
||||||
mkdirSync2(dirname3(path), { recursive: true });
|
mkdirSync2(dirname4(path), { recursive: true });
|
||||||
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
writeFileSync2(path, `${JSON.stringify({ mount: getToolMountStatus() ?? null, mounts: getToolMountStatuses() }, null, 2)}
|
||||||
`, "utf8");
|
`, "utf8");
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
@ -937,14 +1249,8 @@ function tool(name, description, parameters, execute) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
function writeTools(localRoot) {
|
||||||
if (!kbLocalToolsEnabled(snapshot))
|
return [
|
||||||
return () => {};
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot);
|
|
||||||
const toolsApi = ctx.tools;
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== "function")
|
|
||||||
return () => {};
|
|
||||||
const entries = [
|
|
||||||
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
tool("netx__kbWriteMemory", `Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. ` + "bucket: note→日常笔记, rca_review→排障复盘, ai_trace→AI思维链. " + "Default create-only; set overwrite=true to replace same filename. " + "Do not use workspace Write — KB is outside the sandbox.", {
|
||||||
bucket: {
|
bucket: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
@ -1024,7 +1330,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
}, (args) => deleteLocalFile(liveSnapshot(), {
|
}, (args) => deleteLocalFile(liveSnapshot(), {
|
||||||
path: String(args.path ?? "")
|
path: String(args.path ?? "")
|
||||||
})),
|
})),
|
||||||
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
|
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents with netx__kbRead using the listed paths.", {
|
||||||
root: {
|
root: {
|
||||||
type: "string",
|
type: "string",
|
||||||
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
enum: ["memories", "drafts", "suggestions", "refs", "all"],
|
||||||
|
|
@ -1036,6 +1342,39 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
limit: typeof args.limit === "number" ? args.limit : undefined
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
function readTools(snapshot) {
|
||||||
|
return [
|
||||||
|
tool("netx__kbSearch", `Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). ` + "Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; " + "files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. " + "Then open the best hits with netx__kbRead.", {
|
||||||
|
query: reqStr("Keywords (space-separated) or a regex when regex=true"),
|
||||||
|
scope: str("MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all"),
|
||||||
|
regex: bool("Treat query as one case-insensitive JS regex"),
|
||||||
|
limit: num("Max files 1–100 (default 20)")
|
||||||
|
}, (args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ""),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
})),
|
||||||
|
tool("netx__kbRead", `Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. ` + "path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). " + "Long files: page with offset/limit (1-based lines; default 400).", {
|
||||||
|
path: reqStr("KB-relative path, absolute path inside kbRoot, or a paths.* key"),
|
||||||
|
offset: num("First line (1-based, default 1)"),
|
||||||
|
limit: num("Max lines 1–2000 (default 400)")
|
||||||
|
}, (args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ""),
|
||||||
|
offset: typeof args.offset === "number" ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === "number" ? args.limit : undefined
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
if (snapshot.status !== "configured")
|
||||||
|
return () => {};
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot);
|
||||||
|
const toolsApi = ctx.tools;
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== "function")
|
||||||
|
return () => {};
|
||||||
|
const entries = [...readTools(snapshot), ...localRoot ? writeTools(localRoot) : []];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const source = options.source ?? "netxops-kb-local";
|
const source = options.source ?? "netxops-kb-local";
|
||||||
const id = getToolMountId(ctx, source);
|
const id = getToolMountId(ctx, source);
|
||||||
|
|
@ -1057,7 +1396,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
publishToolMountStatus({ ...status, names: [], count: 0, error: String(error) });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
ctx.logger.info("netxops: kb local tools registered count=%s localRoot=%s", disposers.length, localRoot);
|
ctx.logger.info("netxops: kb tools registered count=%s kbRoot=%s localRoot=%s", disposers.length, snapshot.realRoot, localRoot ?? "(none — read-only)");
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
dispose();
|
dispose();
|
||||||
|
|
@ -1067,19 +1406,35 @@ function registerKbLocalTools(ctx, snapshot = getKbContext(), options = {}) {
|
||||||
|
|
||||||
// src/netx/kb-pack-skills.ts
|
// src/netx/kb-pack-skills.ts
|
||||||
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
|
||||||
import { join as join5 } from "node:path";
|
import { join as join8 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
function resolveKbSkillsRoot(snapshot) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.skills?.trim() || "_skills";
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
function kbPackSkillFooter(snapshot) {
|
||||||
|
return [
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
"",
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
"",
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
"",
|
||||||
|
"These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.",
|
||||||
|
"Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.",
|
||||||
|
""
|
||||||
|
].join(`
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
function resolveKbLocalSkillsRoot(snapshot) {
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
const rel = snapshot.paths.localSkills?.trim();
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
if (!rel)
|
if (!rel)
|
||||||
return null;
|
return null;
|
||||||
return join5(snapshot.realRoot, rel);
|
return join8(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
|
|
@ -1098,7 +1453,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
const full = join5(root, entry);
|
const full = join8(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
try {
|
try {
|
||||||
isDir = (await stat2(full)).isDirectory();
|
isDir = (await stat2(full)).isDirectory();
|
||||||
|
|
@ -1120,15 +1475,24 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options)
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted)
|
if (options.signal?.aborted)
|
||||||
break;
|
break;
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s duplicates an already registered pack skill — skip %s", options.label, skill.name, full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: "directory", path: skill.directory },
|
resourceBase: { kind: "directory", path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
|
options.seen.add(skill.name);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.logger.warn("netxops: kb %s skill %s register failed: %s", options.label, skill.name, error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0))
|
for (const dispose of disposers.splice(0))
|
||||||
|
|
@ -1149,17 +1513,23 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
const footer = kbPackSkillFooter(snapshot);
|
||||||
|
const seen = new Set;
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: "pack",
|
label: "pack",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: "local",
|
label: "local",
|
||||||
signal: options.signal
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1361,8 +1731,8 @@ function watchNetxConnection(listener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
import { readFileSync as readFileSync2 } from "node:fs";
|
import { readFileSync as readFileSync5 } from "node:fs";
|
||||||
import { dirname as dirname4, join as join6 } from "node:path";
|
import { dirname as dirname5, join as join9 } from "node:path";
|
||||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||||
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
||||||
|
|
||||||
|
|
@ -2804,8 +3174,8 @@ async function layoutTopologyView(client, args, signal) {
|
||||||
// src/netx/tools.ts
|
// src/netx/tools.ts
|
||||||
function packageVersion() {
|
function packageVersion() {
|
||||||
try {
|
try {
|
||||||
const here = dirname4(fileURLToPath2(import.meta.url));
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync2(join6(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync5(join9(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
@ -3276,8 +3646,8 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
// src/netx/group-tools-plugin.ts
|
// src/netx/group-tools-plugin.ts
|
||||||
function packageVersion2() {
|
function packageVersion2() {
|
||||||
try {
|
try {
|
||||||
const here = dirname5(fileURLToPath3(import.meta.url));
|
const here = dirname6(fileURLToPath3(import.meta.url));
|
||||||
const pkg = JSON.parse(readFileSync3(join7(here, "..", "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync6(join10(here, "..", "package.json"), "utf8"));
|
||||||
return typeof pkg.version === "string" ? pkg.version : "0";
|
return typeof pkg.version === "string" ? pkg.version : "0";
|
||||||
} catch {
|
} catch {
|
||||||
return "0";
|
return "0";
|
||||||
|
|
|
||||||
992
lib/index.js
992
lib/index.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-netxops",
|
"name": "dsh-netxops",
|
||||||
"version": "0.1.70",
|
"version": "0.1.71",
|
||||||
"description": "Network operations, topology and monitoring for DeepSeek Harness.",
|
"description": "Network operations, topology and monitoring for DeepSeek Harness.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
|
|
||||||
## 知识库双平面(KB 已配置时 — 铁律)
|
## 知识库双平面(KB 已配置时 — 铁律)
|
||||||
- **总部包只读**:正式 RCA、theory、packet、`_common`、`_skills`、总部命令库 — 检索用,不改。
|
- **总部包只读**:正式 RCA、theory、packet、`_common`、`_skills`、总部命令库 — 检索用,不改。
|
||||||
|
- **读/检索走宿主工具**:`netx__kbSearch`(关键词/正则,`scope` 用 MANIFEST paths 键如 `rca` / `theory` / `packet`)→ `netx__kbRead`(路径或 `neIndex` / `cmdLib` 等键,长文件用 offset/limit 翻页)。路径以 `kb-context` 的锚点表为准,不自拼。
|
||||||
- **唯一可写区** `{kbLocal}`:`refs/`(本网产品知识)/ `memories/`(过程记忆)/ `drafts/`(案例草稿)/ `suggestions/`(反哺总部)。
|
- **唯一可写区** `{kbLocal}`:`refs/`(本网产品知识)/ `memories/`(过程记忆)/ `drafts/`(案例草稿)/ `suggestions/`(反哺总部)。
|
||||||
- **写文件只走宿主工具**:`netx__kbWriteRef` / `kbWriteMemory` / `kbWriteDraft` / `kbWriteSuggestion` / `kbUpdateLocal` / `kbDeleteLocal`。**禁止**对 KB 用 workspace `Write` / bash。
|
- **写文件只走宿主工具**:`netx__kbWriteRef` / `kbWriteMemory` / `kbWriteDraft` / `kbWriteSuggestion` / `kbUpdateLocal` / `kbDeleteLocal`。**禁止**对 KB 用 workspace `Write` / bash。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1001,7 +1001,7 @@ export function apply(ctx: Context, config: ConfigInput<Config> = Config({} as C
|
||||||
if (endpoint === 'kb.reload') {
|
if (endpoint === 'kb.reload') {
|
||||||
const current = source()
|
const current = source()
|
||||||
const kb = resolveKbRoot(current.kbRoot ?? '')
|
const kb = resolveKbRoot(current.kbRoot ?? '')
|
||||||
publishKbContext(kb)
|
publishKbContext(kb, { force: true })
|
||||||
applyKbEnv(kb)
|
applyKbEnv(kb)
|
||||||
if (kb.status === 'configured') {
|
if (kb.status === 'configured') {
|
||||||
ctx.logger.info(
|
ctx.logger.info(
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,51 @@
|
||||||
* Tools/scripts still use `process.env.KB_*` from `applyKbEnv`.
|
* Tools/scripts still use `process.env.KB_*` from `applyKbEnv`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
import type { Context } from '@deepseek-ai/cordis'
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
import type { KbSnapshot } from './kb-manifest.ts'
|
import { renderKbPathTable, type KbSnapshot } from './kb-manifest.ts'
|
||||||
import { resolveKbLocalRoot } from './kb-local-path.ts'
|
import { resolveKbLocalRoot } from './kb-local-path.ts'
|
||||||
|
|
||||||
const SKILL_NAME = 'kb-context'
|
const SKILL_NAME = 'kb-context'
|
||||||
|
/** Per-file / total caps for inlined `_local/identity/*.md` (system-prompt-grade site rules). */
|
||||||
|
const IDENTITY_FILE_MAX = 8_000
|
||||||
|
const IDENTITY_TOTAL_MAX = 24_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Site identity files (`{paths.local}/identity/*.md`, README excluded), inlined so the
|
||||||
|
* operator's own boundaries reach the model without an extra read.
|
||||||
|
*/
|
||||||
|
export function readKbIdentity(snapshot: KbSnapshot): Array<{ name: string; text: string }> {
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot)
|
||||||
|
if (!localRoot) return []
|
||||||
|
const dir = join(localRoot, 'identity')
|
||||||
|
let names: string[]
|
||||||
|
try {
|
||||||
|
names = readdirSync(dir).filter(name => /\.md$/i.test(name) && !/^readme\.md$/i.test(name)).sort()
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const out: Array<{ name: string; text: string }> = []
|
||||||
|
let budget = IDENTITY_TOTAL_MAX
|
||||||
|
for (const name of names) {
|
||||||
|
if (budget <= 0) break
|
||||||
|
const full = join(dir, name)
|
||||||
|
let text: string
|
||||||
|
try {
|
||||||
|
if (!statSync(full).isFile()) continue
|
||||||
|
text = readFileSync(full, 'utf8').replace(/^\uFEFF/, '').trim()
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!text) continue
|
||||||
|
const cap = Math.min(IDENTITY_FILE_MAX, budget)
|
||||||
|
if (text.length > cap) text = `${text.slice(0, cap)}\n\n…(truncated — read \`${full}\` for the rest)`
|
||||||
|
budget -= text.length
|
||||||
|
out.push({ name, text })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
function skillBody(snapshot: KbSnapshot): { description: string; content: string } {
|
function skillBody(snapshot: KbSnapshot): { description: string; content: string } {
|
||||||
if (snapshot.status === 'configured') {
|
if (snapshot.status === 'configured') {
|
||||||
|
|
@ -19,6 +59,14 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
||||||
const localRow = localRoot
|
const localRow = localRoot
|
||||||
? `| kbLocal | \`${localRoot}\` |`
|
? `| kbLocal | \`${localRoot}\` |`
|
||||||
: '| kbLocal | (MANIFEST paths.local missing) |'
|
: '| kbLocal | (MANIFEST paths.local missing) |'
|
||||||
|
const identity = readKbIdentity(snapshot)
|
||||||
|
const identityBlock = identity.length > 0
|
||||||
|
? [
|
||||||
|
'',
|
||||||
|
'### Site identity (`identity/`, authoritative — follow over generic advice)',
|
||||||
|
...identity.flatMap(({ name, text }) => ['', `#### ${name}`, '', text]),
|
||||||
|
]
|
||||||
|
: []
|
||||||
const localGuide = localRoot
|
const localGuide = localRoot
|
||||||
? [
|
? [
|
||||||
'',
|
'',
|
||||||
|
|
@ -46,7 +94,7 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
||||||
]
|
]
|
||||||
return {
|
return {
|
||||||
description:
|
description:
|
||||||
'Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).',
|
`Load FIRST before any kb-* skill or KB file access: resolved absolute paths (rca / theory / packet / neIndex / cmdLib / local …) for operator ${snapshot.operatorName} (${snapshot.country}) KB v${snapshot.version}, site identity rules, and KB write rules.`,
|
||||||
content: [
|
content: [
|
||||||
'## Knowledge base (configured)',
|
'## Knowledge base (configured)',
|
||||||
'',
|
'',
|
||||||
|
|
@ -62,13 +110,22 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
||||||
`| kbCountry | ${snapshot.country} |`,
|
`| kbCountry | ${snapshot.country} |`,
|
||||||
`| kbVersion | ${snapshot.version} |`,
|
`| kbVersion | ${snapshot.version} |`,
|
||||||
`| kbContent (on) | ${flags} |`,
|
`| kbContent (on) | ${flags} |`,
|
||||||
|
'',
|
||||||
|
'### Path anchors (resolved from MANIFEST `paths.*` — use verbatim, never compose `regions/<country>/<operator>/…`)',
|
||||||
|
'',
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
...identityBlock,
|
||||||
...localGuide,
|
...localGuide,
|
||||||
'',
|
'',
|
||||||
'Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.',
|
'### Reading the HQ pack',
|
||||||
|
'- `netx__kbSearch` — keyword/regex search across rca / theory / packet / common / local (returns file + line hits).',
|
||||||
|
'- `netx__kbRead` — read one KB file (path relative to kbRoot or absolute inside it; optional line window).',
|
||||||
|
'- Start from the indexes: `neIndex` (NE → cases), `patternMap`, `classification`, then open the cited RCA.',
|
||||||
'',
|
'',
|
||||||
'Business playbooks (kb-troubleshoot, kb-retrieve, …) register from',
|
'Environment mirrors: KB_ROOT, KB_LOCAL, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_PATHS (JSON of the anchors above), KB_STATUS.',
|
||||||
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
'',
|
||||||
'plus paths.localSkills when present — use those skills for KB triage;',
|
'Business playbooks (kb-troubleshoot, kb-retrieve, …) register from paths.skills',
|
||||||
|
'when hasSkills is true, plus paths.localSkills when present — use those skills for KB triage;',
|
||||||
'keep netx-ops for live netx evidence only.',
|
'keep netx-ops for live netx evidence only.',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* Host-side tools for writing under MANIFEST `paths.local`
|
* Host-side KB tools. Read/search the whole package when configured; write under MANIFEST
|
||||||
* (memories / drafts / suggestions / refs). Bypasses workspace sandbox.
|
* `paths.local` (memories / drafts / suggestions / refs). Bypasses workspace sandbox.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Context } from '@deepseek-ai/cordis'
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
|
|
@ -15,7 +15,8 @@ import {
|
||||||
listLocalFiles,
|
listLocalFiles,
|
||||||
updateLocalFile,
|
updateLocalFile,
|
||||||
} from './kb-local-ops.ts'
|
} from './kb-local-ops.ts'
|
||||||
import { kbLocalToolsEnabled, resolveKbLocalRoot, type RefArea } from './kb-local-path.ts'
|
import { resolveKbLocalRoot, type RefArea } from './kb-local-path.ts'
|
||||||
|
import { readKbFile, searchKb } from './kb-read-ops.ts'
|
||||||
import { getKbContext } from './kb-runtime.ts'
|
import { getKbContext } from './kb-runtime.ts'
|
||||||
import { toLosslessJson } from './json-safe.ts'
|
import { toLosslessJson } from './json-safe.ts'
|
||||||
import { getToolMountId, getToolMountStatus, publishToolMountStatus, removeToolMountStatus } from './tool-mount-status.ts'
|
import { getToolMountId, getToolMountStatus, publishToolMountStatus, removeToolMountStatus } from './tool-mount-status.ts'
|
||||||
|
|
@ -66,22 +67,8 @@ function tool(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function writeTools(localRoot: string) {
|
||||||
* Register KB local create/update/delete/list tools when snapshot has paths.local.
|
return [
|
||||||
* @returns disposer (no-op when disabled).
|
|
||||||
*/
|
|
||||||
export function registerKbLocalTools(
|
|
||||||
ctx: Context,
|
|
||||||
snapshot: KbSnapshot = getKbContext(),
|
|
||||||
options: { plane?: 'host' | 'preset'; source?: string } = {},
|
|
||||||
): () => void {
|
|
||||||
if (!kbLocalToolsEnabled(snapshot)) return () => {}
|
|
||||||
|
|
||||||
const localRoot = resolveKbLocalRoot(snapshot)!
|
|
||||||
const toolsApi = (ctx as { tools?: { register: (t: unknown) => () => void } }).tools
|
|
||||||
if (!toolsApi || typeof toolsApi.register !== 'function') return () => {}
|
|
||||||
|
|
||||||
const entries = [
|
|
||||||
tool(
|
tool(
|
||||||
'netx__kbWriteMemory',
|
'netx__kbWriteMemory',
|
||||||
`Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. `
|
`Create (or overwrite) one diary-style memory markdown under ${localRoot}/memories/. `
|
||||||
|
|
@ -206,7 +193,7 @@ export function registerKbLocalTools(
|
||||||
tool(
|
tool(
|
||||||
'netx__kbListLocal',
|
'netx__kbListLocal',
|
||||||
`List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). `
|
`List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). `
|
||||||
+ 'Read contents via absolute paths from the listing.',
|
+ 'Read contents with netx__kbRead using the listed paths.',
|
||||||
{
|
{
|
||||||
root: {
|
root: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
|
|
@ -221,6 +208,65 @@ export function registerKbLocalTools(
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTools(snapshot: KbSnapshot) {
|
||||||
|
return [
|
||||||
|
tool(
|
||||||
|
'netx__kbSearch',
|
||||||
|
`Search the operator knowledge base at ${snapshot.realRoot} (read-only HQ pack + _local). `
|
||||||
|
+ 'Plain mode: space-separated keywords (NE host_name, alarm, protocol, symptom…), case-insensitive; '
|
||||||
|
+ 'files ranked by how many distinct terms match (filename hits weigh more), up to 4 hit lines each. '
|
||||||
|
+ 'Then open the best hits with netx__kbRead.',
|
||||||
|
{
|
||||||
|
query: reqStr('Keywords (space-separated) or a regex when regex=true'),
|
||||||
|
scope: str('MANIFEST paths key(s) — e.g. rca, theory, theoryVendor, packet, commonPatterns, local, neIndex — or a relative dir; comma-separate several; default all'),
|
||||||
|
regex: bool('Treat query as one case-insensitive JS regex'),
|
||||||
|
limit: num('Max files 1–100 (default 20)'),
|
||||||
|
},
|
||||||
|
(args) => searchKb(liveSnapshot(), {
|
||||||
|
query: String(args.query ?? ''),
|
||||||
|
scope: args.scope != null ? String(args.scope) : undefined,
|
||||||
|
regex: args.regex === true,
|
||||||
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
tool(
|
||||||
|
'netx__kbRead',
|
||||||
|
`Read one file (or list one directory) inside the knowledge base at ${snapshot.realRoot}. `
|
||||||
|
+ 'path: relative to kbRoot, absolute inside it, or a MANIFEST paths key (rca, neIndex, cmdLib, patternMap, …). '
|
||||||
|
+ 'Long files: page with offset/limit (1-based lines; default 400).',
|
||||||
|
{
|
||||||
|
path: reqStr('KB-relative path, absolute path inside kbRoot, or a paths.* key'),
|
||||||
|
offset: num('First line (1-based, default 1)'),
|
||||||
|
limit: num('Max lines 1–2000 (default 400)'),
|
||||||
|
},
|
||||||
|
(args) => readKbFile(liveSnapshot(), {
|
||||||
|
path: String(args.path ?? ''),
|
||||||
|
offset: typeof args.offset === 'number' ? args.offset : undefined,
|
||||||
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register KB tools: read/search whenever configured, plus local create/update/delete/list
|
||||||
|
* when snapshot has paths.local.
|
||||||
|
* @returns disposer (no-op when disabled).
|
||||||
|
*/
|
||||||
|
export function registerKbLocalTools(
|
||||||
|
ctx: Context,
|
||||||
|
snapshot: KbSnapshot = getKbContext(),
|
||||||
|
options: { plane?: 'host' | 'preset'; source?: string } = {},
|
||||||
|
): () => void {
|
||||||
|
if (snapshot.status !== 'configured') return () => {}
|
||||||
|
|
||||||
|
const localRoot = resolveKbLocalRoot(snapshot)
|
||||||
|
const toolsApi = (ctx as { tools?: { register: (t: unknown) => () => void } }).tools
|
||||||
|
if (!toolsApi || typeof toolsApi.register !== 'function') return () => {}
|
||||||
|
|
||||||
|
const entries = [...readTools(snapshot), ...(localRoot ? writeTools(localRoot) : [])]
|
||||||
|
|
||||||
const disposers: Array<() => void> = []
|
const disposers: Array<() => void> = []
|
||||||
const source = options.source ?? 'netxops-kb-local'
|
const source = options.source ?? 'netxops-kb-local'
|
||||||
|
|
@ -236,9 +282,10 @@ export function registerKbLocalTools(
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
ctx.logger.info(
|
ctx.logger.info(
|
||||||
'netxops: kb local tools registered count=%s localRoot=%s',
|
'netxops: kb tools registered count=%s kbRoot=%s localRoot=%s',
|
||||||
disposers.length,
|
disposers.length,
|
||||||
localRoot,
|
snapshot.realRoot,
|
||||||
|
localRoot ?? '(none — read-only)',
|
||||||
)
|
)
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers) dispose()
|
for (const dispose of disposers) dispose()
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ export function parseManifest(raw: string): {
|
||||||
} {
|
} {
|
||||||
let data: unknown
|
let data: unknown
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(raw)
|
// PowerShell 5.1 `Out-File/Set-Content -Encoding UTF8` writes a BOM; JSON.parse rejects it.
|
||||||
|
data = JSON.parse(raw.replace(/^\uFEFF/, ''))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`MANIFEST is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
`MANIFEST is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
|
@ -335,3 +336,36 @@ export function resolveKbRoot(kbRoot: string, maxDepth = 3): KbSnapshot {
|
||||||
return errorSnapshot(error instanceof Error ? error.message : String(error))
|
return errorSnapshot(error instanceof Error ? error.message : String(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KbResolvedPath {
|
||||||
|
key: string
|
||||||
|
relative: string
|
||||||
|
absolute: string
|
||||||
|
exists: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MANIFEST `paths.*` resolved against realRoot (stable key order).
|
||||||
|
* Pack skills must read these anchors instead of composing `regions/<c>/<o>/…`.
|
||||||
|
*/
|
||||||
|
export function resolveKbPaths(snapshot: KbSnapshot): KbResolvedPath[] {
|
||||||
|
if (snapshot.status !== 'configured') return []
|
||||||
|
const paths: Record<string, string | undefined> = { skills: '_skills', ...snapshot.paths }
|
||||||
|
return Object.entries(paths)
|
||||||
|
.filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim() !== '')
|
||||||
|
.map(([key, rel]) => {
|
||||||
|
const absolute = resolve(snapshot.realRoot, rel)
|
||||||
|
return { key, relative: rel, absolute, exists: existsSync(absolute) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Markdown anchor table shared by `kb-context` and every registered pack skill. */
|
||||||
|
export function renderKbPathTable(snapshot: KbSnapshot): string {
|
||||||
|
const rows = resolveKbPaths(snapshot)
|
||||||
|
if (rows.length === 0) return '(MANIFEST has no paths — legacy package; ask HQ for a repack)'
|
||||||
|
return [
|
||||||
|
'| paths.* | Absolute path |',
|
||||||
|
'| --- | --- |',
|
||||||
|
...rows.map(row => `| ${row.key} | \`${row.absolute}\`${row.exists ? '' : ' (missing)'} |`),
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
import { readdir, stat } from 'node:fs/promises'
|
import { readdir, stat } from 'node:fs/promises'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import type { Context } from '@deepseek-ai/cordis'
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
import type { KbSnapshot } from './kb-manifest.ts'
|
import { renderKbPathTable, type KbSnapshot } from './kb-manifest.ts'
|
||||||
import { loadSkillBundle } from './group-skills.ts'
|
import { loadSkillBundle } from './group-skills.ts'
|
||||||
|
|
||||||
export type KbSkillPlane = 'preset' | 'public'
|
export type KbSkillPlane = 'preset' | 'public'
|
||||||
|
|
@ -35,6 +35,26 @@ export function resolveKbSkillsRoot(snapshot: KbSnapshot): string {
|
||||||
return join(snapshot.realRoot, rel)
|
return join(snapshot.realRoot, rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appended to every pack skill: the playbooks say "read MANIFEST paths, never compose",
|
||||||
|
* so hand them the resolved anchors instead of costing a MANIFEST read per task.
|
||||||
|
*/
|
||||||
|
export function kbPackSkillFooter(snapshot: KbSnapshot): string {
|
||||||
|
return [
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
`## Injected KB anchors (netxops · ${snapshot.operatorName} / ${snapshot.country} v${snapshot.version})`,
|
||||||
|
'',
|
||||||
|
`kbStatus=configured · kbRoot=\`${snapshot.realRoot}\``,
|
||||||
|
'',
|
||||||
|
renderKbPathTable(snapshot),
|
||||||
|
'',
|
||||||
|
'These are already resolved from `MANIFEST.json` `paths.*`; use them verbatim.',
|
||||||
|
'Read/search the pack with `netx__kbRead` / `netx__kbSearch` when present; write only via `netx__kbWrite*`.',
|
||||||
|
'',
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Absolute local-skills root from MANIFEST `paths.localSkills`, or null when absent.
|
* Absolute local-skills root from MANIFEST `paths.localSkills`, or null when absent.
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,7 +69,14 @@ async function registerSkillsUnderRoot(
|
||||||
skillsApi: { register: SkillsRegister },
|
skillsApi: { register: SkillsRegister },
|
||||||
root: string,
|
root: string,
|
||||||
provider: string,
|
provider: string,
|
||||||
options: { logMissing: boolean; label: string; signal?: AbortSignal },
|
options: {
|
||||||
|
logMissing: boolean
|
||||||
|
label: string
|
||||||
|
signal?: AbortSignal
|
||||||
|
footer: string
|
||||||
|
/** Names already registered by an earlier root (pack before local). */
|
||||||
|
seen: Set<string>
|
||||||
|
},
|
||||||
): Promise<Array<() => void>> {
|
): Promise<Array<() => void>> {
|
||||||
let entries: string[]
|
let entries: string[]
|
||||||
try {
|
try {
|
||||||
|
|
@ -91,15 +118,35 @@ async function registerSkillsUnderRoot(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (options.signal?.aborted) break
|
if (options.signal?.aborted) break
|
||||||
|
if (options.seen.has(skill.name)) {
|
||||||
|
ctx.logger.warn(
|
||||||
|
'netxops: kb %s skill %s duplicates an already registered pack skill — skip %s',
|
||||||
|
options.label,
|
||||||
|
skill.name,
|
||||||
|
full,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
name: skill.name,
|
name: skill.name,
|
||||||
description: skill.description,
|
description: skill.description,
|
||||||
content: skill.content,
|
content: `${skill.content.trimEnd()}${options.footer}`,
|
||||||
path: skill.path,
|
path: skill.path,
|
||||||
resourceBase: { kind: 'directory', path: skill.directory },
|
resourceBase: { kind: 'directory', path: skill.directory },
|
||||||
provider,
|
provider,
|
||||||
source: 'custom',
|
source: 'custom',
|
||||||
}))
|
}))
|
||||||
|
options.seen.add(skill.name)
|
||||||
|
} catch (error) {
|
||||||
|
// One rejected bundle must not take the rest of the pack down with it.
|
||||||
|
ctx.logger.warn(
|
||||||
|
'netxops: kb %s skill %s register failed: %s',
|
||||||
|
options.label,
|
||||||
|
skill.name,
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
for (const dispose of disposers.splice(0)) dispose()
|
for (const dispose of disposers.splice(0)) dispose()
|
||||||
|
|
@ -137,11 +184,15 @@ export async function registerKbPackSkills(
|
||||||
|
|
||||||
const skillsRoot = resolveKbSkillsRoot(snapshot)
|
const skillsRoot = resolveKbSkillsRoot(snapshot)
|
||||||
const localRoot = resolveKbLocalSkillsRoot(snapshot)
|
const localRoot = resolveKbLocalSkillsRoot(snapshot)
|
||||||
|
const footer = kbPackSkillFooter(snapshot)
|
||||||
|
const seen = new Set<string>()
|
||||||
try {
|
try {
|
||||||
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
logMissing: true,
|
logMissing: true,
|
||||||
label: 'pack',
|
label: 'pack',
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (localRoot) {
|
if (localRoot) {
|
||||||
|
|
@ -149,6 +200,8 @@ export async function registerKbPackSkills(
|
||||||
logMissing: false,
|
logMissing: false,
|
||||||
label: 'local',
|
label: 'local',
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
|
footer,
|
||||||
|
seen,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
298
src/netx/kb-read-ops.ts
Normal file
298
src/netx/kb-read-ops.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
/**
|
||||||
|
* Read-only access to the whole operator-subset package (HQ pack + `_local`),
|
||||||
|
* jailed to `realRoot`. Host-side so it works regardless of the workspace sandbox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
||||||
|
import { extname, join, relative, resolve, sep } from 'node:path'
|
||||||
|
import { resolveKbPaths, type KbSnapshot } from './kb-manifest.ts'
|
||||||
|
|
||||||
|
const TEXT_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.json', '.yml', '.yaml', '.csv', '.ps1', '.sh', '.cfg', '.conf', '.log'])
|
||||||
|
const SKIP_DIRS = new Set(['.git', 'node_modules'])
|
||||||
|
const READ_DEFAULT_LINES = 400
|
||||||
|
const READ_MAX_LINES = 2_000
|
||||||
|
const READ_MAX_CHARS = 80_000
|
||||||
|
const SEARCH_MAX_FILE_BYTES = 2 * 1024 * 1024
|
||||||
|
const SEARCH_HITS_PER_FILE = 4
|
||||||
|
const CACHE_MAX_CHARS = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
function requireRoot(snapshot: KbSnapshot): string {
|
||||||
|
if (snapshot.status !== 'configured' || !snapshot.realRoot) {
|
||||||
|
throw new Error('knowledge base not configured (set kbRoot to a folder with MANIFEST.json)')
|
||||||
|
}
|
||||||
|
return snapshot.realRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPosix(rel: string): string {
|
||||||
|
return rel.split(sep).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInside(root: string, target: string): boolean {
|
||||||
|
const rel = relative(root, target)
|
||||||
|
return rel === '' || (!rel.startsWith('..') && !rel.split(/[/\\]/).includes('..') && !/^[a-zA-Z]:/.test(rel))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve `pathArg` (absolute, or relative to kbRoot, or `paths.<key>` shorthand like `rca`)
|
||||||
|
* to a real path inside the package. Symlinks are followed and re-checked.
|
||||||
|
*/
|
||||||
|
export function resolveKbReadPath(snapshot: KbSnapshot, pathArg: string): { absolutePath: string; relativePath: string } {
|
||||||
|
const root = requireRoot(snapshot)
|
||||||
|
const trimmed = pathArg.trim().replace(/^["']|["']$/g, '')
|
||||||
|
if (trimmed.includes('\0')) throw new Error('path contains NUL')
|
||||||
|
const anchor = resolveKbPaths(snapshot).find(row => row.key === trimmed)
|
||||||
|
const candidate = anchor
|
||||||
|
? anchor.absolute
|
||||||
|
: (!trimmed || trimmed === '.' || trimmed === '/') ? root : resolve(root, trimmed)
|
||||||
|
if (!isInside(root, candidate)) throw new Error(`path escapes kbRoot: ${candidate}`)
|
||||||
|
let real: string
|
||||||
|
try {
|
||||||
|
real = realpathSync(candidate)
|
||||||
|
} catch {
|
||||||
|
throw new Error(`not found in KB: ${toPosix(relative(root, candidate)) || '.'}`)
|
||||||
|
}
|
||||||
|
const realRoot = realpathSync(root)
|
||||||
|
if (!isInside(realRoot, real)) throw new Error(`path escapes kbRoot after realpath: ${real}`)
|
||||||
|
return { absolutePath: real, relativePath: toPosix(relative(realRoot, real)) || '.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KbReadResult =
|
||||||
|
| {
|
||||||
|
ok: true
|
||||||
|
kind: 'file'
|
||||||
|
absolutePath: string
|
||||||
|
relativePath: string
|
||||||
|
totalLines: number
|
||||||
|
startLine: number
|
||||||
|
endLine: number
|
||||||
|
truncated: boolean
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
ok: true
|
||||||
|
kind: 'directory'
|
||||||
|
absolutePath: string
|
||||||
|
relativePath: string
|
||||||
|
entries: Array<{ name: string; type: 'dir' | 'file'; size?: number }>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a KB file (1-based `offset`, `limit` lines) or list a KB directory.
|
||||||
|
*/
|
||||||
|
export function readKbFile(
|
||||||
|
snapshot: KbSnapshot,
|
||||||
|
args: { path: string; offset?: number; limit?: number },
|
||||||
|
): KbReadResult {
|
||||||
|
const target = resolveKbReadPath(snapshot, args.path)
|
||||||
|
const st = statSync(target.absolutePath)
|
||||||
|
if (st.isDirectory()) {
|
||||||
|
const entries = readdirSync(target.absolutePath, { withFileTypes: true })
|
||||||
|
.filter(entry => !SKIP_DIRS.has(entry.name))
|
||||||
|
.map((entry) => {
|
||||||
|
if (entry.isDirectory()) return { name: entry.name, type: 'dir' as const }
|
||||||
|
let size: number | undefined
|
||||||
|
try { size = statSync(join(target.absolutePath, entry.name)).size } catch { /* ignore */ }
|
||||||
|
return { name: entry.name, type: 'file' as const, size }
|
||||||
|
})
|
||||||
|
.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'dir' ? -1 : 1))
|
||||||
|
return { ok: true, kind: 'directory', ...target, entries }
|
||||||
|
}
|
||||||
|
if (!TEXT_EXTENSIONS.has(extname(target.absolutePath).toLowerCase())) {
|
||||||
|
throw new Error(`not a text KB file: ${target.relativePath}`)
|
||||||
|
}
|
||||||
|
const lines = readFileSync(target.absolutePath, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n').split('\n')
|
||||||
|
const start = Math.max(1, Math.floor(args.offset ?? 1))
|
||||||
|
const limit = Math.min(Math.max(1, Math.floor(args.limit ?? READ_DEFAULT_LINES)), READ_MAX_LINES)
|
||||||
|
let slice = lines.slice(start - 1, start - 1 + limit)
|
||||||
|
let text = slice.join('\n')
|
||||||
|
let truncated = start - 1 + slice.length < lines.length
|
||||||
|
if (text.length > READ_MAX_CHARS) {
|
||||||
|
let used = 0
|
||||||
|
let keep = 0
|
||||||
|
for (const line of slice) {
|
||||||
|
if (used + line.length + 1 > READ_MAX_CHARS) break
|
||||||
|
used += line.length + 1
|
||||||
|
keep += 1
|
||||||
|
}
|
||||||
|
slice = slice.slice(0, Math.max(keep, 1))
|
||||||
|
text = slice.join('\n').slice(0, READ_MAX_CHARS)
|
||||||
|
truncated = true
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
kind: 'file',
|
||||||
|
...target,
|
||||||
|
totalLines: lines.length,
|
||||||
|
startLine: start,
|
||||||
|
endLine: start - 1 + slice.length,
|
||||||
|
truncated,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedFile {
|
||||||
|
mtimeMs: number
|
||||||
|
size: number
|
||||||
|
text: string
|
||||||
|
/** Lower-cased once so plain searches don't re-lowercase every file per term. */
|
||||||
|
lower: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_KEY = Symbol.for('dsh-netxops.kb-search-cache')
|
||||||
|
|
||||||
|
function searchCache(): { files: Map<string, CachedFile>; chars: number } {
|
||||||
|
const root = globalThis as typeof globalThis & { [CACHE_KEY]?: { files: Map<string, CachedFile>; chars: number } }
|
||||||
|
root[CACHE_KEY] ??= { files: new Map(), chars: 0 }
|
||||||
|
return root[CACHE_KEY]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read through a stat-validated cache so repeated searches don't re-read the pack. */
|
||||||
|
function cachedText(path: string, mtimeMs: number, size: number): CachedFile {
|
||||||
|
const cache = searchCache()
|
||||||
|
const hit = cache.files.get(path)
|
||||||
|
if (hit && hit.mtimeMs === mtimeMs && hit.size === size) return hit
|
||||||
|
const text = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
|
||||||
|
if (hit) cache.chars -= hit.text.length
|
||||||
|
if (cache.chars + text.length > CACHE_MAX_CHARS) {
|
||||||
|
cache.files.clear()
|
||||||
|
cache.chars = 0
|
||||||
|
}
|
||||||
|
const entry = { mtimeMs, size, text, lower: text.toLowerCase() }
|
||||||
|
cache.files.set(path, entry)
|
||||||
|
cache.chars += text.length
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkTextFiles(dir: string, out: Array<{ path: string; mtimeMs: number; size: number }>): void {
|
||||||
|
let entries
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir, { withFileTypes: true })
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (SKIP_DIRS.has(entry.name)) continue
|
||||||
|
const full = join(dir, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walkTextFiles(full, out)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!entry.isFile() || !TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue
|
||||||
|
try {
|
||||||
|
const st = statSync(full)
|
||||||
|
if (st.size <= SEARCH_MAX_FILE_BYTES) out.push({ path: full, mtimeMs: st.mtimeMs, size: st.size })
|
||||||
|
} catch {
|
||||||
|
// unreadable — skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KbSearchHit {
|
||||||
|
relativePath: string
|
||||||
|
absolutePath: string
|
||||||
|
score: number
|
||||||
|
matchedTerms: string[]
|
||||||
|
lines: Array<{ line: number; text: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search KB text files. Plain mode: whitespace-separated terms, case-insensitive, files
|
||||||
|
* ranked by distinct terms matched (filename hits weigh more). `regex=true` treats the
|
||||||
|
* query as one JS regular expression.
|
||||||
|
*/
|
||||||
|
export function searchKb(
|
||||||
|
snapshot: KbSnapshot,
|
||||||
|
args: { query: string; scope?: string; regex?: boolean; limit?: number },
|
||||||
|
): { ok: true; scope: string; scannedFiles: number; totalMatches: number; hits: KbSearchHit[] } {
|
||||||
|
const root = requireRoot(snapshot)
|
||||||
|
const query = args.query.trim()
|
||||||
|
if (!query) throw new Error('query is empty')
|
||||||
|
const scopeArg = args.scope?.trim() || 'all'
|
||||||
|
const scopes = scopeArg.split(/[,\s]+/).filter(Boolean)
|
||||||
|
const dirs = scopes.includes('all')
|
||||||
|
? [root]
|
||||||
|
: scopes.map(scope => resolveKbReadPath(snapshot, scope).absolutePath)
|
||||||
|
|
||||||
|
/** `test(lower, raw)`: plain terms match the lower-cased text, regex the raw text. */
|
||||||
|
let matchers: Array<{ term: string; test: (lower: string, raw: string) => boolean }>
|
||||||
|
if (args.regex) {
|
||||||
|
let re: RegExp
|
||||||
|
try {
|
||||||
|
re = new RegExp(query, 'i')
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`invalid regex: ${error instanceof Error ? error.message : String(error)}`)
|
||||||
|
}
|
||||||
|
matchers = [{ term: query, test: (_lower, raw) => re.test(raw) }]
|
||||||
|
} else {
|
||||||
|
const terms = [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))]
|
||||||
|
matchers = terms.map(term => ({ term, test: (lower: string) => lower.includes(term) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: Array<{ path: string; mtimeMs: number; size: number }> = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const dir of dirs) {
|
||||||
|
const st = statSync(dir)
|
||||||
|
const batch: typeof files = []
|
||||||
|
if (st.isFile()) batch.push({ path: dir, mtimeMs: st.mtimeMs, size: st.size })
|
||||||
|
else walkTextFiles(dir, batch)
|
||||||
|
for (const file of batch) {
|
||||||
|
if (seen.has(file.path)) continue
|
||||||
|
seen.add(file.path)
|
||||||
|
files.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hits: KbSearchHit[] = []
|
||||||
|
for (const file of files) {
|
||||||
|
let cached: CachedFile
|
||||||
|
try {
|
||||||
|
cached = cachedText(file.path, file.mtimeMs, file.size)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const rel = toPosix(relative(root, file.path))
|
||||||
|
const matched = new Set<string>()
|
||||||
|
let score = 0
|
||||||
|
for (const m of matchers) {
|
||||||
|
if (m.test(rel.toLowerCase(), rel)) {
|
||||||
|
matched.add(m.term)
|
||||||
|
score += 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Whole-file pre-filter: only split/scan lines of files that contain a term.
|
||||||
|
const inBody = matchers.filter(m => m.test(cached.lower, cached.text))
|
||||||
|
if (inBody.length === 0 && matched.size === 0) continue
|
||||||
|
const lines: Array<{ line: number; text: string }> = []
|
||||||
|
const rows = inBody.length > 0 ? cached.text.split(/\r\n?|\n/) : []
|
||||||
|
let lineHits = 0
|
||||||
|
for (let i = 0; i < rows.length; i += 1) {
|
||||||
|
const row = rows[i]!
|
||||||
|
const lower = row.toLowerCase()
|
||||||
|
let lineHit = false
|
||||||
|
for (const m of inBody) {
|
||||||
|
if (m.test(lower, row)) {
|
||||||
|
matched.add(m.term)
|
||||||
|
lineHit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lineHit) continue
|
||||||
|
lineHits += 1
|
||||||
|
if (lines.length < SEARCH_HITS_PER_FILE) {
|
||||||
|
const trimmed = row.trim()
|
||||||
|
lines.push({ line: i + 1, text: trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Distinct terms dominate; hit volume only breaks ties so big manuals don't swamp RCAs.
|
||||||
|
score += matched.size * 10 + Math.min(lineHits, 30) * 0.1
|
||||||
|
hits.push({
|
||||||
|
relativePath: rel,
|
||||||
|
absolutePath: file.path,
|
||||||
|
score: Math.round(score * 10) / 10,
|
||||||
|
matchedTerms: [...matched],
|
||||||
|
lines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
hits.sort((a, b) => b.score - a.score || a.relativePath.localeCompare(b.relativePath))
|
||||||
|
const limit = Math.min(Math.max(Math.floor(args.limit ?? 20), 1), 100)
|
||||||
|
return { ok: true, scope: scopeArg, scannedFiles: files.length, totalMatches: hits.length, hits: hits.slice(0, limit) }
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { KbSnapshot } from './kb-manifest.ts'
|
import type { KbSnapshot } from './kb-manifest.ts'
|
||||||
import { unconfiguredKbSnapshot } from './kb-manifest.ts'
|
import { resolveKbPaths, unconfiguredKbSnapshot } from './kb-manifest.ts'
|
||||||
import { resolveKbLocalRoot } from './kb-local-path.ts'
|
import { resolveKbLocalRoot } from './kb-local-path.ts'
|
||||||
|
|
||||||
type Listener = () => void
|
type Listener = () => void
|
||||||
|
|
@ -26,6 +26,7 @@ const ENV_KEYS = [
|
||||||
'KB_COUNTRY',
|
'KB_COUNTRY',
|
||||||
'KB_VERSION',
|
'KB_VERSION',
|
||||||
'KB_CONTENT',
|
'KB_CONTENT',
|
||||||
|
'KB_PATHS',
|
||||||
'KB_STATUS',
|
'KB_STATUS',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|
@ -52,10 +53,12 @@ export function getKbContext(): KbSnapshot {
|
||||||
/**
|
/**
|
||||||
* Publish the latest KB snapshot for UI / skill remounts.
|
* Publish the latest KB snapshot for UI / skill remounts.
|
||||||
* @param next - resolved snapshot from `resolveKbRoot`.
|
* @param next - resolved snapshot from `resolveKbRoot`.
|
||||||
|
* @param options.force - notify even when MANIFEST is unchanged (manual reload picks up
|
||||||
|
* edited SKILL.md / new local_skills / identity files on disk).
|
||||||
*/
|
*/
|
||||||
export function publishKbContext(next: KbSnapshot): void {
|
export function publishKbContext(next: KbSnapshot, options: { force?: boolean } = {}): void {
|
||||||
const state = store()
|
const state = store()
|
||||||
if (JSON.stringify(state.snapshot) === JSON.stringify(next)) return
|
if (!options.force && JSON.stringify(state.snapshot) === JSON.stringify(next)) return
|
||||||
state.snapshot = {
|
state.snapshot = {
|
||||||
...next,
|
...next,
|
||||||
content: { ...next.content },
|
content: { ...next.content },
|
||||||
|
|
@ -94,6 +97,9 @@ export function applyKbEnv(snapshot: KbSnapshot): void {
|
||||||
process.env.KB_COUNTRY = snapshot.country
|
process.env.KB_COUNTRY = snapshot.country
|
||||||
process.env.KB_VERSION = snapshot.version
|
process.env.KB_VERSION = snapshot.version
|
||||||
process.env.KB_CONTENT = JSON.stringify(snapshot.content)
|
process.env.KB_CONTENT = JSON.stringify(snapshot.content)
|
||||||
|
process.env.KB_PATHS = JSON.stringify(
|
||||||
|
Object.fromEntries(resolveKbPaths(snapshot).map(row => [row.key, row.absolute])),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset store + env to unconfigured (plugin dispose). */
|
/** Reset store + env to unconfigured (plugin dispose). */
|
||||||
|
|
|
||||||
174
test/kb-loading.test.mjs
Normal file
174
test/kb-loading.test.mjs
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
/**
|
||||||
|
* KB loading: BOM MANIFEST, resolved path anchors, identity inlining, pack footer /
|
||||||
|
* duplicate isolation, forced reload, and jailed read/search tools.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { basename, join } from 'node:path'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { registerKbContextSkill } from '../src/netx/kb-context-skill.ts'
|
||||||
|
import { resolveKbPaths, resolveKbRoot } from '../src/netx/kb-manifest.ts'
|
||||||
|
import { registerKbLocalTools } from '../src/netx/kb-local-tools.ts'
|
||||||
|
import { registerKbPackSkills } from '../src/netx/kb-pack-skills.ts'
|
||||||
|
import { readKbFile, searchKb } from '../src/netx/kb-read-ops.ts'
|
||||||
|
import { applyKbEnv, publishKbContext, resetKbContext, watchKbContext } from '../src/netx/kb-runtime.ts'
|
||||||
|
|
||||||
|
const LOCAL = 'regions/ID/IOH/_local'
|
||||||
|
const BOM = String.fromCharCode(0xfeff)
|
||||||
|
|
||||||
|
function manifest() {
|
||||||
|
return JSON.stringify({
|
||||||
|
schemaVersion: '1.0',
|
||||||
|
packageType: 'operator-subset',
|
||||||
|
operator: { name: 'IOH', country: 'ID' },
|
||||||
|
version: '2026.09-v31',
|
||||||
|
content: { hasRegions: true, hasSkills: true },
|
||||||
|
paths: {
|
||||||
|
rca: 'regions/ID/IOH/00_有效RCA',
|
||||||
|
local: LOCAL,
|
||||||
|
localSkills: `${LOCAL}/local_skills`,
|
||||||
|
neIndex: 'regions/ID/IOH/00_索引/网元索引.md',
|
||||||
|
skills: '_skills',
|
||||||
|
},
|
||||||
|
}, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function skill(dir, name) {
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: ${name} playbook\n---\n\n# ${name}\n`, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal operator subset on disk; MANIFEST written with a UTF-8 BOM (PowerShell 5.1). */
|
||||||
|
async function withKb(run) {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'netxops-kb-load-'))
|
||||||
|
const outside = `${root}-secret.md`
|
||||||
|
try {
|
||||||
|
writeFileSync(join(root, 'MANIFEST.json'), `${BOM}${manifest()}`, 'utf8')
|
||||||
|
const rca = join(root, 'regions/ID/IOH/00_有效RCA/02_路由')
|
||||||
|
mkdirSync(rca, { recursive: true })
|
||||||
|
writeFileSync(join(rca, '20250911-MC-Aceh-Flap.md'), '# Aceh flap\n\n- 涉及网元 | MDN-BCP-CN1\n- BGP session flap\n', 'utf8')
|
||||||
|
writeFileSync(join(rca, '20250101-other.md'), '# other\n\nISIS only\n', 'utf8')
|
||||||
|
mkdirSync(join(root, 'regions/ID/IOH/00_索引'), { recursive: true })
|
||||||
|
writeFileSync(join(root, 'regions/ID/IOH/00_索引/网元索引.md'), 'line1\nline2\nline3\n', 'utf8')
|
||||||
|
mkdirSync(join(root, LOCAL, 'identity'), { recursive: true })
|
||||||
|
writeFileSync(join(root, LOCAL, 'identity/README.md'), 'template only', 'utf8')
|
||||||
|
writeFileSync(join(root, LOCAL, 'identity/话术与边界.md'), '不对外承诺恢复时间', 'utf8')
|
||||||
|
skill(join(root, '_skills/kb-troubleshoot'), 'kb-troubleshoot')
|
||||||
|
skill(join(root, '_skills/kb-retrieve'), 'kb-retrieve')
|
||||||
|
skill(join(root, LOCAL, 'local_skills/kb-retrieve'), 'kb-retrieve')
|
||||||
|
skill(join(root, LOCAL, 'local_skills/ioh-private'), 'ioh-private')
|
||||||
|
writeFileSync(outside, 'outside', 'utf8')
|
||||||
|
await run(root)
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true })
|
||||||
|
rmSync(outside, { force: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeCtx() {
|
||||||
|
const skills = new Map()
|
||||||
|
const tools = new Map()
|
||||||
|
const warnings = []
|
||||||
|
// Mirrors DSH: registering an existing name throws.
|
||||||
|
const register = map => (entry) => {
|
||||||
|
if (map.has(entry.name)) throw new Error(`duplicate: ${entry.name}`)
|
||||||
|
map.set(entry.name, entry)
|
||||||
|
return () => { map.delete(entry.name) }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
skills,
|
||||||
|
tools,
|
||||||
|
warnings,
|
||||||
|
ctx: {
|
||||||
|
logger: { info() {}, warn(...args) { warnings.push(args) }, error() {} },
|
||||||
|
skills: { register: register(skills) },
|
||||||
|
tools: { register: register(tools) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('MANIFEST with UTF-8 BOM resolves as configured', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
assert.equal(kb.status, 'configured', kb.errorMessage)
|
||||||
|
assert.equal(kb.version, '2026.09-v31')
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('resolveKbPaths returns absolute anchors and KB_PATHS mirrors them', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
const rows = Object.fromEntries(resolveKbPaths(kb).map(row => [row.key, row]))
|
||||||
|
assert.equal(rows.rca.exists, true)
|
||||||
|
assert.equal(rows.neIndex.absolute, join(root, 'regions/ID/IOH/00_索引/网元索引.md'))
|
||||||
|
applyKbEnv(kb)
|
||||||
|
assert.equal(JSON.parse(process.env.KB_PATHS).rca, rows.rca.absolute)
|
||||||
|
resetKbContext()
|
||||||
|
assert.equal(process.env.KB_PATHS, undefined)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('kb-context carries path anchors and inlines identity files (not README)', () => withKb((root) => {
|
||||||
|
const { ctx, skills } = fakeCtx()
|
||||||
|
registerKbContextSkill(ctx, resolveKbRoot(root))
|
||||||
|
const body = skills.get('kb-context').content
|
||||||
|
assert.ok(body.includes(join(root, 'regions/ID/IOH/00_索引/网元索引.md')))
|
||||||
|
assert.ok(body.includes('不对外承诺恢复时间'))
|
||||||
|
assert.equal(body.includes('template only'), false)
|
||||||
|
assert.match(skills.get('kb-context').description, /IOH/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('pack skills get the anchor footer; duplicate local name is skipped, not fatal', () => withKb(async (root) => {
|
||||||
|
const { ctx, skills, warnings } = fakeCtx()
|
||||||
|
const dispose = await registerKbPackSkills(ctx, resolveKbRoot(root), { enabled: true })
|
||||||
|
assert.deepEqual([...skills.keys()].sort(), ['ioh-private', 'kb-retrieve', 'kb-troubleshoot'])
|
||||||
|
assert.ok(skills.get('kb-troubleshoot').content.includes(join(root, 'regions/ID/IOH/00_有效RCA')))
|
||||||
|
assert.ok(warnings.some(args => String(args[0]).includes('duplicates')))
|
||||||
|
dispose()
|
||||||
|
assert.equal(skills.size, 0)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('publishKbContext force re-notifies an unchanged snapshot', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
publishKbContext(kb)
|
||||||
|
let calls = 0
|
||||||
|
const stop = watchKbContext(() => { calls += 1 })
|
||||||
|
publishKbContext(kb)
|
||||||
|
assert.equal(calls, 0)
|
||||||
|
publishKbContext(kb, { force: true })
|
||||||
|
assert.equal(calls, 1)
|
||||||
|
stop()
|
||||||
|
resetKbContext()
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('kbSearch ranks multi-term hits first and honours scope', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
const result = searchKb(kb, { query: 'mdn-bcp-cn bgp', scope: 'rca' })
|
||||||
|
assert.ok(result.hits[0].relativePath.endsWith('20250911-MC-Aceh-Flap.md'))
|
||||||
|
assert.deepEqual(result.hits[0].matchedTerms.sort(), ['bgp', 'mdn-bcp-cn'])
|
||||||
|
assert.equal(result.hits[0].lines[0].line, 3)
|
||||||
|
assert.equal(searchKb(kb, { query: 'isis', scope: 'neIndex' }).totalMatches, 0)
|
||||||
|
assert.equal(searchKb(kb, { query: 'IS.S\\s+only', regex: true }).totalMatches, 1)
|
||||||
|
assert.throws(() => searchKb(kb, { query: '(', regex: true }), /invalid regex/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('kbRead pages files, lists dirs, and stays inside kbRoot', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
const page = readKbFile(kb, { path: 'neIndex', offset: 2, limit: 1 })
|
||||||
|
assert.equal(page.text, 'line2')
|
||||||
|
assert.equal(page.truncated, true)
|
||||||
|
const dir = readKbFile(kb, { path: 'rca' })
|
||||||
|
assert.equal(dir.kind, 'directory')
|
||||||
|
assert.deepEqual(dir.entries.map(e => e.name), ['02_路由'])
|
||||||
|
assert.throws(() => readKbFile(kb, { path: `../${basename(root)}-secret.md` }), /escapes kbRoot/)
|
||||||
|
assert.throws(() => readKbFile(kb, { path: `${root}-secret.md` }), /escapes kbRoot/)
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('read tools mount without paths.local; write tools need it', () => withKb((root) => {
|
||||||
|
const kb = resolveKbRoot(root)
|
||||||
|
const readOnly = fakeCtx()
|
||||||
|
registerKbLocalTools(readOnly.ctx, { ...kb, paths: { rca: kb.paths.rca } })
|
||||||
|
assert.deepEqual([...readOnly.tools.keys()].sort(), ['netx__kbRead', 'netx__kbSearch'])
|
||||||
|
const full = fakeCtx()
|
||||||
|
registerKbLocalTools(full.ctx, kb)
|
||||||
|
assert.equal(full.tools.size, 9)
|
||||||
|
}))
|
||||||
|
|
@ -147,10 +147,11 @@ test('knowledge-base tools contribute to the mount inventory and disappear on di
|
||||||
const mount = fakeContext()
|
const mount = fakeContext()
|
||||||
const snapshot = { status: 'configured', realRoot: process.env.DSH_HOME, paths: { local: '_local' } }
|
const snapshot = { status: 'configured', realRoot: process.env.DSH_HOME, paths: { local: '_local' } }
|
||||||
const dispose = registerKbLocalTools(mount.ctx, snapshot, { plane: 'preset', source: 'kb-test' })
|
const dispose = registerKbLocalTools(mount.ctx, snapshot, { plane: 'preset', source: 'kb-test' })
|
||||||
assert.equal(mount.tools.size, 7)
|
// 2 read (kbSearch / kbRead) + 7 local write/list tools.
|
||||||
|
assert.equal(mount.tools.size, 9)
|
||||||
const statuses = getToolMountStatuses()
|
const statuses = getToolMountStatuses()
|
||||||
assert.ok(statuses.some(row => row.source === 'kb-test' && row.count === 7 && row.groups.includes('kb')))
|
assert.ok(statuses.some(row => row.source === 'kb-test' && row.count === 9 && row.groups.includes('kb')))
|
||||||
assert.equal(asToolsMountView({ mounts: statuses }).count, 7)
|
assert.equal(asToolsMountView({ mounts: statuses }).count, 9)
|
||||||
dispose()
|
dispose()
|
||||||
assert.equal(mount.tools.size, 0)
|
assert.equal(mount.tools.size, 0)
|
||||||
assert.equal(getToolMountStatuses().some(row => row.source === 'kb-test'), false)
|
assert.equal(getToolMountStatuses().some(row => row.source === 'kb-test'), false)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue