mrplex 0.0.2 → 0.1.0
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.
- package/README.md +98 -183
- package/dist/cli/main.js +45 -7
- package/dist/cli/main.js.map +1 -1
- package/dist/mcp/tools.js +2 -2
- package/dist/mcp/tools.js.map +1 -1
- package/dist/shell/guard.d.ts +2 -1
- package/dist/shell/guard.js +11 -4
- package/dist/shell/guard.js.map +1 -1
- package/dist/shell/policy.d.ts +25 -3
- package/dist/shell/policy.js +57 -2
- package/dist/shell/policy.js.map +1 -1
- package/dist/sync/converge.d.ts +27 -0
- package/dist/sync/converge.js +46 -0
- package/dist/sync/converge.js.map +1 -0
- package/dist/sync/daemon.d.ts +4 -0
- package/dist/sync/daemon.js +60 -7
- package/dist/sync/daemon.js.map +1 -1
- package/dist/sync/feed.d.ts +16 -0
- package/dist/sync/feed.js +81 -56
- package/dist/sync/feed.js.map +1 -1
- package/dist/sync/fs-store.js +10 -0
- package/dist/sync/fs-store.js.map +1 -1
- package/dist/sync/hot-path.d.ts +83 -0
- package/dist/sync/hot-path.js +232 -0
- package/dist/sync/hot-path.js.map +1 -0
- package/dist/sync/push.js +7 -31
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/reconcile.d.ts +7 -1
- package/dist/sync/reconcile.js +59 -55
- package/dist/sync/reconcile.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
*Markdown Repos, plexed.*
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
mrplex is not a better notes app. It is a small kernel that leaves your files as files and makes the folder a multi-agent substrate.
|
|
6
|
+
|
|
7
|
+
You keep working with ordinary `.md` with YAML frontmatter. mrplex adds versions, optimistic concurrency, CEL filters, full-text search, optional embeddings, and a link graph bound to document identity so renames do not smash backlinks. CLI, MCP, and REST are the same model with different sockets.
|
|
8
|
+
|
|
9
|
+
If you wanted an Obsidian clone, this is the wrong repo. If you're an Obsidian user, now your agents can utilize and manage your vault through mrplex's MCP interface.
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
@@ -12,247 +16,165 @@ Node ≥ 20.11:
|
|
|
12
16
|
npm install -g mrplex
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
`npx mrplex …` works. From a git checkout: `npm install && npm link`.
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
No config required. With no flags, the CLI uses `./mrplex.db`. Pass `-r <repo>` when a command needs a repo.
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
export MRPLEX_DATABASE=./demo.db
|
|
21
|
-
export MRPLEX_REPO=starship
|
|
22
|
-
```
|
|
23
|
+
## Five minutes on the USS Meridian
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
mrplex config set-database ./demo.db
|
|
28
|
-
mrplex config set-repo starship
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Each setting resolves **flag → env → config file → default**, so one-off overrides never require editing anything.
|
|
32
|
-
|
|
33
|
-
> **From a git checkout?** Run `npm install && npm link` (or `npm run cli -- …`). Every `mrplex …` command below is equivalent to `npx mrplex …`.
|
|
34
|
-
|
|
35
|
-
## A five-minute tour
|
|
36
|
-
|
|
37
|
-
This walkthrough uses the **USS Meridian** — a sample starship knowledge base in [`fixtures/starship/`](fixtures/starship/): crew files, mission records, officer logs, and equipment status, all ordinary Markdown with YAML frontmatter.
|
|
38
|
-
|
|
39
|
-
### 1. Sync the sample corpus
|
|
40
|
-
|
|
41
|
-
Clone the repo (for the fixture files), install, and sync the folder into a fresh mrplex repo:
|
|
25
|
+
The fixture in `fixtures/starship/` is a starship knowledge base: crew, missions, logs, equipment. Ordinary Markdown. The product is the questions you can ask without opening every file.
|
|
42
26
|
|
|
43
27
|
```bash
|
|
44
28
|
git clone https://github.com/usergenic/mrplex
|
|
45
29
|
cd mrplex
|
|
46
|
-
npm install && npm link
|
|
47
|
-
|
|
48
|
-
export MRPLEX_DATABASE=./demo.db
|
|
49
|
-
export MRPLEX_REPO=starship
|
|
30
|
+
npm install && npm link
|
|
50
31
|
|
|
51
32
|
mrplex repos create starship
|
|
52
33
|
mrplex sync fixtures/starship --once -r starship
|
|
53
34
|
```
|
|
54
35
|
|
|
55
|
-
`sync`
|
|
36
|
+
`sync` loads a folder, versions it, and keeps a cursor for two-way updates. Wikilinks, inline links, and frontmatter values written as repo-root paths (`/crew/foo.md`) are indexed automatically.
|
|
56
37
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
### 2. Query it like data
|
|
60
|
-
|
|
61
|
-
Frontmatter is queryable with [CEL](https://github.com/google/cel-spec) filters:
|
|
38
|
+
### Ask the folder like data
|
|
62
39
|
|
|
63
40
|
```bash
|
|
64
|
-
# What's broken
|
|
65
|
-
mrplex query --filter 'status == "damaged" || status == "offline"'
|
|
66
|
-
|
|
67
|
-
# Who reports directly to the captain?
|
|
68
|
-
mrplex query --filter '$has_static("crew/kestrel-vance.md", "reports_to")'
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
```text
|
|
72
|
-
PATH STATUS
|
|
73
|
-
equipment/plasma-manifold-3.md damaged
|
|
74
|
-
equipment/shuttle-corvid.md offline
|
|
75
|
-
…
|
|
76
|
-
|
|
77
|
-
PATH TITLE
|
|
78
|
-
crew/aria-okonkwo.md Commander Aria Okonkwo
|
|
79
|
-
crew/dax-thorne.md Lieutenant Commander Dax Thorne
|
|
80
|
-
crew/quill-vasquez.md Doctor Quill Vasquez
|
|
81
|
-
```
|
|
41
|
+
# What's broken?
|
|
42
|
+
mrplex -r starship query --filter 'status == "damaged" || status == "offline"'
|
|
82
43
|
|
|
83
|
-
|
|
44
|
+
# Who reports to the captain?
|
|
45
|
+
mrplex -r starship query --filter '$has("crew/kestrel-vance.md", "reports_to")'
|
|
84
46
|
|
|
85
|
-
|
|
86
|
-
mrplex query --
|
|
87
|
-
mrplex query --filter 'type == "mission"' --text 'Halloway'
|
|
47
|
+
# Filter AND full-text
|
|
48
|
+
mrplex -r starship query --filter 'type == "mission"' --text 'Halloway'
|
|
88
49
|
```
|
|
89
50
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Markdown links, wikilinks, and frontmatter repo-root paths (`/crew/foo.md`) build a derived index bound to document *identity*, so backlinks survive renames. Query membership in CEL:
|
|
51
|
+
`query` returns lean hits. Default projection is paths, not bodies. Hydrate what you need. That is how an agent stays cheap.
|
|
93
52
|
|
|
94
|
-
|
|
95
|
-
# Every mission indexed by the mission log MOC
|
|
96
|
-
mrplex query --filter '$in_static("moc/missions.md")'
|
|
53
|
+
Filter, text, and semantic compose with AND. Semantic rank is a shortlist, not an oracle. Inspect the top hits; do not treat cosine as authority.
|
|
97
54
|
|
|
98
|
-
|
|
99
|
-
mrplex query --filter '$has_static("equipment/plasma-manifold-3.md")'
|
|
100
|
-
```
|
|
55
|
+
### Follow the graph
|
|
101
56
|
|
|
102
|
-
|
|
57
|
+
Links live on document identity, not on the string you typed. Move a file and the graph still knows who pointed at it. The written text may go stale; the relation does not.
|
|
103
58
|
|
|
104
59
|
```bash
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
The missing officer, his mission, the encounter, and the logs that mention him appear as a connected neighborhood — the kind of thread a flat file listing can't give you.
|
|
60
|
+
# Everything a map-of-content claims
|
|
61
|
+
mrplex -r starship query --filter '$in("moc/missions.md")'
|
|
109
62
|
|
|
110
|
-
|
|
63
|
+
# What touches the damaged manifold?
|
|
64
|
+
mrplex -r starship query --filter '$has("equipment/plasma-manifold-3.md")'
|
|
111
65
|
|
|
112
|
-
|
|
113
|
-
mrplex graph --roots
|
|
66
|
+
# Neighborhood, not a file listing
|
|
67
|
+
mrplex -r starship graph --roots missions/the-hollow-signal.md --degrees 2 --direction both --render summary
|
|
68
|
+
mrplex -r starship graph --roots crew/kestrel-vance.md --degrees 2 --direction out --render mermaid
|
|
114
69
|
```
|
|
115
70
|
|
|
116
|
-
###
|
|
71
|
+
### Write without clobbering
|
|
117
72
|
|
|
118
|
-
Every write inserts a
|
|
73
|
+
Every write inserts a version. `prev_version_id` is optimistic concurrency. Stale prev loses; the current version comes back. Delete is a move to `:deleted/…`. Restore is a normal put.
|
|
119
74
|
|
|
120
75
|
```bash
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
mrplex docs mv equipment/manifold-3.md --prev "$V"
|
|
76
|
+
V=$(mrplex -r starship --json docs get equipment/plasma-manifold-3.md | jq -r .version_id)
|
|
77
|
+
mrplex -r starship docs mv equipment/manifold-3.md --prev "$V"
|
|
124
78
|
|
|
125
|
-
|
|
126
|
-
mrplex
|
|
127
|
-
mrplex links stale
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
History and unified diff between any two versions:
|
|
79
|
+
mrplex -r starship query --filter '$has("equipment/manifold-3.md")'
|
|
80
|
+
mrplex -r starship links stale
|
|
131
81
|
|
|
132
|
-
|
|
133
|
-
mrplex docs
|
|
134
|
-
mrplex docs diff equipment/manifold-3.md --from v1 --to v2
|
|
82
|
+
mrplex -r starship docs history equipment/manifold-3.md
|
|
83
|
+
mrplex -r starship docs diff equipment/manifold-3.md --from v1 --to v2
|
|
135
84
|
```
|
|
136
85
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
### 5. Connect an agent
|
|
86
|
+
## Connect an agent
|
|
140
87
|
|
|
141
|
-
CLI, MCP, and REST are surfaces over
|
|
88
|
+
CLI, MCP, and REST are surfaces over one kernel. For a database only you can touch, local stdio with `--unsafe` is the fastest first attachment:
|
|
142
89
|
|
|
143
90
|
```json
|
|
144
91
|
{
|
|
145
92
|
"mcpServers": {
|
|
146
93
|
"mrplex": {
|
|
147
94
|
"command": "mrplex",
|
|
148
|
-
"args": ["mcp-stdio", "--unsafe", "--database", "./
|
|
149
|
-
"env": { "MRPLEX_REPO": "starship" }
|
|
95
|
+
"args": ["mcp-stdio", "--unsafe", "--database", "./mrplex.db"]
|
|
150
96
|
}
|
|
151
97
|
}
|
|
152
98
|
}
|
|
153
99
|
```
|
|
154
100
|
|
|
155
|
-
|
|
101
|
+
`--unsafe` means full-trust kernel: no auth in-process. That is correct for a private local file. It is not a networked default.
|
|
156
102
|
|
|
157
|
-
|
|
103
|
+
The questions that justify the extra process are relational: *what is broken, who maintains it, which missions were hit?* Query and graph first. `docs get` / `docs_get_many` only for the hits you will actually use.
|
|
104
|
+
|
|
105
|
+
## Policy shell (local or networked)
|
|
106
|
+
|
|
107
|
+
When you want principals — MCP as maintainer, a separate admin for repo create/delete — scaffold a policy and mint keys. No YAML to copy:
|
|
158
108
|
|
|
159
109
|
```bash
|
|
160
|
-
mrplex
|
|
161
|
-
|
|
110
|
+
mrplex policy create --principal brendan --author "Brendan Baldwin <brendan@example.com>"
|
|
111
|
+
# → policy.yaml with admin (operator) + brendan (maintainer)
|
|
112
|
+
|
|
113
|
+
KEY=$(mrplex key mint brendan --policy policy.yaml) # MCP day-to-day
|
|
114
|
+
# mrplex key mint admin --policy policy.yaml # when you need repos create/delete
|
|
115
|
+
|
|
116
|
+
mrplex serve --policy policy.yaml --audit audit.jsonl --port 8321 &
|
|
117
|
+
# or: mrplex mcp-stdio --policy policy.yaml --key "$KEY"
|
|
162
118
|
```
|
|
163
119
|
|
|
164
|
-
|
|
120
|
+
`policy create` writes the minimal file; `policy check` validates it (and can dump a principal's entitlement). Edit the file only when you outgrow the defaults.
|
|
121
|
+
|
|
122
|
+
Three shapes: **embedded** (`serve --policy`), **launcher** (`mcp-stdio --policy`), **fronting proxy** (`proxy --policy --upstream`). Edit the policy and `kill -HUP` to reload. OIDC device flow via `mrplex login`. Details in [docs/archive/security.md](docs/archive/security.md).
|
|
123
|
+
|
|
124
|
+
Never expose `serve --unsafe` to an untrusted network. The flag is loud on purpose.
|
|
165
125
|
|
|
166
126
|
## How it works
|
|
167
127
|
|
|
168
|
-
|
|
|
169
|
-
|
|
170
|
-
| **Document**
|
|
171
|
-
| **Version**
|
|
172
|
-
| **Query**
|
|
173
|
-
| **Link graph** | Derived
|
|
174
|
-
| **Graph**
|
|
175
|
-
| **Surfaces**
|
|
176
|
-
|
|
177
|
-
Two layers: a **full-trust kernel** (no in-engine auth) and an optional **access-and-identity shell** (API keys, OIDC, per-path write policy, audit log). See [docs/archive/security.md](docs/archive/security.md) for trust boundaries and deployment shapes.
|
|
178
|
-
|
|
179
|
-
## Features
|
|
180
|
-
|
|
181
|
-
<details>
|
|
182
|
-
<summary>Full feature list</summary>
|
|
183
|
-
|
|
184
|
-
- **Versioned Markdown store** — every write inserts; `docs.put` handles in-place update and move; any past state is addressable
|
|
185
|
-
- **Byte-exact frontmatter** — `frontmatter_raw` (verbatim YAML) or `frontmatter` (JSON); exactly one; round-trips are byte-exact via raw
|
|
186
|
-
- **Optimistic concurrency** — stale `prev_version_id` → `stale_prev` with current version returned
|
|
187
|
-
- **Deletion as move** — `:deleted/…` paths; idempotent delete; restore via `docs.put`
|
|
188
|
-
- **Unified diff** — `docs.diff`, REST `/diff`, MCP `docs_diff`, CLI `mrplex docs diff`; `patch(1)`-applicable output
|
|
189
|
-
- **CEL filter queries** — frontmatter fields + `$`-intrinsics; `list()` polymorphism for scalar-or-list fields
|
|
190
|
-
- **Link graph** — inline, wikilink, and frontmatter path extraction (no per-field config); backlinks survive renames; set algebra (`$in("moc/**") && !$in("moc/draft.md")`); `$in_static` for link-only membership; `links stale` / `repair` / `backfill`
|
|
191
|
-
- **Graph exploration** — BFS with direction lens, visibility filter, graph-only `$degrees` intrinsic; CLI `--render summary|yaml|mermaid|json`
|
|
192
|
-
- **Full-text search** — SQLite FTS5 or Postgres `websearch_to_tsquery`; composes with filter via AND
|
|
193
|
-
- **Semantic search** — pluggable `--embedder` hook; chunker + backlog worker; `$semantic_score` in `select`; no hook → `semantic_unavailable`
|
|
194
|
-
- **HTTP surfaces** — MCP (Streamable HTTP + STDIO), REST with `If-Match` / content negotiation / `MOVE`
|
|
195
|
-
- **CLI** — thin MCP client; `--database` local or `--server` remote; identical commands over both
|
|
196
|
-
- **Storage** — SQLite (default) or Postgres+pgvector; same kernel test suite on both
|
|
197
|
-
- **Path policy** — configurable sigils and disallowed chars; NFC + case-insensitive identity, case-preserved storage
|
|
198
|
-
- **Canonical paths** — API responses use slashless repo-relative paths; leading `/` accepted as root alias on input only
|
|
199
|
-
|
|
200
|
-
Prior design docs in [docs/archive/](docs/archive/) may be out of date where later work supersedes them.
|
|
201
|
-
|
|
202
|
-
</details>
|
|
203
|
-
|
|
204
|
-
## Authentication
|
|
205
|
-
|
|
206
|
-
For anything beyond single-user local use, run the **authenticating shell** — `serve --policy`. It reads declarative YAML (roles, principals, grants, key hashes, OIDC bindings), authenticates each request, and dispatches against a per-principal *guarded* kernel: read visibility narrowed, writes enforced per-path, author derived from the credential, every call audited.
|
|
207
|
-
|
|
208
|
-
```yaml
|
|
209
|
-
# policy.yaml
|
|
210
|
-
roles:
|
|
211
|
-
editor:
|
|
212
|
-
grants:
|
|
213
|
-
- repo: notes
|
|
214
|
-
read: "**"
|
|
215
|
-
write: ["drafts/**", "inbox/**"]
|
|
216
|
-
operator:
|
|
217
|
-
grants:
|
|
218
|
-
- { repo: "*", read: "**", write: "**" }
|
|
219
|
-
destructive: true
|
|
220
|
-
|
|
221
|
-
principals:
|
|
222
|
-
brendan:
|
|
223
|
-
author: Brendan Baldwin <brendan@example.com>
|
|
224
|
-
roles: [operator]
|
|
225
|
-
keys:
|
|
226
|
-
- sha256:... # `mrplex key mint brendan --policy policy.yaml`
|
|
227
|
-
ann:
|
|
228
|
-
roles: [editor]
|
|
229
|
-
oidc: { email: ann@example.com }
|
|
230
|
-
```
|
|
128
|
+
| Piece | Job |
|
|
129
|
+
| -------------- | ---------------------------------------------------------------------------------------- |
|
|
130
|
+
| **Document** | One Markdown file with YAML frontmatter, addressed by repo-relative path |
|
|
131
|
+
| **Version** | Every write appends. `prev_version_id` rejects stale writers |
|
|
132
|
+
| **Query** | CEL over frontmatter + `$path` / `$body` / `$updated_at`; AND with FTS and semantic |
|
|
133
|
+
| **Link graph** | Derived from inline links, wikilinks, and frontmatter repo-root paths. Bound to identity |
|
|
134
|
+
| **Graph** | BFS neighborhood. *How* things connect, not only *which* match |
|
|
135
|
+
| **Surfaces** | CLI (`--database` or `--server`), MCP (`/mcp` or `mcp-stdio`), REST |
|
|
231
136
|
|
|
232
|
-
|
|
233
|
-
mrplex key mint brendan --policy policy.yaml
|
|
234
|
-
mrplex serve --policy policy.yaml --audit audit.jsonl --port 8321 &
|
|
235
|
-
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:8321/repos
|
|
236
|
-
```
|
|
137
|
+
Two layers: a **full-trust kernel** and an optional **access-and-identity shell** (keys, OIDC, per-path grants, audit). The kernel does not pretend to be a user system. Local CLI against a database file is full-trust — possession of the file is root.
|
|
237
138
|
|
|
238
|
-
|
|
139
|
+
What ships today:
|
|
140
|
+
|
|
141
|
+
- Versioned Markdown store; byte-exact `frontmatter_raw` or structured `frontmatter`
|
|
142
|
+
- Optimistic concurrency; delete-as-move; unified diff
|
|
143
|
+
- CEL + `list()` for scalar-or-list fields; `$in` / `$has` / `$backlinks` / `$links` (membership is a filter, not a separate CLI flag)
|
|
144
|
+
- FTS5 or Postgres `websearch_to_tsquery`
|
|
145
|
+
- Pluggable embedder; no hook → `semantic_unavailable` instead of silent junk vectors
|
|
146
|
+
- SQLite default or Postgres+pgvector; same kernel suite on both
|
|
147
|
+
- NFC + case-insensitive identity, case-preserved storage
|
|
239
148
|
|
|
240
149
|
## Embeddings
|
|
241
150
|
|
|
242
|
-
mrplex never calls an embedding
|
|
151
|
+
mrplex never calls an embedding vendor. Wire a hook:
|
|
243
152
|
|
|
244
153
|
```bash
|
|
245
154
|
npm install -g @mrplex/embedder
|
|
246
|
-
export MRPLEX_EMBEDDER=mrplex-embedder # or: mrplex config set-embedder mrplex-embedder
|
|
247
155
|
|
|
248
156
|
mrplex serve --unsafe --embedder mrplex-embedder
|
|
249
157
|
mrplex embed backfill -r starship
|
|
250
|
-
mrplex
|
|
251
|
-
|
|
252
|
-
|
|
158
|
+
mrplex -r starship query --semantic 'distress beacon star map'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
With `--server`, configure the embedder on the host. Protocol: [packages/embedder/README.md](packages/embedder/README.md).
|
|
162
|
+
|
|
163
|
+
Use semantic search to generate candidates. Then filter, hydrate, and read. Rank without inspection is how agents launder a guess into a citation.
|
|
164
|
+
|
|
165
|
+
## Config & environment
|
|
166
|
+
|
|
167
|
+
Resolution is always **flag → env → config file → default**. Defaults: database `./mrplex.db`, no default repo (pass `-r`).
|
|
168
|
+
|
|
169
|
+
Persist defaults when you are tired of typing flags:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
mrplex config set-database ./mrplex.db
|
|
173
|
+
mrplex config set-repo starship
|
|
174
|
+
mrplex config set-author "Brendan Baldwin <brendan@example.com>"
|
|
253
175
|
```
|
|
254
176
|
|
|
255
|
-
|
|
177
|
+
Or with env vars: `MRPLEX_DATABASE`, `MRPLEX_REPO`, `MRPLEX_AUTHOR`, `MRPLEX_EMBEDDER`.
|
|
256
178
|
|
|
257
179
|
## Development
|
|
258
180
|
|
|
@@ -263,22 +185,15 @@ npm test
|
|
|
263
185
|
npm run typecheck
|
|
264
186
|
npm run lint
|
|
265
187
|
npm run build
|
|
266
|
-
```
|
|
267
188
|
|
|
268
|
-
Seed the full dev fixture set (notes + starship):
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
189
|
npm run seed -- --database ./mrplex.db
|
|
272
190
|
```
|
|
273
191
|
|
|
274
|
-
CI
|
|
275
|
-
|
|
276
|
-
### Postgres locally
|
|
192
|
+
CI: typecheck + lint + tests on Ubuntu and macOS × Node 20 and 22, plus Postgres+pgvector parity. If `better-sqlite3` throws `NODE_MODULE_VERSION`, run `npm rebuild better-sqlite3`.
|
|
277
193
|
|
|
278
194
|
```bash
|
|
279
195
|
npm run pg:up
|
|
280
|
-
|
|
281
|
-
mrplex serve --unsafe
|
|
196
|
+
mrplex --database postgres://mrplex:mrplex@localhost:5432/mrplex serve --unsafe
|
|
282
197
|
```
|
|
283
198
|
|
|
284
199
|
## License
|
package/dist/cli/main.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* is forwarded verbatim as a bearer for a shell fronting the server; mrplex
|
|
14
14
|
* itself ignores it. serve deliberately bypasses the seam — it IS the server.
|
|
15
15
|
*/
|
|
16
|
-
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
18
18
|
import { parseDocument as parseYamlDocument } from "yaml";
|
|
19
19
|
import { openLocalClient } from "../client/local.js";
|
|
@@ -35,7 +35,7 @@ import { fileAuditSink } from "../shell/audit.js";
|
|
|
35
35
|
import { mintKey } from "../shell/keys.js";
|
|
36
36
|
import { deviceFlowLogin, loadTokenSet, saveTokenSet, } from "../shell/login.js";
|
|
37
37
|
import { createOidcVerifier } from "../shell/oidc.js";
|
|
38
|
-
import { PolicyError, compile, loadPolicyFile } from "../shell/policy.js";
|
|
38
|
+
import { PolicyError, compile, loadPolicyFile, parsePolicy, scaffoldPolicyYaml, } from "../shell/policy.js";
|
|
39
39
|
import { startProxyServer } from "../shell/proxy.js";
|
|
40
40
|
import { startShellServer } from "../shell/serve.js";
|
|
41
41
|
import { startShellStdio } from "../shell/stdio.js";
|
|
@@ -359,7 +359,7 @@ function renderEntitlement(principalId, e) {
|
|
|
359
359
|
.join("");
|
|
360
360
|
return (`principal: ${principalId}\n` +
|
|
361
361
|
`author: ${e.author}\n` +
|
|
362
|
-
`destructive: ${e.destructive} impersonate: ${e.impersonate}\n` +
|
|
362
|
+
`maintain: ${e.maintain} destructive: ${e.destructive} impersonate: ${e.impersonate}\n` +
|
|
363
363
|
`read:\n${claims(e.read)}` +
|
|
364
364
|
`write:\n${claims(e.write)}`);
|
|
365
365
|
}
|
|
@@ -745,9 +745,9 @@ function buildProgram() {
|
|
|
745
745
|
}
|
|
746
746
|
});
|
|
747
747
|
// -------- key (policy tooling) --------
|
|
748
|
-
// `key mint` and `policy check` read/edit the policy file
|
|
749
|
-
// they take
|
|
750
|
-
// "Policy tooling").
|
|
748
|
+
// `key mint`, `policy create`, and `policy check` read/edit the policy file
|
|
749
|
+
// by definition, so they take a file path directly and never touch the serve
|
|
750
|
+
// gate (auth-shell §1 "Policy tooling").
|
|
751
751
|
const key = program.command("key").description("API-key tooling for the auth shell");
|
|
752
752
|
key
|
|
753
753
|
.command("mint <principal>")
|
|
@@ -774,6 +774,44 @@ function buildProgram() {
|
|
|
774
774
|
});
|
|
775
775
|
// -------- policy --------
|
|
776
776
|
const policy = program.command("policy").description("policy-file tooling for the auth shell");
|
|
777
|
+
policy
|
|
778
|
+
.command("create [file]")
|
|
779
|
+
.description("write a starter policy.yaml (admin + maintainer)")
|
|
780
|
+
.option("--principal <id>", "maintainer principal id (MCP day-to-day)", "local")
|
|
781
|
+
.option("--force", "overwrite the file if it already exists", false)
|
|
782
|
+
.action(function (file) {
|
|
783
|
+
const localOpts = this.opts();
|
|
784
|
+
const gopts = this.optsWithGlobals();
|
|
785
|
+
try {
|
|
786
|
+
const path = file ?? "policy.yaml";
|
|
787
|
+
if (existsSync(path) && !localOpts.force) {
|
|
788
|
+
const err = new Error(`policy: ${path} already exists (pass --force to overwrite)`);
|
|
789
|
+
err.code = "cli_usage";
|
|
790
|
+
throw err;
|
|
791
|
+
}
|
|
792
|
+
const principal = localOpts.principal;
|
|
793
|
+
// Reuse the global --author / MRPLEX_AUTHOR (same meaning: write stamp).
|
|
794
|
+
const author = gopts.author ?? `${principal} <${principal}@localhost>`;
|
|
795
|
+
const text = scaffoldPolicyYaml({ principal, author });
|
|
796
|
+
// Refuse to write anything that wouldn't load — scaffold is pure, but
|
|
797
|
+
// this catches template/option bugs before the operator discovers them.
|
|
798
|
+
parsePolicy(text);
|
|
799
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
800
|
+
writeFileSync(tmp, text);
|
|
801
|
+
renameSync(tmp, path);
|
|
802
|
+
process.stderr.write(`wrote ${path}\n` +
|
|
803
|
+
`next: mrplex key mint ${principal} --policy ${path} # MCP\n` +
|
|
804
|
+
` mrplex key mint admin --policy ${path} # repo create/delete\n` +
|
|
805
|
+
` mrplex serve --policy ${path} --database <url>\n`);
|
|
806
|
+
}
|
|
807
|
+
catch (err) {
|
|
808
|
+
if (err instanceof PolicyError) {
|
|
809
|
+
process.stderr.write(`policy: ${err.message}\n`);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
reportError(err);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
777
815
|
policy
|
|
778
816
|
.command("check [principal]")
|
|
779
817
|
.description("validate a policy file; with a principal, print its effective entitlement")
|
|
@@ -1404,7 +1442,7 @@ function buildProgram() {
|
|
|
1404
1442
|
.option("--once", "run startup reconciliation once, then exit (no watcher)", false)
|
|
1405
1443
|
.option("--interval <ms>", "feed poll interval in ms (daemon; default 5000)", parsePositiveInt)
|
|
1406
1444
|
.option("--debounce <ms>", "burst debounce in ms (daemon; default 5000)", parsePositiveInt)
|
|
1407
|
-
.option("--settle <ms>", "
|
|
1445
|
+
.option("--settle <ms>", "minimum mtime age in ms before sync touches a path (default 0)", parsePositiveInt)
|
|
1408
1446
|
.option("--include <glob>", "include glob (default **/*.md); repeat to add more", (value, prev) => [...(prev ?? []), value])
|
|
1409
1447
|
.option("--exclude <glob>", "exclude glob (wins over include); repeat to add more", (value, prev) => [...(prev ?? []), value])
|
|
1410
1448
|
.option("--dry-run", "report the actions a reconciliation would take, changing nothing", false)
|