8000 feat(cli): lens ext lists, pulls, checks and pushes extension manifests by adeelahmad · Pull Request #563 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content
Merged
2 changes: 1 addition & 1 deletion DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ Todo:
- [x] Canvas editor for tool graphs in the web app (Extensions → Draw a tool)
- [x] Authoring from chat and voice: the assistant drafts and saves extensions behind an approval card
- [x] Extensions page in the web app: list, manifest editor, switch on/off, share, try, versions
- [ ] `lens ext` CLI (push/pull manifests)
- [x] `lens ext` CLI: list, pull, push and check manifests, here or over the API (docs/assistant.md)

Refine later: hook-called tools don't show as steps; web tools reach ports 80/443 only and have no secret
references yet; extensions aren't offered over MCP yet; marketplace on top of plugins.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ listed under [In progress](#in-progress), and what comes next under [Planned](#p
skills, hooks and MCP servers, by code, on the canvas or just by asking. Remote MCP servers connect by address, and
ones that run as a program (`npx`, `uvx`) run in a sandboxed container of their own, off until you turn it on. Add
one with a form or by pasting another app's config, its tokens sealed and each of its tools switched on or off.
Install skills and 8000 plugins from marketplaces in the open plugin format, or from a repository or an archive.
Install skills and plugins from marketplaces in the open plugin format, or from a repository or an archive, and keep
your own as files in a folder with `lens ext` (pull, push, check).
([Extending the assistant](docs/assistant.md#extending-the-assistant-tools-skills-hooks-and-plugins),
[adding an MCP server](docs/assistant.md#adding-an-mcp-server), [the MCP runner](docs/assistant.md#the-mcp-runner);
[checks](docs/requirements.md#extend-the-assistant))
Expand Down
6 changes: 6 additions & 0 deletions changelog.d/lens-ext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- **`lens ext`: extensions as files.** `lens ext pull` writes your extensions' manifests into a folder (skills and
prompt tools as Markdown, the rest as YAML), `lens ext push` saves the changed ones as new versions and the new ones
as extensions, `lens ext check` checks them without saving, and `lens ext list` lists them (`--json` for agents). It
works on this install's database, or on any Lens server with `--url` and an API token. A pulled file says its
version, so a push won't save over changes made in Lens since, and a pull won't overwrite changes made in the file,
without `--force` ([Manifests as files](docs/assistant.md#manifests-as-files-lens-ext)).
23 changes: 23 additions & 0 deletions docs/assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,29 @@ when: someone asks for a recap of a meeting
Find the meeting, read it, and answer with three bullets: decisions, owners, dates.
```

### Manifests as files: `lens ext`

`lens ext` keeps extensions as files in a folder, for a git repository or an agent that writes them:

```bash
lens ext list # the extensions you can see; --json for scripts and agents
lens ext pull --dir extensions # yours (admins: all) as files; names or ids pick some, --all adds shared ones
lens ext check extensions # read and check them without saving; exits 1 when one doesn't check
lens ext push extensions # changed ones become new versions, new ones extensions; --notes says what changed
```

With no address it works on this install's database, as the first admin or the account `--as` names (run it with
`docker compose exec backend lens ext …`). With `--url` and `--token` (or `LENS_URL` and `LENS_TOKEN`) it works on
any Lens server through `/api/v1/extensions`, as the [API token](authentication.md#api-tokens)'s owner; pushing takes a
write token.

A skill or a prompt tool is pulled as `<name>.md` (Markdown with frontmatter), the rest as `<name>.yaml`; a folder's
other `.md` files, a README, are left alone. A pulled file says the version it was pulled at (`version: 3`, which the
check ignores), so neither side's changes are lost: a push refuses when the extension changed in Lens since that
version, and a pull refuses to overwrite a file changed since it was pulled, each until it's given `--force`. Pushing
a file that says what Lens has, written differently or with the defaults left out, saves nothing. `visibility` and
`namespaces` in a file share the extension as they say when it's pushed; without them, who sees it stays as it is.

### Skills and plugins from marketplaces

Lens reads Anthropic's open plugin and skill formats as they are, so what's made for Claude installs here unchanged:
Expand Down
1 change: 1 addition & 0 deletions docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ lens embed # index what isn't yet searchable by
lens reindex # after changing search.engine or search.stemming
lens migrations # database upgrades: run, pending or failed
lens backup # back the database up into <data_dir>/backups
lens ext pull --dir extensions # extensions' manifests as files; lens ext push sends changes back
```

Docker on macOS can't use the Apple GPU. Keep the database and API in Docker and run a native worker against the same
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ request, to another check that proves the same thing; loosening a requirement or
| Remote MCP servers connect by address | [test_mcp_servers.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_mcp_servers.py) `test_an_admin_adds_a_server_and_the_assistant_uses_it` |
| MCP servers that run as a program (`npx`, `uvx`) run in a sandboxed container of their own | [test_mcp_servers.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_mcp_servers.py) `test_a_server_that_runs_as_a_program_runs_in_the_runner`; [mcp-runner.yml](https://github.com/adeelahmad/lense/blob/main/.github/workflows/mcp-runner.yml) `Runs an npm server and a PyPI server` |
| An MCP server added by form or a pasted config, its tokens sealed and each tool switched on or off | [test_mcp_servers.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_mcp_servers.py) `test_a_pasted_config_reads_as_servers_to_add`, `test_its_secrets_are_sealed_and_never_shown`, `test_each_tool_is_switched_and_says_whether_it_asks`; [mcp-server-ui.test.tsx](https://github.com/adeelahmad/lense/blob/main/nextjs-frontend/__tests__/mcp-server-ui.test.tsx) `adding an MCP server` |
| Extensions kept as files: `lens ext` lists, pulls, checks and pushes manifests, here or over the API, losing neither side's changes | [test_cli_ext.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_cli_ext.py) `test_pull_edit_and_push_on_this_install`, `test_over_the_api_with_a_token` |
| Skills and plugins from marketplaces in the open plugin format, a repository or an archive | [test_marketplaces.py](https://github.com/adeelahmad/lense/blob/main/fastapi_backend/tests/api/test_marketplaces.py) `test_a_marketplace_an_admin_adds`, `test_imported_from_a_link_or_a_file` |

## Bring in everything
Expand Down
5 changes: 5 additions & 0 deletions fastapi_backend/app/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def _main_base(argv=None):
p = sub.add_parser("migrations", help="list the database upgrades: run, pending or failed (docs/database.md)")
p.add_argument("--run", action="store_true", help="run the pending ones now (Lens also runs them when it starts)")
sub.add_parser("backup", help="back the database up into <data_dir>/backups (docs/database.md, Backups)")
sub.add_parser("ext", help="extensions' manifests as files: list, pull, push, check (lens ext -h)", add_help=False)
sub.add_parser("reindex", help="rebuild the search index (after changing search.engine or search.stemming)")
p = sub.add_parser("encrypt", help="encrypt the files Lens keeps, and keep encrypting new ones (docs/encryption.md)")
p.add_argument("--off", action="store_true", help="decrypt them again and stop encrypting new ones")
Expand Down Expand Up @@ -463,4 +464,8 @@ def main(argv=None):
break
if i < len(argv) and argv[i] in PLATFORM_CMDS:
return platform_main(argv[i:], config)
if i < len(argv) and argv[i] == "ext": # extensions' manifests as files (cli_ext.py)
from . import cli_ext

return cli_ext.main(argv[i + 1 :], config)
return _main_base(argv)
Loading
Loading
0