lore-solo 0.1.0__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.
- lore_solo-0.1.0/LICENSE +21 -0
- lore_solo-0.1.0/MANIFEST.in +5 -0
- lore_solo-0.1.0/PKG-INFO +122 -0
- lore_solo-0.1.0/README.md +98 -0
- lore_solo-0.1.0/build_support.py +24 -0
- lore_solo-0.1.0/lore/__init__.py +3 -0
- lore_solo-0.1.0/lore/__main__.py +43 -0
- lore_solo-0.1.0/lore/connect.py +232 -0
- lore_solo-0.1.0/lore/core/__init__.py +1 -0
- lore_solo-0.1.0/lore/core/budget.py +40 -0
- lore_solo-0.1.0/lore/core/codebases.py +101 -0
- lore_solo-0.1.0/lore/core/config.py +39 -0
- lore_solo-0.1.0/lore/core/context.py +248 -0
- lore_solo-0.1.0/lore/core/decisions.py +60 -0
- lore_solo-0.1.0/lore/core/graph.py +184 -0
- lore_solo-0.1.0/lore/core/idempotency.py +36 -0
- lore_solo-0.1.0/lore/core/inbox.py +333 -0
- lore_solo-0.1.0/lore/core/okf.py +64 -0
- lore_solo-0.1.0/lore/core/onboarding.py +51 -0
- lore_solo-0.1.0/lore/core/paths.py +75 -0
- lore_solo-0.1.0/lore/core/personas.py +298 -0
- lore_solo-0.1.0/lore/core/picker.py +53 -0
- lore_solo-0.1.0/lore/core/provider.py +372 -0
- lore_solo-0.1.0/lore/core/tests/pack.py +7 -0
- lore_solo-0.1.0/lore/core/tests/test_connect.py +67 -0
- lore_solo-0.1.0/lore/core/tests/test_context.py +95 -0
- lore_solo-0.1.0/lore/core/tests/test_files.py +123 -0
- lore_solo-0.1.0/lore/core/tests/test_graph.py +79 -0
- lore_solo-0.1.0/lore/core/tests/test_inbox.py +118 -0
- lore_solo-0.1.0/lore/core/tests/test_install.py +43 -0
- lore_solo-0.1.0/lore/core/tests/test_library_flow.py +130 -0
- lore_solo-0.1.0/lore/core/tests/test_mcp.py +101 -0
- lore_solo-0.1.0/lore/core/tests/test_picker.py +32 -0
- lore_solo-0.1.0/lore/core/tests/test_provider.py +175 -0
- lore_solo-0.1.0/lore/core/usage.py +93 -0
- lore_solo-0.1.0/lore/core/workspace.py +88 -0
- lore_solo-0.1.0/lore/install_cmd.py +101 -0
- lore_solo-0.1.0/lore/mcp.py +5 -0
- lore_solo-0.1.0/lore/mcp_server.py +121 -0
- lore_solo-0.1.0/lore/py.typed +0 -0
- lore_solo-0.1.0/lore/setup/__init__.py +1 -0
- lore_solo-0.1.0/lore/setup/graph_job.py +96 -0
- lore_solo-0.1.0/lore/setup/middleware.py +30 -0
- lore_solo-0.1.0/lore/setup/settings.py +21 -0
- lore_solo-0.1.0/lore/setup/tests/test_site.py +217 -0
- lore_solo-0.1.0/lore/setup/urls.py +40 -0
- lore_solo-0.1.0/lore/setup/views.py +715 -0
- lore_solo-0.1.0/lore/setup_cmd.py +221 -0
- lore_solo-0.1.0/lore_solo.egg-info/PKG-INFO +122 -0
- lore_solo-0.1.0/lore_solo.egg-info/SOURCES.txt +106 -0
- lore_solo-0.1.0/lore_solo.egg-info/dependency_links.txt +1 -0
- lore_solo-0.1.0/lore_solo.egg-info/entry_points.txt +2 -0
- lore_solo-0.1.0/lore_solo.egg-info/requires.txt +13 -0
- lore_solo-0.1.0/lore_solo.egg-info/top_level.txt +1 -0
- lore_solo-0.1.0/pyproject.toml +64 -0
- lore_solo-0.1.0/setup.cfg +4 -0
- lore_solo-0.1.0/web/app/codebase/[id]/page.tsx +154 -0
- lore_solo-0.1.0/web/app/codebase/page.tsx +91 -0
- lore_solo-0.1.0/web/app/connect/page.tsx +95 -0
- lore_solo-0.1.0/web/app/globals.css +129 -0
- lore_solo-0.1.0/web/app/layout.tsx +36 -0
- lore_solo-0.1.0/web/app/locked/page.tsx +16 -0
- lore_solo-0.1.0/web/app/page.tsx +176 -0
- lore_solo-0.1.0/web/app/personas/[id]/page.tsx +181 -0
- lore_solo-0.1.0/web/app/personas/page.tsx +121 -0
- lore_solo-0.1.0/web/app/replay/page.tsx +55 -0
- lore_solo-0.1.0/web/app/settings/page.tsx +278 -0
- lore_solo-0.1.0/web/app/skills/[...id]/page.tsx +124 -0
- lore_solo-0.1.0/web/app/skills/page.tsx +248 -0
- lore_solo-0.1.0/web/components/Shell.tsx +53 -0
- lore_solo-0.1.0/web/components/app-sidebar.tsx +100 -0
- lore_solo-0.1.0/web/components/error-alert.tsx +10 -0
- lore_solo-0.1.0/web/components/folder-picker.tsx +48 -0
- lore_solo-0.1.0/web/components/onboarding.tsx +675 -0
- lore_solo-0.1.0/web/components/site-header.tsx +55 -0
- lore_solo-0.1.0/web/components/ui/alert.tsx +75 -0
- lore_solo-0.1.0/web/components/ui/avatar.tsx +108 -0
- lore_solo-0.1.0/web/components/ui/badge.tsx +51 -0
- lore_solo-0.1.0/web/components/ui/breadcrumb.tsx +124 -0
- lore_solo-0.1.0/web/components/ui/button.tsx +57 -0
- lore_solo-0.1.0/web/components/ui/card.tsx +102 -0
- lore_solo-0.1.0/web/components/ui/dropdown-menu.tsx +267 -0
- lore_solo-0.1.0/web/components/ui/field.tsx +238 -0
- lore_solo-0.1.0/web/components/ui/input.tsx +19 -0
- lore_solo-0.1.0/web/components/ui/label.tsx +19 -0
- lore_solo-0.1.0/web/components/ui/scroll-area.tsx +54 -0
- lore_solo-0.1.0/web/components/ui/select.tsx +200 -0
- lore_solo-0.1.0/web/components/ui/separator.tsx +24 -0
- lore_solo-0.1.0/web/components/ui/sheet.tsx +138 -0
- lore_solo-0.1.0/web/components/ui/sidebar.tsx +723 -0
- lore_solo-0.1.0/web/components/ui/skeleton.tsx +13 -0
- lore_solo-0.1.0/web/components/ui/sonner.tsx +49 -0
- lore_solo-0.1.0/web/components/ui/table.tsx +115 -0
- lore_solo-0.1.0/web/components/ui/textarea.tsx +17 -0
- lore_solo-0.1.0/web/components/ui/tooltip.tsx +65 -0
- lore_solo-0.1.0/web/components.json +25 -0
- lore_solo-0.1.0/web/hooks/use-mobile.ts +19 -0
- lore_solo-0.1.0/web/lib/api.ts +62 -0
- lore_solo-0.1.0/web/lib/providers.ts +20 -0
- lore_solo-0.1.0/web/lib/routes.ts +11 -0
- lore_solo-0.1.0/web/lib/utils.ts +1 -0
- lore_solo-0.1.0/web/middleware.ts +29 -0
- lore_solo-0.1.0/web/next-env.d.ts +6 -0
- lore_solo-0.1.0/web/next.config.ts +26 -0
- lore_solo-0.1.0/web/package-lock.json +5703 -0
- lore_solo-0.1.0/web/package.json +31 -0
- lore_solo-0.1.0/web/postcss.config.mjs +1 -0
- lore_solo-0.1.0/web/tsconfig.json +40 -0
lore_solo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Lore Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lore_solo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lore-solo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Governed persona and skill context for a solo developer’s coding agent.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/starrynight-web/LORE
|
|
7
|
+
Project-URL: Repository, https://github.com/starrynight-web/LORE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: PyYAML>=6
|
|
12
|
+
Requires-Dist: tiktoken>=0.7
|
|
13
|
+
Requires-Dist: fastmcp>=2
|
|
14
|
+
Requires-Dist: django>=5.1
|
|
15
|
+
Requires-Dist: graphifyy==0.9.93
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
18
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
19
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
20
|
+
Requires-Dist: jsonschema>=4; extra == "dev"
|
|
21
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
22
|
+
Requires-Dist: types-jsonschema; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Lore
|
|
26
|
+
|
|
27
|
+
[](https://github.com/starrynight-web/LORE/actions/workflows/ci.yml)
|
|
28
|
+
|
|
29
|
+
Lore gives one developer’s coding agent a persona, the skills filed under that persona, and a short map of the codebase. The agent calls Lore over MCP and gets one persona back, inside a token budget. Every answer is written down as a decision.
|
|
30
|
+
|
|
31
|
+
The project folder is the database. Personas and skills are markdown under `.lore/`. The setup site only makes those files easier to create. Closing the site does not stop the agent.
|
|
32
|
+
|
|
33
|
+
The older architecture documents describe the previous Go design.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
The setup site needs Node.js. The agent does not. With [uv](https://docs.astral.sh/uv/) installed, the same commands work on macOS, Linux, and Windows:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx lore-solo install
|
|
41
|
+
uvx lore-solo setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`install` puts the `lore-solo` command on this computer, in `~/.lore/bin`. `setup` prints an address and keeps running until you press Ctrl+C. Open that link. Run `uvx lore-solo setup` again whenever you want the website back. Save a provider key, add skills, create a persona, then give the connect prompt to your agent. Personas you write stay drafts until you approve them.
|
|
45
|
+
|
|
46
|
+
The other commands are the same shape:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx lore-solo connect
|
|
50
|
+
uvx lore-solo mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`connect` registers Lore with the editors already installed. The editor then starts `lore-solo mcp` on its own.
|
|
54
|
+
|
|
55
|
+
From a clone of the `solo` branch, the same four commands are:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
git clone -b solo https://github.com/starrynight-web/LORE.git lore-solo
|
|
59
|
+
cd lore-solo
|
|
60
|
+
./scripts/lore-solo install
|
|
61
|
+
./scripts/lore-solo setup
|
|
62
|
+
./scripts/lore-solo connect
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On Windows, from that clone:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
.\scripts\lore-solo.ps1 install
|
|
69
|
+
.\scripts\lore-solo.ps1 setup
|
|
70
|
+
.\scripts\lore-solo.ps1 connect
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The agent calls Lore from any project. A project with its own `.lore` directory uses that. Every other project uses the personas saved from setup.
|
|
74
|
+
|
|
75
|
+
Connect shows a prompt to paste into an agent. With uv, that prompt is `uvx lore-solo connect` on macOS, Linux, and Windows. From the clone it runs `scripts/lore-solo connect` (or `scripts/lore-solo.ps1 connect` on Windows). Writing `.cursor/mcp.json` inside one project stays available as a backup:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"lore": {
|
|
81
|
+
"command": "uvx",
|
|
82
|
+
"args": ["lore-solo", "mcp"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Restart the editor. It starts Lore on its own. Lore looks for `.lore/` from the working directory upward, then uses the personas saved from setup.
|
|
89
|
+
|
|
90
|
+
`examples/persona-pack` is a small project with a frontend persona, a backend persona, and a codebase map.
|
|
91
|
+
|
|
92
|
+
## What the agent receives
|
|
93
|
+
|
|
94
|
+
`get_context` returns one-line summaries of the stable skills for that persona, then a few nodes from the codebase map when one exists. `get_skill` returns the full steps for one skill. A draft persona asks you to approve it. An unknown persona returns nothing governed.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
.venv/bin/pip install -e ".[dev]"
|
|
100
|
+
.venv/bin/ruff check lore
|
|
101
|
+
.venv/bin/mypy
|
|
102
|
+
.venv/bin/pytest
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`lore/core` reads and writes the files. `lore/mcp.py` is the stdio server the IDE starts. `lore/setup` and `web/` are the setup site. `schemas/lore.result.v1.json` is the answer envelope.
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
| Document | Contents |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| [docs/LORE_OVERVIEW.md](docs/LORE_OVERVIEW.md) | Earlier product overview |
|
|
112
|
+
| [docs/LORE_ARCHITECTURE.md](docs/LORE_ARCHITECTURE.md) | Earlier target architecture |
|
|
113
|
+
| [docs/PROJECT_REQUIREMENTS.md](docs/PROJECT_REQUIREMENTS.md) | Earlier requirements |
|
|
114
|
+
| [docs/PROJECT_CHARTER.md](docs/PROJECT_CHARTER.md) | Earlier scope and milestones |
|
|
115
|
+
| [docs/solo/HOW_IT_WORKS.md](docs/solo/HOW_IT_WORKS.md) | How the solo slice runs |
|
|
116
|
+
| [docs/solo/UI.md](docs/solo/UI.md) | Setup site pages and how to change them |
|
|
117
|
+
| [docs/solo/TASKLIST.md](docs/solo/TASKLIST.md) | Build record and the current shape |
|
|
118
|
+
| [docs/solo/HOSTING.md](docs/solo/HOSTING.md) | What a later paid host would sync |
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Lore
|
|
2
|
+
|
|
3
|
+
[](https://github.com/starrynight-web/LORE/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Lore gives one developer’s coding agent a persona, the skills filed under that persona, and a short map of the codebase. The agent calls Lore over MCP and gets one persona back, inside a token budget. Every answer is written down as a decision.
|
|
6
|
+
|
|
7
|
+
The project folder is the database. Personas and skills are markdown under `.lore/`. The setup site only makes those files easier to create. Closing the site does not stop the agent.
|
|
8
|
+
|
|
9
|
+
The older architecture documents describe the previous Go design.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
The setup site needs Node.js. The agent does not. With [uv](https://docs.astral.sh/uv/) installed, the same commands work on macOS, Linux, and Windows:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx lore-solo install
|
|
17
|
+
uvx lore-solo setup
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`install` puts the `lore-solo` command on this computer, in `~/.lore/bin`. `setup` prints an address and keeps running until you press Ctrl+C. Open that link. Run `uvx lore-solo setup` again whenever you want the website back. Save a provider key, add skills, create a persona, then give the connect prompt to your agent. Personas you write stay drafts until you approve them.
|
|
21
|
+
|
|
22
|
+
The other commands are the same shape:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uvx lore-solo connect
|
|
26
|
+
uvx lore-solo mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`connect` registers Lore with the editors already installed. The editor then starts `lore-solo mcp` on its own.
|
|
30
|
+
|
|
31
|
+
From a clone of the `solo` branch, the same four commands are:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone -b solo https://github.com/starrynight-web/LORE.git lore-solo
|
|
35
|
+
cd lore-solo
|
|
36
|
+
./scripts/lore-solo install
|
|
37
|
+
./scripts/lore-solo setup
|
|
38
|
+
./scripts/lore-solo connect
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
On Windows, from that clone:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
.\scripts\lore-solo.ps1 install
|
|
45
|
+
.\scripts\lore-solo.ps1 setup
|
|
46
|
+
.\scripts\lore-solo.ps1 connect
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The agent calls Lore from any project. A project with its own `.lore` directory uses that. Every other project uses the personas saved from setup.
|
|
50
|
+
|
|
51
|
+
Connect shows a prompt to paste into an agent. With uv, that prompt is `uvx lore-solo connect` on macOS, Linux, and Windows. From the clone it runs `scripts/lore-solo connect` (or `scripts/lore-solo.ps1 connect` on Windows). Writing `.cursor/mcp.json` inside one project stays available as a backup:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"lore": {
|
|
57
|
+
"command": "uvx",
|
|
58
|
+
"args": ["lore-solo", "mcp"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Restart the editor. It starts Lore on its own. Lore looks for `.lore/` from the working directory upward, then uses the personas saved from setup.
|
|
65
|
+
|
|
66
|
+
`examples/persona-pack` is a small project with a frontend persona, a backend persona, and a codebase map.
|
|
67
|
+
|
|
68
|
+
## What the agent receives
|
|
69
|
+
|
|
70
|
+
`get_context` returns one-line summaries of the stable skills for that persona, then a few nodes from the codebase map when one exists. `get_skill` returns the full steps for one skill. A draft persona asks you to approve it. An unknown persona returns nothing governed.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
.venv/bin/pip install -e ".[dev]"
|
|
76
|
+
.venv/bin/ruff check lore
|
|
77
|
+
.venv/bin/mypy
|
|
78
|
+
.venv/bin/pytest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`lore/core` reads and writes the files. `lore/mcp.py` is the stdio server the IDE starts. `lore/setup` and `web/` are the setup site. `schemas/lore.result.v1.json` is the answer envelope.
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
| Document | Contents |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| [docs/LORE_OVERVIEW.md](docs/LORE_OVERVIEW.md) | Earlier product overview |
|
|
88
|
+
| [docs/LORE_ARCHITECTURE.md](docs/LORE_ARCHITECTURE.md) | Earlier target architecture |
|
|
89
|
+
| [docs/PROJECT_REQUIREMENTS.md](docs/PROJECT_REQUIREMENTS.md) | Earlier requirements |
|
|
90
|
+
| [docs/PROJECT_CHARTER.md](docs/PROJECT_CHARTER.md) | Earlier scope and milestones |
|
|
91
|
+
| [docs/solo/HOW_IT_WORKS.md](docs/solo/HOW_IT_WORKS.md) | How the solo slice runs |
|
|
92
|
+
| [docs/solo/UI.md](docs/solo/UI.md) | Setup site pages and how to change them |
|
|
93
|
+
| [docs/solo/TASKLIST.md](docs/solo/TASKLIST.md) | Build record and the current shape |
|
|
94
|
+
| [docs/solo/HOSTING.md](docs/solo/HOSTING.md) | What a later paid host would sync |
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Copy the setup site into the wheel. Dependencies and build output stay out."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from setuptools.command.build_py import build_py
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BuildPy(build_py):
|
|
12
|
+
def run(self) -> None:
|
|
13
|
+
super().run()
|
|
14
|
+
src = Path(__file__).resolve().parent / "web"
|
|
15
|
+
if not (src / "package.json").is_file():
|
|
16
|
+
return
|
|
17
|
+
dest = Path(self.build_lib) / "lore" / "web_src"
|
|
18
|
+
if dest.exists():
|
|
19
|
+
shutil.rmtree(dest)
|
|
20
|
+
shutil.copytree(
|
|
21
|
+
src,
|
|
22
|
+
dest,
|
|
23
|
+
ignore=shutil.ignore_patterns("node_modules", ".next", "tsconfig.tsbuildinfo"),
|
|
24
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""`lore-solo install`, `setup`, `connect`, and `mcp`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv: list[str] | None = None) -> int:
|
|
9
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
10
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
11
|
+
print("usage: lore-solo install|setup|connect|mcp", file=sys.stderr)
|
|
12
|
+
return 0
|
|
13
|
+
command, rest = args[0], args[1:]
|
|
14
|
+
if command == "install":
|
|
15
|
+
from lore.install_cmd import run as run_install
|
|
16
|
+
|
|
17
|
+
run_install(rest)
|
|
18
|
+
return 0
|
|
19
|
+
if command == "setup":
|
|
20
|
+
from lore.setup_cmd import run as run_setup
|
|
21
|
+
|
|
22
|
+
run_setup(rest)
|
|
23
|
+
return 0
|
|
24
|
+
if command == "connect":
|
|
25
|
+
from lore.connect import register_agents
|
|
26
|
+
|
|
27
|
+
result = register_agents()
|
|
28
|
+
for path in result["written"]:
|
|
29
|
+
print(path)
|
|
30
|
+
for skipped in result["skipped"]:
|
|
31
|
+
print(f"skipped {skipped}", file=sys.stderr)
|
|
32
|
+
return 0 if result["written"] else 1
|
|
33
|
+
if command == "mcp":
|
|
34
|
+
from lore.mcp import run as run_mcp
|
|
35
|
+
|
|
36
|
+
run_mcp(rest)
|
|
37
|
+
return 0
|
|
38
|
+
print(f"unknown command {command!r}; use install, setup, connect, or mcp", file=sys.stderr)
|
|
39
|
+
return 2
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""MCP entry for Lore, and the editor configs the connect command writes.
|
|
2
|
+
|
|
3
|
+
The command is a local process. The editor starts it with the open project as
|
|
4
|
+
its working directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def invocation() -> tuple[str, list[str]]:
|
|
17
|
+
"""The program an editor should start, and the arguments that select MCP."""
|
|
18
|
+
explicit = os.environ.get("LORE_COMMAND", "").strip()
|
|
19
|
+
if explicit:
|
|
20
|
+
return explicit, ["mcp"]
|
|
21
|
+
installed = _installed_command()
|
|
22
|
+
if installed is not None:
|
|
23
|
+
return str(installed), ["mcp"]
|
|
24
|
+
for name in ("lore-solo", "lore"):
|
|
25
|
+
found = shutil.which(name)
|
|
26
|
+
if found:
|
|
27
|
+
return found, ["mcp"]
|
|
28
|
+
return "uvx", ["lore-solo", "mcp"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def lore_command() -> str:
|
|
32
|
+
"""The program editors should start."""
|
|
33
|
+
return invocation()[0]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _installed_command() -> Path | None:
|
|
37
|
+
bin_dir = Path.home() / ".lore" / "bin"
|
|
38
|
+
for name in ("lore-solo", "lore"):
|
|
39
|
+
base = bin_dir / name
|
|
40
|
+
if os.name == "nt":
|
|
41
|
+
windows = base.with_suffix(".exe")
|
|
42
|
+
if windows.is_file():
|
|
43
|
+
return windows
|
|
44
|
+
if base.is_file():
|
|
45
|
+
return base
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def mcp_args() -> list[str]:
|
|
50
|
+
return invocation()[1]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def mcp_entry(
|
|
54
|
+
kind: str,
|
|
55
|
+
command: str | None = None,
|
|
56
|
+
client: str = "",
|
|
57
|
+
args: list[str] | None = None,
|
|
58
|
+
) -> dict[str, object]:
|
|
59
|
+
chosen, chosen_args = invocation()
|
|
60
|
+
if command is not None:
|
|
61
|
+
chosen = command
|
|
62
|
+
if args is not None:
|
|
63
|
+
chosen_args = args
|
|
64
|
+
elif command is not None:
|
|
65
|
+
chosen_args = ["mcp"]
|
|
66
|
+
entry: dict[str, object] = {"command": chosen, "args": chosen_args}
|
|
67
|
+
if kind == "vscode":
|
|
68
|
+
entry["type"] = "stdio"
|
|
69
|
+
name = client.strip()
|
|
70
|
+
if name:
|
|
71
|
+
entry["env"] = {"LORE_CLIENT": name}
|
|
72
|
+
return entry
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def install_command() -> str:
|
|
76
|
+
launcher = os.environ.get("LORE_LAUNCHER", "").strip()
|
|
77
|
+
if launcher:
|
|
78
|
+
return f"{launcher} connect"
|
|
79
|
+
return "uvx lore-solo connect"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def windows_command(command: str) -> str:
|
|
83
|
+
if command.startswith("uvx "):
|
|
84
|
+
return command
|
|
85
|
+
suffix = " connect"
|
|
86
|
+
if command.endswith(suffix):
|
|
87
|
+
script = command[: -len(suffix)]
|
|
88
|
+
if script.endswith(".ps1"):
|
|
89
|
+
return command
|
|
90
|
+
return f"{script}.ps1 connect"
|
|
91
|
+
return "scripts/lore-solo.ps1 connect"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def agent_prompt(command: str | None = None) -> str:
|
|
95
|
+
chosen = command or install_command()
|
|
96
|
+
windows = windows_command(chosen)
|
|
97
|
+
if windows == chosen:
|
|
98
|
+
steps = f"On macOS, Linux, or Windows, run:\n\n{chosen}\n\n"
|
|
99
|
+
else:
|
|
100
|
+
steps = (
|
|
101
|
+
"On macOS or Linux, run this in the shell:\n\n"
|
|
102
|
+
f"{chosen}\n\n"
|
|
103
|
+
"On Windows, run this in PowerShell:\n\n"
|
|
104
|
+
f"{windows}\n\n"
|
|
105
|
+
)
|
|
106
|
+
return (
|
|
107
|
+
"Register Lore with the editors on this computer. "
|
|
108
|
+
f"{steps}"
|
|
109
|
+
"When it finishes, confirm Lore is registered with the editors it found. "
|
|
110
|
+
"A project that already has a .lore directory keeps using that directory."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def host_system(value: str | None = None) -> str:
|
|
115
|
+
raw = (value or os.environ.get("LORE_HOST_OS") or platform.system()).lower()
|
|
116
|
+
if raw.startswith("darwin") or raw == "macos":
|
|
117
|
+
return "darwin"
|
|
118
|
+
if raw.startswith("win"):
|
|
119
|
+
return "windows"
|
|
120
|
+
return "linux"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _write_json(path: Path, key: str, entry: dict[str, object]) -> None:
|
|
124
|
+
data: dict[str, object] = {}
|
|
125
|
+
if path.is_file():
|
|
126
|
+
raw = path.read_text(encoding="utf-8").strip()
|
|
127
|
+
if raw:
|
|
128
|
+
loaded = json.loads(raw)
|
|
129
|
+
if not isinstance(loaded, dict):
|
|
130
|
+
raise ValueError(f"{path} is not a JSON object.")
|
|
131
|
+
data = loaded
|
|
132
|
+
backup = path.with_name(path.name + ".lore.bak")
|
|
133
|
+
if not backup.exists():
|
|
134
|
+
shutil.copyfile(path, backup)
|
|
135
|
+
servers = data.get(key)
|
|
136
|
+
if not isinstance(servers, dict):
|
|
137
|
+
servers = {}
|
|
138
|
+
servers["lore"] = entry
|
|
139
|
+
data[key] = servers
|
|
140
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _strip_toml_section(text: str, heading: str) -> str:
|
|
145
|
+
kept: list[str] = []
|
|
146
|
+
skipping = False
|
|
147
|
+
blanks: list[str] = []
|
|
148
|
+
for line in text.splitlines():
|
|
149
|
+
stripped = line.strip()
|
|
150
|
+
if stripped.startswith("["):
|
|
151
|
+
skipping = stripped == heading or stripped.startswith(heading + " ")
|
|
152
|
+
if skipping:
|
|
153
|
+
continue
|
|
154
|
+
if stripped == "":
|
|
155
|
+
blanks.append(line)
|
|
156
|
+
continue
|
|
157
|
+
kept.extend(blanks)
|
|
158
|
+
blanks = []
|
|
159
|
+
kept.append(line)
|
|
160
|
+
return "\n".join(kept).rstrip()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _write_codex(path: Path, command: str, client: str = "", args: list[str] | None = None) -> None:
|
|
164
|
+
existing = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
165
|
+
if path.is_file():
|
|
166
|
+
backup = path.with_name(path.name + ".lore.bak")
|
|
167
|
+
if not backup.exists():
|
|
168
|
+
shutil.copyfile(path, backup)
|
|
169
|
+
kept = _strip_toml_section(existing, "[mcp_servers.lore]")
|
|
170
|
+
rendered = ", ".join(json.dumps(part) for part in (args if args is not None else mcp_args()))
|
|
171
|
+
env = ""
|
|
172
|
+
if client.strip():
|
|
173
|
+
env = f"env = {{ LORE_CLIENT = {json.dumps(client.strip())} }}\n"
|
|
174
|
+
block = f"[mcp_servers.lore]\ncommand = {json.dumps(command)}\nargs = [{rendered}]\n{env}"
|
|
175
|
+
body = f"{kept}\n\n{block}" if kept else block
|
|
176
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
path.write_text(body if body.endswith("\n") else body + "\n", encoding="utf-8")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _targets(home: Path, system: str) -> list[tuple[str, Path, str, str]]:
|
|
181
|
+
"""(label, file, container key, editor). An editor is included when its folder exists."""
|
|
182
|
+
if system == "darwin":
|
|
183
|
+
desktop = home / "Library" / "Application Support" / "Claude"
|
|
184
|
+
vscode = home / "Library" / "Application Support" / "Code" / "User"
|
|
185
|
+
elif system == "windows":
|
|
186
|
+
desktop = home / "AppData" / "Roaming" / "Claude"
|
|
187
|
+
vscode = home / "AppData" / "Roaming" / "Code" / "User"
|
|
188
|
+
else:
|
|
189
|
+
desktop = home / ".config" / "Claude"
|
|
190
|
+
vscode = home / ".config" / "Code" / "User"
|
|
191
|
+
rows: list[tuple[str, Path, str, str]] = []
|
|
192
|
+
cursor = home / ".cursor"
|
|
193
|
+
if cursor.is_dir() or (cursor / "mcp.json").exists():
|
|
194
|
+
rows.append(("Cursor", cursor / "mcp.json", "mcpServers", "cursor"))
|
|
195
|
+
claude = home / ".claude.json"
|
|
196
|
+
if claude.exists() or (home / ".claude").is_dir():
|
|
197
|
+
rows.append(("Claude Code", claude, "mcpServers", "other"))
|
|
198
|
+
if desktop.is_dir():
|
|
199
|
+
rows.append(("Claude Desktop", desktop / "claude_desktop_config.json", "mcpServers", "other"))
|
|
200
|
+
if (home / ".codex").is_dir():
|
|
201
|
+
rows.append(("Codex", home / ".codex" / "config.toml", "", "codex"))
|
|
202
|
+
if vscode.is_dir():
|
|
203
|
+
rows.append(("VS Code", vscode / "mcp.json", "servers", "vscode"))
|
|
204
|
+
gemini = home / ".gemini" / "config"
|
|
205
|
+
if gemini.is_dir():
|
|
206
|
+
rows.append(("Antigravity", gemini / "mcp_config.json", "mcpServers", "other"))
|
|
207
|
+
return rows
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def register_agents(
|
|
211
|
+
home: Path | None = None,
|
|
212
|
+
system: str | None = None,
|
|
213
|
+
) -> dict[str, list[str]]:
|
|
214
|
+
"""Write Lore into each detected editor. Other servers in those files stay."""
|
|
215
|
+
if home is None:
|
|
216
|
+
raw = os.environ.get("LORE_HOST_HOME", "")
|
|
217
|
+
home = Path(raw) if raw else Path.home()
|
|
218
|
+
host = host_system(system)
|
|
219
|
+
command, args = invocation()
|
|
220
|
+
written: list[str] = []
|
|
221
|
+
skipped: list[str] = []
|
|
222
|
+
for label, path, key, editor in _targets(home, host):
|
|
223
|
+
try:
|
|
224
|
+
if editor == "codex":
|
|
225
|
+
_write_codex(path, command, client=label, args=args)
|
|
226
|
+
else:
|
|
227
|
+
_write_json(path, key, mcp_entry(editor, command, client=label, args=args))
|
|
228
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
229
|
+
skipped.append(f"{label}: {path}")
|
|
230
|
+
continue
|
|
231
|
+
written.append(str(path))
|
|
232
|
+
return {"written": written, "skipped": skipped}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""File-backed knowledge. No network and no database."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Count tokens for the 800-token answer cap.
|
|
2
|
+
|
|
3
|
+
The count is cl100k_base when the tiktoken encoder is installed. That is the
|
|
4
|
+
same encoding the previous Go engine used. If the encoder cannot be loaded,
|
|
5
|
+
the count is one token per three UTF-8 bytes, rounded up. Tests pin both
|
|
6
|
+
behaviors by name.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from typing import Protocol
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Encoder(Protocol):
|
|
16
|
+
def encode(self, text: str) -> list[int]: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=1)
|
|
20
|
+
def _encoder() -> _Encoder | None:
|
|
21
|
+
try:
|
|
22
|
+
import tiktoken
|
|
23
|
+
except ImportError:
|
|
24
|
+
return None
|
|
25
|
+
try:
|
|
26
|
+
return tiktoken.get_encoding("cl100k_base")
|
|
27
|
+
except (OSError, ValueError, RuntimeError):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def count_tokens(text: str) -> int:
|
|
32
|
+
encoder = _encoder()
|
|
33
|
+
if encoder is not None:
|
|
34
|
+
return len(encoder.encode(text))
|
|
35
|
+
return _bytes_fallback(text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bytes_fallback(text: str) -> int:
|
|
39
|
+
"""One token per three UTF-8 bytes, rounded up. Used only if tiktoken is missing."""
|
|
40
|
+
return (len(text.encode("utf-8")) + 2) // 3
|