[
  {
    "path": ".github/workflows/go-test.yml",
    "content": "name: Golang CI and Test\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build-on-ubuntu2204:\n    strategy:\n      matrix:\n        os: [ darwin, windows, linux ]\n        arch: [amd64, arm64]\n    runs-on: ubuntu-22.04\n    name: test on ${{ matrix.os }} ${{ matrix.arch }}\n    steps:\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '1.24.1'\n      - uses: actions/checkout@v4\n        with:\n          submodules: 'recursive'\n          fetch-depth: 0\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v8\n        with:\n          version: v2.1\n          skip-cache: true\n          problem-matchers: true\n      - name: Test (go test)\n        run: |\n          make clean\n          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env\n          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make test\n      - name: MoLing Build\n        run: |\n          make clean\n          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env\n          TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: MoLing Release\non:\n  push:\n    tags:\n      - \"v*\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-on-ubuntu2204:\n    strategy:\n      matrix:\n        os: [ darwin, windows, linux ]\n        arch: [ amd64, arm64 ]\n    runs-on: ubuntu-22.04\n    name: Release on ${{ matrix.os }} ${{ matrix.arch }}\n    steps:\n      - uses: actions/setup-go@v5\n        with:\n          go-version: '1.24.1'\n      - uses: actions/checkout@v4\n        with:\n          submodules: 'recursive'\n          fetch-depth: 0\n      - name: MoLing Build\n        run: |\n          make clean\n          SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make env\n          SNAPSHOT_VERSION=${{ github.ref_name }} TARGET_OS=${{ matrix.os }} TARGET_ARCH=${{ matrix.arch }} make build\n          pwd\n          ls -al ./bin\n      - name: Create Archive\n        run: |\n          mkdir -p ./dist\n          pwd\n          ls -al ./bin\n          if [ \"${{ matrix.os }}\" = \"windows\" ]; then\n            cd ./bin && zip -qr ./../dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.zip ./bin/ . && cd ..\n          else\n            tar -czvf dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz -C ./bin/ .\n          fi\n      - name: Upload Release Asset\n        uses: softprops/action-gh-release@v2\n        if: startsWith(github.ref, 'refs/tags/')\n        with:\n          tag_name: ${{ github.ref_name }}\n          generate_release_notes: true\n          files: |\n            ./dist/moling-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.arch }}.*"
  },
  {
    "path": ".gitignore",
    "content": "/services/mo*\n/bin/moling"
  },
  {
    "path": ".golangci.yml",
    "content": "# This configuration file is not a recommendation.\n#\n# We intentionally use a limited set of linters.\n# This configuration file is used with different version of golangci-lint to avoid regressions:\n# the linters can change between version,\n# their configuration may be not compatible or their reports can be different,\n# and this can break some of our tests.\n# Also, some linters are not relevant for the project (e.g. linters related to SQL).\n#\n# We have specific constraints, so we use a specific configuration.\n#\n# See the file `.golangci.reference.yml` to have a list of all available configuration options.\n\nversion: \"2\"\n\nlinters:\n  default: none\n  # This list of linters is not a recommendation (same thing for all this configuration file).\n  # We intentionally use a limited set of linters.\n  # See the comment on top of this file.\n  enable:\n    - errcheck\n    - staticcheck\n    - errorlint\n\n  settings:\n    errorlint:\n      asserts: false\n    staticcheck:\n      checks:\n        # Invalid regular expression.\n        # https://staticcheck.dev/docs/checks/#SA1000\n        - all\n        - \"-ST1000\"\n        - \"-S1023\"\n        - \"-S1005\"\n        - \"-QF1004\"\n\n  exclusions:\n    paths:\n      - dist/\n      - docs/\n      - tests/\n      - bin/\n      - images/\n      - install/\n      - utils/\n\nformatters:\n  enable:\n    - gofmt\n    - goimports\n  settings:\n    gofmt:\n      rewrite-rules:\n        - pattern: 'interface{}'\n          replacement: 'any'\n    goimports:\n      local-prefixes:\n        - github.com/gojue/moling\n  exclusions:\n    paths:\n      - dist/\n      - docs/\n      - tests/\n      - bin/\n      - images/\n      - install/\n      - utils/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# v0.4.0 (2025-05-31)\n\n## What's Changed\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.2...v0.4.0\n<hr>\n\n# v0.3.2 (2025-05-20)\n\n## What's Changed\n\n* feat: update command arguments and add new client configurations for Trae and Trae CN by @cfc4n\n  in https://github.com/gojue/moling/pull/35\n* feat: update command arguments and improve logging for service initialization by @cfc4n\n  in https://github.com/gojue/moling/pull/37\n\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.1...v0.3.2\n<hr>\n\n# v0.3.1 (2025-04-22)\n\n## What's Changed\n\n* fix: handle parent process exit to ensure MCP Server shutdown by @cfc4n\n  in [#33](https://github.com/gojue/moling/pull/33)\n* feat: update default prompts for browser, command, and filesystem modules by @cfc4n\n  in [#34](https://github.com/gojue/moling/pull/34)\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.3.0...v0.3.1\n<hr>\n\n# v0.3.0 (2025-04-18)\n\n## What's Changed\n\n* feat: add client configuration paths for Trae and update README by @cfc4n\n  in [#27](https://github.com/gojue/moling/pull/27)\n* feat: support Cursor IDE (MCP Client) by @cfc4n in [#28](https://github.com/gojue/moling/pull/28)\n* feat: Improves code consistency, adds PID management, and enhances user prompts by @cfc4n\n  in [#29](https://github.com/gojue/moling/pull/29)\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.2.0...v0.3.0\n<hr>\n\n# v0.2.0 (2025-04-13)\n\n## What's Changed\n\n* feat: add support for advanced logging configuration by @cfc4n in [#25](https://github.com/gojue/moling/pull/25)\n* fix: resolve issue with incorrect breakpoint handling by @cfc4n in [#26](https://github.com/gojue/moling/pull/26)\n* docs: update README with debugging examples by @cfc4n in [#27](https://github.com/gojue/moling/pull/27)\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.1.1...v0.2.0\n<hr>\n\n# v0.1.1 (2025-04-08)\n## What's Changed\n\n* feat: update CLI descriptions for clarity and accuracy by @cfc4n in https://github.com/gojue/moling/pull/17\n* refactor: change log levels from Info to Debug and Warn for improved … by @cfc4n\n  in https://github.com/gojue/moling/pull/19\n* fix: update README files to replace image links with direct URLs by @cfc4n in https://github.com/gojue/moling/pull/20\n* feat: add MCP Client configuration to install script by @cfc4n in https://github.com/gojue/moling/pull/22\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.1.0...v0.1.1\n<hr>\n\n# v0.1.0 (2025-04-04)\n\n## What's Changed\n\n* Improve builder by @cfc4n in https://github.com/gojue/moling/pull/16\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.8...v0.1.0\n<hr>\n\n# v0.0.8 (2025-04-04)\n\n## What's Changed\n\n* docs: add Japanese README file by @eltociear in https://github.com/gojue/moling/pull/13\n* feat: implement log file rotation and enhance logging configuration by @cfc4n\n  in https://github.com/gojue/moling/pull/14\n* chore: update dependencies for chromedp and mcp-go to latest versions by @cfc4n\n  in https://github.com/gojue/moling/pull/15\n\n## New Contributors\n\n* @eltociear made their first contribution in https://github.com/gojue/moling/pull/13\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.7...v0.0.8\n<hr>\n\n# v0.0.7 (2025-03-31)\n\n## What's Changed\n\n* feat: add logging enhancements and listen address flag for SSE mode by @cfc4n\n  in https://github.com/gojue/moling/pull/11\n* feat: add client command for automated MCP Server configuration management by @cfc4n\n  in https://github.com/gojue/moling/pull/12\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.6...v0.0.7\n<hr>\n\n# v0.0.6 (2025-03-30)\n\n## What's Changed\n\n* feat: enhance service initialization and configuration loading by @cfc4n in https://github.com/gojue/moling/pull/9\n* feat: update installation script and README for improved user experie… by @cfc4n\n  in https://github.com/gojue/moling/pull/10\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.5...v0.0.6\n<hr>\n\n# v0.0.5 (2025-03-29)\n## What's Changed\n\n* fix #4 invalid data path by @cfc4n in https://github.com/gojue/moling/pull/5\n* feat: add automated release notes generation in release workflow by @cfc4n in https://github.com/gojue/moling/pull/6\n* Improve logging & error handling, rename configs, simplify service logic by @cfc4n\n  in https://github.com/gojue/moling/pull/8\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.3...v0.0.5\n<hr>\n\n# v0.0.3 (2025-03-28)\n## What's Changed\n\n* fix: build failed with GOOS=windows by @cfc4n in https://github.com/gojue/moling/pull/1\n* feat: refactor server initialization and directory creation logic by @cfc4n in https://github.com/gojue/moling/pull/2\n* feat: enhance configuration management and logging in the MoLing server by @cfc4n\n  in https://github.com/gojue/moling/pull/3\n\n## New Contributors\n\n* @cfc4n made their first contribution in https://github.com/gojue/moling/pull/1\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.2...v0.0.3\n<hr>\n\n# v0.0.2 (2025-03-27)\n\n## What's Changed\n- Enhanced command validation with global configuration and support for piped commands\n- Added installation scripts for Windows/Linux and updated documentation\n- Implemented directory creation checks for base path and subdirectories\n- Corrected spelling of \"MoLing\" and \"macOS\" in documentation\n- Updated filename format for saved images\n- Refactored configuration management to use BasePath and improved browser initialization\n- Added listen address configuration and improved server initialization\n- Enhanced configuration management and logging\n- Added configuration management and service registration\n- Added browser service with logging\n- Updated GitHub links in README for accurate repository references\n\n**Full Changelog**: https://github.com/gojue/moling/compare/v0.0.1...v0.0.2\n<hr>\n\n# v0.0.1 (2025-03-23)\n\n## What's Changed\n1. support `filesystem` service\n2. support `command line` service\n3. add GitHub action workflow\n\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright CFC4N [cfc4ncs@gmail.com]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "Makefile",
    "content": "include variables.mk\ninclude functions.mk\n\n.PHONY: all | env\nall: clean build\n\t@echo $(shell date)\n\n.ONESHELL:\nSHELL = /bin/bash\n\n.PHONY: env\nenv:\n\t@echo ---------------------------------------\n\t@echo \"MoLing Makefile Environment:\"\n\t@echo ---------------------------------------\n\t@echo \"SNAPSHOT_VERSION         $(SNAPSHOT_VERSION)\"\n\t@echo ---------------------------------------\n\t@echo \"OS_NAME                  $(OS_NAME)\"\n\t@echo \"OS_ARCH                  $(OS_ARCH)\"\n\t@echo \"TARGET_OS                $(TARGET_OS)\"\n\t@echo \"TARGET_ARCH              $(TARGET_ARCH)\"\n\t@echo \"GO_VERSION               $(GO_VERSION)\"\n\t@echo ---------------------------------------\n\t@echo \"CMD_GIT                  $(CMD_GIT)\"\n\t@echo \"CMD_GO                   $(CMD_GO)\"\n\t@echo \"CMD_INSTALL              $(CMD_INSTALL)\"\n\t@echo \"CMD_MD5                  $(CMD_MD5)\"\n\t@echo ---------------------------------------\n\t@echo \"VERSION_NUM              $(VERSION_NUM)\"\n\t@echo \"LAST_GIT_TAG             $(LAST_GIT_TAG)\"\n\t@echo ---------------------------------------\n\n\n.PHONY: help\nhelp:\n\t@echo \"# environment\"\n\t@echo \"    $$ make env\t\t\t\t\t# show makefile environment/variables\"\n\t@echo \"\"\n\t@echo \"# build\"\n\t@echo \"    $$ make all\t\t\t\t\t# build MoLing\"\n\t@echo \"\"\n\t@echo \"# clean\"\n\t@echo \"    $$ make clean\t\t\t\t# wipe ./bin/\"\n\t@echo \"\"\n\t@echo \"# test\"\n\n.PHONY: clean build\n\n.PHONY: clean\nclean:\n\t$(CMD_RM) -f $(OUT_BIN)*\n\n.PHONY: build\nbuild:clean\n\t$(call gobuild,$(TARGET_OS),$(TARGET_ARCH))\n\n# Format the code\n.PHONY: format\nformat:\n\t@echo \"  ->  Formatting code\"\n\tgolangci-lint run --disable-all -E errcheck -E staticcheck\n\n.PHONY: test\ntest:\n\tCGO_ENABLED=1 go test -v -race ./...\n"
  },
  {
    "path": "Makefile.release",
    "content": "include variables.mk\ninclude functions.mk\n\n.PHONY: all build package clean\n\n# 定义目标系统和架构\nTARGET_SYSTEMS := darwin linux windows\nTARGET_ARCHS := amd64 arm64\n\n# 输出目录\nDIST_DIR := dist\n\nall: build\n\n# 遍历系统和架构，编译程序\nbuild:\n\t@for os in $(TARGET_SYSTEMS); do \\\n\t\tfor arch in $(TARGET_ARCHS); do \\\n\t\t\techo \"Building for $$os/$$arch...\"; \\\n\t\t\t$(MAKE) TARGET_OS=$$os TARGET_ARCH=$$arch ; \\\n\t\t\techo \"Packaging for $$os/$$arch...\"; \\\n\t\t\tOUT_DIR=moling\\_$$os\\_$$arch\\_$(COMMIT); \\\n\t\t\tmkdir -p $(DIST_DIR)/$$OUT_DIR; \\\n\t\t\tcp bin/moling$$([ \"$$os\" = \"windows\" ] && echo \".exe\") $(DIST_DIR)/$$OUT_DIR/; \\\n\t\t\tif [ \"$$os\" = \"windows\" ]; then \\\n\t\t\t\t(cd $(DIST_DIR) && zip -r $$OUT_DIR.zip $$OUT_DIR); \\\n            else \\\n\t\t\t\t(cd $(DIST_DIR) && tar -czf $$OUT_DIR.tar.gz $$OUT_DIR); \\\n            fi; \\\n\t\tdone; \\\n\tdone\n\n# 清理生成的文件\nclean:\n\t$(CMD_RM) -rf bin/* $(DIST_DIR)/*"
  },
  {
    "path": "README.md",
    "content": "## MoLing MCP Server\n\nEnglish | [中文](./README_ZH_HANS.md) | [日本語](./README_JA_JP.md)\n\n[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)\n[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)\n[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)\n[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)\n\n---\n\n![](./images/logo.svg)\n\n### Introduction\nMoLing is a computer-use and browser-use MCP Server that implements system interaction through operating system APIs, enabling file system operations such as reading, writing, merging, statistics, and aggregation, as well as the ability to execute system commands. It is a dependency-free local office automation assistant.\n\n### Advantages\n> [!IMPORTANT]\n> Requiring no installation of any dependencies, MoLing can be run directly and is compatible with multiple operating systems, including Windows, Linux, and macOS. \n> This eliminates the hassle of dealing with environment conflicts involving Node.js, Python, Docker and other development environments.\n\n### Features\n\n> [!CAUTION]\n> Command-line operations are dangerous and should be used with caution.\n\n- **File System Operations**: Reading, writing, merging, statistics, and aggregation\n- **Command-line Terminal**: Execute system commands directly\n- **Browser Control**: Powered by `github.com/chromedp/chromedp`\n    - Chrome browser is required.\n    - In Windows, the full path to Chrome needs to be configured in the system environment variables.\n- **Future Plans**:\n    - Personal PC data organization\n    - Document writing assistance\n    - Schedule planning\n    - Life assistant features\n\n> [!WARNING]\n> Currently, MoLing has only been tested on macOS, and other operating systems may have issues.\n\n### Supported MCP Clients\n\n- [Claude](https://claude.ai/)\n- [Cline](https://cline.bot/)\n- [Cherry Studio](https://cherry-ai.com/)\n- etc. (who support MCP protocol)\n\n#### Demos\n\nhttps://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7\n\nMoLing in [Claude](https://claude.ai/)\n![](./images/screenshot_claude.png)\n\n#### Configuration Format\n\n##### MCP Server (MoLing) configuration\n\nThe configuration file will be generated at `/Users/username/.moling/config/config.json`, and you can modify its\ncontents as needed.\n\nIf the file does not exist, you can create it using `moling config --init`.\n\n##### MCP Client configuration\nFor example, to configure the Claude client, add the following configuration:\n\n> [!TIP]\n> \n> Only 3-6 lines of configuration are needed.\n> \n> Claude config path: `~/Library/Application\\ Support/Claude/claude_desktop_config`\n\n```json\n{\n  \"mcpServers\": {\n    \"MoLing\": {\n      \"command\": \"/usr/local/bin/moling\",\n      \"args\": []\n    }\n  }\n}\n```\n\nand, `/usr/local/bin/moling` is the path to the MoLing server binary you downloaded.\n\n**Automatic Configuration**\n\nrun `moling client --install` to automatically install the configuration for the MCP client.\n\nMoLing will automatically detect the MCP client and install the configuration for you. including: Cline, Claude, Roo\nCode, etc.\n\n### Operation Modes\n\n- **Stdio Mode**: CLI-based interactive mode for user-friendly experience\n- **SSE Mode**: Server-Side Rendering mode optimized for headless/automated environments\n\n### Installation\n\n#### Option 1: Install via Script\n##### Linux/MacOS\n```shell\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)\"\n```\n##### Windows\n\n> [!WARNING]\n> Not tested, unsure if it works.\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex\"\n```\n\n#### Option 2: Direct Download\n1. Download the installation package from [releases page](https://github.com/gojue/moling/releases)\n2. Extract the package\n3. Run the server:\n   ```sh\n   ./moling\n   ```\n\n#### Option 3: Build from Source\n1. Clone the repository:\n```sh\ngit clone https://github.com/gojue/moling.git\ncd moling\n```\n2. Build the project (requires Golang toolchain):\n```sh\nmake build\n```\n3. Run the compiled binary:\n```sh\n./bin/moling\n```\n\n### Usage\nAfter starting the server, connect using any supported MCP client by configuring it to point to your MoLing server address.\n\n### License\nApache License 2.0. See [LICENSE](LICENSE) for details.\n"
  },
  {
    "path": "README_JA_JP.md",
    "content": "## MoLing MCP サーバー\n\n[English](./README.md) | [中文](./README_ZH_HANS.md) | 日本語\n\n[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)\n[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)\n[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)\n[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)\n\n---\n\n![](./images/logo.svg)\n\n### 紹介\nMoLingは、オペレーティングシステムAPIを介してシステム操作を実装するコンピュータ使用およびブラウザ使用のMCPサーバーであり、ファイルシステム操作（読み取り、書き込み、マージ、統計、集計）やシステムコマンドの実行を可能にします。依存関係のないローカルオフィス自動化アシスタントです。\n\n### 利点\n> [!IMPORTANT]\n> 依存関係のインストールを必要とせず、MoLingは直接実行でき、Windows、Linux、macOSなどの複数のオペレーティングシステムと互換性があります。\n> これにより、Node.js、Python、Dockerなどの開発環境に関連する環境の競合を処理する手間が省けます。\n\n### 機能\n\n> [!CAUTION]\n> コマンドライン操作は危険であり、慎重に使用する必要があります。\n\n- **ファイルシステム操作**：読み取り、書き込み、マージ、統計、集計\n- **コマンドラインターミナル**：システムコマンドを直接実行\n- **ブラウザ制御**：`github.com/chromedp/chromedp`によって提供される\n    - Chromeブラウザのインストールが必要です。\n    - Windows環境では、環境変数にChromeのフルパスを設定する必要があります。\n- **将来の計画**：\n    - 個人PCデータの整理\n    - ドキュメント作成支援\n    - スケジュール計画\n    - 生活アシスタント機能\n\n> [!WARNING]\n> 現在、MoLingはmacOSでのみテストされており、他のオペレーティングシステムでは問題が発生する可能性があります。\n\n### サポートされているMCPクライアント\n\n- [Claude](https://claude.ai/)\n- [Cline](https://cline.bot/)\n- [Cherry Studio](https://cherry-ai.com/)\n- その他（MCPプロトコルをサポートするクライアント）\n\n#### スクリーンショット\n\nhttps://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7\n\n[Claude](https://claude.ai/)に統合されたMoLing\n![](./images/screenshot_claude.png)\n\n#### 設定形式\n\n##### MCPサーバー（MoLing）設定\n\n設定ファイルは`/Users/username/.moling/config/config.json`に生成され、必要に応じて内容を変更できます。\n\nファイルが存在しない場合は、`moling config --init`を使用して作成できます。\n\n##### MCPクライアント設定\n例として、Claudeクライアントを設定するには、次の設定を追加します：\n\n> [!TIP]\n> \n> 3〜6行の設定のみが必要です。\n> \n> Claude設定パス：`~/Library/Application\\ Support/Claude/claude_desktop_config`\n\n```json\n{\n  \"mcpServers\": {\n    \"MoLing\": {\n      \"command\": \"/usr/local/bin/moling\",\n      \"args\": []\n    }\n  }\n}\n```\n\nまた、`/usr/local/bin/moling`はダウンロードしたMoLingサーバーバイナリのパスです。\n\n**自動設定**\n\n`moling client --install`を実行して、MCPクライアントの設定を自動的にインストールします。\n\nMoLingはMCPクライアントを自動的に検出し、設定をインストールします。Cline、Claude、Roo Codeなどを含みます。\n\n### 動作モード\n\n- **Stdioモード**：CLIベースのインタラクティブモードで、ユーザーフレンドリーな体験を提供\n- **SSEモード**：ヘッドレス/自動化環境に最適化されたサーバーサイドレンダリングモード\n\n### インストール\n\n#### オプション1：スクリプトを使用してインストール\n##### Linux/MacOS\n```shell\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)\"\n```\n##### Windows\n\n> [!WARNING]\n> テストされていないため、動作するかどうかは不明です。\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex\"\n```\n\n#### オプション2：直接ダウンロード\n1. [リリースページ](https://github.com/gojue/moling/releases)からインストールパッケージをダウンロード\n2. パッケージを解凍\n3. サーバーを実行：\n   ```sh\n   ./moling\n   ```\n\n#### オプション3：ソースからビルド\n1. リポジトリをクローン：\n```sh\ngit clone https://github.com/gojue/moling.git\ncd moling\n```\n2. プロジェクトをビルド（Golangツールチェーンが必要）：\n```sh\nmake build\n```\n3. コンパイルされたバイナリを実行：\n```sh\n./bin/moling\n```\n\n### 使用方法\nサーバーを起動した後、サポートされているMCPクライアントを使用して、MoLingサーバーアドレスに接続します。\n\n### ライセンス\nApache License 2.0。詳細は[LICENSE](LICENSE)ファイルを参照してください。\n"
  },
  {
    "path": "README_ZH_HANS.md",
    "content": "## MoLing MCP 服务器\n\n[English](./README.md) | 中文 | [日本語](./README_JA_JP.md)\n\n[![GitHub stars](https://img.shields.io/github/stars/gojue/moling.svg?label=Stars&logo=github)](https://github.com/gojue/moling/stargazers)\n[![GitHub forks](https://img.shields.io/github/forks/gojue/moling?label=Forks&logo=github)](https://github.com/gojue/moling/forks)\n[![CI](https://github.com/gojue/moling/actions/workflows/go-test.yml/badge.svg)](https://github.com/gojue/moling/actions/workflows/go-test.yml)\n[![Github Version](https://img.shields.io/github/v/release/gojue/moling?display_name=tag&include_prereleases&sort=semver)](https://github.com/gojue/moling/releases)\n\n---\n\n![](./images/logo.svg)\n\n### 简介\nMoLing是一个computer-use和browser-use的MCP Server，基于操作系统API实现了系统交互，浏览器模拟控制，可以实现文件系统的读写、合并、统计、聚合等操作，也可以执行系统命令操作。是一个无需任何依赖的本地办公自动化助手。\n\n### 优势\n> [!IMPORTANT]\n> 没有任何安装依赖，直接运行，兼容Windows、Linux、macOS等操作系统。\n> 再也不用苦恼NodeJS、Python、Docker等环境冲突等问题。\n\n### 功能特性\n\n> [!CAUTION]\n> 命令行操作具备一定风险性，且不可回滚，使用需谨慎，默认配置为只读的命令列表。\n\n- **文件系统操作**：读取、写入、合并、统计和聚合\n- **命令行终端**：直接执行系统命令\n- **浏览器控制**：基于 `github.com/chromedp/chromedp`\n  - 需要安装Chrome浏览器\n  - Windows系统中，需要在环境变量中配置Chrome的完整路径\n- **未来计划**：\n    - 个人电脑资料整理\n    - 文档编写辅助\n    - 行程规划\n    - 生活助手功能\n\n> [!WARNING]\n> 当前, MoLing仅在macOS测试通过，Linux和Windows未经验证。\n\n### 支持的MCP客户端\n\n- [Claude](https://claude.ai/)\n- [Cline](https://cline.bot/)\n- [Cherry Studio](https://cherry-ai.com/)\n- 其他（支持MCP协议的客户端）\n\n#### 演示\n\nhttps://github.com/user-attachments/assets/229c4dd5-23b4-4b53-9e25-3eba8734b5b7\n\n集成在[Claude](https://claude.ai/)中的MoLing\n![](./images/screenshot_claude.png)\n\n#### 配置格式\n\n##### MCP Server（MoLing）配置\n\n配置文件会生成在`/Users/username/.moling/config/config.json`下，你可以自行修改内容。若文件不存在，你可以通过\n`moling config --init`创建它。\n\n##### MCP Client配置\n以Claude客户端为例，在配置文件中添加如下配置：\n\n> [!TIP]\n> \n> 仅需添加3-6行的配置。\n> Claude配置文件路径：`~/Library/Application\\ Support/Claude/claude_desktop_config`\n\n```json\n{\n  \"mcpServers\": {\n    \"MoLing\": {\n      \"command\": \"/usr/local/bin/moling\",\n      \"args\": []\n    }\n  }\n}\n```\n\n另外， `/usr/local/bin/moling` 是你存放`MoLing` Server可执行文件的路径，可以自己指定。\n\n**自动配置**\n\n运行 `moling client --install` 命令将会自动为本机的所有MCP客户端安装MoLing。包括Cline、 Claude、 Roo Code等等。\n\n### 运行模式\n\n- **Stdio模式**：本地命令行交互模式，依赖于终端输入输出，适合人机交互\n- **SSE模式**：远程通讯模式，适合远程部署，远程调用\n\n### 安装指南\n\n\n#### 方法一： 脚本安装\n#### Linux/MacOS\n```shell\n/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.sh)\"\n```\n\n##### Windows\n\n> [!WARNING]\n> 未测试，不确定是否正常。\n\n```powershell\npowershell -ExecutionPolicy ByPass -c \"irm https://raw.githubusercontent.com/gojue/moling/HEAD/install/install.ps1 | iex\"\n```\n\n\n#### 方法二：直接下载\n1. 从[发布页面](https://github.com/gojue/moling/releases)下载安装包\n2. 解压安装包\n3. 运行服务器：\n```sh\n./moling\n```\n\n#### 方法三：从源码编译\n1. 克隆代码库：\n```sh\ngit clone https://github.com/gojue/moling.git\ncd moling\n```\n2. 编译项目（需要Golang工具链）：\n```sh\nmake build\n```\n3. 运行编译后的程序：\n```sh\n./bin/moling\n```\n\n### 使用说明\n启动服务器后，使用任何支持的MCP客户端配置连接到您的MoLing服务器地址即可。\n\n### 许可证\nApache License 2.0。详见[LICENSE](LICENSE)文件。\n"
  },
  {
    "path": "bin/.gitkeep",
    "content": ""
  },
  {
    "path": "cli/cmd/client.go",
    "content": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n *  Repository: https://github.com/gojue/moling\n *\n */\n\npackage cmd\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/gojue/moling/client\"\n)\n\nvar clientCmd = &cobra.Command{\n\tUse:   \"client\",\n\tShort: \"Provides automated access to MoLing MCP Server for local MCP clients, Cline, Roo Code, and Claude, etc.\",\n\tLong: `Automatically checks the MCP clients installed on the current computer, displays them, and automatically adds the MoLing MCP Server configuration to enable one-click activation, reducing the hassle of manual configuration.\nCurrently supports the following clients: Cline, Roo Code, Claude\n    moling client -l --list   List the current installed MCP clients\n    moling client -i --install Add MoLing MCP Server configuration to the currently installed MCP clients on this computer\n`,\n\tRunE: ClientCommandFunc,\n}\n\nvar (\n\tlist    bool\n\tinstall bool\n)\n\n// ClientCommandFunc executes the \"config\" command.\nfunc ClientCommandFunc(command *cobra.Command, args []string) error {\n\tlogger := initLogger(mlConfig.BasePath)\n\tconsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}\n\tmulti := zerolog.MultiLevelWriter(consoleWriter, logger)\n\tlogger = zerolog.New(multi).With().Timestamp().Logger()\n\tmlConfig.SetLogger(logger)\n\tlogger.Debug().Msg(\"Start to show MCP Clients\")\n\tmcpConfig := client.NewMCPServerConfig(CliDescription, CliName, MCPServerName)\n\texePath, err := os.Executable()\n\tif err == nil {\n\t\tlogger.Debug().Str(\"exePath\", exePath).Msg(\"executable path, will use this path to find the config file\")\n\t\tmcpConfig.Command = exePath\n\t}\n\tcm := client.NewManager(logger, mcpConfig)\n\tif install {\n\t\tlogger.Info().Msg(\"Start to add MCP Server configuration into MCP Clients.\")\n\t\tcm.SetupConfig()\n\t\tlogger.Info().Msg(\"Add MCP Server configuration into MCP Clients successfully.\")\n\t\treturn nil\n\t}\n\tlogger.Info().Msg(\"Start to list MCP Clients\")\n\tcm.ListClient()\n\tlogger.Info().Msg(\"List MCP Clients successfully.\")\n\treturn nil\n}\n\nfunc init() {\n\tclientCmd.PersistentFlags().BoolVar(&list, \"list\", false, \"List the current installed MCP clients\")\n\tclientCmd.PersistentFlags().BoolVarP(&install, \"install\", \"i\", false, \"Add MoLing MCP Server configuration to the currently installed MCP clients on this computer. default is all\")\n\trootCmd.AddCommand(clientCmd)\n}\n"
  },
  {
    "path": "cli/cmd/config.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/services\"\n)\n\nvar configCmd = &cobra.Command{\n\tUse:   \"config\",\n\tShort: \"Show the configuration of the current service list\",\n\tLong: `Show the configuration of the current service list. You can refer to the configuration file to modify the configuration.\n`,\n\tRunE: ConfigCommandFunc,\n}\n\nvar (\n\tinitial bool\n)\n\n// ConfigCommandFunc executes the \"config\" command.\nfunc ConfigCommandFunc(command *cobra.Command, args []string) error {\n\tvar err error\n\tlogger := initLogger(mlConfig.BasePath)\n\tconsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}\n\tmulti := zerolog.MultiLevelWriter(consoleWriter, logger)\n\tlogger = zerolog.New(multi).With().Timestamp().Logger()\n\tmlConfig.SetLogger(logger)\n\tlogger.Info().Msg(\"Start to show config\")\n\tctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig)\n\tctx = context.WithValue(ctx, comm.MoLingLoggerKey, logger)\n\n\t// 当前配置文件检测\n\thasConfig := false\n\tvar nowConfig []byte\n\tnowConfigJSON := make(map[string]any)\n\tconfigFilePath := filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)\n\tif nowConfig, err = os.ReadFile(configFilePath); err == nil {\n\t\thasConfig = true\n\t}\n\tif hasConfig {\n\t\terr = json.Unmarshal(nowConfig, &nowConfigJSON)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unmarshaling JSON: %w, payload:%s\", err, string(nowConfig))\n\t\t}\n\t}\n\n\tbf := bytes.Buffer{}\n\tbf.WriteString(\"\\n{\\n\")\n\n\t// 写入GlobalConfig\n\tmlConfigJSON, err := json.Marshal(mlConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling GlobalConfig: %w\", err)\n\t}\n\tbf.WriteString(\"\\t\\\"MoLingConfig\\\":\\n\")\n\tbf.WriteString(fmt.Sprintf(\"\\t%s,\\n\", mlConfigJSON))\n\tfirst := true\n\tfor srvName, nsv := range services.ServiceList() {\n\t\t// 获取服务对应的配置\n\t\tcfg, ok := nowConfigJSON[string(srvName)].(map[string]any)\n\n\t\tsrv, err := nsv(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// srv Loadconfig\n\t\tif ok {\n\t\t\terr = srv.LoadConfig(cfg)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error loading config for service %s: %w\", srv.Name(), err)\n\t\t\t}\n\t\t} else {\n\t\t\tlogger.Debug().Str(\"service\", string(srv.Name())).Msg(\"Service not found in config, using default config\")\n\t\t}\n\t\t// srv Init\n\t\terr = srv.Init()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error initializing service %s: %w\", srv.Name(), err)\n\t\t}\n\t\tif !first {\n\t\t\tbf.WriteString(\",\\n\")\n\t\t}\n\t\tbf.WriteString(fmt.Sprintf(\"\\t\\\"%s\\\":\\n\", srv.Name()))\n\t\tbf.WriteString(fmt.Sprintf(\"\\t%s\\n\", srv.Config()))\n\t\tfirst = false\n\t}\n\tbf.WriteString(\"}\\n\")\n\t// 解析原始 JSON 字符串\n\tvar data any\n\terr = json.Unmarshal(bf.Bytes(), &data)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error unmarshaling JSON: %w, payload:%s\", err, bf.String())\n\t}\n\n\t// 格式化 JSON\n\tformattedJSON, err := json.MarshalIndent(data, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling JSON: %w\", err)\n\t}\n\n\t// 如果不存在配置文件\n\tif !hasConfig {\n\t\tlogger.Info().Msgf(\"Configuration file %s does not exist. Creating a new one.\", configFilePath)\n\t\terr = os.WriteFile(configFilePath, formattedJSON, 0644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error writing configuration file: %w\", err)\n\t\t}\n\t\tlogger.Info().Msgf(\"Configuration file %s created successfully.\", configFilePath)\n\t}\n\tlogger.Info().Str(\"config\", configFilePath).Msg(\"Current loaded configuration file path\")\n\tlogger.Info().Msg(\"You can modify the configuration file to change the settings.\")\n\tif !initial {\n\t\tlogger.Info().Msgf(\"Configuration details: \\n%s\\n\", formattedJSON)\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tconfigCmd.PersistentFlags().BoolVar(&initial, \"init\", false, fmt.Sprintf(\"Save configuration to %s\", filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)))\n\trootCmd.AddCommand(configCmd)\n}\n"
  },
  {
    "path": "cli/cmd/perrun.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage cmd\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\n// mlsCommandPreFunc is a pre-run function for the MoLing command.\nfunc mlsCommandPreFunc(cmd *cobra.Command, args []string) error {\n\terr := utils.CreateDirectory(mlConfig.BasePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, dirName := range mlDirectories {\n\t\terr = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/root.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/rs/zerolog\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/gojue/moling/cli/cobrautl\"\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/server\"\n\t\"github.com/gojue/moling/pkg/services\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\nconst (\n\tCliName            = \"moling\"\n\tCliNameZh          = \"魔灵\"\n\tMCPServerName      = \"MoLing MCP Server\"\n\tCliDescription     = \"MoLing is a computer-use and browser-use based MCP server. It is a locally deployed, dependency-free office AI assistant.\"\n\tCliDescriptionZh   = \"MoLing（魔灵）是一款基于computer-use和浏browser-use的 MCP 服务器，它是一个本地部署、无依赖的办公 AI 助手。\"\n\tCliHomepage        = \"https://gojue.cc/moling\"\n\tCliAuthor          = \"CFC4N <cfc4ncs@gmail.com>\"\n\tCliGithubRepo      = \"https://github.com/gojue/moling\"\n\tCliDescriptionLong = `\nMoLing is a computer-based MCP Server that implements system interaction through operating system APIs, enabling file system operations such as reading, writing, merging, statistics, and aggregation, as well as the ability to execute system commands. It is a dependency-free local office automation assistant.\n\nRequiring no installation of any dependencies, MoLing can be run directly and is compatible with multiple operating systems, including Windows, Linux, and macOS. This eliminates the hassle of dealing with environment conflicts involving Node.js, Python, and other development environments.\n\nUsage:\n  moling\n  moling -l 127.0.0.1:6789\n  moling -h\n  moling client -i\n  moling config \n`\n\tCliDescriptionLongZh = `MoLing（魔灵）是一个computer-use的MCP Server，基于操作系统API实现了系统交互，可以实现文件系统的读写、合并、统计、聚合等操作，也可以执行系统命令操作。是一个无需任何依赖的本地办公自动化助手。\n没有任何安装依赖，直接运行，兼容Windows、Linux、macOS等操作系统。再也不用苦恼NodeJS、Python等环境冲突等问题。\n\nUsage:\n  moling\n  moling -l 127.0.0.1:29118\n  moling -h\n  moling client -i\n  moling config \n`\n)\n\nconst (\n\tMLConfigName = \"config.json\"     // config file name of MoLing Server\n\tMLRootPath   = \".moling\"         // config file name of MoLing Server\n\tMLPidName    = \"moling.pid\"      // pid file name\n\tLogFileName  = \"moling.log\"      //\tlog file name\n\tMaxLogSize   = 1024 * 1024 * 512 // 512MB\n)\n\nvar (\n\tGitVersion = \"unknown_arm64_v0.0.0_2025-03-22 20:08\"\n\tmlConfig   = &config.MoLingConfig{\n\t\tVersion:    GitVersion,\n\t\tConfigFile: filepath.Join(\"config\", MLConfigName),\n\t\tBasePath:   filepath.Join(os.TempDir(), MLRootPath), // will set in mlsCommandPreFunc\n\t}\n\n\t// mlDirectories is a list of directories to be created in the base path\n\tmlDirectories = []string{\n\t\t\"logs\",    // log file\n\t\t\"config\",  // config file\n\t\t\"browser\", // browser cache\n\t\t\"data\",    // data\n\t\t\"cache\",\n\t}\n)\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:        CliName,\n\tShort:      CliDescription,\n\tSuggestFor: []string{\"molin\", \"moli\", \"mling\"},\n\n\tLong: CliDescriptionLong,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\tRunE:              mlsCommandFunc,\n\tPersistentPreRunE: mlsCommandPreFunc,\n}\n\nfunc usageFunc(c *cobra.Command) error {\n\treturn cobrautl.UsageFunc(c, GitVersion)\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\trootCmd.SetUsageFunc(usageFunc)\n\trootCmd.SetHelpTemplate(`{{.UsageString}}`)\n\trootCmd.CompletionOptions.DisableDefaultCmd = true\n\trootCmd.Version = GitVersion\n\trootCmd.SetVersionTemplate(`{{with .Name}}{{printf \"%s \" .}}{{end}}{{printf \"version:\\t%s\" .Version}}\n`)\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\t// set default config file path\n\tcurrentUser, err := user.Current()\n\tif err == nil {\n\t\tmlConfig.BasePath = filepath.Join(currentUser.HomeDir, MLRootPath)\n\t}\n\n\tcobra.EnablePrefixMatching = true\n\t// Cobra also supports local flags, which will only run\n\t// when this action is called directly.\n\trootCmd.PersistentFlags().StringVar(&mlConfig.BasePath, \"base_path\", mlConfig.BasePath, \"MoLing Base Data Path, automatically set by the system, cannot be changed, display only.\")\n\trootCmd.PersistentFlags().BoolVarP(&mlConfig.Debug, \"debug\", \"d\", false, \"Debug mode, default is false.\")\n\trootCmd.PersistentFlags().StringVarP(&mlConfig.ListenAddr, \"listen_addr\", \"l\", \"\", \"listen address for SSE mode. default:'', not listen, used STDIO mode.\")\n\trootCmd.PersistentFlags().StringVarP(&mlConfig.AuthToken, \"token\", \"t\", \"\", \"auth token for SSE mode. Auto-generated if empty. Clients must supply it as ?token=<token> or Authorization: Bearer <token>.\")\n\trootCmd.PersistentFlags().StringVarP(&mlConfig.Module, \"module\", \"m\", \"all\", \"module to load, default: all; others: Browser,FileSystem,Command, etc. Multiple modules are separated by commas\")\n\trootCmd.SilenceUsage = true\n}\n\n// initLogger init logger\nfunc initLogger(mlDataPath string) zerolog.Logger {\n\tvar logger zerolog.Logger\n\tvar err error\n\tlogFile := filepath.Join(mlDataPath, \"logs\", LogFileName)\n\tzerolog.SetGlobalLevel(zerolog.InfoLevel)\n\tif mlConfig.Debug {\n\t\tzerolog.SetGlobalLevel(zerolog.DebugLevel)\n\t}\n\n\t// 初始化 RotateWriter\n\trw, err := utils.NewRotateWriter(logFile, MaxLogSize) // 512MB 阈值\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to open log file %s: %s\", logFile, err.Error()))\n\t}\n\tlogger = zerolog.New(rw).With().Timestamp().Logger()\n\tlogger.Info().Uint32(\"MaxLogSize\", MaxLogSize).Msgf(\"Log files are automatically rotated when they exceed the size threshold, and saved to %s.1 and %s.2 respectively\", LogFileName, LogFileName)\n\treturn logger\n}\n\nfunc mlsCommandFunc(command *cobra.Command, args []string) error {\n\tloger := initLogger(mlConfig.BasePath)\n\tmlConfig.SetLogger(loger)\n\tvar err error\n\tvar nowConfig []byte\n\tvar nowConfigJSON map[string]any\n\n\t// 增加实例重复运行检测\n\tpidFilePath := filepath.Join(mlConfig.BasePath, MLPidName)\n\tloger.Info().Str(\"pid\", pidFilePath).Msg(\"Starting MoLing MCP Server...\")\n\terr = utils.CreatePIDFile(pidFilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 当前配置文件检测\n\tloger.Info().Str(\"ServerName\", MCPServerName).Str(\"version\", GitVersion).Msg(\"start\")\n\tconfigFilePath := filepath.Join(mlConfig.BasePath, mlConfig.ConfigFile)\n\tif nowConfig, err = os.ReadFile(configFilePath); err == nil {\n\t\terr = json.Unmarshal(nowConfig, &nowConfigJSON)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error unmarshaling JSON: %w, config file:%s\", err, configFilePath)\n\t\t}\n\t}\n\tloger.Info().Str(\"config_file\", configFilePath).Msg(\"load config file\")\n\tctx := context.WithValue(context.Background(), comm.MoLingConfigKey, mlConfig)\n\tctx = context.WithValue(ctx, comm.MoLingLoggerKey, loger)\n\tctxNew, cancelFunc := context.WithCancel(ctx)\n\n\tvar modules []string\n\tif mlConfig.Module != \"all\" {\n\t\tmodules = strings.Split(mlConfig.Module, \",\")\n\t}\n\tvar srvs []abstract.Service\n\tvar closers = make(map[string]func() error)\n\tfor srvName, nsv := range services.ServiceList() {\n\t\tif len(modules) > 0 {\n\t\t\tif !utils.StringInSlice(string(srvName), modules) {\n\t\t\t\tloger.Debug().Str(\"moduleName\", string(srvName)).Msgf(\"module %s not in %v, skip\", string(srvName), modules)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tloger.Debug().Str(\"moduleName\", string(srvName)).Msgf(\"starting %s service\", srvName)\n\t\t}\n\t\tcfg, ok := nowConfigJSON[string(srvName)].(map[string]any)\n\t\tsrv, err := nsv(ctxNew)\n\t\tif err != nil {\n\t\t\tloger.Error().Err(err).Msgf(\"failed to create service %s\", srv.Name())\n\t\t\tbreak\n\t\t}\n\t\tif ok {\n\t\t\terr = srv.LoadConfig(cfg)\n\t\t\tif err != nil {\n\t\t\t\tloger.Error().Err(err).Msgf(\"failed to load config for service %s\", srv.Name())\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\terr = srv.Init()\n\t\tif err != nil {\n\t\t\tloger.Error().Err(err).Msgf(\"failed to init service %s\", srv.Name())\n\t\t\tbreak\n\t\t}\n\t\tsrvs = append(srvs, srv)\n\t\tclosers[string(srv.Name())] = srv.Close\n\t}\n\t// MCPServer\n\tsrv, err := server.NewMoLingServer(ctxNew, srvs, *mlConfig)\n\tif err != nil {\n\t\tloger.Error().Err(err).Msg(\"failed to create server\")\n\t\tcancelFunc()\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\terr = srv.Serve()\n\t\tif err != nil {\n\t\t\tloger.Error().Err(err).Msg(\"failed to start server\")\n\t\t\tcancelFunc()\n\t\t\treturn\n\t\t}\n\t}()\n\n\t// 创建一个信号通道\n\tsigChan := make(chan os.Signal, 2)\n\tsignal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)\n\n\t// 创建一个 goroutine 来判断父进程是否退出\n\t// Claude Desktop 0.9.2 退出时，没有向MCP Server发送 SIGTERM信号，导致MCP 不能正常退出。\n\t// fix https://github.com/gojue/moling/issues/32\n\tgo func() {\n\t\tppid := os.Getppid()\n\t\tfor {\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t\tnewPpid := os.Getppid()\n\t\t\tif newPpid == 1 {\n\t\t\t\tloger.Info().Msgf(\"parent process changed,origin PPid:%d, New PPid:%d\", ppid, newPpid)\n\t\t\t\tloger.Warn().Msg(\"parent process exited\")\n\t\t\t\tsigChan <- syscall.SIGTERM\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}()\n\n\t// 等待信号\n\t_ = <-sigChan\n\tloger.Info().Msg(\"Received signal, shutting down...\")\n\n\t// close all services\n\t// close all services\n\tvar wg sync.WaitGroup\n\tdone := make(chan struct{})\n\n\t// 在goroutine中等待所有服务关闭\n\tgo func() {\n\t\tfor srvName, closer := range closers {\n\t\t\twg.Add(1)\n\t\t\tgo func(name string, closeFn func() error) {\n\t\t\t\tdefer wg.Done()\n\t\t\t\terr := closeFn()\n\t\t\t\tif err != nil {\n\t\t\t\t\tloger.Error().Err(err).Msgf(\"failed to close service %s\", name)\n\t\t\t\t} else {\n\t\t\t\t\tloger.Info().Msgf(\"service %s closed\", name)\n\t\t\t\t}\n\t\t\t}(srvName, closer)\n\t\t}\n\n\t\t// 等待所有服务关闭\n\t\twg.Wait()\n\t\tclose(done)\n\t}()\n\n\t// 使用select等待完成或超时\n\tselect {\n\tcase <-time.After(5 * time.Second):\n\t\tcancelFunc()\n\t\tloger.Info().Msg(\"timeout, all services closed forcefully\")\n\tcase <-done:\n\t\tcancelFunc()\n\t\tloger.Info().Msg(\"all services closed\")\n\t}\n\terr = utils.RemovePIDFile(pidFilePath)\n\tif err != nil {\n\t\tloger.Error().Err(err).Msgf(\"failed to remove pid file %s\", pidFilePath)\n\t\treturn err\n\t}\n\tloger.Info().Msgf(\"removed pid file %s\", pidFilePath)\n\tloger.Info().Msg(\" Bye!\")\n\treturn nil\n}\n"
  },
  {
    "path": "cli/cmd/utils.go",
    "content": "package cmd\n"
  },
  {
    "path": "cli/cobrautl/help.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage cobrautl\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"text/template\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n)\n\nvar (\n\tcommandUsageTemplate *template.Template\n\ttemplFuncs           = template.FuncMap{\n\t\t\"descToLines\": func(s string) []string {\n\t\t\t// trim leading/trailing whitespace and split into slice of lines\n\t\t\treturn strings.Split(strings.Trim(s, \"\\n\\t \"), \"\\n\")\n\t\t},\n\t\t\"cmdName\": func(cmd *cobra.Command, startCmd *cobra.Command) string {\n\t\t\tparts := []string{cmd.Name()}\n\t\t\tfor cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {\n\t\t\t\tcmd = cmd.Parent()\n\t\t\t\tparts = append([]string{cmd.Name()}, parts...)\n\t\t\t}\n\t\t\treturn strings.Join(parts, \" \")\n\t\t},\n\t}\n)\n\nfunc init() {\n\tcommandUsage := `\n{{ $cmd := .Cmd }}\\\n{{ $cmdname := cmdName .Cmd .Cmd.Root }}\\\nNAME:\n{{ if not .Cmd.HasParent }}\\\n{{printf \"\\t%s - %s\" .Cmd.Name .Cmd.Short}}\n{{else}}\\\n{{printf \"\\t%s - %s\" $cmdname .Cmd.Short}}\n{{end}}\\\n\nUSAGE:\n{{printf \"\\t%s\" .Cmd.UseLine}}\n{{ if not .Cmd.HasParent }}\\\n\nVERSION:\n{{printf \"\\t%s\" .Version}}\n{{end}}\\\n{{if .Cmd.HasSubCommands}}\\\n\nCOMMANDS:\n{{range .SubCommands}}\\\n{{ $cmdname := cmdName . $cmd }}\\\n{{ if .Runnable }}\\\n{{printf \"\\t%s\\t%s\" $cmdname .Short}}\n{{end}}\\\n{{end}}\\\n{{end}}\\\n{{ if .Cmd.Long }}\\\n\nDESCRIPTION:\n{{range $line := descToLines .Cmd.Long}}{{printf \"\\t%s\" $line}}\n{{end}}\\\n{{end}}\\\n{{if .Cmd.HasLocalFlags}}\\\n\nOPTIONS:\n{{.LocalFlags}}\\\n{{end}}\\\n{{if .Cmd.HasInheritedFlags}}\\\n\nGLOBAL OPTIONS:\n{{.GlobalFlags}}\\\n{{end}}\n`[1:]\n\n\tcommandUsageTemplate = template.Must(template.New(\"command_usage\").Funcs(templFuncs).Parse(strings.Replace(commandUsage, \"\\\\\\n\", \"\", -1)))\n}\n\nfunc molingFlagUsages(flagSet *pflag.FlagSet) string {\n\tx := new(bytes.Buffer)\n\n\tflagSet.VisitAll(func(flag *pflag.Flag) {\n\t\tif len(flag.Deprecated) > 0 {\n\t\t\treturn\n\t\t}\n\t\tvar format string\n\t\tif len(flag.Shorthand) > 0 {\n\t\t\tformat = \"  -%s, --%s\"\n\t\t} else {\n\t\t\tformat = \"   %s   --%s\"\n\t\t}\n\t\tif len(flag.NoOptDefVal) > 0 {\n\t\t\tformat = format + \"[\"\n\t\t}\n\t\tif flag.Value.Type() == \"string\" {\n\t\t\t// put quotes on the value\n\t\t\tformat = format + \"=%q\"\n\t\t} else {\n\t\t\tformat = format + \"=%s\"\n\t\t}\n\t\tif len(flag.NoOptDefVal) > 0 {\n\t\t\tformat = format + \"]\"\n\t\t}\n\t\tformat = format + \"\\t%s\\n\"\n\t\tshorthand := flag.Shorthand\n\t\tfmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)\n\t})\n\n\treturn x.String()\n}\n\nfunc getSubCommands(cmd *cobra.Command) []*cobra.Command {\n\tvar subCommands []*cobra.Command\n\tfor _, subCmd := range cmd.Commands() {\n\t\tsubCommands = append(subCommands, subCmd)\n\t\tsubCommands = append(subCommands, getSubCommands(subCmd)...)\n\t}\n\treturn subCommands\n}\n\nfunc UsageFunc(cmd *cobra.Command, version string) error {\n\tsubCommands := getSubCommands(cmd)\n\ttabOut := getTabOutWithWriter(os.Stdout)\n\terr := commandUsageTemplate.Execute(tabOut, struct {\n\t\tCmd         *cobra.Command\n\t\tLocalFlags  string\n\t\tGlobalFlags string\n\t\tSubCommands []*cobra.Command\n\t\tVersion     string\n\t}{\n\t\tcmd,\n\t\tmolingFlagUsages(cmd.LocalFlags()),\n\t\tmolingFlagUsages(cmd.InheritedFlags()),\n\t\tsubCommands,\n\t\tversion,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = tabOut.Flush()\n\treturn err\n}\n\nfunc getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {\n\taTabOut := new(tabwriter.Writer)\n\taTabOut.Init(writer, 0, 8, 1, '\\t', 0)\n\treturn aTabOut\n}\n"
  },
  {
    "path": "cli/main.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage cli\n\nimport (\n\t\"github.com/gojue/moling/cli/cmd\"\n)\n\nfunc Start() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "client/client.go",
    "content": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n *  Repository: https://github.com/gojue/moling\n *\n */\n\npackage client\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nvar (\n\t// ClineConfigPath is the path to the Cline config file.\n\tclientLists = make(map[string]string, 3)\n)\n\nconst MCPServersKey = \"mcpServers\"\n\n// MCPServerConfig represents the configuration for the MCP Client.\ntype MCPServerConfig struct {\n\tDescription string   `json:\"description\"`       // Description of the MCP Server\n\tIsActive    bool     `json:\"isActive\"`          // Is the MCP Server active\n\tCommand     string   `json:\"command,omitempty\"` // Command to start the MCP Server, STDIO mode only\n\tArgs        []string `json:\"args,omitempty\"`    // Arguments to pass to the command, STDIO mode only\n\tBaseURL     string   `json:\"baseUrl,omitempty\"` // Base URL of the MCP Server, SSE mode only\n\tTimeOut     uint16   `json:\"timeout,omitempty\"` // Timeout for the MCP Server, default is 300 seconds\n\tServerName  string\n}\n\n// NewMCPServerConfig creates a new MCPServerConfig instance.\nfunc NewMCPServerConfig(description string, command string, srvName string) MCPServerConfig {\n\treturn MCPServerConfig{\n\t\tDescription: description,\n\t\tIsActive:    true,\n\t\tCommand:     command,\n\t\tArgs:        []string{\"-m\", \"Browser\"},\n\t\tBaseURL:     \"\",\n\t\tServerName:  srvName,\n\t\tTimeOut:     300,\n\t}\n}\n\n// Manager manages the configuration of different clients.\ntype Manager struct {\n\tlogger    zerolog.Logger\n\tclients   map[string]string\n\tmcpConfig MCPServerConfig\n}\n\n// NewManager creates a new ClientManager instance.\nfunc NewManager(lger zerolog.Logger, mcpConfig MCPServerConfig) (cm *Manager) {\n\tcm = &Manager{\n\t\tclients:   make(map[string]string, 3),\n\t\tlogger:    lger,\n\t\tmcpConfig: mcpConfig,\n\t}\n\tcm.clients = clientLists\n\treturn cm\n}\n\n// ListClient lists all the clients and checks if they exist.\nfunc (c *Manager) ListClient() {\n\tfor name, path := range c.clients {\n\t\tc.logger.Debug().Msgf(\"Client %s: %s\", name, path)\n\t\tif !c.checkExist(path) {\n\t\t\t// path not exists\n\t\t\tc.logger.Info().Str(\"Client Name\", name).Bool(\"exist\", false).Msg(\"Client is not exist\")\n\t\t} else {\n\t\t\tc.logger.Info().Str(\"Client Name\", name).Bool(\"exist\", true).Msg(\"Client is exist\")\n\t\t}\n\t}\n\treturn\n}\n\n// SetupConfig sets up the configuration for the clients.\nfunc (c *Manager) SetupConfig() {\n\tfor name, path := range c.clients {\n\t\tc.logger.Debug().Msgf(\"Client %s: %s\", name, path)\n\t\tif !c.checkExist(path) {\n\t\t\tcontinue\n\t\t}\n\t\t// read config file\n\t\tfile, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\tc.logger.Error().Str(\"Client Name\", name).Msgf(\"Failed to open config file %s: %s\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\tc.logger.Debug().Str(\"Client Name\", name).Str(\"config\", string(file)).Send()\n\t\tb, err := c.appendConfig(c.mcpConfig.ServerName, file)\n\t\tif err != nil {\n\t\t\tc.logger.Error().Str(\"Client Name\", name).Msgf(\"Failed to append config file %s: %s\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\tc.logger.Debug().Str(\"Client Name\", name).Str(\"newConfig\", string(b)).Send()\n\t\t// write config file\n\t\terr = os.WriteFile(path, b, 0644)\n\t\tif err != nil {\n\t\t\tc.logger.Error().Str(\"Client Name\", name).Msgf(\"Failed to write config file %s: %s\", path, err)\n\t\t\tcontinue\n\t\t}\n\t\tc.logger.Info().Str(\"Client Name\", name).Msgf(\"Successfully added config to %s\", path)\n\t}\n}\n\n// appendConfig appends the mlMCPConfig to the client config.\nfunc (c *Manager) appendConfig(name string, payload []byte) ([]byte, error) {\n\tvar err error\n\tvar jsonMap map[string]any\n\tvar jsonBytes []byte\n\terr = json.Unmarshal(payload, &jsonMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tjsonMcpServer, ok := jsonMap[MCPServersKey].(map[string]any)\n\tif !ok {\n\t\treturn nil, errors.New(\"MCPServersKey not found in JSON\")\n\t}\n\tjsonMcpServer[name] = c.mcpConfig\n\tjsonMap[MCPServersKey] = jsonMcpServer\n\tjsonBytes, err = json.MarshalIndent(jsonMap, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn jsonBytes, nil\n}\n\n// checkExist checks if the file at the given path exists.\nfunc (c *Manager) checkExist(path string) bool {\n\t_, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tc.logger.Debug().Msgf(\"Client config file %s does not exist\", path)\n\t\t\treturn false\n\t\t}\n\t\tc.logger.Info().Msgf(\"check file failed, error:%s\", err.Error())\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "client/client_config.go",
    "content": "//go:build !windows\n\n/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n *  Repository: https://github.com/gojue/moling\n *\n */\n\npackage client\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tclientLists[\"VSCode Cline\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Code\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae CN Cline\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae CN\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae Cline\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae\", \"User\", \"mcp.json\")\n\tclientLists[\"Trae CN\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae CN\", \"User\", \"mcp.json\")\n\tclientLists[\"VSCode Roo\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Code\", \"User\", \"globalStorage\", \"rooveterinaryinc.roo-cline\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Trae CN Roo\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae CN\", \"User\", \"globalStorage\", \"rooveterinaryinc.roo-cline\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Trae Roo\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Trae\", \"User\", \"globalStorage\", \"rooveterinaryinc.roo-cline\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Claude\"] = filepath.Join(os.Getenv(\"HOME\"), \"Library\", \"Application Support\", \"Claude\", \"claude_desktop_config.json\")\n\tclientLists[\"Cursor\"] = filepath.Join(os.Getenv(\"HOME\"), \".cursor\", \"mcp.json\")\n}\n"
  },
  {
    "path": "client/client_config_windows.go",
    "content": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n *  Repository: https://github.com/gojue/moling\n *\n */\n\npackage client\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc init() {\n\tclientLists[\"VSCODE Cline\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Code\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae CN Cline\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae CN\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae Cline\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"cline_mcp_settings.json\")\n\tclientLists[\"Trae\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae\", \"User\", \"mcp.json\")\n\tclientLists[\"Trae CN\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae CN\", \"User\", \"mcp.json\")\n\tclientLists[\"VSCODE Roo Code\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Code\", \"User\", \"globalStorage\", \"rooveterinaryinc.roo-cline\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Trae CN Roo\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae CN\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Trae Roo\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Trae\", \"User\", \"globalStorage\", \"saoudrizwan.claude-dev\", \"settings\", \"mcp_settings.json\")\n\tclientLists[\"Claude\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Claude\", \"claude_desktop_config.json\")\n\tclientLists[\"Cursor\"] = filepath.Join(os.Getenv(\"APPDATA\"), \"Cursor\", \"mcp.json\")\n}\n"
  },
  {
    "path": "client/client_test.go",
    "content": "/*\n *\n *  Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n *  Licensed under the Apache License, Version 2.0 (the \"License\");\n *  you may not use this file except in compliance with the License.\n *  You may obtain a copy of the License at\n *\n *    http://www.apache.org/licenses/LICENSE-2.0\n *\n *  Unless required by applicable law or agreed to in writing, software\n *  distributed under the License is distributed on an \"AS IS\" BASIS,\n *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *  See the License for the specific language governing permissions and\n *  limitations under the License.\n *\n *  Repository: https://github.com/gojue/moling\n *\n */\n\npackage client\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/rs/zerolog\"\n)\n\nfunc TestClientManager_ListClient(t *testing.T) {\n\tlogger := zerolog.New(os.Stdout)\n\tmcpConfig := NewMCPServerConfig(\"MoLing UnitTest Description\", \"moling_test\", \"MoLing MCP Server\")\n\tcm := NewManager(logger, mcpConfig)\n\t// Mock client list\n\tclientLists[\"TestClient\"] = \"/path/to/nonexistent/file\"\n\n\tcm.ListClient()\n\t// Check logs or other side effects as needed\n}\n\n/*\n\tfunc TestClientManager_SetupConfig(t *testing.T) {\n\t\tlogger := zerolog.New(os.Stdout)\n\t\tmcpConfig := NewMCPServerConfig(\"MoLing UnitTest Description\", \"moling_test\", \"MoLing MCP Server\")\n\t\tcm := NewManager(logger, mcpConfig)\n\n\t\t// Mock client list\n\t\tclientLists[\"TestClient\"] = \"/path/to/nonexistent/file\"\n\n\t\tcm.SetupConfig()\n\t\t// Check logs or other side effects as needed\n\t}\n\n\tfunc TestClientManager_appendConfig(t *testing.T) {\n\t\tlogger := zerolog.New(os.Stdout)\n\t\tmcpConfig := NewMCPServerConfig(\"MoLing UnitTest Description\", \"moling_test\", \"MoLing MCP Server\")\n\t\tcm := NewManager(logger, mcpConfig)\n\n\t\t// Mock payload\n\t\tpayload := []byte(`{\n\t  \"Cline\": {\n\t    \"description\": \"MoLing UnitTest Description\",\n\t    \"isActive\": true,\n\t    \"command\": \"moling_test\"\n\t  },\n\t  \"mcpServers\": {\n\t    \"testABC\": {\n\t      \"args\": [\n\t        \"--allow-dir\",\n\t        \"/tmp/,/Users/username/Downloads\"\n\t      ],\n\t      \"command\": \"npx\",\n\t      \"timeout\": 300\n\t    }\n\t  }\n\t}`)\n\n\tresult, err := cm.appendConfig(\"TestClient\", payload)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %s\", err.Error())\n\t}\n\n\tvar resultMap map[string]any\n\terr = json.Unmarshal(result, &resultMap)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected valid JSON, got error %s\", err.Error())\n\t}\n\n\tif resultMap[\"existingKey\"] != \"existingValue\" {\n\t\tt.Errorf(\"Expected existingKey to be existingValue, got %v\", resultMap[\"existingKey\"])\n\t}\n\n}\n*/\n\nfunc TestClientManager_checkExist(t *testing.T) {\n\tlogger := zerolog.New(os.Stdout)\n\tmcpConfig := NewMCPServerConfig(\"MoLing UnitTest Description\", \"moling_test\", \"MoLing MCP Server\")\n\tcm := NewManager(logger, mcpConfig)\n\n\t// Test with a non-existent file\n\texists := cm.checkExist(\"/path/to/nonexistent/file\")\n\tif exists {\n\t\tt.Errorf(\"Expected file to not exist\")\n\t}\n\n\t// Test with an existing file\n\tfile, err := os.CreateTemp(\"\", \"testfile\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp file: %s\", err.Error())\n\t}\n\tdefer func() {\n\t\t_ = os.Remove(file.Name())\n\t}()\n\tt.Logf(\"Created temp file: %s\", file.Name())\n\texists = cm.checkExist(file.Name())\n\tif !exists {\n\t\tt.Errorf(\"Expected file to exist\")\n\t}\n}\n"
  },
  {
    "path": "dist/.gitkeep",
    "content": ""
  },
  {
    "path": "functions.mk",
    "content": "define allow-override\n  $(if $(or $(findstring environment,$(origin $(1))),\\\n            $(findstring command line,$(origin $(1)))),,\\\n    $(eval $(1) = $(2)))\nendef\n\n# TARGET_OS , TARGET_ARCH\ndefine gobuild\n\tCGO_ENABLED=0 \\\n\tGOOS=$(1) GOARCH=$(2) \\\n\t$(eval OUT_BIN_SUFFIX=$(if $(filter $(1),windows),.exe,)) \\\n\t$(CMD_GO) build -trimpath -mod=readonly -ldflags \"-w -s -X 'github.com/gojue/moling/cli/cmd.GitVersion=$(1)_$(2)_$(VERSION_NUM)'\" -o $(OUT_BIN)$(OUT_BIN_SUFFIX)\n\t$(CMD_FILE) $(OUT_BIN)$(OUT_BIN_SUFFIX)\nendef\n\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gojue/moling\n\ngo 1.24.1\n\nrequire (\n\tgithub.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe\n\tgithub.com/chromedp/chromedp v0.13.6\n\tgithub.com/mark3labs/mcp-go v0.29.0\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/spf13/cobra v1.9.1\n\tgithub.com/spf13/pflag v1.0.6\n)\n\nrequire (\n\tgithub.com/chromedp/sysutil v1.1.0 // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.4.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/spf13/cast v1.8.0 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgolang.org/x/sys v0.33.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe h1:roGYW+2lkWq2EdEOrSOxj8+L07gG1q6iF3xeKUHfcDQ=\ngithub.com/chromedp/cdproto v0.0.0-20250518235601-40b4c35ec9fe/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=\ngithub.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=\ngithub.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=\ngithub.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=\ngithub.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=\ngithub.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=\ngithub.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/mark3labs/mcp-go v0.29.0 h1:sH1NBcumKskhxqYzhXfGc201D7P76TVXiT0fGVhabeI=\ngithub.com/mark3labs/mcp-go v0.29.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=\ngithub.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=\ngithub.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=\ngithub.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=\ngithub.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=\ngithub.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=\ngolang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "install/install.ps1",
    "content": "#!/usr/bin/env pwsh\n\nSet-StrictMode -Version Latest\n\nWrite-Output \"Welcome to MoLing MCP Server initialization script.\"\nWrite-Output \"Home page: https://gojue.cc/moling\"\nWrite-Output \"Github: https://github.com/gojue/moling\"\n\n# Determine the OS and architecture\n$OS = (Get-CimInstance Win32_OperatingSystem).Caption\n$ARCH = (Get-CimInstance Win32_Processor).Architecture\n\nswitch ($ARCH) {\n    9 { $ARCH = \"amd64\" }\n    5 { $ARCH = \"arm64\" }\n    default {\n        Write-Error \"Unsupported architecture: $ARCH\"\n        exit 1\n    }\n}\n\n# Determine the download URL\n$VERSION = \"v0.0.1\"\n$BASE_URL = \"https://github.com/gojue/moling/releases/download/$VERSION\"\n$FILE_NAME = \"moling-$VERSION-windows-$ARCH.zip\"\n$DOWNLOAD_URL = \"$BASE_URL/$FILE_NAME\"\n\n# Download the installation package\nWrite-Output \"Downloading $DOWNLOAD_URL...\"\nInvoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $FILE_NAME\n\n# Extract the package\nWrite-Output \"Extracting $FILE_NAME...\"\nExpand-Archive -Path $FILE_NAME -DestinationPath \"moling\"\n\n# Move the binary to C:\\Program Files\n$destination = \"C:\\Program Files\\moling\"\nif (-Not (Test-Path -Path $destination)) {\n    New-Item -ItemType Directory -Path $destination\n}\nMove-Item -Path \"moling\\moling.exe\" -Destination \"$destination\\moling.exe\"\n\n# Add to PATH\n$env:Path += \";$destination\"\n[System.Environment]::SetEnvironmentVariable(\"Path\", $env:Path, [System.EnvironmentVariableTarget]::Machine)\n\n# MCP Client configuration\n& \"$destination\\moling.exe\" client --install\n\n# Clean up\nRemove-Item -Recurse -Force \"moling\"\nRemove-Item -Force $FILE_NAME\n\nWrite-Output \"MoLing has been installed successfully!\""
  },
  {
    "path": "install/install.sh",
    "content": "#!/bin/bash\n\nset -e\n\necho \"Welcome to MoLing MCP Server initialization script.\"\necho \"Home page: https://gojue.cc/moling\"\necho \"Github: https://github.com/gojue/moling\"\n\n# Determine the OS and architecture\nOS=$(uname -s | tr '[:upper:]' '[:lower:]')\nARCH=$(uname -m)\n\ncase $ARCH in\n  x86_64)\n    ARCH=\"amd64\"\n    ;;\n  arm64|aarch64)\n    ARCH=\"arm64\"\n    ;;\n  *)\n    echo \"Unsupported architecture: $ARCH\"\n    exit 1\n    ;;\nesac\n\n# Determine the download URL\nVERSION=$(curl -s https://api.github.com/repos/gojue/moling/releases/latest | grep 'tag_name' | cut -d\\\" -f4)\nBASE_URL=\"https://github.com/gojue/moling/releases/download/${VERSION}\"\nFILE_NAME=\"moling-${VERSION}-${OS}-${ARCH}.tar.gz\"\n\nDOWNLOAD_URL=\"${BASE_URL}/${FILE_NAME}\"\n\n# Download the installation package\necho \"Downloading ${DOWNLOAD_URL}...\"\ncurl -LO \"${DOWNLOAD_URL}\"\necho \"Download completed. filename: ${FILE_NAME}\"\n# Extract the package\ntar -xzf \"${FILE_NAME}\"\n\n# Move the binary to /usr/local/bin\nmv moling /usr/local/bin/moling\nchmod +x /usr/local/bin/moling\n\n# Clean up\nrm -rf moling \"${FILE_NAME}\"\n\n# Check if the installation was successful\nif command -v moling &> /dev/null; then\n    echo \"MoLing installation was successful!\"\nelse\n    echo \"MoLing installation failed.\"\n    exit 1\nfi\n\n# initialize the configuration\necho \"Initializing MoLing configuration...\"\nmoling config --init\necho \"MoLing configuration initialized successfully!\"\n\necho \"setup MCP Server configuration into MCP Client\"\nmoling client -i\necho \"MCP Client configuration setup successfully!\""
  },
  {
    "path": "main.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\npackage main\n\nimport \"github.com/gojue/moling/cli\"\n\nfunc main() {\n\tcli.Start()\n}\n"
  },
  {
    "path": "pkg/comm/comm.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage comm\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/config\"\n)\n\ntype MoLingServerType string\n\ntype contextKey string\n\n// MoLingConfigKey is a context key for storing the version of MoLing\nconst (\n\tMoLingConfigKey contextKey = \"moling_config\"\n\tMoLingLoggerKey contextKey = \"moling_logger\"\n)\n\n// InitTestEnv initializes the test environment by creating a temporary log file and setting up the logger.\nfunc InitTestEnv() (zerolog.Logger, context.Context, error) {\n\tlogFile := filepath.Join(os.TempDir(), \"moling.log\")\n\tzerolog.SetGlobalLevel(zerolog.DebugLevel)\n\tvar logger zerolog.Logger\n\tf, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)\n\tif err != nil {\n\t\treturn zerolog.Logger{}, nil, err\n\t}\n\tlogger = zerolog.New(f).With().Timestamp().Logger()\n\tmlConfig := &config.MoLingConfig{\n\t\tConfigFile: filepath.Join(\"config\", \"test_config.json\"),\n\t\tBasePath:   os.TempDir(),\n\t}\n\tctx := context.WithValue(context.Background(), MoLingConfigKey, mlConfig)\n\tctx = context.WithValue(ctx, MoLingLoggerKey, logger)\n\treturn logger, ctx, nil\n}\n"
  },
  {
    "path": "pkg/comm/errors.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage comm\n\nimport \"errors\"\n\nvar (\n\tErrConfigNotLoaded = errors.New(\"config not loaded, please call LoadConfig() first\")\n)\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage config\n\nimport (\n\t\"github.com/rs/zerolog\"\n)\n\n// Config is an interface that defines a method for checking configuration validity.\ntype Config interface {\n\t// Check validates the configuration and returns an error if the configuration is invalid.\n\tCheck() error\n}\n\n// MoLingConfig is a struct that holds the configuration for the MoLing server.\ntype MoLingConfig struct {\n\tConfigFile string `json:\"config_file\"` // The path to the configuration file.\n\tBasePath   string `json:\"base_path\"`   // The base path for the server, used for storing files. automatically created if not exists. eg: /Users/user1/.moling\n\t//AllowDir   []string `json:\"allow_dir\"`   // The directories that are allowed to be accessed by the server.\n\tVersion    string `json:\"version\"`     // The version of the MoLing server.\n\tListenAddr string `json:\"listen_addr\"` // The address to listen on for SSE mode.\n\tDebug      bool   `json:\"debug\"`       // Debug mode, if true, the server will run in debug mode.\n\tModule     string `json:\"module\"`      // The module to load, default: all\n\tUsername   string // The username of the user running the server.\n\tHomeDir    string // The home directory of the user running the server. macOS: /Users/user1, Linux: /home/user1\n\tSystemInfo string // The system information of the user running the server. macOS: Darwin 15.3.3, Linux: Ubuntu 20.04.1 LTS\n\n\t// for MCP Server Config\n\tDescription string // Description of the MCP Server, default: CliDescription\n\tCommand     string //\tCommand to start the MCP Server, STDIO mode only,  default: CliName\n\tArgs        string // Arguments to pass to the command, STDIO mode only, default: empty\n\tBaseURL     string // BaseURL , SSE mode only.\n\tServerName  string // ServerName MCP ServerName, add to the MCP Client config\n\tAuthToken   string // AuthToken for SSE mode authentication. Auto-generated if empty.\n\tlogger      zerolog.Logger\n}\n\nfunc (cfg *MoLingConfig) Check() error {\n\tpanic(\"not implemented yet\") // TODO: Implement Check\n}\n\nfunc (cfg *MoLingConfig) Logger() zerolog.Logger {\n\treturn cfg.logger\n}\n\nfunc (cfg *MoLingConfig) SetLogger(logger zerolog.Logger) {\n\tcfg.logger = logger\n}\n"
  },
  {
    "path": "pkg/config/config_test.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage config\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\n// TestConfigLoad tests the loading of the configuration from a JSON file.\nfunc TestConfigLoad(t *testing.T) {\n\tconfigFile := \"config_test.json\"\n\tcfg := &MoLingConfig{}\n\tcfg.ConfigFile = \"config.json\"\n\tcfg.BasePath = \"/tmp/moling\"\n\tcfg.Version = \"1.0.0\"\n\tcfg.ListenAddr = \":8080\"\n\tcfg.Debug = true\n\tcfg.Username = \"user1\"\n\tcfg.HomeDir = \"/Users/user1\"\n\tcfg.SystemInfo = \"Darwin 15.3.3\"\n\n\tjsonData, err := os.ReadFile(configFile)\n\tif err != nil {\n\t\tt.Fatalf(\"failed to read config file: %s\", err.Error())\n\t}\n\tvar jsonMap map[string]any\n\tif err := json.Unmarshal(jsonData, &jsonMap); err != nil {\n\t\tt.Fatalf(\"Failed to unmarshal JSON: %s\", err.Error())\n\t}\n\tmlConfig, ok := jsonMap[\"MoLingConfig\"].(map[string]any)\n\tif !ok {\n\t\tt.Fatalf(\"failed to parse MoLingConfig from JSON\")\n\t}\n\tif err := utils.MergeJSONToStruct(cfg, mlConfig); err != nil {\n\t\tt.Fatalf(\"failed to merge JSON to struct: %s\", err.Error())\n\t}\n\tt.Logf(\"Config loaded, MoLing Config.BasePath: %s\", cfg.BasePath)\n\tif cfg.BasePath != \"/newpath/.moling\" {\n\t\tt.Fatalf(\"expected BasePath to be '/newpath/.moling', got '%s'\", cfg.BasePath)\n\t}\n}\n"
  },
  {
    "path": "pkg/config/config_test.json",
    "content": "{\n  \"BrowserServer\": {\n  },\n  \"CommandServer\": {\n    \"allowed_commands\": [\n      \"ls\",\n      \"cat\",\n      \"echo\"\n    ]\n  },\n  \"FilesystemServer\": {\n    \"allowed_dirs\": [\n      \"/tmp/.moling/data/\"\n    ],\n    \"cache_path\": \"/tmp/.moling/data\"\n  },\n  \"MoLingConfig\": {\n    \"HomeDir\": \"\",\n    \"SystemInfo\": \"\",\n    \"Username\": \"\",\n    \"base_path\": \"/newpath/.moling\",\n    \"config_file\": \"config/config.json\",\n    \"debug\": false,\n    \"listen_addr\": \"\",\n    \"version\": \"darwin-arm64-20250330084836-0077553\"\n  },\n  \"MoaServer\": {}\n}"
  },
  {
    "path": "pkg/server/server.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\npackage server\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/subtle\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n)\n\ntype MoLingServer struct {\n\tctx        context.Context\n\tserver     *server.MCPServer\n\tservices   []abstract.Service\n\tlogger     zerolog.Logger\n\tmlConfig   config.MoLingConfig\n\tlistenAddr string // SSE mode listen address, if empty, use STDIO mode.\n\tauthToken  string // Auth token for SSE mode. Required for all SSE requests.\n}\n\nfunc NewMoLingServer(ctx context.Context, srvs []abstract.Service, mlConfig config.MoLingConfig) (*MoLingServer, error) {\n\tmcpServer := server.NewMCPServer(\n\t\tmlConfig.ServerName,\n\t\tmlConfig.Version,\n\t\tserver.WithResourceCapabilities(true, true),\n\t\tserver.WithLogging(),\n\t\tserver.WithPromptCapabilities(true),\n\t)\n\n\t// Resolve auth token for SSE mode.  16 random bytes encoded as 32 hex chars.\n\tauthToken := mlConfig.AuthToken\n\tif mlConfig.ListenAddr != \"\" && authToken == \"\" {\n\t\ttokenBytes := make([]byte, 16)\n\t\tif _, err := rand.Read(tokenBytes); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to generate SSE auth token: %w\", err)\n\t\t}\n\t\tauthToken = hex.EncodeToString(tokenBytes)\n\t}\n\n\t// Set the context for the server\n\tms := &MoLingServer{\n\t\tctx:        ctx,\n\t\tserver:     mcpServer,\n\t\tservices:   srvs,\n\t\tlistenAddr: mlConfig.ListenAddr,\n\t\tlogger:     ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger),\n\t\tmlConfig:   mlConfig,\n\t\tauthToken:  authToken,\n\t}\n\terr := ms.init()\n\treturn ms, err\n}\n\nfunc (m *MoLingServer) init() error {\n\tvar err error\n\tfor _, srv := range m.services {\n\t\tm.logger.Debug().Str(\"serviceName\", string(srv.Name())).Msg(\"Loading service\")\n\t\terr = m.loadService(srv)\n\t\tif err != nil {\n\t\t\tm.logger.Info().Err(err).Str(\"serviceName\", string(srv.Name())).Msg(\"Failed to load service\")\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (m *MoLingServer) loadService(srv abstract.Service) error {\n\n\t// Add resources\n\tfor r, rhf := range srv.Resources() {\n\t\tm.server.AddResource(r, rhf)\n\t}\n\n\t// Add Resource Templates\n\tfor rt, rthf := range srv.ResourceTemplates() {\n\t\tm.server.AddResourceTemplate(rt, rthf)\n\t}\n\n\t// Add Tools\n\tm.server.AddTools(srv.Tools()...)\n\n\t// Add Notification Handlers\n\tfor n, nhf := range srv.NotificationHandlers() {\n\t\tm.server.AddNotificationHandler(n, nhf)\n\t}\n\n\t// Add Prompts\n\tfor _, pe := range srv.Prompts() {\n\t\t// Add Prompt\n\t\tm.server.AddPrompt(pe.Prompt(), pe.Handler())\n\t}\n\treturn nil\n}\n\n// requireJSONContentType is a middleware that rejects POST requests whose\n// Content-Type is not application/json.  Browsers treat text/plain,\n// application/x-www-form-urlencoded, and multipart/form-data as \"simple\"\n// requests, meaning no CORS preflight is sent.  Enforcing application/json\n// here ensures that cross-origin requests always go through the preflight\n// check and cannot bypass CORS protection.\nfunc requireJSONContentType(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method == http.MethodPost {\n\t\t\tct := r.Header.Get(\"Content-Type\")\n\t\t\t// Strip optional parameters (e.g. \"; charset=utf-8\") before comparing.\n\t\t\tmediaType := strings.ToLower(strings.TrimSpace(strings.SplitN(ct, \";\", 2)[0]))\n\t\t\tif mediaType != \"application/json\" {\n\t\t\t\thttp.Error(w, \"Content-Type must be application/json\", http.StatusUnsupportedMediaType)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\n// corsRemoverResponseWriter wraps http.ResponseWriter to strip the wildcard\n// Access-Control-Allow-Origin header hardcoded by the upstream mcp-go library,\n// preventing cross-origin browser access to the SSE endpoint.\ntype corsRemoverResponseWriter struct {\n\thttp.ResponseWriter\n\tflusher http.Flusher\n\tcleaned bool\n}\n\nfunc newCORSRemoverResponseWriter(w http.ResponseWriter) *corsRemoverResponseWriter {\n\tflusher, _ := w.(http.Flusher)\n\treturn &corsRemoverResponseWriter{\n\t\tResponseWriter: w,\n\t\tflusher:        flusher,\n\t}\n}\n\nfunc (w *corsRemoverResponseWriter) cleanCORSHeader() {\n\tif !w.cleaned {\n\t\tw.ResponseWriter.Header().Del(\"Access-Control-Allow-Origin\")\n\t\tw.cleaned = true\n\t}\n}\n\nfunc (w *corsRemoverResponseWriter) WriteHeader(code int) {\n\tw.cleanCORSHeader()\n\tw.ResponseWriter.WriteHeader(code)\n}\n\nfunc (w *corsRemoverResponseWriter) Write(b []byte) (int, error) {\n\tw.cleanCORSHeader()\n\treturn w.ResponseWriter.Write(b)\n}\n\nfunc (w *corsRemoverResponseWriter) Flush() {\n\tif w.flusher != nil {\n\t\tw.flusher.Flush()\n\t}\n}\n\n// sseSecurityMiddleware enforces token-based authentication and removes the\n// wildcard CORS header from all responses.  Clients must supply the token either\n// as an Authorization: Bearer <token> header or as a ?token=<token> query\n// parameter.\nfunc sseSecurityMiddleware(token string, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t// Accept token from Authorization header (Bearer scheme only) or query parameter.\n\t\t// Use constant-time comparison to prevent timing attacks.\n\t\tvar bearerToken string\n\t\tif authHeader := r.Header.Get(\"Authorization\"); strings.HasPrefix(authHeader, \"Bearer \") {\n\t\t\tbearerToken = authHeader[len(\"Bearer \"):]\n\t\t}\n\t\tqueryToken := r.URL.Query().Get(\"token\")\n\t\ttokenBytes := []byte(token)\n\t\tvalidBearer := subtle.ConstantTimeCompare([]byte(bearerToken), tokenBytes) == 1\n\t\tvalidQuery := subtle.ConstantTimeCompare([]byte(queryToken), tokenBytes) == 1\n\t\tif !validBearer && !validQuery {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tnext.ServeHTTP(newCORSRemoverResponseWriter(w), r)\n\t})\n}\n\nfunc (m *MoLingServer) Serve() error {\n\tmLogger := log.New(m.logger, m.mlConfig.ServerName, 0)\n\tif m.listenAddr != \"\" {\n\t\tltnAddr := fmt.Sprintf(\"http://%s\", strings.TrimPrefix(m.listenAddr, \"http://\"))\n\t\tconsoleWriter := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}\n\t\tmulti := zerolog.MultiLevelWriter(consoleWriter, m.logger)\n\t\tm.logger = zerolog.New(multi).With().Timestamp().Logger()\n\t\tm.logger.Info().Str(\"listenAddr\", m.listenAddr).Str(\"BaseURL\", ltnAddr).Msg(\"Starting SSE server\")\n\t\tm.logger.Warn().Msgf(\"The SSE server URL must be: %s. Please do not make mistakes, even if it is another IP or domain name on the same computer, it cannot be mixed.\", ltnAddr)\n\t\t// Print auth token to stdout only — avoid persisting credentials to log file.\n\t\tstdoutLogger := zerolog.New(consoleWriter).With().Timestamp().Logger()\n\t\tstdoutLogger.Warn().Msgf(\"SSE auth token: %s\", m.authToken)\n\t\tstdoutLogger.Warn().Msgf(\"SSE server URL with token: %s/sse?token=%s\", ltnAddr, m.authToken)\n\t\thttpSrv := &http.Server{Addr: m.listenAddr}\n\t\tsseServer := server.NewSSEServer(m.server, server.WithBaseURL(ltnAddr), server.WithHTTPServer(httpSrv))\n\t\thttpSrv.Handler = sseSecurityMiddleware(m.authToken, requireJSONContentType(sseServer))\n\n\t\treturn sseServer.Start(m.listenAddr)\n\t}\n\tm.logger.Info().Msg(\"Starting STDIO server\")\n\treturn server.ServeStdio(m.server, server.WithErrorLogger(mLogger))\n}\n"
  },
  {
    "path": "pkg/server/server_test.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage server\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/services/filesystem\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\nfunc TestNewMLServer(t *testing.T) {\n\t// Create a new MoLingConfig\n\tmlConfig := config.MoLingConfig{\n\t\tBasePath: filepath.Join(os.TempDir(), \"moling_test\"),\n\t}\n\tmlDirectories := []string{\n\t\t\"logs\",    // log file\n\t\t\"config\",  // config file\n\t\t\"browser\", // browser cache\n\t\t\"data\",    // data\n\t\t\"cache\",\n\t}\n\terr := utils.CreateDirectory(mlConfig.BasePath)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to create base directory: %s\", err.Error())\n\t}\n\tfor _, dirName := range mlDirectories {\n\t\terr = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Failed to create directory %s: %s\", dirName, err.Error())\n\t\t}\n\t}\n\tlogger, ctx, err := comm.InitTestEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize test environment: %s\", err.Error())\n\t}\n\tlogger.Info().Msg(\"TestBrowserServer\")\n\tmlConfig.SetLogger(logger)\n\n\t// Create a new server with the filesystem service\n\tfs, err := filesystem.NewFilesystemServer(ctx)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to create filesystem server: %s\", err.Error())\n\t}\n\terr = fs.Init()\n\tif err != nil {\n\t\tt.Errorf(\"Failed to initialize filesystem server: %s\", err.Error())\n\t}\n\tsrvs := []abstract.Service{\n\t\tfs,\n\t}\n\tsrv, err := NewMoLingServer(ctx, srvs, mlConfig)\n\tif err != nil {\n\t\tt.Errorf(\"Failed to create server: %s\", err.Error())\n\t}\n\terr = srv.Serve()\n\tif err != nil {\n\t\tt.Errorf(\"Failed to start server: %s\", err.Error())\n\t}\n\tt.Logf(\"Server started successfully: %v\", srv)\n}\n\n// TestSSESecurityMiddleware verifies that sseSecurityMiddleware enforces token\n// authentication and that corsRemoverResponseWriter strips the wildcard CORS header.\nfunc TestSSESecurityMiddleware(t *testing.T) {\n\tconst token = \"test-secret-token\"\n\n\t// A stub upstream handler that sets CORS wildcard and writes a body.\n\tstub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.WriteHeader(http.StatusOK)\n\t\t_, _ = w.Write([]byte(\"ok\"))\n\t})\n\n\thandler := sseSecurityMiddleware(token, stub)\n\n\tt.Run(\"no token returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/sse\", nil)\n\t\trr := httptest.NewRecorder()\n\t\thandler.ServeHTTP(rr, req)\n\t\tif rr.Code != http.StatusUnauthorized {\n\t\t\tt.Errorf(\"expected 401, got %d\", rr.Code)\n\t\t}\n\t})\n\n\tt.Run(\"wrong token returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/sse?token=wrong\", nil)\n\t\trr := httptest.NewRecorder()\n\t\thandler.ServeHTTP(rr, req)\n\t\tif rr.Code != http.StatusUnauthorized {\n\t\t\tt.Errorf(\"expected 401, got %d\", rr.Code)\n\t\t}\n\t})\n\n\tt.Run(\"raw token in Authorization header without Bearer scheme returns 401\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/sse\", nil)\n\t\treq.Header.Set(\"Authorization\", token) // missing \"Bearer \" prefix\n\t\trr := httptest.NewRecorder()\n\t\thandler.ServeHTTP(rr, req)\n\t\tif rr.Code != http.StatusUnauthorized {\n\t\t\tt.Errorf(\"expected 401, got %d\", rr.Code)\n\t\t}\n\t})\n\n\tt.Run(\"valid query param token passes and removes CORS header\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/sse?token=\"+token, nil)\n\t\trr := httptest.NewRecorder()\n\t\thandler.ServeHTTP(rr, req)\n\t\tif rr.Code != http.StatusOK {\n\t\t\tt.Errorf(\"expected 200, got %d\", rr.Code)\n\t\t}\n\t\tif got := rr.Header().Get(\"Access-Control-Allow-Origin\"); got != \"\" {\n\t\t\tt.Errorf(\"expected CORS header to be removed, got %q\", got)\n\t\t}\n\t})\n\n\tt.Run(\"valid Bearer token passes and removes CORS header\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(http.MethodGet, \"/sse\", nil)\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\t\trr := httptest.NewRecorder()\n\t\thandler.ServeHTTP(rr, req)\n\t\tif rr.Code != http.StatusOK {\n\t\t\tt.Errorf(\"expected 200, got %d\", rr.Code)\n\t\t}\n\t\tif got := rr.Header().Get(\"Access-Control-Allow-Origin\"); got != \"\" {\n\t\t\tt.Errorf(\"expected CORS header to be removed, got %q\", got)\n\t\t}\n\t})\n}\n\n// TestRequireJSONContentType verifies that the middleware blocks POST requests\n// with non-application/json Content-Types (which browsers treat as \"simple\n// requests\" and therefore never trigger a CORS preflight).\nfunc TestRequireJSONContentType(t *testing.T) {\n\t// A simple downstream handler that always returns 200.\n\tok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\thandler := requireJSONContentType(ok)\n\n\ttests := []struct {\n\t\tname           string\n\t\tmethod         string\n\t\tcontentType    string\n\t\texpectedStatus int\n\t}{\n\t\t// Non-POST requests must pass through regardless of Content-Type.\n\t\t{\"GET no CT\", http.MethodGet, \"\", http.StatusOK},\n\t\t{\"GET text/plain\", http.MethodGet, \"text/plain\", http.StatusOK},\n\t\t// POST with application/json (with and without charset param) must pass.\n\t\t{\"POST application/json\", http.MethodPost, \"application/json\", http.StatusOK},\n\t\t{\"POST application/json; charset=utf-8\", http.MethodPost, \"application/json; charset=utf-8\", http.StatusOK},\n\t\t// POST with \"simple\" Content-Types that bypass CORS preflight must be rejected.\n\t\t{\"POST text/plain\", http.MethodPost, \"text/plain\", http.StatusUnsupportedMediaType},\n\t\t{\"POST application/x-www-form-urlencoded\", http.MethodPost, \"application/x-www-form-urlencoded\", http.StatusUnsupportedMediaType},\n\t\t{\"POST multipart/form-data\", http.MethodPost, \"multipart/form-data\", http.StatusUnsupportedMediaType},\n\t\t// POST with empty or missing Content-Type must also be rejected.\n\t\t{\"POST no CT\", http.MethodPost, \"\", http.StatusUnsupportedMediaType},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\treq := httptest.NewRequest(tc.method, \"/message\", nil)\n\t\t\tif tc.contentType != \"\" {\n\t\t\t\treq.Header.Set(\"Content-Type\", tc.contentType)\n\t\t\t}\n\t\t\trr := httptest.NewRecorder()\n\t\t\thandler.ServeHTTP(rr, req)\n\t\t\tif rr.Code != tc.expectedStatus {\n\t\t\t\tt.Errorf(\"expected status %d, got %d\", tc.expectedStatus, rr.Code)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestNewMoLingServerGeneratesToken verifies that a random auth token is\n// generated when ListenAddr is set and no token is provided in the config.\nfunc TestNewMoLingServerGeneratesToken(t *testing.T) {\n\tmlConfig := config.MoLingConfig{\n\t\tBasePath:   filepath.Join(os.TempDir(), \"moling_test\"),\n\t\tListenAddr: \"127.0.0.1:0\",\n\t}\n\tfor _, dirName := range []string{\"logs\", \"config\", \"browser\", \"data\", \"cache\"} {\n\t\t_ = utils.CreateDirectory(filepath.Join(mlConfig.BasePath, dirName))\n\t}\n\tlogger, ctx, err := comm.InitTestEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"InitTestEnv: %v\", err)\n\t}\n\tmlConfig.SetLogger(logger)\n\n\tsrv, err := NewMoLingServer(ctx, []abstract.Service{}, mlConfig)\n\tif err != nil {\n\t\tt.Fatalf(\"NewMoLingServer: %v\", err)\n\t}\n\tif srv.authToken == \"\" {\n\t\tt.Error(\"expected a non-empty auth token to be generated\")\n\t}\n}\n"
  },
  {
    "path": "pkg/services/abstract/abstract.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//\thttp://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage abstract\n\nimport (\n\t\"context\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/mark3labs/mcp-go/server\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n)\n\ntype ServiceFactory func(ctx context.Context) (Service, error)\n\n// Service defines the interface for a service with various handlers and tools.\ntype Service interface {\n\tCtx() context.Context\n\t// Resources returns a map of resources and their corresponding handler functions.\n\tResources() map[mcp.Resource]server.ResourceHandlerFunc\n\t// ResourceTemplates returns a map of resource templates and their corresponding handler functions.\n\tResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc\n\t// Prompts returns a map of prompts and their corresponding handler functions.\n\tPrompts() []PromptEntry\n\t// Tools returns a slice of server tools.\n\tTools() []server.ServerTool\n\t// NotificationHandlers returns a map of notification handlers.\n\tNotificationHandlers() map[string]server.NotificationHandlerFunc\n\n\t// Config returns the configuration of the service as a string.\n\tConfig() string\n\t// LoadConfig loads the configuration for the service from a map.\n\tLoadConfig(jsonData map[string]any) error\n\n\t// Init initializes the service with the given context and configuration.\n\tInit() error\n\n\tMlConfig() *config.MoLingConfig\n\n\t// Name returns the name of the service.\n\tName() comm.MoLingServerType\n\n\t// Close closes the service and releases any resources it holds.\n\tClose() error\n}\n"
  },
  {
    "path": "pkg/services/abstract/mlservice.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage abstract\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/mark3labs/mcp-go/server\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\ntype PromptEntry struct {\n\tPromptVar   mcp.Prompt\n\tHandlerFunc server.PromptHandlerFunc\n}\n\nfunc (pe *PromptEntry) Prompt() mcp.Prompt {\n\treturn pe.PromptVar\n}\n\nfunc (pe *PromptEntry) Handler() server.PromptHandlerFunc {\n\treturn pe.HandlerFunc\n}\n\n// NewMLService creates a new MLService with the given context and logger.\nfunc NewMLService(ctx context.Context, logger zerolog.Logger, cfg *config.MoLingConfig) MLService {\n\treturn MLService{\n\t\tContext:  ctx,\n\t\tLogger:   logger,\n\t\tmlConfig: cfg,\n\t}\n}\n\n// MLService implements the Service interface and provides methods to manage resources, templates, prompts, tools, and notification handlers.\ntype MLService struct {\n\tContext context.Context\n\tLogger  zerolog.Logger // The logger for the service\n\n\tlock                 *sync.Mutex\n\tresources            map[mcp.Resource]server.ResourceHandlerFunc\n\tresourcesTemplates   map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc\n\tprompts              []PromptEntry\n\ttools                []server.ServerTool\n\tnotificationHandlers map[string]server.NotificationHandlerFunc\n\tmlConfig             *config.MoLingConfig // The configuration for the service\n}\n\n// InitResources initializes the MLService with empty maps and a mutex.\nfunc (mls *MLService) InitResources() error {\n\tmls.lock = &sync.Mutex{}\n\tmls.resources = make(map[mcp.Resource]server.ResourceHandlerFunc)\n\tmls.resourcesTemplates = make(map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc)\n\tmls.prompts = make([]PromptEntry, 0)\n\tmls.notificationHandlers = make(map[string]server.NotificationHandlerFunc)\n\tmls.tools = []server.ServerTool{}\n\treturn nil\n}\n\n// Ctx returns the context of the MLService.\nfunc (mls *MLService) Ctx() context.Context {\n\treturn mls.Context\n}\n\n// AddResource adds a resource and its handler function to the service.\nfunc (mls *MLService) AddResource(rs mcp.Resource, hr server.ResourceHandlerFunc) {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\tmls.resources[rs] = hr\n}\n\n// AddResourceTemplate adds a resource template and its handler function to the service.\nfunc (mls *MLService) AddResourceTemplate(rt mcp.ResourceTemplate, hr server.ResourceTemplateHandlerFunc) {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\tmls.resourcesTemplates[rt] = hr\n}\n\n// AddPrompt adds a prompt and its handler function to the service.\nfunc (mls *MLService) AddPrompt(pe PromptEntry) {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\tmls.prompts = append(mls.prompts, pe)\n}\n\n// AddTool adds a tool and its handler function to the service.\nfunc (mls *MLService) AddTool(tool mcp.Tool, handler server.ToolHandlerFunc) {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\tmls.tools = append(mls.tools, server.ServerTool{Tool: tool, Handler: handler})\n}\n\n// AddNotificationHandler adds a notification handler to the service.\nfunc (mls *MLService) AddNotificationHandler(name string, handler server.NotificationHandlerFunc) {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\tmls.notificationHandlers[name] = handler\n}\n\n// Resources returns the map of resources and their handler functions.\nfunc (mls *MLService) Resources() map[mcp.Resource]server.ResourceHandlerFunc {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\treturn mls.resources\n}\n\n// ResourceTemplates returns the map of resource templates and their handler functions.\nfunc (mls *MLService) ResourceTemplates() map[mcp.ResourceTemplate]server.ResourceTemplateHandlerFunc {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\treturn mls.resourcesTemplates\n}\n\n// Prompts returns the map of prompts and their handler functions.\nfunc (mls *MLService) Prompts() []PromptEntry {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\treturn mls.prompts\n}\n\n// Tools returns the slice of server tools.\nfunc (mls *MLService) Tools() []server.ServerTool {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\treturn mls.tools\n}\n\n// NotificationHandlers returns the map of notification handlers.\nfunc (mls *MLService) NotificationHandlers() map[string]server.NotificationHandlerFunc {\n\tmls.lock.Lock()\n\tdefer mls.lock.Unlock()\n\treturn mls.notificationHandlers\n}\n\n// MlConfig returns the configuration of the MoLing service.\nfunc (mls *MLService) MlConfig() *config.MoLingConfig {\n\treturn mls.mlConfig\n}\n\n// Config returns the configuration of the service as a string.\nfunc (mls *MLService) Config() string {\n\tpanic(\"not implemented yet\") // TODO: Implement\n}\n\n// Name returns the name of the service.\nfunc (mls *MLService) Name() string {\n\tpanic(\"not implemented yet\") // TODO: Implement\n}\n\n// LoadConfig loads the configuration for the service from a map.\nfunc (mls *MLService) LoadConfig(jsonData map[string]any) error {\n\t//panic(\"not implemented yet\") // TODO: Implement\n\terr := utils.MergeJSONToStruct(mls.mlConfig, jsonData)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn mls.mlConfig.Check()\n}\n"
  },
  {
    "path": "pkg/services/abstract/mlservice_test.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage abstract\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\nfunc TestMLService_AddResource(t *testing.T) {\n\tservice := &MLService{}\n\terr := service.InitResources()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize MLService: %s\", err.Error())\n\t}\n\tresource := mcp.Resource{Name: \"testResource\"}\n\thandler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\n\t\treturn []mcp.ResourceContents{\n\t\t\tmcp.TextResourceContents{\n\t\t\t\tText:     \"text\",\n\t\t\t\tURI:      \"uri\",\n\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tservice.AddResource(resource, handler)\n\n\tif len(service.resources) != 1 {\n\t\tt.Errorf(\"Expected 1 resource, got %d\", len(service.resources))\n\t}\n\tif service.resources[resource] == nil {\n\t\tt.Errorf(\"Handler for resource not found\")\n\t}\n}\n\nfunc TestMLService_AddResourceTemplate(t *testing.T) {\n\tservice := &MLService{}\n\terr := service.InitResources()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize MLService: %s\", err.Error())\n\t}\n\ttemplate := mcp.ResourceTemplate{Name: \"testTemplate\"}\n\thandler := func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\n\t\treturn []mcp.ResourceContents{\n\t\t\tmcp.TextResourceContents{\n\t\t\t\tText:     \"text\",\n\t\t\t\tURI:      \"uri\",\n\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tservice.AddResourceTemplate(template, handler)\n\n\tif len(service.resourcesTemplates) != 1 {\n\t\tt.Errorf(\"Expected 1 resource template, got %d\", len(service.resourcesTemplates))\n\t}\n\tif service.resourcesTemplates[template] == nil {\n\t\tt.Errorf(\"Handler for resource template not found\")\n\t}\n}\n\nfunc TestMLService_AddPrompt(t *testing.T) {\n\tservice := &MLService{}\n\terr := service.InitResources()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize MLService: %s\", err.Error())\n\t}\n\tprompt := \"testPrompt\"\n\thandler := func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\t\tpms := make([]mcp.PromptMessage, 0)\n\t\tpms = append(pms, mcp.PromptMessage{\n\t\t\tRole: mcp.RoleUser,\n\t\t\tContent: mcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: \"Prompt response\",\n\t\t\t},\n\t\t})\n\t\treturn &mcp.GetPromptResult{\n\t\t\tDescription: \"prompt description\",\n\t\t\tMessages:    pms,\n\t\t}, nil\n\t}\n\tpe := PromptEntry{\n\t\tPromptVar:   mcp.Prompt{Name: \"testPrompt\"},\n\t\tHandlerFunc: handler,\n\t}\n\tservice.AddPrompt(pe)\n\n\tif len(service.prompts) != 1 {\n\t\tt.Errorf(\"Expected 1 prompt, got %d\", len(service.prompts))\n\t}\n\tfor _, p := range service.prompts {\n\t\tif p.PromptVar.Name != prompt {\n\t\t\tt.Errorf(\"Expected prompt name %s, got %s\", prompt, p.PromptVar.Name)\n\t\t}\n\t}\n}\n\nfunc TestMLService_AddTool(t *testing.T) {\n\tservice := &MLService{}\n\terr := service.InitResources()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize MLService: %s\", err.Error())\n\t}\n\ttool := mcp.Tool{Name: \"testTool\"}\n\thandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t\treturn &mcp.CallToolResult{\n\t\t\tContent: []mcp.Content{\n\t\t\t\tmcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: \"Prompt response\",\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\tservice.AddTool(tool, handler)\n\n\tif len(service.tools) != 1 {\n\t\tt.Errorf(\"Expected 1 tool, got %d\", len(service.tools))\n\t}\n\n\t// After\n\tif service.tools[0].Tool.Name != tool.Name {\n\t\tt.Errorf(\"Tool not added correctly\")\n\t}\n}\n\nfunc TestMLService_AddNotificationHandler(t *testing.T) {\n\tservice := &MLService{}\n\terr := service.InitResources()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize MLService: %s\", err.Error())\n\t}\n\tname := \"testHandler\"\n\thandler := func(ctx context.Context, n mcp.JSONRPCNotification) {\n\t\tt.Logf(\"Received notification: %s\", n.Method)\n\t}\n\n\tservice.AddNotificationHandler(name, handler)\n\n\tif len(service.notificationHandlers) != 1 {\n\t\tt.Errorf(\"Expected 1 notification handler, got %d\", len(service.notificationHandlers))\n\t}\n\tif service.notificationHandlers[name] == nil {\n\t\tt.Errorf(\"Handler for notification not found\")\n\t}\n}\n"
  },
  {
    "path": "pkg/services/browser/browser.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\n// Package services provides a set of services for the MoLing application.\npackage browser\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/chromedp/chromedp\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\nconst (\n\tBrowserDataPath                         = \"browser\" // Path to store browser data\n\tBrowserServerName comm.MoLingServerType = \"Browser\"\n)\n\n// BrowserServer represents the configuration for the browser service.\ntype BrowserServer struct {\n\tabstract.MLService\n\tconfig       *BrowserConfig\n\tname         string // The name of the service\n\tcancelAlloc  context.CancelFunc\n\tcancelChrome context.CancelFunc\n}\n\n// NewBrowserServer creates a new BrowserServer instance with the given context and configuration.\nfunc NewBrowserServer(ctx context.Context) (abstract.Service, error) {\n\tbc := NewBrowserConfig()\n\tglobalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)\n\tbc.BrowserDataPath = filepath.Join(globalConf.BasePath, BrowserDataPath)\n\tbc.DataPath = filepath.Join(globalConf.BasePath, \"data\")\n\tlogger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"BrowserServer: invalid logger type: %T\", ctx.Value(comm.MoLingLoggerKey))\n\t}\n\tloggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {\n\t\te.Str(\"Service\", string(BrowserServerName))\n\t})\n\tbs := &BrowserServer{\n\t\tMLService: abstract.NewMLService(ctx, logger.Hook(loggerNameHook), globalConf),\n\t\tconfig:    bc,\n\t}\n\n\terr := bs.InitResources()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn bs, nil\n}\n\n// Init initializes the browser server by creating a new context.\nfunc (bs *BrowserServer) Init() error {\n\t// Initialize the browser server\n\terr := bs.initBrowser(bs.config.BrowserDataPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize browser: %w\", err)\n\t}\n\terr = utils.CreateDirectory(bs.config.DataPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create data directory: %w\", err)\n\t}\n\n\t// Create a new context for the browser\n\topts := append(\n\t\tchromedp.DefaultExecAllocatorOptions[:],\n\t\tchromedp.UserAgent(bs.config.UserAgent),\n\t\tchromedp.Flag(\"lang\", bs.config.DefaultLanguage),\n\t\tchromedp.Flag(\"disable-blink-features\", \"AutomationControlled\"),\n\t\tchromedp.Flag(\"enable-automation\", false),\n\t\tchromedp.Flag(\"disable-features\", \"Translate\"),\n\t\tchromedp.Flag(\"hide-scrollbars\", false),\n\t\tchromedp.Flag(\"mute-audio\", true),\n\t\t//chromedp.Flag(\"no-sandbox\", true),\n\t\tchromedp.Flag(\"disable-infobars\", true),\n\t\tchromedp.Flag(\"disable-extensions\", true),\n\t\tchromedp.Flag(\"CommandLineFlagSecurityWarningsEnabled\", false),\n\t\tchromedp.Flag(\"disable-notifications\", true),\n\t\tchromedp.Flag(\"disable-dev-shm-usage\", true),\n\t\tchromedp.Flag(\"autoplay-policy\", \"user-gesture-required\"),\n\t\tchromedp.CombinedOutput(bs.Logger),\n\t\t// (1920, 1080), (1366, 768), (1440, 900), (1280, 800)\n\t\tchromedp.WindowSize(1280, 800),\n\t\tchromedp.UserDataDir(bs.config.BrowserDataPath),\n\t\tchromedp.IgnoreCertErrors,\n\t)\n\n\t// headless mode\n\tif bs.config.Headless {\n\t\topts = append(opts, chromedp.Flag(\"headless\", true))\n\t\topts = append(opts, chromedp.Flag(\"disable-gpu\", true))\n\t\topts = append(opts, chromedp.Flag(\"disable-webgl\", true))\n\t}\n\n\tbs.Context, bs.cancelAlloc = chromedp.NewExecAllocator(context.Background(), opts...)\n\n\tbs.Context, bs.cancelChrome = chromedp.NewContext(bs.Context,\n\t\tchromedp.WithErrorf(bs.Logger.Error().Msgf),\n\t\tchromedp.WithDebugf(bs.Logger.Debug().Msgf),\n\t)\n\n\tpe := abstract.PromptEntry{\n\t\tPromptVar: mcp.Prompt{\n\t\t\tName:        \"browser_prompt\",\n\t\t\tDescription: \"Get the relevant functions and prompts of the Browser MCP Server\",\n\t\t\t//Arguments:   make([]mcp.PromptArgument, 0),\n\t\t},\n\t\tHandlerFunc: bs.handlePrompt,\n\t}\n\tbs.AddPrompt(pe)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_navigate\",\n\t\tmcp.WithDescription(\"Navigate to a URL\"),\n\t\tmcp.WithTitleAnnotation(\"Navigate Browser\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"url\",\n\t\t\tmcp.Description(\"URL to navigate to\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleNavigate)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_screenshot\",\n\t\tmcp.WithDescription(\"Take a screenshot of the current page or a specific element\"),\n\t\tmcp.WithTitleAnnotation(\"Take Screenshot\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"name\",\n\t\t\tmcp.Description(\"Name for the screenshot\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"selector\",\n\t\t\tmcp.Description(\"CSS selector for element to screenshot\"),\n\t\t),\n\t\tmcp.WithNumber(\"width\",\n\t\t\tmcp.Description(\"Width in pixels (default: 1700)\"),\n\t\t),\n\t\tmcp.WithNumber(\"height\",\n\t\t\tmcp.Description(\"Height in pixels (default: 1100)\"),\n\t\t),\n\t), bs.handleScreenshot)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_click\",\n\t\tmcp.WithDescription(\"Click an element on the page\"),\n\t\tmcp.WithTitleAnnotation(\"Click Element\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"selector\",\n\t\t\tmcp.Description(\"CSS selector for element to click\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleClick)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_fill\",\n\t\tmcp.WithDescription(\"Fill out an input field\"),\n\t\tmcp.WithTitleAnnotation(\"Fill Input\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"selector\",\n\t\t\tmcp.Description(\"CSS selector for input field\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"value\",\n\t\t\tmcp.Description(\"Value to fill\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleFill)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_select\",\n\t\tmcp.WithDescription(\"Select an element on the page with Select tag\"),\n\t\tmcp.WithTitleAnnotation(\"Select Option\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"selector\",\n\t\t\tmcp.Description(\"CSS selector for element to select\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"value\",\n\t\t\tmcp.Description(\"Value to select\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleSelect)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_hover\",\n\t\tmcp.WithDescription(\"Hover an element on the page\"),\n\t\tmcp.WithTitleAnnotation(\"Hover Element\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"selector\",\n\t\t\tmcp.Description(\"CSS selector for element to hover\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleHover)\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_evaluate\",\n\t\tmcp.WithDescription(\"Execute JavaScript in the browser console\"),\n\t\tmcp.WithTitleAnnotation(\"Evaluate JavaScript\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"script\",\n\t\t\tmcp.Description(\"JavaScript code to execute\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleEvaluate)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_debug_enable\",\n\t\tmcp.WithDescription(\"Enable JavaScript debugging\"),\n\t\tmcp.WithTitleAnnotation(\"Toggle Debugging\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithBoolean(\"enabled\",\n\t\t\tmcp.Description(\"Enable or disable debugging\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleDebugEnable)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_set_breakpoint\",\n\t\tmcp.WithDescription(\"Set a JavaScript breakpoint\"),\n\t\tmcp.WithTitleAnnotation(\"Set Breakpoint\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"url\",\n\t\t\tmcp.Description(\"URL of the script\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithNumber(\"line\",\n\t\t\tmcp.Description(\"Line number\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithNumber(\"column\",\n\t\t\tmcp.Description(\"Column number (optional)\"),\n\t\t),\n\t\tmcp.WithString(\"condition\",\n\t\t\tmcp.Description(\"Breakpoint condition (optional)\"),\n\t\t),\n\t), bs.handleSetBreakpoint)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_remove_breakpoint\",\n\t\tmcp.WithDescription(\"Remove a JavaScript breakpoint\"),\n\t\tmcp.WithTitleAnnotation(\"Remove Breakpoint\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"breakpointId\",\n\t\t\tmcp.Description(\"Breakpoint ID to remove\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), bs.handleRemoveBreakpoint)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_pause\",\n\t\tmcp.WithDescription(\"Pause JavaScript execution\"),\n\t\tmcp.WithTitleAnnotation(\"Pause Execution\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t), bs.handlePause)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_resume\",\n\t\tmcp.WithDescription(\"Resume JavaScript execution\"),\n\t\tmcp.WithTitleAnnotation(\"Resume Execution\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t), bs.handleResume)\n\n\tbs.AddTool(mcp.NewTool(\n\t\t\"browser_get_callstack\",\n\t\tmcp.WithDescription(\"Get current call stack when paused\"),\n\t\tmcp.WithTitleAnnotation(\"Get Call Stack\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t), bs.handleGetCallstack)\n\treturn nil\n}\n\n// init initializes the browser server by creating the user data directory.\nfunc (bs *BrowserServer) initBrowser(userDataDir string) error {\n\t_, err := os.Stat(userDataDir)\n\tif err != nil && !os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"failed to stat user data directory: %w\", err)\n\t}\n\n\t// Check if the directory exists, if it does, we can reuse it\n\tif err == nil {\n\t\t//  判断浏览器运行锁\n\t\tsingletonLock := filepath.Join(userDataDir, \"SingletonLock\")\n\t\t_, err = os.Stat(singletonLock)\n\t\tif err == nil {\n\t\t\tbs.Logger.Debug().Msg(\"Browser is already running, removing SingletonLock\")\n\t\t\terr = os.RemoveAll(singletonLock)\n\t\t\tif err != nil {\n\t\t\t\tbs.Logger.Error().Str(\"Lock\", singletonLock).Msgf(\"Browser can't work due to failed removal of SingletonLock: %s\", err.Error())\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\t// Create the directory\n\terr = os.MkdirAll(userDataDir, 0755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create user data directory: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (bs *BrowserServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\t// 处理浏览器提示\n\treturn &mcp.GetPromptResult{\n\t\tDescription: \"\",\n\t\tMessages: []mcp.PromptMessage{\n\t\t\t{\n\t\t\t\tRole: mcp.RoleUser,\n\t\t\t\tContent: mcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: bs.config.prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\n// handleNavigate handles the navigation action.\nfunc (bs *BrowserServer) handleNavigate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\turl, ok := args[\"url\"].(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"url must be a string\")\n\t}\n\n\terr := chromedp.Run(bs.Context, chromedp.Navigate(url))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to navigate: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Navigated to %s\", url)), nil\n}\n\n// handleScreenshot handles the screenshot action.\nfunc (bs *BrowserServer) handleScreenshot(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tname, ok := args[\"name\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"name must be a string\"), nil\n\t}\n\tselector, _ := args[\"selector\"].(string)\n\twidth, _ := args[\"width\"].(int)\n\theight, _ := args[\"height\"].(int)\n\tif width == 0 {\n\t\twidth = 1280\n\t}\n\tif height == 0 {\n\t\theight = 800\n\t}\n\tvar buf []byte\n\tvar err error\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\tif selector == \"\" {\n\t\terr = chromedp.Run(runCtx, chromedp.FullScreenshot(&buf, 90))\n\t} else {\n\t\terr = chromedp.Run(bs.Context, chromedp.Screenshot(selector, &buf, chromedp.NodeVisible))\n\t}\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to take screenshot: %s\", err.Error())), nil\n\t}\n\n\tnewName := filepath.Join(bs.config.DataPath, fmt.Sprintf(\"%s_%d.png\", strings.TrimRight(name, \".png\"), rand.Int()))\n\terr = os.WriteFile(newName, buf, 0644)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to save screenshot: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Screenshot saved to:%s\", newName)), nil\n}\n\n// handleClick handles the click action on a specified element.\nfunc (bs *BrowserServer) handleClick(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tselector, ok := args[\"selector\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"selector must be a string:%v\", selector)), nil\n\t}\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\terr := chromedp.Run(runCtx,\n\t\tchromedp.WaitReady(\"body\", chromedp.ByQuery), // 等待页面就绪\n\t\tchromedp.WaitVisible(selector, chromedp.ByQuery),\n\t\tchromedp.Click(selector, chromedp.NodeVisible),\n\t)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"failed to click element: %s\", err.Error()).Error()), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Clicked element %s\", selector)), nil\n}\n\n// handleFill handles the fill action on a specified input field.\nfunc (bs *BrowserServer) handleFill(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tselector, ok := args[\"selector\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to fill selector:%v\", args[\"selector\"])), nil\n\t}\n\n\tvalue, ok := args[\"value\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to fill input field: %v, selector:%v\", args[\"value\"], selector)), nil\n\t}\n\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\terr := chromedp.Run(runCtx, chromedp.SendKeys(selector, value, chromedp.NodeVisible))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to fill input field: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Filled input %s with value %s\", selector, value)), nil\n}\n\nfunc (bs *BrowserServer) handleSelect(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tselector, ok := args[\"selector\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to select selector:%v\", args[\"selector\"])), nil\n\t}\n\tvalue, ok := args[\"value\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to select value:%v\", args[\"value\"])), nil\n\t}\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\terr := chromedp.Run(runCtx, chromedp.SetValue(selector, value, chromedp.NodeVisible))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"failed to select value: %s\", err.Error()).Error()), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Selected value %s for element %s\", value, selector)), nil\n}\n\n// handleHover handles the hover action on a specified element.\nfunc (bs *BrowserServer) handleHover(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tselector, ok := args[\"selector\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"selector must be a string:%v\", selector)), nil\n\t}\n\tvar res bool\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\t// Use json.Marshal to safely embed the selector in JS, preventing code injection.\n\tselectorJSON, err := json.Marshal(selector)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"invalid selector: %s\", err.Error())), nil\n\t}\n\terr = chromedp.Run(runCtx, chromedp.Evaluate(`document.querySelector(`+string(selectorJSON)+`).dispatchEvent(new Event('mouseover'))`, &res))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"failed to hover over element: %s\", err.Error()).Error()), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Hovered over element %s, result:%t\", selector, res)), nil\n}\n\nfunc (bs *BrowserServer) handleEvaluate(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tscript, ok := args[\"script\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"script must be a string\"), nil\n\t}\n\tvar result any\n\trunCtx, cancelFunc := context.WithTimeout(bs.Context, time.Duration(bs.config.SelectorQueryTimeout)*time.Second)\n\tdefer cancelFunc()\n\terr := chromedp.Run(runCtx, chromedp.Evaluate(script, &result))\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"failed to execute script: %s\", err.Error()).Error()), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Script executed successfully: %v\", result)), nil\n}\n\nfunc (bs *BrowserServer) Close() error {\n\tbs.Logger.Debug().Msg(\"Closing browser server\")\n\tbs.cancelAlloc()\n\tbs.cancelChrome()\n\t// Cancel the context to stop the browser\n\tctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\n\tdefer cancel()\n\treturn chromedp.Cancel(ctx)\n}\n\n// Config returns the configuration of the service as a string.\nfunc (bs *BrowserServer) Config() string {\n\tcfg, err := json.Marshal(bs.config)\n\tif err != nil {\n\t\tbs.Logger.Err(err).Msg(\"failed to marshal config\")\n\t\treturn \"{}\"\n\t}\n\treturn string(cfg)\n}\n\nfunc (bs *BrowserServer) Name() comm.MoLingServerType {\n\treturn BrowserServerName\n}\n\n// LoadConfig loads the configuration from a JSON object.\nfunc (bs *BrowserServer) LoadConfig(jsonData map[string]any) error {\n\terr := utils.MergeJSONToStruct(bs.config, jsonData)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn bs.config.Check()\n}\n"
  },
  {
    "path": "pkg/services/browser/browser_config.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\n// Package services provides a set of services for the MoLing application.\npackage browser\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nconst BrowserPromptDefault = `\nYou are an AI-powered browser automation assistant capable of performing a wide range of web interactions and debugging tasks. Your capabilities include:\n\n1. **Navigation**: Navigate to any specified URL to load web pages.\n\n2. **Screenshot Capture**: Take full-page screenshots or capture specific elements using CSS selectors, with customizable dimensions (default: 1700x1100 pixels).\n\n3. **Element Interaction**:\n   - Click on elements identified by CSS selectors\n   - Hover over specified elements\n   - Fill input fields with provided values\n   - Select options in dropdown menus\n\n4. **JavaScript Execution**:\n   - Run arbitrary JavaScript code in the browser context\n   - Evaluate scripts and return results\n\n5. **Debugging Tools**:\n   - Enable/disable JavaScript debugging mode\n   - Set breakpoints at specific script locations (URL + line number + optional column/condition)\n   - Remove existing breakpoints by ID\n   - Pause and resume script execution\n   - Retrieve current call stack when paused\n\nFor all actions requiring element selection, you must use precise CSS selectors. When capturing screenshots, you can specify either the entire page or target specific elements. For debugging operations, you can precisely control execution flow and inspect runtime behavior.\n\nPlease provide clear instructions including:\n- The specific action you want performed\n- Required parameters (URLs, selectors, values, etc.)\n- Any optional parameters (dimensions, conditions, etc.)\n- Expected outcomes where relevant\n\nYou should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back with clear status updates, success/failure indicators, and any relevant output or captured data.\n`\n\ntype BrowserConfig struct {\n\tPromptFile           string `json:\"prompt_file\"` // PromptFile is the prompt file for the browser.\n\tprompt               string\n\tHeadless             bool   `json:\"headless\"`\n\tTimeout              int    `json:\"timeout\"`\n\tProxy                string `json:\"proxy\"`\n\tUserAgent            string `json:\"user_agent\"`\n\tDefaultLanguage      string `json:\"default_language\"`\n\tURLTimeout           int    `json:\"url_timeout\"`            // URLTimeout is the timeout for loading a URL. time.Second\n\tSelectorQueryTimeout int    `json:\"selector_query_timeout\"` // SelectorQueryTimeout is the timeout for CSS selector queries. time.Second\n\tDataPath             string `json:\"data_path\"`              // DataPath is the path to the data directory.\n\tBrowserDataPath      string `json:\"browser_data_path\"`      // BrowserDataPath is the path to the browser data directory.\n}\n\nfunc (cfg *BrowserConfig) Check() error {\n\tcfg.prompt = BrowserPromptDefault\n\tif cfg.Timeout <= 0 {\n\t\treturn fmt.Errorf(\"timeout must be greater than 0\")\n\t}\n\tif cfg.URLTimeout <= 0 {\n\t\treturn fmt.Errorf(\"URL timeout must be greater than 0\")\n\t}\n\tif cfg.SelectorQueryTimeout <= 0 {\n\t\treturn fmt.Errorf(\"selector Query timeout must be greater than 0\")\n\t}\n\tif cfg.PromptFile != \"\" {\n\t\tread, err := os.ReadFile(cfg.PromptFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read prompt file:%s, error: %w\", cfg.PromptFile, err)\n\t\t}\n\t\tcfg.prompt = string(read)\n\t}\n\treturn nil\n}\n\n// NewBrowserConfig creates a new BrowserConfig with default values.\nfunc NewBrowserConfig() *BrowserConfig {\n\treturn &BrowserConfig{\n\t\tHeadless:             false,\n\t\tTimeout:              30,\n\t\tURLTimeout:           10,\n\t\tSelectorQueryTimeout: 10,\n\t\tUserAgent:            \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36\",\n\t\tDefaultLanguage:      \"en-US\",\n\t\tDataPath:             filepath.Join(os.TempDir(), \".moling\", \"data\"),\n\t}\n}\n"
  },
  {
    "path": "pkg/services/browser/browser_debugger.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\n// Package services provides a set of services for the MoLing application.\npackage browser\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/chromedp/cdproto/target\"\n\t\"github.com/chromedp/chromedp\"\n\t\"github.com/mark3labs/mcp-go/mcp\"\n)\n\n// handleDebugEnable handles the enabling and disabling of debugging in the browser.\nfunc (bs *BrowserServer) handleDebugEnable(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tenabled, ok := args[\"enabled\"].(bool)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"enabled must be a boolean\"), nil\n\t}\n\n\tvar err error\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\n\tif enabled {\n\t\terr = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\t\tt := chromedp.FromContext(ctx).Target\n\t\t\t// 使用Execute方法执行AttachToTarget命令\n\t\t\tparams := target.AttachToTarget(t.TargetID).WithFlatten(true)\n\t\t\treturn t.Execute(ctx, \"Target.attachToTarget\", params, nil)\n\t\t}))\n\t} else {\n\t\terr = chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\t\tt := chromedp.FromContext(ctx).Target\n\t\t\t// 使用Execute方法执行DetachFromTarget命令\n\t\t\tparams := target.DetachFromTarget().WithSessionID(t.SessionID)\n\t\t\treturn t.Execute(ctx, \"Target.detachFromTarget\", params, nil)\n\t\t}))\n\t}\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to %s debugging: %v\",\n\t\t\tmap[bool]string{true: \"enable\", false: \"disable\"}[enabled], err)), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Debugging %s\",\n\t\tmap[bool]string{true: \"enabled\", false: \"disabled\"}[enabled])), nil\n}\n\n// handleSetBreakpoint handles setting a breakpoint in the browser.\nfunc (bs *BrowserServer) handleSetBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\turl, ok := args[\"url\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"url must be a string\"), nil\n\t}\n\n\tline, ok := args[\"line\"].(float64)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"line must be a number\"), nil\n\t}\n\n\tcolumn, _ := args[\"column\"].(float64)\n\tcondition, _ := args[\"condition\"].(string)\n\n\tvar breakpointID string\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\terr := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\tt := chromedp.FromContext(ctx).Target\n\t\tparams := map[string]any{\n\t\t\t\"url\":       url,\n\t\t\t\"line\":      int(line),\n\t\t\t\"column\":    int(column),\n\t\t\t\"condition\": condition,\n\t\t}\n\n\t\tvar result map[string]any\n\t\t// 使用Execute方法执行Debugger.setBreakpoint命令\n\t\tif err := t.Execute(ctx, \"Debugger.setBreakpoint\", params, &result); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbreakpointID, ok = result[\"breakpointId\"].(string)\n\t\tif !ok {\n\t\t\tbreakpointID = \"\"\n\t\t\treturn fmt.Errorf(\"failed to get breakpoint ID\")\n\t\t}\n\t\treturn nil\n\t}))\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to set breakpoint: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Breakpoint set with ID: %s\", breakpointID)), nil\n}\n\n// handleRemoveBreakpoint handles removing a breakpoint in the browser.\nfunc (bs *BrowserServer) handleRemoveBreakpoint(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tbreakpointID, ok := args[\"breakpointId\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"breakpointId must be a string\"), nil\n\t}\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\terr := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\tt := chromedp.FromContext(ctx).Target\n\t\t// 使用Execute方法执行Debugger.removeBreakpoint命令\n\t\treturn t.Execute(ctx, \"Debugger.removeBreakpoint\", map[string]any{\n\t\t\t\"breakpointId\": breakpointID,\n\t\t}, nil)\n\t}))\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to remove breakpoint: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Breakpoint %s removed\", breakpointID)), nil\n}\n\n// handlePause handles pausing the JavaScript execution in the browser.\nfunc (bs *BrowserServer) handlePause(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\terr := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\tt := chromedp.FromContext(ctx).Target\n\t\t// 使用Execute方法执行Debugger.pause命令\n\t\treturn t.Execute(ctx, \"Debugger.pause\", nil, nil)\n\t}))\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to pause execution: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(\"JavaScript execution paused\"), nil\n}\n\n// handleResume handles resuming the JavaScript execution in the browser.\nfunc (bs *BrowserServer) handleResume(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\terr := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\tt := chromedp.FromContext(ctx).Target\n\t\t// 使用Execute方法执行Debugger.resume命令\n\t\treturn t.Execute(ctx, \"Debugger.resume\", nil, nil)\n\t}))\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to resume execution: %s\", err.Error())), nil\n\t}\n\treturn mcp.NewToolResultText(\"JavaScript execution resumed\"), nil\n}\n\n// handleStepOver handles stepping over the next line of JavaScript code in the browser.\nfunc (bs *BrowserServer) handleGetCallstack(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\tvar callstack any\n\trctx, cancel := context.WithCancel(bs.Context)\n\tdefer cancel()\n\terr := chromedp.Run(rctx, chromedp.ActionFunc(func(ctx context.Context) error {\n\t\tt := chromedp.FromContext(ctx).Target\n\t\t// 使用Execute方法执行Debugger.getStackTrace命令\n\t\treturn t.Execute(ctx, \"Debugger.getStackTrace\", nil, &callstack)\n\t}))\n\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to get call stack: %s\", err.Error())), nil\n\t}\n\n\tcallstackJSON, err := json.Marshal(callstack)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"failed to marshal call stack: %s\", err.Error())), nil\n\t}\n\n\treturn mcp.NewToolResultText(fmt.Sprintf(\"Current call stack: %s\", string(callstackJSON))), nil\n}\n"
  },
  {
    "path": "pkg/services/browser/browser_test.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage browser\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n)\n\nfunc TestBrowserServer(t *testing.T) {\n\t//\n\t//cfg := &BrowserConfig{\n\t//\tHeadless:        true,\n\t//\tTimeout:         30,\n\t//\tUserAgent:       \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3\",\n\t//\tDefaultLanguage: \"en-US\",\n\t//\tURLTimeout:      10,\n\t//\tSelectorQueryTimeout:      10,\n\t//}\n\tlogger, ctx, err := comm.InitTestEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize test environment: %s\", err.Error())\n\t}\n\tlogger.Info().Msg(\"TestBrowserServer\")\n\n\t_, err = NewBrowserServer(ctx)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create BrowserServer: %s\", err.Error())\n\t}\n}\n\n// TestHoverSelectorEscaping verifies that the selector is safely JSON-encoded\n// before being embedded in the JavaScript expression, preventing code injection.\nfunc TestHoverSelectorEscaping(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tselector string\n\t\t// wantPrefix checks that the selector is embedded as a JSON-encoded double-quoted string\n\t\twantJSPrefix string\n\t\twantJSSuffix string\n\t}{\n\t\t{\n\t\t\tname:         \"normal selector\",\n\t\t\tselector:     \"body\",\n\t\t\twantJSPrefix: `document.querySelector(\"body\").dispatchEvent`,\n\t\t},\n\t\t{\n\t\t\tname:     \"injection attempt with single quotes and comma operator\",\n\t\t\tselector: \"body'),document.title='PWNED',document.querySelector('body\",\n\t\t\t// After JSON encoding, the selector is a double-quoted string literal.\n\t\t\t// The single quotes and commas stay inside the string and are NOT executable.\n\t\t\twantJSPrefix: `document.querySelector(\"body'),document.title='PWNED',document.querySelector('body\").dispatchEvent`,\n\t\t},\n\t\t{\n\t\t\tname:         \"injection attempt with semicolons and IIFE\",\n\t\t\tselector:     \"body'); (function(){ /* exfiltration */ })(); document.querySelector('body\",\n\t\t\twantJSPrefix: `document.querySelector(\"body'); (function(){ /* exfiltration */ })(); document.querySelector('body\").dispatchEvent`,\n\t\t},\n\t\t{\n\t\t\tname:     \"selector with double quotes is escaped by JSON\",\n\t\t\tselector: `div[data-id=\"foo\"]`,\n\t\t\t// json.Marshal escapes inner double quotes as \\\", so they cannot break out of the JS string.\n\t\t\twantJSPrefix: `document.querySelector(\"div[data-id=\\\"foo\\\"]\").dispatchEvent`,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tselectorJSON, err := json.Marshal(tc.selector)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"json.Marshal failed: %v\", err)\n\t\t\t}\n\t\t\tjs := `document.querySelector(` + string(selectorJSON) + `).dispatchEvent(new Event('mouseover'))`\n\n\t\t\t// The embedded selector must be wrapped in JSON double-quotes (not single-quotes).\n\t\t\t// This ensures injected single-quote characters cannot break out of the JS string context.\n\t\t\tif !strings.HasPrefix(string(selectorJSON), `\"`) || !strings.HasSuffix(string(selectorJSON), `\"`) {\n\t\t\t\tt.Errorf(\"selector was not JSON-encoded as a double-quoted string: %s\", string(selectorJSON))\n\t\t\t}\n\n\t\t\tif tc.wantJSPrefix != \"\" && !strings.HasPrefix(js, tc.wantJSPrefix) {\n\t\t\t\tt.Errorf(\"JS expression did not start with expected prefix\\n  want: %s\\n   got: %s\", tc.wantJSPrefix, js)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/services/command/command.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\n// Package services Description: This file contains the implementation of the CommandServer interface for macOS and  Linux.\npackage command\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\nvar (\n\t// ErrCommandNotFound is returned when the command is not found.\n\tErrCommandNotFound = fmt.Errorf(\"command not found\")\n\t// ErrCommandNotAllowed is returned when the command is not allowed.\n\tErrCommandNotAllowed = fmt.Errorf(\"command not allowed\")\n)\n\nconst (\n\tCommandServerName comm.MoLingServerType = \"Command\"\n)\n\n// CommandServer implements the Service interface and provides methods to execute named commands.\ntype CommandServer struct {\n\tabstract.MLService\n\tconfig    *CommandConfig\n\tosName    string\n\tosVersion string\n}\n\n// NewCommandServer creates a new CommandServer with the given allowed commands.\nfunc NewCommandServer(ctx context.Context) (abstract.Service, error) {\n\tvar err error\n\tcc := NewCommandConfig()\n\tgConf, ok := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"CommandServer: invalid config type\")\n\t}\n\n\tlger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"CommandServer: invalid logger type\")\n\t}\n\n\tloggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {\n\t\te.Str(\"Service\", string(CommandServerName))\n\t})\n\n\tcs := &CommandServer{\n\t\tMLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), gConf),\n\t\tconfig:    cc,\n\t}\n\n\terr = cs.InitResources()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cs, nil\n}\n\nfunc (cs *CommandServer) Init() error {\n\tvar err error\n\tpe := abstract.PromptEntry{\n\t\tPromptVar: mcp.Prompt{\n\t\t\tName:        \"command_prompt\",\n\t\t\tDescription: \"get command prompt\",\n\t\t\t//Arguments:   make([]mcp.PromptArgument, 0),\n\t\t},\n\t\tHandlerFunc: cs.handlePrompt,\n\t}\n\tcs.AddPrompt(pe)\n\tcs.AddTool(mcp.NewTool(\n\t\t\"execute_command\",\n\t\tmcp.WithDescription(\"Execute a named command.Only support command execution on macOS and will strictly follow safety guidelines, ensuring that commands are safe and secure\"),\n\t\tmcp.WithTitleAnnotation(\"Execute Command\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"command\",\n\t\t\tmcp.Description(\"The command to execute\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), cs.handleExecuteCommand)\n\treturn err\n}\n\nfunc (cs *CommandServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\treturn &mcp.GetPromptResult{\n\t\tDescription: \"\",\n\t\tMessages: []mcp.PromptMessage{\n\t\t\t{\n\t\t\t\tRole: mcp.RoleUser,\n\t\t\t\tContent: mcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fmt.Sprintf(cs.config.prompt, cs.MlConfig().SystemInfo),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\n// handleExecuteCommand handles the execution of a named command.\nfunc (cs *CommandServer) handleExecuteCommand(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tcommand, ok := args[\"command\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"command must be a string\").Error()), nil\n\t}\n\n\t// Check if the command is allowed\n\tif !cs.isAllowedCommand(command) {\n\t\tcs.Logger.Err(ErrCommandNotAllowed).Str(\"command\", command).Msgf(\"If you want to allow this command, add it to %s\", filepath.Join(cs.MlConfig().BasePath, \"config\", cs.MlConfig().ConfigFile))\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: Command '%s' is not allowed\", command)), nil\n\t}\n\n\t// Execute the command\n\toutput, err := ExecCommand(command)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error executing command: %v\", err)), nil\n\t}\n\n\treturn mcp.NewToolResultText(output), nil\n}\n\n// shellInjectionPatterns are substrings that indicate potential command injection attempts.\n// Commands containing any of these patterns are rejected before allowlist evaluation.\nvar shellInjectionPatterns = []string{\";\", \"\\n\", \"`\", \"$(\", \"${\"}\n\n// isAllowedCommand checks if the command is allowed based on the configuration.\nfunc (cs *CommandServer) isAllowedCommand(command string) bool {\n\tcommand = strings.TrimSpace(command)\n\tif command == \"\" {\n\t\treturn false\n\t}\n\n\t// Reject commands containing shell injection metacharacters.\n\tfor _, pattern := range shellInjectionPatterns {\n\t\tif strings.Contains(command, pattern) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Handle && (logical AND) — must be checked before single &.\n\tif strings.Contains(command, \"&&\") {\n\t\tparts := strings.Split(command, \"&&\")\n\t\tfor _, part := range parts {\n\t\t\tif !cs.isAllowedCommand(strings.TrimSpace(part)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Handle || (logical OR) — must be checked before single |.\n\tif strings.Contains(command, \"||\") {\n\t\tparts := strings.Split(command, \"||\")\n\t\tfor _, part := range parts {\n\t\t\tif !cs.isAllowedCommand(strings.TrimSpace(part)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Handle | (pipe).\n\tif strings.Contains(command, \"|\") {\n\t\tparts := strings.Split(command, \"|\")\n\t\tfor _, part := range parts {\n\t\t\tif !cs.isAllowedCommand(strings.TrimSpace(part)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Handle & (background execution).\n\tif strings.Contains(command, \"&\") {\n\t\tparts := strings.Split(command, \"&\")\n\t\tfor _, part := range parts {\n\t\t\tif !cs.isAllowedCommand(strings.TrimSpace(part)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t// Extract the command name (first token) and check it against the allowlist.\n\tfields := strings.Fields(command)\n\tif len(fields) == 0 {\n\t\treturn false\n\t}\n\tcmdName := fields[0]\n\tfor _, allowed := range cs.config.allowedCommands {\n\t\tif cmdName == strings.TrimSpace(allowed) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Config returns the configuration of the service as a string.\nfunc (cs *CommandServer) Config() string {\n\tcs.config.AllowedCommand = strings.Join(cs.config.allowedCommands, \",\")\n\tcfg, err := json.Marshal(cs.config)\n\tif err != nil {\n\t\tcs.Logger.Err(err).Msg(\"failed to marshal config\")\n\t\treturn \"{}\"\n\t}\n\tcs.Logger.Debug().Str(\"config\", string(cfg)).Msg(\"CommandServer config\")\n\treturn string(cfg)\n}\n\nfunc (cs *CommandServer) Name() comm.MoLingServerType {\n\treturn CommandServerName\n}\n\nfunc (cs *CommandServer) Close() error {\n\t// Cancel the context to stop the browser\n\tcs.Logger.Debug().Msg(\"CommandServer closed\")\n\treturn nil\n}\n\n// LoadConfig loads the configuration from a JSON object.\nfunc (cs *CommandServer) LoadConfig(jsonData map[string]any) error {\n\terr := utils.MergeJSONToStruct(cs.config, jsonData)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// split the AllowedCommand string into a slice\n\tcs.config.allowedCommands = strings.Split(cs.config.AllowedCommand, \",\")\n\treturn cs.config.Check()\n}\n"
  },
  {
    "path": "pkg/services/command/command_config.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage command\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nconst (\n\tCommandPromptDefault = `\nYou are a powerful terminal command assistant capable of executing various command-line on %s operations and management tasks. Your capabilities include:\n\n1. **File and Directory Management**:\n    - List files and subdirectories in a directory\n    - Create new files or directories\n    - Delete specified files or directories\n    - Copy and move files and directories\n    - Rename files or directories\n\n2. **File Content Operations**:\n    - View the contents of text files\n    - Edit file contents\n    - Redirect output to a file\n    - Search file contents\n\n3. **System Information Retrieval**:\n    - Retrieve system information (e.g., CPU usage, memory usage, etc.)\n    - View the current user and their permissions\n    - Check the current working directory\n\n4. **Network Operations**:\n    - Check network connection status (e.g., using the ping command)\n    - Query domain information (e.g., using the whois command)\n    - Manage network services (e.g., start, stop, and restart services)\n\n5. **Process Management**:\n    - List currently running processes\n    - Terminate specified processes\n    - Adjust process priorities\n\nBefore executing any actions, please provide clear instructions, including:\n- The specific command you want to execute\n- Required parameters (file paths, directory names, etc.)\n- Any optional parameters (e.g., modification options, output formats, etc.)\n- Relevant expected results or output\n\nWhen dealing with sensitive operations or destructive commands, please confirm before execution. Report back with clear status updates, success/failure indicators, and any relevant output or results.\n`\n)\n\n// CommandConfig represents the configuration for allowed commands.\ntype CommandConfig struct {\n\tPromptFile      string `json:\"prompt_file\"` // PromptFile is the prompt file for the command.\n\tprompt          string\n\tAllowedCommand  string `json:\"allowed_command\"` // AllowedCommand is a list of allowed command. split by comma. e.g. ls,cat,echo\n\tallowedCommands []string\n}\n\nvar (\n\tallowedCmdDefault = []string{\n\t\t\"ls\", \"cat\", \"echo\", \"pwd\", \"head\", \"tail\", \"grep\", \"find\", \"stat\", \"df\",\n\t\t\"du\", \"free\", \"top\", \"ps\", \"uptime\", \"who\", \"w\", \"last\", \"uname\", \"hostname\",\n\t\t\"ifconfig\", \"netstat\", \"ping\", \"traceroute\", \"route\", \"ip\", \"ss\", \"lsof\", \"vmstat\",\n\t\t\"iostat\", \"mpstat\", \"sar\", \"uptime\", \"cut\", \"sort\", \"uniq\", \"wc\", \"awk\", \"sed\",\n\t\t\"diff\", \"cmp\", \"comm\", \"file\", \"basename\", \"dirname\", \"chmod\", \"chown\", \"curl\",\n\t\t\"nslookup\", \"dig\", \"host\", \"ssh\", \"scp\", \"sftp\", \"ftp\", \"wget\", \"tar\", \"gzip\",\n\t\t\"scutil\", \"networksetup\", \"git\", \"cd\",\n\t}\n)\n\n// NewCommandConfig creates a new CommandConfig with the given allowed commands.\nfunc NewCommandConfig() *CommandConfig {\n\treturn &CommandConfig{\n\t\tallowedCommands: allowedCmdDefault,\n\t\tAllowedCommand:  strings.Join(allowedCmdDefault, \",\"),\n\t}\n}\n\n// Check validates the allowed commands in the CommandConfig.\nfunc (cc *CommandConfig) Check() error {\n\tcc.prompt = CommandPromptDefault\n\tvar cnt int\n\tcnt = len(cc.allowedCommands)\n\n\t// Check if any command is empty\n\tfor _, cmd := range cc.allowedCommands {\n\t\tif cmd == \"\" {\n\t\t\tcnt -= 1\n\t\t}\n\t}\n\n\tif cnt <= 0 {\n\t\treturn fmt.Errorf(\"no allowed commands specified\")\n\t}\n\tif cc.PromptFile != \"\" {\n\t\tread, err := os.ReadFile(cc.PromptFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read prompt file:%s, error: %w\", cc.PromptFile, err)\n\t\t}\n\t\tcc.prompt = string(read)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/command/command_exec.go",
    "content": "//go:build !windows\n\n// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage command\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"time\"\n)\n\n// ExecCommand executes a command and returns its output.\nfunc ExecCommand(command string) (string, error) {\n\tvar cmd *exec.Cmd\n\tctx, cfunc := context.WithTimeout(context.Background(), time.Second*10)\n\tdefer cfunc()\n\tcmd = exec.CommandContext(ctx, \"sh\", \"-c\", command)\n\toutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\tswitch {\n\t\tcase errors.Is(err, exec.ErrNotFound):\n\t\t\t// 命令未找到\n\t\t\treturn \"\", errors.New(\"command not found\")\n\t\tcase errors.Is(ctx.Err(), context.DeadlineExceeded):\n\t\t\t// 超时时仅返回输出，不返回错误\n\t\t\treturn string(output), nil\n\t\tdefault:\n\t\t\treturn string(output), nil\n\t\t}\n\t}\n\n\treturn string(output), nil\n}\n"
  },
  {
    "path": "pkg/services/command/command_exec_test.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage command\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n)\n\n// MockCommandServer is a mock implementation of CommandServer for testing purposes.\ntype MockCommandServer struct {\n\tCommandServer\n}\n\n// TestExecuteCommand tests the ExecCommand function.\nfunc TestExecuteCommand(t *testing.T) {\n\texecCmd := \"echo 'Hello, World!'\"\n\t// Test a simple command\n\toutput, err := ExecCommand(execCmd)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\texpectedOutput := \"Hello, World!\\n\"\n\tif output != expectedOutput {\n\t\tt.Errorf(\"Expected output %q, got %q\", expectedOutput, output)\n\t}\n\tt.Logf(\"Command output: %s\", output)\n\t// Test a command with a timeout\n\tctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*10)\n\tdefer cancel()\n\n\texecCmd = \"curl ifconfig.me|grep Time\"\n\toutput, err = ExecCommand(execCmd)\n\tif err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\tt.Logf(\"Command output: %s\", output)\n\tcmd := exec.CommandContext(ctx, \"sleep\", \"1\")\n\terr = cmd.Run()\n\tif err == nil {\n\t\tt.Fatalf(\"Expected timeout error, got nil\")\n\t}\n\tif !errors.Is(ctx.Err(), context.DeadlineExceeded) {\n\t\tt.Errorf(\"Expected context deadline exceeded error, got %v\", ctx.Err())\n\t}\n}\n\nfunc TestAllowCmd(t *testing.T) {\n\t// Test with a command that is allowed\n\t_, ctx, err := comm.InitTestEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize test environment: %v\", err)\n\t}\n\n\tcs, err := NewCommandServer(ctx)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create CommandServer: %v\", err)\n\t}\n\n\tcc := StructToMap(NewCommandConfig())\n\tt.Logf(\"CommandConfig: %v\", cc)\n\terr = cs.LoadConfig(cc)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load config: %v\", err)\n\t}\n\tcmd := \"cd /var/logs/notfound && git log --since=\\\"today\\\" --pretty=format:\\\"%h - %an, %ar : %s\\\"\"\n\tcs1 := cs.(*CommandServer)\n\tif !cs1.isAllowedCommand(cmd) {\n\t\tt.Errorf(\"Command '%s' is not allowed\", cmd)\n\t}\n\tt.Log(\"Command is allowed:\", cmd)\n}\n\n// TestIsAllowedCommandInjection verifies that shell injection attempts are blocked.\nfunc TestIsAllowedCommandInjection(t *testing.T) {\n\t_, ctx, err := comm.InitTestEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to initialize test environment: %v\", err)\n\t}\n\n\tcs, err := NewCommandServer(ctx)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create CommandServer: %v\", err)\n\t}\n\n\tcc := StructToMap(NewCommandConfig())\n\terr = cs.LoadConfig(cc)\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to load config: %v\", err)\n\t}\n\tcs1 := cs.(*CommandServer)\n\n\tinjectionAttempts := []struct {\n\t\tname    string\n\t\tcommand string\n\t}{\n\t\t{\"semicolon injection\", \"echo hello; id\"},\n\t\t{\"semicolon with spaces\", \"echo hello ; whoami\"},\n\t\t{\"command substitution $()\", \"echo $(id)\"},\n\t\t{\"command substitution with cat\", \"echo $(cat /etc/passwd)\"},\n\t\t{\"backtick substitution\", \"echo `whoami`\"},\n\t\t{\"backtick substitution nested\", \"echo `id`\"},\n\t\t{\"newline injection\", \"echo hello\\nid\"},\n\t\t{\"variable expansion ${}\", \"echo ${PATH}\"},\n\t\t{\"semicolon with allowed cmd\", \"ls; id\"},\n\t\t{\"semicolon chaining\", \"echo x; echo y; id\"},\n\t}\n\n\tfor _, tc := range injectionAttempts {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif cs1.isAllowedCommand(tc.command) {\n\t\t\t\tt.Errorf(\"injection attempt should be blocked: %q\", tc.command)\n\t\t\t}\n\t\t})\n\t}\n\n\t// Verify legitimate commands still work.\n\tlegitimateCmds := []struct {\n\t\tname    string\n\t\tcommand string\n\t}{\n\t\t{\"simple echo\", \"echo hello\"},\n\t\t{\"ls with flag\", \"ls -la\"},\n\t\t{\"pipe allowed cmds\", \"cat /etc/hostname | grep -v localhost\"},\n\t\t{\"logical AND allowed cmds\", \"echo hello && echo world\"},\n\t\t{\"logical OR allowed cmds\", \"echo hello || echo world\"},\n\t\t{\"git command\", \"git log --oneline\"},\n\t}\n\n\tfor _, tc := range legitimateCmds {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif !cs1.isAllowedCommand(tc.command) {\n\t\t\t\tt.Errorf(\"legitimate command should be allowed: %q\", tc.command)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// 将 struct 转换为 map\nfunc StructToMap(obj any) map[string]any {\n\tresult := make(map[string]any)\n\tval := reflect.ValueOf(obj)\n\tif val.Kind() == reflect.Ptr {\n\t\tval = val.Elem()\n\t}\n\tif val.Kind() != reflect.Struct {\n\t\treturn nil\n\t}\n\ttyp := val.Type()\n\tfor i := 0; i < val.NumField(); i++ {\n\t\tfield := typ.Field(i)\n\t\tvalue := val.Field(i)\n\t\t// 跳过未导出的字段\n\t\tif field.PkgPath != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\t// 获取字段的 json tag（如果存在）\n\t\tkey := field.Name\n\t\tif tag := field.Tag.Get(\"json\"); tag != \"\" {\n\t\t\tkey = tag\n\t\t}\n\t\tresult[key] = value.Interface()\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "pkg/services/command/command_exec_windows.go",
    "content": "//go:build windows\n\n// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\n// Package services Description: This file contains the implementation of the CommandServer interface for Windows.\npackage command\n\nimport (\n\t\"os/exec\"\n)\n\n// ExecCommand executes a command and returns its output.\nfunc ExecCommand(command string) (string, error) {\n\tvar cmd *exec.Cmd\n\tcmd = exec.Command(\"cmd\", \"/C\", command)\n\toutput, err := cmd.CombinedOutput()\n\treturn string(output), err\n}\n"
  },
  {
    "path": "pkg/services/filesystem/file_system.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n// Source: https://github.com/mark3labs/mcp-filesystem-server\n\n// Package services provides the implementation of the FileSystemServer, which allows access to files and directories on the local file system.\npackage filesystem\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mark3labs/mcp-go/mcp\"\n\t\"github.com/rs/zerolog\"\n\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/config\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/utils\"\n)\n\nconst (\n\t// MaxInlineSize Maximum size for inline content (5MB)\n\tMaxInlineSize = 1024 * 1024 * 5\n\t// MaxBase64Size Maximum size for base64 encoding (1MB)\n\tMaxBase64Size = 1024 * 1024 * 1\n)\nconst (\n\tFilesystemServerName comm.MoLingServerType = \"FileSystem\"\n)\n\ntype FileInfo struct {\n\tSize        int64     `json:\"size\"`\n\tCreated     time.Time `json:\"created\"`\n\tModified    time.Time `json:\"modified\"`\n\tAccessed    time.Time `json:\"accessed\"`\n\tIsDirectory bool      `json:\"isDirectory\"`\n\tIsFile      bool      `json:\"isFile\"`\n\tPermissions string    `json:\"permissions\"`\n}\n\ntype FilesystemServer struct {\n\tabstract.MLService\n\tconfig *FileSystemConfig\n}\n\nfunc NewFilesystemServer(ctx context.Context) (abstract.Service, error) {\n\t// Validate the config\n\tvar err error\n\tglobalConf := ctx.Value(comm.MoLingConfigKey).(*config.MoLingConfig)\n\tuserDataDir := filepath.Join(globalConf.BasePath, \"data\")\n\n\tfc := NewFileSystemConfig(userDataDir)\n\n\tlger, ok := ctx.Value(comm.MoLingLoggerKey).(zerolog.Logger)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"FilesystemServer: invalid logger type\")\n\t}\n\n\tloggerNameHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {\n\t\te.Str(\"Service\", string(FilesystemServerName))\n\t})\n\n\tfs := &FilesystemServer{\n\t\tMLService: abstract.NewMLService(ctx, lger.Hook(loggerNameHook), globalConf),\n\t\tconfig:    fc,\n\t}\n\n\terr = fs.InitResources()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to initialize filesystem server: %w\", err)\n\t}\n\n\treturn fs, nil\n}\n\nfunc (fs *FilesystemServer) Init() error {\n\t// Register resource handlers\n\tfs.AddResource(mcp.NewResource(\"file://\", \"File System\",\n\t\tmcp.WithResourceDescription(\"Access to files and directories on the local file system\"),\n\t), fs.handleReadResource)\n\n\tpe := abstract.PromptEntry{\n\t\tPromptVar: mcp.Prompt{\n\t\t\tName:        \"filesystem_prompt\",\n\t\t\tDescription: \"Get the relevant functions and prompts of the FileSystem MCP Server.\",\n\t\t},\n\t\tHandlerFunc: fs.handlePrompt,\n\t}\n\tfs.AddPrompt(pe)\n\n\t// Register tool handlers\n\tfs.AddTool(mcp.NewTool(\"read_file\",\n\t\tmcp.WithDescription(\"Read the complete contents of a file from the file system.\"),\n\t\tmcp.WithTitleAnnotation(\"Read File\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative path to the file to read\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleReadFile)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"write_file\",\n\t\tmcp.WithDescription(\"Create a new file or overwrite an existing file with new content.\"),\n\t\tmcp.WithTitleAnnotation(\"Write File\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative Path where to write the file\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"content\",\n\t\t\tmcp.Description(\"Content to write to the file\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleWriteFile)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"list_directory\",\n\t\tmcp.WithDescription(\"Get a detailed listing of all files and directories in a specified path.\"),\n\t\tmcp.WithTitleAnnotation(\"List Directory\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative Path of the directory to list\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleListDirectory)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"create_directory\",\n\t\tmcp.WithDescription(\"Create a new directory or ensure a directory exists.\"),\n\t\tmcp.WithTitleAnnotation(\"Create Directory\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative Path of the directory to create\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleCreateDirectory)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"move_file\",\n\t\tmcp.WithDescription(\"Move or rename files and directories.\"),\n\t\tmcp.WithTitleAnnotation(\"Move File\"),\n\t\tmcp.WithDestructiveHintAnnotation(true),\n\t\tmcp.WithString(\"source\",\n\t\t\tmcp.Description(\"Relative Source path of the file or directory\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"destination\",\n\t\t\tmcp.Description(\"Relative Destination path\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleMoveFile)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"search_files\",\n\t\tmcp.WithDescription(\"Recursively search for files and directories matching a pattern.\"),\n\t\tmcp.WithTitleAnnotation(\"Search Files\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative Starting path for the search\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t\tmcp.WithString(\"pattern\",\n\t\t\tmcp.Description(\"Relative Search pattern to match against file names\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleSearchFiles)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"get_file_info\",\n\t\tmcp.WithDescription(\"Retrieve detailed metadata about a file or directory.\"),\n\t\tmcp.WithTitleAnnotation(\"Get File Info\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t\tmcp.WithString(\"path\",\n\t\t\tmcp.Description(\"Relative Path to the file or directory\"),\n\t\t\tmcp.Required(),\n\t\t),\n\t), fs.handleGetFileInfo)\n\n\tfs.AddTool(mcp.NewTool(\n\t\t\"list_allowed_directories\",\n\t\tmcp.WithDescription(\"Returns the list of directories that this server is allowed to access.\"),\n\t\tmcp.WithTitleAnnotation(\"List Allowed Directories\"),\n\t\tmcp.WithReadOnlyHintAnnotation(true),\n\t), fs.handleListAllowedDirectories)\n\treturn nil\n}\n\n// handlePrompt handles the prompt request for the FilesystemServer\nfunc (fs *FilesystemServer) handlePrompt(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {\n\treturn &mcp.GetPromptResult{\n\t\tDescription: \"\",\n\t\tMessages: []mcp.PromptMessage{\n\t\t\t{\n\t\t\t\tRole: mcp.RoleUser,\n\t\t\t\tContent: mcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fs.config.prompt,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\n// isPathInAllowedDirs checks if a path is within any of the allowed directories\nfunc (fs *FilesystemServer) isPathInAllowedDirs(path string) bool {\n\t// Ensure path is absolute and clean\n\tabsPath, err := filepath.Abs(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// Add trailing separator to ensure we're checking a directory or a file within a directory\n\t// and not a prefix match (e.g., /tmp/foo should not match /tmp/foobar)\n\tif !strings.HasSuffix(absPath, string(filepath.Separator)) {\n\t\t// If it'fss a file, we need to check its directory\n\t\tif info, err := os.Stat(absPath); err == nil && !info.IsDir() {\n\t\t\tabsPath = filepath.Dir(absPath) + string(filepath.Separator)\n\t\t} else {\n\t\t\tabsPath = absPath + string(filepath.Separator)\n\t\t}\n\t}\n\n\t// Check if the path is within any of the allowed directories\n\tfor _, dir := range fs.config.allowedDirs {\n\t\tif strings.HasPrefix(absPath, dir) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (fs *FilesystemServer) validatePath(requestedPath string) (string, error) {\n\t// Always convert to absolute path first\n\tvar hasPrefix bool\n\tvar firstDir string\n\tfor _, dir := range fs.config.allowedDirs {\n\t\tif firstDir == \"\" {\n\t\t\tfirstDir = dir\n\t\t}\n\t\tif strings.HasPrefix(requestedPath, dir) {\n\t\t\thasPrefix = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !hasPrefix {\n\t\trequestedPath = filepath.Join(firstDir, requestedPath)\n\t}\n\tabs, err := filepath.Abs(requestedPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid path: %w\", err)\n\t}\n\n\t// Check if path is within allowed directories\n\tif !fs.isPathInAllowedDirs(abs) {\n\t\treturn \"\", fmt.Errorf(\"access denied - path outside allowed directories: %s\", abs)\n\t}\n\n\t// Handle symlinks\n\trealPath, err := filepath.EvalSymlinks(abs)\n\tif err != nil {\n\t\tif !os.IsNotExist(err) {\n\t\t\treturn \"\", err\n\t\t}\n\t\t// For new files, check parent directory\n\t\tparent := filepath.Dir(abs)\n\t\trealParent, err := filepath.EvalSymlinks(parent)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"parent directory does not exist: %s\", parent)\n\t\t}\n\n\t\tif !fs.isPathInAllowedDirs(realParent) {\n\t\t\treturn \"\", fmt.Errorf(\n\t\t\t\t\"access denied - parent directory outside allowed directories\",\n\t\t\t)\n\t\t}\n\t\treturn abs, nil\n\t}\n\n\t// Check if the real path (after resolving symlinks) is still within allowed directories\n\tif !fs.isPathInAllowedDirs(realPath) {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"access denied - symlink target outside allowed directories\",\n\t\t)\n\t}\n\n\treturn realPath, nil\n}\n\nfunc (fs *FilesystemServer) getFileStats(path string) (FileInfo, error) {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn FileInfo{}, err\n\t}\n\n\treturn FileInfo{\n\t\tSize:        info.Size(),\n\t\tCreated:     info.ModTime(), // Note: ModTime used as birth time isn't always available\n\t\tModified:    info.ModTime(),\n\t\tAccessed:    info.ModTime(), // Note: Access time isn't always available\n\t\tIsDirectory: info.IsDir(),\n\t\tIsFile:      !info.IsDir(),\n\t\tPermissions: fmt.Sprintf(\"%o\", info.Mode().Perm()),\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) searchFiles(rootPath, pattern string) ([]string, error) {\n\tvar results []string\n\tpattern = strings.ToLower(pattern)\n\n\terr := filepath.Walk(\n\t\trootPath,\n\t\tfunc(path string, info os.FileInfo, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn nil // Skip errors and continue\n\t\t\t}\n\n\t\t\t// Try to validate path\n\t\t\tif _, err := fs.validatePath(path); err != nil {\n\t\t\t\treturn nil // Skip invalid paths\n\t\t\t}\n\n\t\t\tif strings.Contains(strings.ToLower(info.Name()), pattern) {\n\t\t\t\tresults = append(results, path)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn results, nil\n}\n\n// Resource handler\nfunc (fs *FilesystemServer) handleReadResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {\n\turi := request.Params.URI\n\tfs.Logger.Debug().Str(\"uri\", uri).Msg(\"handleReadResource\")\n\n\t// Check if it'fss a file:// URI\n\tif !strings.HasPrefix(uri, \"file://\") {\n\t\treturn nil, fmt.Errorf(\"unsupported URI scheme: %s\", uri)\n\t}\n\n\t// Extract the path from the URI\n\tpath := strings.TrimPrefix(uri, \"file://\")\n\n\t// Validate the path\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Get file info\n\tfileInfo, err := os.Stat(validPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If it'fss a directory, return a listing\n\tif fileInfo.IsDir() {\n\t\tentries, err := os.ReadDir(validPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar result strings.Builder\n\t\tresult.WriteString(fmt.Sprintf(\"Directory listing for: %s\\n\\n\", validPath))\n\n\t\tfor _, entry := range entries {\n\t\t\tentryPath := filepath.Join(validPath, entry.Name())\n\t\t\tentryURI := utils.PathToResourceURI(entryPath)\n\n\t\t\tif entry.IsDir() {\n\t\t\t\tresult.WriteString(fmt.Sprintf(\"[DIR]  %s (%s)\\n\", entry.Name(), entryURI))\n\t\t\t} else {\n\t\t\t\tinfo, err := entry.Info()\n\t\t\t\tif err == nil {\n\t\t\t\t\tresult.WriteString(fmt.Sprintf(\"[FILE] %s (%s) - %d bytes\\n\",\n\t\t\t\t\t\tentry.Name(), entryURI, info.Size()))\n\t\t\t\t} else {\n\t\t\t\t\tresult.WriteString(fmt.Sprintf(\"[FILE] %s (%s)\\n\", entry.Name(), entryURI))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn []mcp.ResourceContents{\n\t\t\tmcp.TextResourceContents{\n\t\t\t\tURI:      uri,\n\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\tText:     result.String(),\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// It'fss a file, determine how to handle it\n\tmimeType := utils.DetectMimeType(validPath)\n\n\t// Check file size\n\tif fileInfo.Size() > MaxInlineSize {\n\t\t// File is too large to inline, return a reference instead\n\t\treturn []mcp.ResourceContents{\n\t\t\tmcp.TextResourceContents{\n\t\t\t\tURI:      uri,\n\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\tText:     fmt.Sprintf(\"File is too large to display inline (%d bytes). Use the read_file tool to access specific portions.\", fileInfo.Size()),\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// Read the file content\n\tcontent, err := os.ReadFile(validPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Handle based on content type\n\tif utils.IsTextFile(mimeType) {\n\t\t// It'fss a text file, return as text\n\t\treturn []mcp.ResourceContents{\n\t\t\tmcp.TextResourceContents{\n\t\t\t\tURI:      uri,\n\t\t\t\tMIMEType: mimeType,\n\t\t\t\tText:     string(content),\n\t\t\t},\n\t\t}, nil\n\t} else {\n\t\t// It'fss a binary file\n\t\tif fileInfo.Size() <= MaxBase64Size {\n\t\t\t// Small enough for base64 encoding\n\t\t\treturn []mcp.ResourceContents{\n\t\t\t\tmcp.BlobResourceContents{\n\t\t\t\t\tURI:      uri,\n\t\t\t\t\tMIMEType: mimeType,\n\t\t\t\t\tBlob:     base64.StdEncoding.EncodeToString(content),\n\t\t\t\t},\n\t\t\t}, nil\n\t\t} else {\n\t\t\t// Too large for base64, return a reference\n\t\t\treturn []mcp.ResourceContents{\n\t\t\t\tmcp.TextResourceContents{\n\t\t\t\t\tURI:      uri,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText:     fmt.Sprintf(\"Binary file (%s, %d bytes). Use the read_file tool to access specific portions.\", mimeType, fileInfo.Size()),\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\t}\n}\n\n// Tool handlers\n\nfunc (fs *FilesystemServer) handleReadFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"Path must be a string\"), nil\n\t}\n\n\t// 判断 前缀是不是已经包含了\n\t//path = filepath.Join(fss.config.CachePath, path)\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"validate Path Error: %v\", err)), nil\n\t}\n\n\t// Check if it'fss a directory\n\tinfo, err := os.Stat(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"check directory error: %v\", err)), nil\n\t}\n\n\tif info.IsDir() {\n\t\t// For directories, return a resource reference instead\n\t\tresourceURI := utils.PathToResourceURI(validPath)\n\t\treturn &mcp.CallToolResult{\n\t\t\tContent: []mcp.Content{\n\t\t\t\tmcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fmt.Sprintf(\"This is a directory. Use the resource URI to browse its contents: %s\", resourceURI),\n\t\t\t\t},\n\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\tType: \"resource\",\n\t\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\t\tText:     fmt.Sprintf(\"Directory: %s\", validPath),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// Determine MIME type\n\tmimeType := utils.DetectMimeType(validPath)\n\n\t// Check file size\n\tif info.Size() > MaxInlineSize {\n\t\t// File is too large to inline, return a resource reference\n\t\tresourceURI := utils.PathToResourceURI(validPath)\n\t\treturn &mcp.CallToolResult{\n\t\t\tContent: []mcp.Content{\n\t\t\t\tmcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fmt.Sprintf(\"File is too large to display inline (%d bytes). Access it via resource URI: %s\", info.Size(), resourceURI),\n\t\t\t\t},\n\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\tType: \"resource\",\n\t\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\t\tText:     fmt.Sprintf(\"Large file: %s (%s, %d bytes)\", validPath, mimeType, info.Size()),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\t}\n\n\t// Read file content\n\tcontent, err := os.ReadFile(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error reading file: %v\", err)), nil\n\t}\n\n\t// Handle based on content type\n\tif utils.IsTextFile(mimeType) {\n\t\t// It'fss a text file, return as text\n\t\treturn mcp.NewToolResultText(string(content)), nil\n\t} else if utils.IsImageFile(mimeType) {\n\t\t// It'fss an image file, return as image content\n\t\tif info.Size() <= MaxBase64Size {\n\t\t\treturn &mcp.CallToolResult{\n\t\t\t\tContent: []mcp.Content{\n\t\t\t\t\tmcp.TextContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: fmt.Sprintf(\"Image file: %s (%s, %d bytes)\", validPath, mimeType, info.Size()),\n\t\t\t\t\t},\n\t\t\t\t\tmcp.ImageContent{\n\t\t\t\t\t\tType:     \"image\",\n\t\t\t\t\t\tData:     base64.StdEncoding.EncodeToString(content),\n\t\t\t\t\t\tMIMEType: mimeType,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t} else {\n\t\t\t// Too large for base64, return a reference\n\t\t\tresourceURI := utils.PathToResourceURI(validPath)\n\t\t\treturn &mcp.CallToolResult{\n\t\t\t\tContent: []mcp.Content{\n\t\t\t\t\tmcp.TextContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: fmt.Sprintf(\"Image file is too large to display inline (%d bytes). Access it via resource URI: %s\", info.Size(), resourceURI),\n\t\t\t\t\t},\n\t\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\t\tType: \"resource\",\n\t\t\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\t\t\tText:     fmt.Sprintf(\"Large image: %s (%s, %d bytes)\", validPath, mimeType, info.Size()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\t} else {\n\t\t// It'fss another type of binary file\n\t\tresourceURI := utils.PathToResourceURI(validPath)\n\n\t\tif info.Size() <= MaxBase64Size {\n\t\t\t// Small enough for base64 encoding\n\t\t\treturn &mcp.CallToolResult{\n\t\t\t\tContent: []mcp.Content{\n\t\t\t\t\tmcp.TextContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: fmt.Sprintf(\"Binary file: %s (%s, %d bytes)\", validPath, mimeType, info.Size()),\n\t\t\t\t\t},\n\t\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\t\tType: \"resource\",\n\t\t\t\t\t\tResource: mcp.BlobResourceContents{\n\t\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\t\tMIMEType: mimeType,\n\t\t\t\t\t\t\tBlob:     base64.StdEncoding.EncodeToString(content),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t} else {\n\t\t\t// Too large for base64, return a reference\n\t\t\treturn &mcp.CallToolResult{\n\t\t\t\tContent: []mcp.Content{\n\t\t\t\t\tmcp.TextContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: fmt.Sprintf(\"Binary file: %s (%s, %d bytes). Access it via resource URI: %s\", validPath, mimeType, info.Size(), resourceURI),\n\t\t\t\t\t},\n\t\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\t\tType: \"resource\",\n\t\t\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\t\t\tText:     fmt.Sprintf(\"Binary file: %s (%s, %d bytes)\", validPath, mimeType, info.Size()),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\t}\n}\n\nfunc (fs *FilesystemServer) handleWriteFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"Path must be a string\"), nil\n\t}\n\tcontent, ok := args[\"content\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"Content must be a string\"), nil\n\t}\n\n\t//path = filepath.Join(fss.config.CachePath, path)\n\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn &mcp.CallToolResult{\n\t\t\tContent: []mcp.Content{\n\t\t\t\tmcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fmt.Sprintf(\"Error: %v\", err),\n\t\t\t\t},\n\t\t\t},\n\t\t\tIsError: true,\n\t\t}, nil\n\t}\n\n\t// Check if it'fss a directory\n\tif info, err := os.Stat(validPath); err == nil && info.IsDir() {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: Cannot write to a directory:%s\", validPath)), nil\n\t}\n\n\t// Create parent directories if they don't exist\n\tparentDir := filepath.Dir(validPath)\n\tif err := os.MkdirAll(parentDir, 0755); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error creating parent directories: %v\", err)), nil\n\t}\n\n\tif err := os.WriteFile(validPath, []byte(content), 0644); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error writing file: %v\", err)), nil\n\t}\n\n\t// Get file info for the response\n\tinfo, err := os.Stat(validPath)\n\tif err != nil {\n\t\t// File was written but we couldn't get info\n\t\treturn mcp.NewToolResultText(fmt.Sprintf(\"Successfully wrote to %s\", path)), nil\n\t}\n\n\tresourceURI := utils.PathToResourceURI(validPath)\n\treturn &mcp.CallToolResult{\n\t\tContent: []mcp.Content{\n\t\t\tmcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: fmt.Sprintf(\"Successfully wrote %d bytes to %s\", info.Size(), path),\n\t\t\t},\n\t\t\tmcp.EmbeddedResource{\n\t\t\t\tType: \"resource\",\n\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText:     fmt.Sprintf(\"File: %s (%d bytes)\", validPath, info.Size()),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) handleListDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"Path must be a string\"), nil\n\t}\n\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"validate path error: %v, path:%s\", err, validPath)), nil\n\t}\n\n\t// Check if it'fss a directory\n\tinfo, err := os.Stat(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Check directory %s Error: %v\", validPath, err)), nil\n\t}\n\n\tif !info.IsDir() {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: Path is not a directory:%s\", validPath)), nil\n\t}\n\n\tentries, err := os.ReadDir(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error reading directory: %v\", err)), nil\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(fmt.Sprintf(\"Directory listing for: %s\\n\\n\", validPath))\n\n\tfor _, entry := range entries {\n\t\tentryPath := filepath.Join(validPath, entry.Name())\n\t\tresourceURI := utils.PathToResourceURI(entryPath)\n\n\t\tif entry.IsDir() {\n\t\t\tresult.WriteString(fmt.Sprintf(\"[DIR]  %s (%s)\\n\", entry.Name(), resourceURI))\n\t\t} else {\n\t\t\tinfo, err := entry.Info()\n\t\t\tif err == nil {\n\t\t\t\tresult.WriteString(fmt.Sprintf(\"[FILE] %s (%s) - %d bytes\\n\",\n\t\t\t\t\tentry.Name(), resourceURI, info.Size()))\n\t\t\t} else {\n\t\t\t\tresult.WriteString(fmt.Sprintf(\"[FILE] %s (%s)\\n\", entry.Name(), resourceURI))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return both text content and embedded resource\n\tresourceURI := utils.PathToResourceURI(validPath)\n\treturn &mcp.CallToolResult{\n\t\tContent: []mcp.Content{\n\t\t\tmcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: result.String(),\n\t\t\t},\n\t\t\tmcp.EmbeddedResource{\n\t\t\t\tType: \"resource\",\n\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText:     fmt.Sprintf(\"Directory: %s\", validPath),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) handleCreateDirectory(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"path must be a string\"), nil\n\t}\n\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: %v\", err)), nil\n\t}\n\n\t// Check if path already exists\n\tif info, err := os.Stat(validPath); err == nil {\n\t\tif info.IsDir() {\n\t\t\tresourceURI := utils.PathToResourceURI(validPath)\n\t\t\treturn &mcp.CallToolResult{\n\t\t\t\tContent: []mcp.Content{\n\t\t\t\t\tmcp.TextContent{\n\t\t\t\t\t\tType: \"text\",\n\t\t\t\t\t\tText: fmt.Sprintf(\"Directory already exists: %s\", path),\n\t\t\t\t\t},\n\t\t\t\t\tmcp.EmbeddedResource{\n\t\t\t\t\t\tType: \"resource\",\n\t\t\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\t\t\tText:     fmt.Sprintf(\"Directory: %s\", validPath),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, nil\n\t\t}\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: Path exists but is not a directory: %s\", path)), nil\n\t}\n\n\tif err := os.MkdirAll(validPath, 0755); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error creating directory: %v\", err)), nil\n\t}\n\n\tresourceURI := utils.PathToResourceURI(validPath)\n\treturn &mcp.CallToolResult{\n\t\tContent: []mcp.Content{\n\t\t\tmcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: fmt.Sprintf(\"Successfully created directory %s\", path),\n\t\t\t},\n\t\t\tmcp.EmbeddedResource{\n\t\t\t\tType: \"resource\",\n\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText:     fmt.Sprintf(\"Directory: %s\", validPath),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) handleMoveFile(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tsource, ok := args[\"source\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"source must be a string\"), nil\n\t}\n\tdestination, ok := args[\"destination\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"destination must be a string\"), nil\n\t}\n\n\tvalidSource, err := fs.validatePath(source)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error with source path: %v\", err)), nil\n\t}\n\n\t// Check if source exists\n\tif _, err := os.Stat(validSource); os.IsNotExist(err) {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: Source does not exist: %s\", source)), nil\n\t}\n\n\tvalidDest, err := fs.validatePath(destination)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error with destination path: %v\", err)), nil\n\t}\n\n\t// Create parent directory for destination if it doesn't exist\n\tdestDir := filepath.Dir(validDest)\n\tif err := os.MkdirAll(destDir, 0755); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error creating destination directory: %v\", err)), nil\n\t}\n\n\tif err := os.Rename(validSource, validDest); err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error moving file: %v\", err)), nil\n\t}\n\n\tresourceURI := utils.PathToResourceURI(validDest)\n\treturn &mcp.CallToolResult{\n\t\tContent: []mcp.Content{\n\t\t\tmcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: fmt.Sprintf(\n\t\t\t\t\t\"Successfully moved %s to %s\",\n\t\t\t\t\tsource,\n\t\t\t\t\tdestination,\n\t\t\t\t),\n\t\t\t},\n\t\t\tmcp.EmbeddedResource{\n\t\t\t\tType: \"resource\",\n\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText:     fmt.Sprintf(\"Moved file: %s\", validDest),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) handleSearchFiles(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"path must be a string\"), nil\n\t}\n\tpattern, ok := args[\"pattern\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(\"pattern must be a string\"), nil\n\t}\n\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: %v\", err)), nil\n\t}\n\n\t// Check if it'fss a directory\n\tinfo, err := os.Stat(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error: %v\", err)), nil\n\t}\n\n\tif !info.IsDir() {\n\t\treturn mcp.NewToolResultError(\"Error: Search path must be a directory\"), nil\n\t}\n\n\tresults, err := fs.searchFiles(validPath, pattern)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error searching files: %v\", err)), nil\n\t}\n\n\tif len(results) == 0 {\n\t\treturn mcp.NewToolResultText(fmt.Sprintf(\"No files found matching pattern '%s' in %s\", pattern, path)), nil\n\t}\n\n\t// Format results with resource URIs\n\tvar formattedResults strings.Builder\n\tformattedResults.WriteString(fmt.Sprintf(\"Found %d results:\\n\\n\", len(results)))\n\n\tfor _, result := range results {\n\t\tresourceURI := utils.PathToResourceURI(result)\n\t\tinfo, err := os.Stat(result)\n\t\tif err == nil {\n\t\t\tif info.IsDir() {\n\t\t\t\tformattedResults.WriteString(fmt.Sprintf(\"[DIR]  %s (%s)\\n\", result, resourceURI))\n\t\t\t} else {\n\t\t\t\tformattedResults.WriteString(fmt.Sprintf(\"[FILE] %s (%s) - %d bytes\\n\",\n\t\t\t\t\tresult, resourceURI, info.Size()))\n\t\t\t}\n\t\t} else {\n\t\t\tformattedResults.WriteString(fmt.Sprintf(\"%s (%s)\\n\", result, resourceURI))\n\t\t}\n\t}\n\n\treturn mcp.NewToolResultText(formattedResults.String()), nil\n}\n\nfunc (fs *FilesystemServer) handleGetFileInfo(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\targs := request.GetArguments()\n\tpath, ok := args[\"path\"].(string)\n\tif !ok {\n\t\treturn mcp.NewToolResultError(fmt.Errorf(\"path %v must be a string\", args[\"path\"]).Error()), nil\n\t}\n\n\tvalidPath, err := fs.validatePath(path)\n\tif err != nil {\n\t\treturn &mcp.CallToolResult{\n\t\t\tContent: []mcp.Content{\n\t\t\t\tmcp.TextContent{\n\t\t\t\t\tType: \"text\",\n\t\t\t\t\tText: fmt.Sprintf(\"Error: %v\", err),\n\t\t\t\t},\n\t\t\t},\n\t\t\tIsError: true,\n\t\t}, nil\n\t}\n\n\tinfo, err := fs.getFileStats(validPath)\n\tif err != nil {\n\t\treturn mcp.NewToolResultError(fmt.Sprintf(\"Error getting file info: %v\", err)), nil\n\t}\n\n\t// Get MIME type for files\n\tmimeType := \"directory\"\n\tif info.IsFile {\n\t\tmimeType = utils.DetectMimeType(validPath)\n\t}\n\n\tresourceURI := utils.PathToResourceURI(validPath)\n\n\t// Determine file type text\n\tvar fileTypeText string\n\tif info.IsDirectory {\n\t\tfileTypeText = \"Directory\"\n\t} else {\n\t\tfileTypeText = \"File\"\n\t}\n\n\treturn &mcp.CallToolResult{\n\t\tContent: []mcp.Content{\n\t\t\tmcp.TextContent{\n\t\t\t\tType: \"text\",\n\t\t\t\tText: fmt.Sprintf(\n\t\t\t\t\t\"File information for: %s\\n\\nSize: %d bytes\\nCreated: %s\\nModified: %s\\nAccessed: %s\\nIsDirectory: %v\\nIsFile: %v\\nPermissions: %s\\nMIME Type: %s\\nResource URI: %s\",\n\t\t\t\t\tvalidPath,\n\t\t\t\t\tinfo.Size,\n\t\t\t\t\tinfo.Created.Format(time.RFC3339),\n\t\t\t\t\tinfo.Modified.Format(time.RFC3339),\n\t\t\t\t\tinfo.Accessed.Format(time.RFC3339),\n\t\t\t\t\tinfo.IsDirectory,\n\t\t\t\t\tinfo.IsFile,\n\t\t\t\t\tinfo.Permissions,\n\t\t\t\t\tmimeType,\n\t\t\t\t\tresourceURI,\n\t\t\t\t),\n\t\t\t},\n\t\t\tmcp.EmbeddedResource{\n\t\t\t\tType: \"resource\",\n\t\t\t\tResource: mcp.TextResourceContents{\n\t\t\t\t\tURI:      resourceURI,\n\t\t\t\t\tMIMEType: \"text/plain\",\n\t\t\t\t\tText: fmt.Sprintf(\"%s: %s (%s, %d bytes)\",\n\t\t\t\t\t\tfileTypeText,\n\t\t\t\t\t\tvalidPath,\n\t\t\t\t\t\tmimeType,\n\t\t\t\t\t\tinfo.Size),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, nil\n}\n\nfunc (fs *FilesystemServer) handleListAllowedDirectories(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {\n\t// Remove the trailing separator for display purposes\n\tdisplayDirs := make([]string, len(fs.config.allowedDirs))\n\tfor i, dir := range fs.config.allowedDirs {\n\t\tdisplayDirs[i] = strings.TrimSuffix(dir, string(filepath.Separator))\n\t}\n\n\tvar result strings.Builder\n\tresult.WriteString(\"Allowed directories:\")\n\n\tfor _, dir := range displayDirs {\n\t\tresourceURI := utils.PathToResourceURI(dir)\n\t\tresult.WriteString(fmt.Sprintf(\"%s (%s)\\n\", dir, resourceURI))\n\t}\n\n\treturn mcp.NewToolResultText(result.String()), nil\n}\n\n// Config returns the configuration of the service as a string.\nfunc (fs *FilesystemServer) Config() string {\n\tfs.config.AllowedDir = strings.Join(fs.config.allowedDirs, \",\")\n\tcfg, err := json.Marshal(fs.config)\n\tif err != nil {\n\t\tfs.Logger.Err(err).Msg(\"failed to marshal config\")\n\t\treturn \"{}\"\n\t}\n\treturn string(cfg)\n}\n\nfunc (fs *FilesystemServer) Name() comm.MoLingServerType {\n\treturn FilesystemServerName\n}\n\nfunc (fs *FilesystemServer) Close() error {\n\t// Cancel the context to stop the browser\n\tfs.Logger.Debug().Msg(\"closing FilesystemServer\")\n\treturn nil\n}\n\n// LoadConfig loads the configuration from a JSON object.\nfunc (fs *FilesystemServer) LoadConfig(jsonData map[string]any) error {\n\terr := utils.MergeJSONToStruct(fs.config, jsonData)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfs.config.allowedDirs = strings.Split(fs.config.AllowedDir, \",\")\n\treturn fs.config.Check()\n}\n"
  },
  {
    "path": "pkg/services/filesystem/file_system_config.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage filesystem\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nconst (\n\t// FileSystemPromptDefault is the default prompt for the file system.\n\tFileSystemPromptDefault = `\nYou are a powerful local filesystem management assistant capable of performing various file operations and management tasks. Your capabilities include:\n\n1. **File Browsing**: Navigate to specified directories to load lists of files and folders.\n\n2. **File Operations**:\n   - Create new files or folders\n   - Delete specified files or folders\n   - Copy and move files and folders\n   - Rename files or folders\n\n3. **File Content Operations**:\n   - Read the contents of text files and return them\n   - Write text to specified files\n   - Append content to existing files\n\n4. **File Information Retrieval**:\n   - Retrieve properties of files or folders (e.g., size, creation date, modification date)\n   - Check if files or folders exist\n\n5. **Search Functionality**:\n   - Search for files in specified directories, supporting wildcard matching\n   - Filter search results by file type or modification date\n\nFor all actions, please provide clear instructions, including:\n- The specific action you want to perform\n- Required parameters (directory paths, filenames, content, etc.)\n- Any optional parameters (e.g., new filenames, search patterns, etc.)\n- Relevant expected outcomes\n\nYou should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back with clear status updates, success/failure indicators, and any relevant output or results.\n`\n)\n\nvar (\n\tallowedDirsDefault = os.TempDir()\n)\n\n// FileSystemConfig represents the configuration for the file system.\ntype FileSystemConfig struct {\n\tPromptFile  string `json:\"prompt_file\"` // PromptFile is the prompt file for the file system.\n\tprompt      string\n\tAllowedDir  string `json:\"allowed_dir\"` // AllowedDirs is a list of allowed directories. split by comma. e.g. /tmp,/var/tmp\n\tallowedDirs []string\n\tCachePath   string `json:\"cache_path\"` // CachePath is the root path for the file system.\n}\n\n// NewFileSystemConfig creates a new FileSystemConfig with the given allowed directories.\nfunc NewFileSystemConfig(path string) *FileSystemConfig {\n\tpaths := strings.Split(path, \",\")\n\tpath = \"\"\n\tif strings.TrimSpace(path) == \"\" {\n\t\tpath = allowedDirsDefault\n\t\tpaths = []string{allowedDirsDefault}\n\t}\n\n\treturn &FileSystemConfig{\n\t\tAllowedDir:  path,\n\t\tCachePath:   path,\n\t\tallowedDirs: paths,\n\t}\n}\n\n// Check validates the allowed directories in the FileSystemConfig.\nfunc (fc *FileSystemConfig) Check() error {\n\tfc.prompt = FileSystemPromptDefault\n\tnormalized := make([]string, 0, len(fc.allowedDirs))\n\tfor _, dir := range fc.allowedDirs {\n\t\tabs, err := filepath.Abs(strings.TrimSpace(dir))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to resolve path %s: %w\", dir, err)\n\t\t}\n\t\tinfo, err := os.Stat(abs)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to access directory %s: %w\", abs, err)\n\t\t}\n\t\tif !info.IsDir() {\n\t\t\treturn fmt.Errorf(\"path is not a directory: %s\", abs)\n\t\t}\n\n\t\tnormalized = append(normalized, filepath.Clean(abs)+string(filepath.Separator))\n\t}\n\tfc.allowedDirs = normalized\n\n\tif fc.PromptFile != \"\" {\n\t\tread, err := os.ReadFile(fc.PromptFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read prompt file:%s, error: %w\", fc.PromptFile, err)\n\t\t}\n\t\tfc.prompt = string(read)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/services/filesystem/file_system_windows.go",
    "content": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Repository: https://github.com/gojue/moling\n */\n\npackage filesystem\n\nimport \"os\"\n\nfunc init() {\n\tdirName, err := os.UserCacheDir()\n\tif err != nil {\n\t\tdirName, err = os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tallowedDirsDefault = dirName\n}\n"
  },
  {
    "path": "pkg/services/register.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage services\n\nimport (\n\t\"github.com/gojue/moling/pkg/comm\"\n\t\"github.com/gojue/moling/pkg/services/abstract\"\n\t\"github.com/gojue/moling/pkg/services/browser\"\n\t\"github.com/gojue/moling/pkg/services/command\"\n\t\"github.com/gojue/moling/pkg/services/filesystem\"\n)\n\nvar serviceLists = make(map[comm.MoLingServerType]abstract.ServiceFactory)\n\n// RegisterServ register service\nfunc RegisterServ(n comm.MoLingServerType, f abstract.ServiceFactory) {\n\tserviceLists[n] = f\n}\n\n// ServiceList  get service lists\nfunc ServiceList() map[comm.MoLingServerType]abstract.ServiceFactory {\n\treturn serviceLists\n}\n\nfunc init() {\n\t// Register the filesystem service\n\tRegisterServ(filesystem.FilesystemServerName, filesystem.NewFilesystemServer)\n\t// Register the browser service\n\tRegisterServ(browser.BrowserServerName, browser.NewBrowserServer)\n\t// Register the command service\n\tRegisterServ(command.CommandServerName, command.NewCommandServer)\n}\n"
  },
  {
    "path": "pkg/utils/pid.go",
    "content": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Repository: https://github.com/gojue/moling\n */\n\npackage utils\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nvar pidFile *os.File\n\n// CreatePIDFile creates and locks a PID file to prevent multiple instances.\nfunc CreatePIDFile(pidFilePath string) error {\n\t// Open or create the PID file\n\tfile, err := os.OpenFile(pidFilePath, os.O_RDWR|os.O_CREATE, 0644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open PID file: %w\", err)\n\t}\n\n\t// Try to lock the file using platform-specific code\n\tlocked, err := lockFile(file)\n\tif err != nil {\n\t\t_ = file.Close()\n\t\treturn fmt.Errorf(\"failed to lock PID file: %w\", err)\n\t}\n\tif !locked {\n\t\t_ = file.Close()\n\t\treturn fmt.Errorf(\"another instance is already running: %s\", pidFilePath)\n\t}\n\n\t// Write the current PID to the file\n\terr = file.Truncate(0)\n\tif err != nil {\n\t\t_ = unlockFile(file)\n\t\t_ = file.Close()\n\t\treturn fmt.Errorf(\"failed to truncate PID file: %w\", err)\n\t}\n\t_, err = file.WriteString(fmt.Sprintf(\"%d\\n\", os.Getpid()))\n\tif err != nil {\n\t\t_ = unlockFile(file)\n\t\t_ = file.Close()\n\t\treturn fmt.Errorf(\"failed to write PID to file: %w\", err)\n\t}\n\n\t// Keep the file open to maintain the lock\n\tpidFile = file\n\treturn nil\n}\n\n// RemovePIDFile releases the lock and removes the PID file.\nfunc RemovePIDFile(pidFilePath string) error {\n\tif pidFile != nil {\n\t\terr := unlockFile(pidFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unlock PID file: %w\", err)\n\t\t}\n\t\t_ = pidFile.Close()\n\t\tpidFile = nil\n\t\terr = os.Remove(pidFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove PID file: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/pid_unix.go",
    "content": "//go:build darwin || linux || freebsd || openbsd || netbsd\n\n/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Repository: https://github.com/gojue/moling\n */\n\npackage utils\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n)\n\nfunc lockFile(file *os.File) (bool, error) {\n\terr := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)\n\tif err != nil {\n\t\tif errors.Is(err, syscall.EWOULDBLOCK) {\n\t\t\treturn false, nil // 已经被锁定，返回false但不返回错误\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\nfunc unlockFile(file *os.File) error {\n\treturn syscall.Flock(int(file.Fd()), syscall.LOCK_UN)\n}\n"
  },
  {
    "path": "pkg/utils/pid_windows.go",
    "content": "//go:build windows\n\n/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Repository: https://github.com/gojue/moling\n */\n\npackage utils\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nvar (\n\tkernel32     = syscall.NewLazyDLL(\"kernel32.dll\")\n\tlockFileEx   = kernel32.NewProc(\"LockFileEx\")\n\tunlockFileEx = kernel32.NewProc(\"UnlockFileEx\")\n)\n\nconst (\n\tLockfileExclusiveLock   = 2\n\tLockfileFailImmediately = 1\n)\nconst ErrorLockViolation = syscall.Errno(33) // 0x21\n\n// lockFile locks the given file using Windows API.\nfunc lockFile(file *os.File) (bool, error) {\n\thandle := syscall.Handle(file.Fd())\n\tvar overlapped syscall.Overlapped\n\n\tflags := LockfileExclusiveLock | LockfileFailImmediately\n\tr, _, err := lockFileEx.Call(\n\t\tuintptr(handle),\n\t\tuintptr(flags),\n\t\t0,\n\t\t1,\n\t\t0,\n\t\tuintptr(unsafe.Pointer(&overlapped)),\n\t)\n\n\tif r == 0 {\n\t\tif !errors.Is(err, ErrorLockViolation) {\n\t\t\treturn false, err\n\t\t}\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// unlockFile unlocks the given file using Windows API.\nfunc unlockFile(file *os.File) error {\n\thandle := syscall.Handle(file.Fd())\n\tvar overlapped syscall.Overlapped\n\n\tr, _, err := unlockFileEx.Call(\n\t\tuintptr(handle),\n\t\t0,\n\t\t1,\n\t\t0,\n\t\tuintptr(unsafe.Pointer(&overlapped)),\n\t)\n\n\tif r == 0 {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/utils/rotewriter.go",
    "content": "/*\n * Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Repository: https://github.com/gojue/moling\n */\n\npackage utils\n\nimport (\n\t\"os\"\n\t\"sync\"\n)\n\n// RotateWriter 是一个简单的日志轮转写入器\ntype RotateWriter struct {\n\tfilePaths    [2]string // 两个日志文件的路径\n\tcurrentIndex int       // 当前写入的文件索引（0或1）\n\tmaxSize      int64     // 文件大小阈值（字节）\n\tcount        uint16    // 当前文件的写入次数\n\tmu           sync.Mutex\n\tfile         *os.File // 当前打开的文件句柄\n}\n\n// NewRotateWriter 创建一个新的 RotateWriter 实例\nfunc NewRotateWriter(filePath string, maxSize int64) (*RotateWriter, error) {\n\trw := &RotateWriter{\n\t\tfilePaths:    [2]string{filePath + \".1\", filePath + \".2\"},\n\t\tcurrentIndex: 0,\n\t\tmaxSize:      maxSize,\n\t\tcount:        0,\n\t}\n\n\t// 初始化文件，确保两个文件都存在\n\tfor _, path := range rw.filePaths {\n\t\tfile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfile.Close()\n\t}\n\n\t// 打开当前文件\n\tfile, err := os.OpenFile(rw.filePaths[rw.currentIndex], os.O_WRONLY|os.O_APPEND, 0644)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trw.file = file\n\n\treturn rw, nil\n}\n\n// Write 实现 io.Writer 接口\nfunc (rw *RotateWriter) Write(p []byte) (n int, err error) {\n\terr = rw.CheckFileSize()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\t// 写入日志\n\treturn rw.file.Write(p)\n}\n\n// CheckFileSize 检查当前文件大小\nfunc (rw *RotateWriter) CheckFileSize() error {\n\trw.mu.Lock()\n\tdefer rw.mu.Unlock()\n\tif rw.count >= 10 {\n\t\t// 检查当前文件大小\n\t\tfileInfo, err := rw.file.Stat()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// 如果文件大小超过阈值，切换到另一个文件并清空\n\t\tif fileInfo.Size() >= rw.maxSize {\n\t\t\t// 关闭当前文件\n\t\t\trw.file.Close()\n\n\t\t\t// 切换到另一个文件\n\t\t\trw.currentIndex = (rw.currentIndex + 1) % 2\n\n\t\t\t// 清空目标文件\n\t\t\tfile, err := os.OpenFile(rw.filePaths[rw.currentIndex], os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trw.file = file\n\t\t}\n\t\trw.count = 0\n\t}\n\n\trw.count++\n\treturn nil\n}\n\n// Close 关闭 RotateWriter\nfunc (rw *RotateWriter) Close() error {\n\trw.mu.Lock()\n\tdefer rw.mu.Unlock()\n\treturn rw.file.Close()\n}\n"
  },
  {
    "path": "pkg/utils/utils.go",
    "content": "// Copyright 2025 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//   http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n//\n// Repository: https://github.com/gojue/moling\n\npackage utils\n\nimport (\n\t\"fmt\"\n\t\"mime\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// CreateDirectory checks if a directory exists, and creates it if it doesn't\nfunc CreateDirectory(path string) error {\n\t_, err := os.Stat(path)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\terr = os.MkdirAll(path, 0o755)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// StringInSlice checks if a string is in a slice of strings\nfunc StringInSlice(s string, modules []string) bool {\n\tfor _, module := range modules {\n\t\tif module == s {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// MergeJSONToStruct 将JSON中的字段合并到结构体中\nfunc MergeJSONToStruct(target any, jsonMap map[string]any) error {\n\t// 获取目标结构体的反射值\n\tval := reflect.ValueOf(target).Elem()\n\ttyp := val.Type()\n\n\t// 遍历JSON map中的每个字段\n\tfor jsonKey, jsonValue := range jsonMap {\n\t\t// 遍历结构体的每个字段\n\t\tfor i := 0; i < typ.NumField(); i++ {\n\t\t\tfield := typ.Field(i)\n\t\t\t// 检查JSON字段名是否与结构体的JSON tag匹配\n\t\t\tif field.Tag.Get(\"json\") == jsonKey {\n\t\t\t\t// 获取结构体字段的反射值\n\t\t\t\tfieldVal := val.Field(i)\n\t\t\t\t// 检查字段是否可设置\n\t\t\t\tif fieldVal.CanSet() {\n\t\t\t\t\t// 将JSON值转换为结构体字段的类型\n\t\t\t\t\tjsonVal := reflect.ValueOf(jsonValue)\n\t\t\t\t\tif jsonVal.Type().ConvertibleTo(fieldVal.Type()) {\n\t\t\t\t\t\tfieldVal.Set(jsonVal.Convert(fieldVal.Type()))\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn fmt.Errorf(\"type mismatch for field %s, value:%v\", jsonKey, jsonValue)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// DetectMimeType tries to determine the MIME type of a file\nfunc DetectMimeType(path string) string {\n\t// First try by extension\n\text := filepath.Ext(path)\n\tif ext != \"\" {\n\t\tmimeType := mime.TypeByExtension(ext)\n\t\tif mimeType != \"\" {\n\t\t\treturn mimeType\n\t\t}\n\t}\n\n\t// If that fails, try to read a bit of the file\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"application/octet-stream\" // Default\n\t}\n\tdefer file.Close()\n\n\t// Read first 512 bytes to detect content type\n\tbuffer := make([]byte, 512)\n\tn, err := file.Read(buffer)\n\tif err != nil {\n\t\treturn \"application/octet-stream\" // Default\n\t}\n\n\t// Use http.DetectContentType\n\treturn http.DetectContentType(buffer[:n])\n}\n\n// IsTextFile determines if a file is likely a text file based on MIME type\nfunc IsTextFile(mimeType string) bool {\n\treturn strings.HasPrefix(mimeType, \"text/\") ||\n\t\tmimeType == \"application/json\" ||\n\t\tmimeType == \"application/xml\" ||\n\t\tmimeType == \"application/javascript\" ||\n\t\tmimeType == \"application/x-javascript\" ||\n\t\tstrings.Contains(mimeType, \"+xml\") ||\n\t\tstrings.Contains(mimeType, \"+json\")\n}\n\n// IsImageFile determines if a file is an image based on MIME type\nfunc IsImageFile(mimeType string) bool {\n\treturn strings.HasPrefix(mimeType, \"image/\")\n}\n\n// PathToResourceURI converts a file path to a resource URI\nfunc PathToResourceURI(path string) string {\n\treturn \"file://\" + path\n}\n"
  },
  {
    "path": "prompts/ filesystem.md",
    "content": "## 汉字\n\n你是一个功能强大的本地文件系统管理助手，能够执行多种文件操作和管理任务。你的能力包括：\n\n1. **文件浏览**：导航到指定目录以加载文件和文件夹列表。\n\n2. **文件操作**：\n    - 创建新文件或文件夹\n    - 删除指定的文件或文件夹\n    - 复制和移动文件及文件夹\n    - 重命名文件或文件夹\n\n3. **文件内容操作**：\n    - 读取文本文件并返回内容\n    - 写入文本到指定文件\n    - 追加内容到现有文件\n\n4. **文件信息检索**：\n    - 获取文件或文件夹的属性（例如大小、创建日期、修改日期）\n    - 检查文件或文件夹是否存在\n\n5. **搜索功能**：\n    - 在指定目录中搜索文件，支持通配符匹配\n    - 根据文件类型或修改日期筛选搜索结果\n\n执行所有操作时，请提供清晰的说明，包括：\n\n- 你想执行的具体操作\n- 所需的参数（目录路径、文件名、内容等）\n- 任何可选参数（例如新文件名、搜索模式等）\n- 相关的预期结果\n\n在处理敏感操作或破坏性命令时，你应确认操作后再执行。报告清晰的状态更新、成功/失败指示，以及任何相关的输出或处理结果。\n\n## English\n\nYou are a powerful local filesystem management assistant capable of performing various file operations and management\ntasks. Your capabilities include:\n\n1. **File Browsing**: Navigate to specified directories to load lists of files and folders.\n\n2. **File Operations**:\n    - Create new files or folders\n    - Delete specified files or folders\n    - Copy and move files and folders\n    - Rename files or folders\n\n3. **File Content Operations**:\n    - Read the contents of text files and return them\n    - Write text to specified files\n    - Append content to existing files\n\n4. **File Information Retrieval**:\n    - Retrieve properties of files or folders (e.g., size, creation date, modification date)\n    - Check if files or folders exist\n\n5. **Search Functionality**:\n    - Search for files in specified directories, supporting wildcard matching\n    - Filter search results by file type or modification date\n\nFor all actions, please provide clear instructions, including:\n\n- The specific action you want to perform\n- Required parameters (directory paths, filenames, content, etc.)\n- Any optional parameters (e.g., new filenames, search patterns, etc.)\n- Relevant expected outcomes\n\nYou should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back\nwith clear status updates, success/failure indicators, and any relevant output or results."
  },
  {
    "path": "prompts/browser.md",
    "content": "## 汉字\n\n你是一个由人工智能驱动的浏览器自动化助手，能够执行广泛的网页交互和调试任务。你的能力包括：\n\n1. **导航**：导航到任何指定的URL以加载网页。\n\n2. **屏幕截图**：拍摄完整网页截图或使用CSS选择器捕获特定元素，支持自定义尺寸（默认：1700x1100像素）。\n\n3. **元素交互**：\n    - 点击由CSS选择器识别的元素\n    - 将鼠标悬停在指定元素上\n    - 用提供的值填写输入字段\n    - 在下拉菜单中选择选项\n\n4. **JavaScript执行**：\n    - 在浏览器上下文中运行任意JavaScript代码\n    - 评估脚本并返回结果\n\n5. **调试工具**：\n    - 启用/禁用JavaScript调试模式\n    - 在特定脚本位置设置断点（URL + 行号 + 可选列/条件）\n    - 按ID移除现有断点\n    - 暂停和恢复脚本执行\n    - 在暂停时检索当前调用栈\n\n对于所有需要元素选择的操作，您必须使用精确的CSS选择器。当进行屏幕截图时，您可以指定整个页面或目标特定元素。对于调试操作，您可以精确控制执行流程并检查运行时行为。\n\n请提供清晰的说明，包括：\n\n- 你想执行的具体操作\n- 所需的参数（URL、选择器、值等）\n- 任何可选参数（尺寸、条件等）\n- 相关的预期结果\n\n在处理敏感操作或破坏性命令时，您应在执行前确认操作。请报告清晰的状态更新、成功/失败指示，以及任何相关的输出或捕获的数据。\n\n## English\n\nYou are an AI-powered browser automation assistant capable of performing a wide range of web interactions and debugging\ntasks. Your capabilities include:\n\n1. **Navigation**: Navigate to any specified URL to load web pages.\n\n2. **Screenshot Capture**: Take full-page screenshots or capture specific elements using CSS selectors, with\n   customizable dimensions (default: 1700x1100 pixels).\n\n3. **Element Interaction**:\n    - Click on elements identified by CSS selectors\n    - Hover over specified elements\n    - Fill input fields with provided values\n    - Select options in dropdown menus\n\n4. **JavaScript Execution**:\n    - Run arbitrary JavaScript code in the browser context\n    - Evaluate scripts and return results\n\n5. **Debugging Tools**:\n    - Enable/disable JavaScript debugging mode\n    - Set breakpoints at specific script locations (URL + line number + optional column/condition)\n    - Remove existing breakpoints by ID\n    - Pause and resume script execution\n    - Retrieve current call stack when paused\n\nFor all actions requiring element selection, you must use precise CSS selectors. When capturing screenshots, you can\nspecify either the entire page or target specific elements. For debugging operations, you can precisely control\nexecution flow and inspect runtime behavior.\n\nPlease provide clear instructions including:\n\n- The specific action you want performed\n- Required parameters (URLs, selectors, values, etc.)\n- Any optional parameters (dimensions, conditions, etc.)\n- Expected outcomes where relevant\n\nYou should confirm actions before execution when dealing with sensitive operations or destructive commands. Report back\nwith clear status updates, success/failure indicators, and any relevant output or captured data.\n"
  },
  {
    "path": "prompts/command.md",
    "content": "## 汉字\n\n你是{%system_os%}上的一个功能强大的终端命令助手，能够执行各种命令行操作和管理任务。你的能力包括：\n\n1. **文件与目录管理**：\n    - 列出目录中的文件和子目录\n    - 创建新文件或目录\n    - 删除指定文件或目录\n    - 复制和移动文件及目录\n    - 重命名文件或目录\n\n2. **文件内容操作**：\n    - 查看文本文件内容\n    - 编辑文件内容\n    - 将输出重定向到文件\n    - 搜索文件内容\n\n3. **系统信息检索**：\n    - 获取系统信息（如CPU使用率、内存使用情况等）\n    - 查看当前用户及其权限\n    - 检查当前工作目录\n\n4. **网络操作**：\n    - 检查网络连接状态（如ping命令）\n    - 查询域名信息（如whois命令）\n    - 管理网络服务（如启动、停止和重启服务）\n\n5. **进程管理**：\n    - 列出当前运行的进程\n    - 杀死指定进程\n    - 调整进程优先级\n\n在执行所有操作之前，请提供清晰的说明，包括：\n\n- 你想执行的具体命令\n- 所需的参数（文件路径、目录名称等）\n- 任何可选参数（如修改选项、输出格式等）\n- 相关的预期结果或输出\n\n在处理敏感操作或破坏性命令时，请确认操作后再执行。报告清晰的状态更新、成功/失败指示，以及任何相关输出或结果。\n\n## English\n\nYou are a powerful terminal command assistant capable of executing various command-line on {%system_os%} operations and\nmanagement tasks. Your capabilities include:\n\n1. **File and Directory Management**:\n    - List files and subdirectories in a directory\n    - Create new files or directories\n    - Delete specified files or directories\n    - Copy and move files and directories\n    - Rename files or directories\n\n2. **File Content Operations**:\n    - View the contents of text files\n    - Edit file contents\n    - Redirect output to a file\n    - Search file contents\n\n3. **System Information Retrieval**:\n    - Retrieve system information (e.g., CPU usage, memory usage, etc.)\n    - View the current user and their permissions\n    - Check the current working directory\n\n4. **Network Operations**:\n    - Check network connection status (e.g., using the ping command)\n    - Query domain information (e.g., using the whois command)\n    - Manage network services (e.g., start, stop, and restart services)\n\n5. **Process Management**:\n    - List currently running processes\n    - Terminate specified processes\n    - Adjust process priorities\n\nBefore executing any actions, please provide clear instructions, including:\n\n- The specific command you want to execute\n- Required parameters (file paths, directory names, etc.)\n- Any optional parameters (e.g., modification options, output formats, etc.)\n- Relevant expected results or output\n\nWhen dealing with sensitive operations or destructive commands, please confirm before execution. Report back with clear\nstatus updates, success/failure indicators, and any relevant output or results.\n"
  },
  {
    "path": "variables.mk",
    "content": "CMD_MAKE = make\nCMD_TR ?= tr\nCMD_CUT ?= cut\nCMD_AWK ?= awk\nCMD_SED ?= sed\nCMD_FILE ?= file\nCMD_GIT ?= git\nCMD_CLANG ?= clang\nCMD_RM ?= rm\nCMD_INSTALL ?= install\nCMD_MKDIR ?= mkdir\nCMD_TOUCH ?= touch\nCMD_GO ?= go\nCMD_GREP ?= grep\nCMD_CAT ?= cat\nCMD_MD5 ?= md5sum\nCMD_TAR ?= tar\nCMD_CHECKSUM ?= sha256sum\nCMD_GITHUB ?= gh\nCMD_MV ?= mv\nCMD_CP ?= cp\nCMD_CD ?= cd\nCMD_ECHO ?= echo\n\n\n#\n# tools version\n#\n\nGO_VERSION = $(shell $(CMD_GO) version 2>/dev/null | $(CMD_AWK) '{print $$3}' | $(CMD_SED) 's:go::g' | $(CMD_CUT) -d. -f1,2)\nGO_VERSION_MAJ = $(shell $(CMD_ECHO) $(GO_VERSION) | $(CMD_CUT) -d'.' -f1)\nGO_VERSION_MIN = $(shell $(CMD_ECHO) $(GO_VERSION) | $(CMD_CUT) -d'.' -f2)\n\n# tags date info\nVERSION_NUM ?= v0.0.0\nNOW_DATE := $(shell date +\"%Y-%m-%d %H:%M:%S\")\nTAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1)\nTAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true)\nCOMMIT := $(shell git rev-parse --short HEAD)\nDATE := $(shell git log -1 --format=%cd --date=format:\"%Y %m %d\")\nLAST_GIT_TAG := $(COMMIT)_$(NOW_DATE)\n\nifndef SNAPSHOT_VERSION\n\tVERSION_NUM = $(LAST_GIT_TAG)\nelse\n\tVERSION_NUM = $(SNAPSHOT_VERSION)_$(NOW_DATE)\nendif\n\n#\n# environment\n#\n#SNAPSHOT_VERSION ?= $(shell git rev-parse HEAD)\nBUILD_DATE := $(shell date +%Y-%m-%d)\nTARGET_TAG :=\nOS_NAME ?= $(shell uname -s|tr 'A-Z' 'a-z')\nOS_ARCH ?= $(shell uname -m)\nOS_VERSION_SHORT := $(shell uname -r | cut -d'-' -f 1)\nTARGET_OS ?= $(OS_NAME)\nTARGET_ARCH ?= $(if $(filter x86_64,$(OS_ARCH)),amd64,arm64)\nOUT_BIN := bin/moling"
  }
]