lightnow-cli 1.6.1__tar.gz → 1.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/ci.yml +2 -2
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/release.yml +3 -3
- lightnow_cli-1.7.1/.release-please-manifest.json +3 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/CHANGELOG.md +14 -0
- lightnow_cli-1.7.1/PKG-INFO +138 -0
- lightnow_cli-1.7.1/README.md +90 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/__init__.py +1 -1
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/integrations.py +142 -12
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/pyproject.toml +1 -1
- lightnow_cli-1.7.1/tests/test_import_preconditions.py +197 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_integrations.py +305 -1
- lightnow_cli-1.6.1/.release-please-manifest.json +0 -3
- lightnow_cli-1.6.1/PKG-INFO +0 -344
- lightnow_cli-1.6.1/README.md +0 -296
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.github/workflows/release-please.yml +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/.gitignore +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/CONTRIBUTING.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/LICENSE +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/Makefile +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/SECURITY.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/ci/create_client_artifact.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/ci/test_client_artifact.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/docs/development.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/docs/release.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/README.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/docs.md +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/openapi.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/examples/server.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/authenticated_http.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/client.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/__init__.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/auth.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/context.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/publish.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/query.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/runner.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/commands/validate.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/config.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/main.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/target.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/tls.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/lightnow_cli/validation.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/release-please-config.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/renovate.json +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/__init__.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_auth_flow.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_authenticated_http.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_cli.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_client.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_config.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_connection_target.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_context.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_tls.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_updates.py +0 -0
- {lightnow_cli-1.6.1 → lightnow_cli-1.7.1}/tests/test_validation.py +0 -0
|
@@ -26,7 +26,7 @@ jobs:
|
|
|
26
26
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
27
27
|
|
|
28
28
|
- name: Set up Python
|
|
29
|
-
uses: actions/setup-python@
|
|
29
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
|
|
30
30
|
with:
|
|
31
31
|
python-version: ${{ matrix.python-version }}
|
|
32
32
|
cache: pip
|
|
@@ -62,7 +62,7 @@ jobs:
|
|
|
62
62
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
63
63
|
|
|
64
64
|
- name: Set up Python
|
|
65
|
-
uses: actions/setup-python@
|
|
65
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
|
|
66
66
|
with:
|
|
67
67
|
python-version: "3.12"
|
|
68
68
|
cache: pip
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
19
19
|
|
|
20
20
|
- name: Set up Python
|
|
21
|
-
uses: actions/setup-python@
|
|
21
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
|
|
22
22
|
with:
|
|
23
23
|
python-version: "3.12"
|
|
24
24
|
cache: pip
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
40
40
|
|
|
41
41
|
- name: Set up Python
|
|
42
|
-
uses: actions/setup-python@
|
|
42
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7
|
|
43
43
|
with:
|
|
44
44
|
python-version: "3.12"
|
|
45
45
|
cache: pip
|
|
@@ -95,7 +95,7 @@ jobs:
|
|
|
95
95
|
path: dist
|
|
96
96
|
|
|
97
97
|
- name: Publish distributions to PyPI
|
|
98
|
-
uses: pypa/gh-action-pypi-publish@
|
|
98
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
|
|
99
99
|
|
|
100
100
|
update-homebrew:
|
|
101
101
|
name: Update Homebrew formula
|
|
@@ -4,6 +4,20 @@ All notable changes to the LightNow CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows semantic versioning.
|
|
6
6
|
|
|
7
|
+
## [1.7.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.7.0...v1.7.1) (2026-11-14)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
* honor profile revisions when importing MCP config ([#50](https://github.com/lightnow-ai/lightnow-cli/issues/50)) ([343ab0d](https://github.com/lightnow-ai/lightnow-cli/commit/343ab0d545c19d3c9a44fb9097ed06e5f00ca3fb))
|
|
13
|
+
|
|
14
|
+
## [1.7.0](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.1...v1.7.0) (2026-11-13)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* support LibreChat local proxy configuration ([#47](https://github.com/lightnow-ai/lightnow-cli/issues/47)) ([fffb3ca](https://github.com/lightnow-ai/lightnow-cli/commit/fffb3ca2ac8c87f08e9938bfcb54e3952fcb23f7))
|
|
20
|
+
|
|
7
21
|
## [1.6.1](https://github.com/lightnow-ai/lightnow-cli/compare/v1.6.0...v1.6.1) (2026-11-09)
|
|
8
22
|
|
|
9
23
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lightnow-cli
|
|
3
|
+
Version: 1.7.1
|
|
4
|
+
Summary: Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/lightnow-ai/lightnow-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/lightnow-ai/lightnow-cli
|
|
7
|
+
Author: LightNow Team
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,lightnow,mcp,model-context-protocol,registry
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: filelock>=3.16.1
|
|
22
|
+
Requires-Dist: httpx>=0.24.0
|
|
23
|
+
Requires-Dist: jsonschema>=4.17.0
|
|
24
|
+
Requires-Dist: packaging>=24.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: pyjwt>=2.8.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: truststore>=0.10.0
|
|
30
|
+
Requires-Dist: typer>=0.9.0
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: flake8>=6.0.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: responses>=0.23.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: types-jsonschema>=4.25.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
|
45
|
+
Provides-Extra: keyring
|
|
46
|
+
Requires-Dist: keyring>=25.6.0; extra == 'keyring'
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# LightNow CLI
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/lightnow-cli/)
|
|
52
|
+
|
|
53
|
+
Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
|
|
54
|
+
|
|
55
|
+
The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
|
|
56
|
+
runs their connections to the servers in your Runtime profiles.
|
|
57
|
+
|
|
58
|
+
## Get started
|
|
59
|
+
|
|
60
|
+
You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
|
|
61
|
+
profile with the server connections you want to use.
|
|
62
|
+
|
|
63
|
+
### 1. Install the CLI and proxy
|
|
64
|
+
|
|
65
|
+
With Homebrew:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
brew tap lightnow-ai/tap
|
|
69
|
+
brew install lightnow-cli lightnow-proxy
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
uv tool install lightnow-cli
|
|
76
|
+
uv tool install lightnow-proxy
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
[pipx](https://pipx.pypa.io/stable/installation/) is also supported.
|
|
80
|
+
|
|
81
|
+
### 2. Sign in and connect a client
|
|
82
|
+
|
|
83
|
+
If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
|
|
84
|
+
before syncing.
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
lightnow login
|
|
88
|
+
lightnow sync --client codex --profile default --local-proxy
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replace `default` with your profile's name. Restart the client and check its MCP
|
|
92
|
+
tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
|
|
93
|
+
Antigravity, Gemini CLI and LibreChat.
|
|
94
|
+
|
|
95
|
+
See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
|
|
96
|
+
for each client's setup and configuration paths.
|
|
97
|
+
|
|
98
|
+
For LibreChat, run the CLI and proxy on its backend and select its configuration:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
LibreChat users with access to this connection share the backend account's
|
|
105
|
+
LightNow permissions. Reload LibreChat after changing its configuration.
|
|
106
|
+
|
|
107
|
+
### 3. Check the connection
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
lightnow status
|
|
111
|
+
lightnow config-status --client codex
|
|
112
|
+
lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
|
|
116
|
+
|
|
117
|
+
## Useful commands
|
|
118
|
+
|
|
119
|
+
| Task | Command |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| Find servers | `lightnow search sonarqube` |
|
|
122
|
+
| View your favorites | `lightnow favorites` |
|
|
123
|
+
| Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
|
|
124
|
+
| Check for updates | `lightnow update --check` |
|
|
125
|
+
| Update the CLI and proxy | `lightnow update` |
|
|
126
|
+
|
|
127
|
+
Use `lightnow --help` or `lightnow <command> --help` for available options.
|
|
128
|
+
For profiles, organization connections and other workflows, see the
|
|
129
|
+
[CLI reference](https://docs.lightnow.ai/reference/cli).
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
|
|
134
|
+
[release process](docs/release.md) and [security policy](SECURITY.md).
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
[Apache License 2.0](LICENSE).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# LightNow CLI
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/lightnow-cli/)
|
|
4
|
+
|
|
5
|
+
Find MCP servers and connect your AI assistants to the tools you manage in LightNow.
|
|
6
|
+
|
|
7
|
+
The CLI signs you in and configures your clients. [LightNow Proxy](https://github.com/lightnow-ai/lightnow-proxy)
|
|
8
|
+
runs their connections to the servers in your Runtime profiles.
|
|
9
|
+
|
|
10
|
+
## Get started
|
|
11
|
+
|
|
12
|
+
You need a [LightNow account](https://lightnow.ai/), an AI client and a Runtime
|
|
13
|
+
profile with the server connections you want to use.
|
|
14
|
+
|
|
15
|
+
### 1. Install the CLI and proxy
|
|
16
|
+
|
|
17
|
+
With Homebrew:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
brew tap lightnow-ai/tap
|
|
21
|
+
brew install lightnow-cli lightnow-proxy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or with [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.11 or later:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uv tool install lightnow-cli
|
|
28
|
+
uv tool install lightnow-proxy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
[pipx](https://pipx.pypa.io/stable/installation/) is also supported.
|
|
32
|
+
|
|
33
|
+
### 2. Sign in and connect a client
|
|
34
|
+
|
|
35
|
+
If your client already has MCP servers, [review the import options](https://docs.lightnow.ai/how-to/import-existing-mcp-config)
|
|
36
|
+
before syncing.
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
lightnow login
|
|
40
|
+
lightnow sync --client codex --profile default --local-proxy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Replace `default` with your profile's name. Restart the client and check its MCP
|
|
44
|
+
tools list. The CLI supports Codex, Claude Code, Claude Desktop, Cursor, VS Code,
|
|
45
|
+
Antigravity, Gemini CLI and LibreChat.
|
|
46
|
+
|
|
47
|
+
See [Connect AI assistants](https://docs.lightnow.ai/getting-started/sync-mcp-clients)
|
|
48
|
+
for each client's setup and configuration paths.
|
|
49
|
+
|
|
50
|
+
For LibreChat, run the CLI and proxy on its backend and select its configuration:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
lightnow sync --client librechat --profile default --local-proxy --config-path /path/to/librechat.yaml
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
LibreChat users with access to this connection share the backend account's
|
|
57
|
+
LightNow permissions. Reload LibreChat after changing its configuration.
|
|
58
|
+
|
|
59
|
+
### 3. Check the connection
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
lightnow status
|
|
63
|
+
lightnow config-status --client codex
|
|
64
|
+
lightnow-proxy --config ~/.lightnow/lightnow-proxy/codex.yaml --health
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
For help, see [Troubleshoot the connection](https://docs.lightnow.ai/how-to/debug-local-proxy).
|
|
68
|
+
|
|
69
|
+
## Useful commands
|
|
70
|
+
|
|
71
|
+
| Task | Command |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| Find servers | `lightnow search sonarqube` |
|
|
74
|
+
| View your favorites | `lightnow favorites` |
|
|
75
|
+
| Use organization-managed client settings | `lightnow sync --client codex --from-settings` |
|
|
76
|
+
| Check for updates | `lightnow update --check` |
|
|
77
|
+
| Update the CLI and proxy | `lightnow update` |
|
|
78
|
+
|
|
79
|
+
Use `lightnow --help` or `lightnow <command> --help` for available options.
|
|
80
|
+
For profiles, organization connections and other workflows, see the
|
|
81
|
+
[CLI reference](https://docs.lightnow.ai/reference/cli).
|
|
82
|
+
|
|
83
|
+
## Contributing
|
|
84
|
+
|
|
85
|
+
See [development setup](docs/development.md), [contributing](CONTRIBUTING.md),
|
|
86
|
+
[release process](docs/release.md) and [security policy](SECURITY.md).
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[Apache License 2.0](LICENSE).
|
|
@@ -121,7 +121,7 @@ VSCODE_VIRTUAL_TOOLS_THRESHOLD = 128
|
|
|
121
121
|
LOCAL_PROXY_STDIO_SUPPORT_MESSAGE = (
|
|
122
122
|
"Local Proxy Mode stdio currently supports Codex TOML, "
|
|
123
123
|
"Antigravity JSON, Claude Code JSON, Claude Desktop JSON, "
|
|
124
|
-
"Cursor JSON, Gemini CLI JSON,
|
|
124
|
+
"Cursor JSON, Gemini CLI JSON, VS Code JSON, and LibreChat YAML only."
|
|
125
125
|
)
|
|
126
126
|
LOCAL_PROXY_HTTP_SUPPORT_MESSAGE = (
|
|
127
127
|
"Local Proxy Mode HTTP currently supports Codex TOML only."
|
|
@@ -237,6 +237,8 @@ def local_proxy_support_error(
|
|
|
237
237
|
) -> Optional[str]:
|
|
238
238
|
"""Return why a client/format/transport combination lacks Local Proxy support."""
|
|
239
239
|
if local_proxy_transport == "stdio":
|
|
240
|
+
if client == "librechat" and export_format == "yaml":
|
|
241
|
+
return None
|
|
240
242
|
if client in LOCAL_PROXY_JSON_CLIENTS and export_format == "json":
|
|
241
243
|
return None
|
|
242
244
|
if client == "codex" and export_format == "toml":
|
|
@@ -271,12 +273,29 @@ def backup_path_for(path: Path) -> Path:
|
|
|
271
273
|
|
|
272
274
|
def direct_server_aliases(content: str, export_format: str) -> list[str]:
|
|
273
275
|
"""Return non-LightNow MCP server aliases found in a client config."""
|
|
274
|
-
if export_format not in {"json", "toml"}:
|
|
276
|
+
if export_format not in {"json", "toml", "yaml"}:
|
|
275
277
|
return []
|
|
276
278
|
try:
|
|
277
279
|
entries = extract_mcp_entries(content, export_format)
|
|
278
280
|
except ValueError:
|
|
279
281
|
return []
|
|
282
|
+
if export_format == "yaml":
|
|
283
|
+
return sorted(
|
|
284
|
+
alias
|
|
285
|
+
for alias, entry in entries.items()
|
|
286
|
+
if not (
|
|
287
|
+
command_looks_like(
|
|
288
|
+
str(entry.get("command") or ""), LOCAL_PROXY_EXECUTABLE
|
|
289
|
+
)
|
|
290
|
+
or command_looks_like(
|
|
291
|
+
str(entry.get("command") or ""), LEGACY_LOCAL_PROXY_EXECUTABLE
|
|
292
|
+
)
|
|
293
|
+
or (
|
|
294
|
+
is_lightnow_connection_alias(alias)
|
|
295
|
+
and is_local_proxy_url(str(entry.get("url") or ""))
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
)
|
|
280
299
|
return sorted(alias for alias in entries if alias not in LIGHTNOW_PROXY_ALIASES)
|
|
281
300
|
|
|
282
301
|
|
|
@@ -626,13 +645,18 @@ def sync(
|
|
|
626
645
|
patch_aliases = previous_managed["aliases"]
|
|
627
646
|
if local_proxy and export_format == "json":
|
|
628
647
|
patch_aliases = ["LightNow" if connection == "lightnow" else connection]
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
648
|
+
if local_proxy and client == "librechat" and export_format == "yaml":
|
|
649
|
+
patched = patch_local_proxy_yaml_config(
|
|
650
|
+
existing, generated, remove_unmanaged_client_servers
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
patched = patch_config(
|
|
654
|
+
existing,
|
|
655
|
+
generated,
|
|
656
|
+
export_format,
|
|
657
|
+
patch_aliases,
|
|
658
|
+
previous_managed["input_ids"],
|
|
659
|
+
)
|
|
636
660
|
if (
|
|
637
661
|
not local_proxy
|
|
638
662
|
and not runner
|
|
@@ -874,7 +898,9 @@ def import_config(
|
|
|
874
898
|
] = "default",
|
|
875
899
|
tenant: Annotated[
|
|
876
900
|
Optional[str],
|
|
877
|
-
typer.Option(
|
|
901
|
+
typer.Option(
|
|
902
|
+
"--tenant", help="Organization ID; defaults to the selected context"
|
|
903
|
+
),
|
|
878
904
|
] = None,
|
|
879
905
|
config_path: Annotated[
|
|
880
906
|
Optional[Path],
|
|
@@ -965,6 +991,12 @@ def build_local_proxy_export(
|
|
|
965
991
|
if support_error:
|
|
966
992
|
raise ValueError(support_error)
|
|
967
993
|
if local_proxy_transport == "stdio":
|
|
994
|
+
if client == "librechat" and export_format == "yaml":
|
|
995
|
+
return render_local_proxy_librechat_yaml(
|
|
996
|
+
local_proxy_config_path
|
|
997
|
+
or default_local_proxy_config_path(client, connection_alias),
|
|
998
|
+
connection_alias,
|
|
999
|
+
)
|
|
968
1000
|
if client in LOCAL_PROXY_MCP_SERVERS_JSON_CLIENTS and export_format == "json":
|
|
969
1001
|
return render_local_proxy_mcp_servers_json(
|
|
970
1002
|
local_proxy_config_path
|
|
@@ -1099,6 +1131,18 @@ def render_local_proxy_mcp_servers_json(
|
|
|
1099
1131
|
return json.dumps(payload, indent=2) + "\n"
|
|
1100
1132
|
|
|
1101
1133
|
|
|
1134
|
+
def render_local_proxy_librechat_yaml(
|
|
1135
|
+
local_proxy_config_path: Path, connection_alias: str = "lightnow"
|
|
1136
|
+
) -> str:
|
|
1137
|
+
"""Render an operator-owned LibreChat stdio entry on its backend host."""
|
|
1138
|
+
payload = json.loads(
|
|
1139
|
+
render_local_proxy_mcp_servers_json(local_proxy_config_path, connection_alias)
|
|
1140
|
+
)
|
|
1141
|
+
for entry in payload["mcpServers"].values():
|
|
1142
|
+
entry["type"] = "stdio"
|
|
1143
|
+
return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True)
|
|
1144
|
+
|
|
1145
|
+
|
|
1102
1146
|
def render_local_proxy_vscode_json(
|
|
1103
1147
|
local_proxy_config_path: Path, connection_alias: str = "lightnow"
|
|
1104
1148
|
) -> str:
|
|
@@ -1657,7 +1701,34 @@ def import_profile_config(
|
|
|
1657
1701
|
) -> dict[str, Any]:
|
|
1658
1702
|
"""Import rendered client config into a LightNow runtime profile."""
|
|
1659
1703
|
url = f"{api_url.rstrip('/')}/integrations/import"
|
|
1704
|
+
headers = {"Accept": "application/json"}
|
|
1660
1705
|
try:
|
|
1706
|
+
if not dry_run:
|
|
1707
|
+
current = request_with_refresh(
|
|
1708
|
+
"GET",
|
|
1709
|
+
f"{api_url.rstrip('/')}/integrations/profiles",
|
|
1710
|
+
headers=headers,
|
|
1711
|
+
token=token,
|
|
1712
|
+
tenant=tenant,
|
|
1713
|
+
timeout=30.0,
|
|
1714
|
+
)
|
|
1715
|
+
if current.status_code == 401:
|
|
1716
|
+
raise authentication_error_from_response(current)
|
|
1717
|
+
if current.status_code != 200:
|
|
1718
|
+
raise ValueError(f"HTTP {current.status_code}: {redact(current.text)}")
|
|
1719
|
+
etag = current.headers.get("ETag")
|
|
1720
|
+
if (
|
|
1721
|
+
etag is None
|
|
1722
|
+
or re.fullmatch(r'"[\x21\x23-\x7e\x80-\xff]*"', etag) is None
|
|
1723
|
+
):
|
|
1724
|
+
raise ValueError(
|
|
1725
|
+
"Could not read the current profile version. No import was applied."
|
|
1726
|
+
)
|
|
1727
|
+
headers["If-Match"] = etag
|
|
1728
|
+
# The snapshot request may have refreshed an expired session.
|
|
1729
|
+
authorization = current.request.headers.get("Authorization", "")
|
|
1730
|
+
if authorization.startswith("Bearer "):
|
|
1731
|
+
token = authorization.removeprefix("Bearer ")
|
|
1661
1732
|
response = request_with_refresh(
|
|
1662
1733
|
"POST",
|
|
1663
1734
|
url,
|
|
@@ -1668,7 +1739,7 @@ def import_profile_config(
|
|
|
1668
1739
|
"profile": {"name": profile},
|
|
1669
1740
|
"replace": replace,
|
|
1670
1741
|
},
|
|
1671
|
-
headers=
|
|
1742
|
+
headers=headers,
|
|
1672
1743
|
token=token,
|
|
1673
1744
|
tenant=tenant,
|
|
1674
1745
|
timeout=30.0,
|
|
@@ -2084,6 +2155,11 @@ def extract_mcp_entries(content: str, export_format: str) -> dict[str, dict[str,
|
|
|
2084
2155
|
if isinstance(entry, dict):
|
|
2085
2156
|
entries[str(alias)] = entry
|
|
2086
2157
|
return entries
|
|
2158
|
+
if export_format == "yaml":
|
|
2159
|
+
payload = load_yaml_client_config(content)
|
|
2160
|
+
return {
|
|
2161
|
+
str(alias): entry for alias, entry in payload.get("mcpServers", {}).items()
|
|
2162
|
+
}
|
|
2087
2163
|
raise ValueError(f"unsupported_format:{export_format}")
|
|
2088
2164
|
|
|
2089
2165
|
|
|
@@ -2106,7 +2182,7 @@ def is_local_proxy_url(value: str) -> bool:
|
|
|
2106
2182
|
def warning_hint(code: str, client: str) -> Optional[str]:
|
|
2107
2183
|
"""Return a short recovery hint for a posture warning code."""
|
|
2108
2184
|
sync_command = f"lightnow sync --client {client} --local-proxy"
|
|
2109
|
-
if code.startswith("invalid_json"
|
|
2185
|
+
if code.startswith(("invalid_json", "invalid_toml", "invalid_yaml")):
|
|
2110
2186
|
return (
|
|
2111
2187
|
"the config file could not be parsed; fix it or restore the "
|
|
2112
2188
|
".lightnow.bak backup, then re-run the sync."
|
|
@@ -2439,6 +2515,60 @@ def merge_local_proxy_toml_connection(
|
|
|
2439
2515
|
return "\n\n".join(part for part in (kept, generated.strip()) if part)
|
|
2440
2516
|
|
|
2441
2517
|
|
|
2518
|
+
class ClientConfigSafeLoader(yaml.SafeLoader):
|
|
2519
|
+
"""Reject duplicate YAML keys rather than silently discarding configuration."""
|
|
2520
|
+
|
|
2521
|
+
def construct_mapping(
|
|
2522
|
+
self, node: yaml.nodes.MappingNode, deep: bool = False
|
|
2523
|
+
) -> dict[Any, Any]:
|
|
2524
|
+
keys = [
|
|
2525
|
+
self.construct_object(key, deep=deep)
|
|
2526
|
+
for key, _ in node.value
|
|
2527
|
+
if key.tag != "tag:yaml.org,2002:merge"
|
|
2528
|
+
]
|
|
2529
|
+
if len(set(keys)) != len(keys):
|
|
2530
|
+
raise ValueError("invalid_yaml:duplicate_key")
|
|
2531
|
+
return super().construct_mapping(node, deep=deep)
|
|
2532
|
+
|
|
2533
|
+
|
|
2534
|
+
def load_yaml_client_config(content: str) -> dict[str, Any]:
|
|
2535
|
+
"""Read a LibreChat mapping without exposing source values in parse errors."""
|
|
2536
|
+
try:
|
|
2537
|
+
payload = (
|
|
2538
|
+
yaml.load(content, Loader=ClientConfigSafeLoader) if content.strip() else {}
|
|
2539
|
+
)
|
|
2540
|
+
except (yaml.YAMLError, TypeError) as exc:
|
|
2541
|
+
raise ValueError("invalid_yaml:parse_failed") from exc
|
|
2542
|
+
if payload is None:
|
|
2543
|
+
payload = {}
|
|
2544
|
+
if not isinstance(payload, dict):
|
|
2545
|
+
raise ValueError("invalid_yaml:root_not_object")
|
|
2546
|
+
servers = payload.get("mcpServers", {})
|
|
2547
|
+
if not isinstance(servers, dict) or any(
|
|
2548
|
+
not isinstance(alias, str) or not isinstance(entry, dict)
|
|
2549
|
+
for alias, entry in servers.items()
|
|
2550
|
+
):
|
|
2551
|
+
raise ValueError("invalid_yaml:mcpServers_not_object")
|
|
2552
|
+
return payload
|
|
2553
|
+
|
|
2554
|
+
|
|
2555
|
+
def patch_local_proxy_yaml_config(
|
|
2556
|
+
existing: str, generated: str, remove_unmanaged: bool
|
|
2557
|
+
) -> str:
|
|
2558
|
+
"""Merge one LibreChat connection while respecting the unmanaged-server policy."""
|
|
2559
|
+
current = load_yaml_client_config(existing)
|
|
2560
|
+
incoming = load_yaml_client_config(generated)
|
|
2561
|
+
entries = current.get("mcpServers", {})
|
|
2562
|
+
if remove_unmanaged:
|
|
2563
|
+
unmanaged = set(direct_server_aliases(existing, "yaml"))
|
|
2564
|
+
entries = {
|
|
2565
|
+
alias: entry for alias, entry in entries.items() if alias not in unmanaged
|
|
2566
|
+
}
|
|
2567
|
+
entries.update(incoming["mcpServers"])
|
|
2568
|
+
current["mcpServers"] = entries
|
|
2569
|
+
return yaml.safe_dump(current, sort_keys=False, allow_unicode=True)
|
|
2570
|
+
|
|
2571
|
+
|
|
2442
2572
|
def patch_json_config(
|
|
2443
2573
|
existing: str,
|
|
2444
2574
|
generated: str,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lightnow-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.7.1"
|
|
8
8
|
description = "Official LightNow CLI for MCP registry discovery, client config sync, and local MCP runner workflows"
|
|
9
9
|
authors = [{name = "LightNow Team"}]
|
|
10
10
|
license = {text = "Apache-2.0"}
|