morphdb 0.1.2__tar.gz → 0.1.4__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.
Files changed (45) hide show
  1. {morphdb-0.1.2 → morphdb-0.1.4}/PKG-INFO +178 -75
  2. {morphdb-0.1.2 → morphdb-0.1.4}/README.md +177 -74
  3. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/__init__.py +1 -1
  4. morphdb-0.1.4/morphdb/cli/__init__.py +31 -0
  5. morphdb-0.1.4/morphdb/cli/dashboard.py +879 -0
  6. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/main.py +88 -15
  7. morphdb-0.1.4/morphdb/cli/mcp.py +519 -0
  8. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/skill.py +10 -8
  9. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/db.py +31 -0
  10. morphdb-0.1.4/morphdb/fieldindex.py +204 -0
  11. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/fieldtypes.py +13 -3
  12. morphdb-0.1.4/morphdb/objects.py +667 -0
  13. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/routes.py +13 -7
  14. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/schema.py +19 -0
  15. morphdb-0.1.4/morphdb/skill/SKILL.md +311 -0
  16. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/PKG-INFO +178 -75
  17. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/SOURCES.txt +5 -0
  18. {morphdb-0.1.2 → morphdb-0.1.4}/pyproject.toml +1 -1
  19. {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_cli.py +42 -9
  20. {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_core.py +5 -3
  21. morphdb-0.1.4/tests/test_field_index.py +341 -0
  22. {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_hardening.py +1 -1
  23. morphdb-0.1.4/tests/test_includes.py +99 -0
  24. morphdb-0.1.4/tests/test_mcp.py +447 -0
  25. {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_relations.py +136 -1
  26. morphdb-0.1.2/morphdb/cli/__init__.py +0 -22
  27. morphdb-0.1.2/morphdb/cli/dashboard.py +0 -134
  28. morphdb-0.1.2/morphdb/objects.py +0 -395
  29. morphdb-0.1.2/morphdb/skill/SKILL.md +0 -258
  30. {morphdb-0.1.2 → morphdb-0.1.4}/LICENSE +0 -0
  31. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/__main__.py +0 -0
  32. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/apps.py +0 -0
  33. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/associations.py +0 -0
  34. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/cli/service.py +0 -0
  35. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/errors.py +0 -0
  36. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/router.py +0 -0
  37. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/server.py +0 -0
  38. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/skill/scripts/morphdb_schema.py +0 -0
  39. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb/util.py +0 -0
  40. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/dependency_links.txt +0 -0
  41. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/entry_points.txt +0 -0
  42. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/requires.txt +0 -0
  43. {morphdb-0.1.2 → morphdb-0.1.4}/morphdb.egg-info/top_level.txt +0 -0
  44. {morphdb-0.1.2 → morphdb-0.1.4}/setup.cfg +0 -0
  45. {morphdb-0.1.2 → morphdb-0.1.4}/tests/test_apps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphdb
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints.
5
5
  Author: morphdb contributors
6
6
  License: MIT
@@ -30,21 +30,123 @@ Dynamic: license-file
30
30
  **A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
31
31
 
32
32
  Reshape the data model as fast as your coding agent iterates — the frontend
33
- keeps calling the same small set of generic, deterministic endpoints.
33
+ keeps calling the same small set of generic, deterministic endpoints. One
34
+ process hosts many isolated apps (one per site), zero dependencies, backed by
35
+ SQLite.
34
36
 
37
+ 📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install morphdb
35
43
  ```
36
- you (the coding agent) the frontend you build
37
- ────────────────────── ──────────────────────
38
- reshape the schema freely │ calls fixed generic endpoints
39
- PUT /schema/{type} │ POST /objects/{type}
40
- GET /schema │ GET /objects/{type}?field=…
41
- DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
42
- │ │
43
- └────────────── MorphDB ───────────┘
44
- (one process · many apps · SQLite)
45
- every call: X-App-Key: <app>
44
+
45
+ Manage the local server with the `morphdb` CLI:
46
+
47
+ ```bash
48
+ morphdb start # run in the background (default 127.0.0.1:8787)
49
+ morphdb status # running? where? how many apps?
50
+ morphdb stop # stop it
51
+ morphdb run # run in the foreground instead (blocking)
52
+ morphdb dashboard # read-only web view of every app + its tables
53
+ morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
54
+ ```
55
+
56
+ Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
57
+ `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
58
+ `--host`, `--port`, `--db`. From a source checkout with no install, the
59
+ foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
60
+
61
+ To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
62
+ to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
63
+
64
+ **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
65
+ `https://db.example.com`) and the schema CLI — plus any frontend that reads
66
+ `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
67
+ of localhost. It's a client-side setting that names a *backend*, not a database
68
+ connection string.
69
+
70
+ ## Use it
71
+
72
+ With the server running (`morphdb start`):
73
+
74
+ ```bash
75
+ BASE=http://127.0.0.1:8787
76
+
77
+ # 0. register an app; send its key as X-App-Key on every schema/object call
78
+ curl -X POST $BASE/app -d '{"key":"my-site"}'
79
+ H="X-App-Key: my-site"
80
+
81
+ # 1. define types + a relation
82
+ curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
83
+ curl -X PUT $BASE/schema/task -H "$H" -d '{
84
+ "fields": {"title":"string",
85
+ "done":{"type":"boolean","index":true},
86
+ "priority":{"type":"number","index":true}},
87
+ "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
88
+
89
+ # 2. create + read + query
90
+ U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
91
+ curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
92
+ curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
93
+ curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
94
+
95
+ # 3. morph the schema later — existing rows just gain the new field as null
96
+ curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
97
+ ```
98
+
99
+ See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
100
+
101
+ ## Command-line interface
102
+
103
+ `morphdb` runs the server as a **background service** — `start` launches it
104
+ detached and hands your terminal straight back; `status` / `stop` find it again
105
+ via a pid file under the state dir.
106
+
107
+ | Command | What it does |
108
+ | --- | --- |
109
+ | `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
110
+ | `morphdb status` | Is it running? URL, pid, health, and app count. |
111
+ | `morphdb stop` | Stop the background server. |
112
+ | `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
113
+ | `morphdb run` | Run in the **foreground** (blocking) instead. |
114
+ | `morphdb dashboard` | Open a read-only web view of every app and its tables. |
115
+ | `morphdb install-skill` | Install the bundled Claude Code skill (below). |
116
+ | `morphdb --version` | Print the version. |
117
+
118
+ `start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
119
+ and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
120
+ `dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
121
+ state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
122
+ `$MORPHDB_HOME`.
123
+
124
+ ```bash
125
+ morphdb start # background, default 127.0.0.1:8787
126
+ morphdb start --port 9000 --db ./my.sqlite3
127
+ morphdb status # -> running (pid …) at http://… [healthy]
128
+ morphdb dashboard # opens http://127.0.0.1:8788
129
+ morphdb stop
130
+ morphdb run # foreground instead (Ctrl-C to quit)
46
131
  ```
47
132
 
133
+ ### Install the Claude Code skill
134
+
135
+ `install-skill` writes the bundled MorphDB skill into a Claude skills directory,
136
+ so a coding agent automatically reaches for MorphDB when building a data-backed
137
+ site:
138
+
139
+ ```bash
140
+ morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
141
+ morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
142
+ morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
143
+ ```
144
+
145
+ It installs the skill **bundled in the installed package** (not live from
146
+ GitHub) and is **idempotent** — re-running overwrites with the current version.
147
+ To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
148
+ Claude Code afterward to pick it up.
149
+
48
150
  ## Why
49
151
 
50
152
  AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
@@ -58,6 +160,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
58
160
  **no migration, no row rewrite, no downtime** — regardless of how much data
59
161
  exists. Meanwhile the frontend talks to generic endpoints that never change.
60
162
 
163
+ ```
164
+ you (the coding agent) the frontend you build
165
+ ────────────────────── ──────────────────────
166
+ reshape the schema freely │ calls fixed generic endpoints
167
+ PUT /schema/{type} │ POST /objects/{type}
168
+ GET /schema │ GET /objects/{type}?field=…
169
+ DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
170
+ │ │
171
+ └────────────── MorphDB ───────────┘
172
+ (one process · many apps · SQLite)
173
+ every call: X-App-Key: <app>
174
+ ```
175
+
61
176
  ## The shape of it
62
177
 
63
178
  One MorphDB process hosts **many apps** (one per website), fully isolated from
@@ -126,63 +241,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
126
241
  > Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
127
242
  > horizontal scale, or production durability guarantees.
128
243
 
129
- ## Install / run
130
-
131
- ```bash
132
- pip install morphdb
133
- ```
134
-
135
- Manage the local server with the `morphdb` CLI:
136
-
137
- ```bash
138
- morphdb start # run in the background (default 127.0.0.1:8787)
139
- morphdb status # running? where? how many apps?
140
- morphdb stop # stop it
141
- morphdb run # run in the foreground instead (blocking)
142
- morphdb dashboard # read-only web view of every app + its tables
143
- morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
144
- ```
145
-
146
- Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
147
- `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
148
- `--host`, `--port`, `--db`. From a source checkout with no install, the
149
- foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
150
-
151
- Then: `curl http://127.0.0.1:8787/help` for a live reference.
152
-
153
- **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
154
- `https://db.example.com`) and the schema CLI — plus any frontend that reads
155
- `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
156
- of localhost. It's a client-side setting that names a *backend*, not a database
157
- connection string.
158
-
159
- ## Quickstart
160
-
161
- ```bash
162
- BASE=http://127.0.0.1:8787
163
-
164
- # 0. register an app; send its key as X-App-Key on every schema/object call
165
- curl -X POST $BASE/app -d '{"key":"my-site"}'
166
- H="X-App-Key: my-site"
167
-
168
- # 1. define types + a relation
169
- curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
170
- curl -X PUT $BASE/schema/task -H "$H" -d '{
171
- "fields": {"title":"string","done":"boolean","priority":"number"},
172
- "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
173
-
174
- # 2. create + read + query
175
- U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
176
- curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
177
- curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
178
- curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
179
-
180
- # 3. morph the schema later — existing rows just gain the new field as null
181
- curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
182
- ```
183
-
184
- See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
185
-
186
244
  ## Data model
187
245
 
188
246
  | Concept | What it is |
@@ -267,15 +325,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
267
325
 
268
326
  ### Query operators
269
327
 
270
- Append `__op` to a field name: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`,
271
- `contains` (substring), `in` (comma-separated), `exists` (`true`/`false`).
272
- Filtering is on **fields**, not relations.
328
+ A field is filterable/sortable only if its schema marks it **`"index": true`**
329
+ (opt-in, default off). Filtering or sorting an un-indexed field returns a 400
330
+ telling you to index it; turning the flag on backfills existing objects
331
+ automatically, turning it off is instant. `json` fields can't be indexed.
332
+
333
+ Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
334
+ `lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
335
+ (`true`/`false`).
273
336
 
274
337
  ```
338
+ # priority, title, done, status all declared with "index": true
275
339
  GET /objects/task?priority__gte=3&title__contains=buy&done=false
276
- GET /objects/task?status__in=open,blocked&sort=_created_at&order=desc&limit=50
340
+ GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
277
341
  ```
278
342
 
343
+ You can also filter by a **relation** — treat it like an ORM foreign key, not a
344
+ manual join. Filtering by a relation matches objects linked to a given neighbor,
345
+ and resolves through the indexed edge table (so it is index-backed):
346
+
347
+ | Query | Meaning |
348
+ | --- | --- |
349
+ | `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
350
+ | `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
351
+ | `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
352
+ | `?assignee__exists=true` | has any `assignee` (`false` → has none) |
353
+
354
+ ```
355
+ # "stages of business X that are still in build" — relation + field, one query
356
+ GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
357
+ ```
358
+
359
+ Relation filters compose with field filters, `sort`, and pagination. Scalar
360
+ comparisons (`gt`/`lt`/`contains`) are field-only; relations support
361
+ `eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
362
+ field** — you keep one-read traversal *and* get filtering, indexed, for free.
363
+
364
+ ### Including related objects
365
+
366
+ By default a relation reads back as a guid (to-one) or list of guids (to-many).
367
+ Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
368
+ into the full neighbor objects, nested Prisma-style:
369
+
370
+ ```
371
+ GET /objects/post?include=author,comments,comments.author
372
+ # each post.author becomes a full user; post.comments a list of full comments,
373
+ # and each comment.author a full user too.
374
+ ```
375
+
376
+ Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
377
+ and batched (one query per relation per level — no N+1). Writes stay flat: create
378
+ and update with guids, never nested objects.
379
+
279
380
  ## Errors
280
381
 
281
382
  JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
@@ -310,8 +411,10 @@ allowed, `413` body too large, `500` internal.
310
411
  old type simply reads as unset (the field's default, or null) until it's
311
412
  written again; reads and queries apply this rule identically, so they always
312
413
  agree. Re-adding a dropped field at the same type recovers its values.
313
- - **Filtering is field-only.** Query operators apply to raw fields; relations
314
- are read/written on the object body but not filtered server-side (yet).
414
+ - **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
415
+ can be filtered or sorted (it gets a row in the indexed `field_index` table);
416
+ an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
417
+ are always filterable via the indexed edge table — no flag needed.
315
418
  - **Integer magnitude.** Numbers are stored and read back exactly at any size.
316
419
  Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
317
420
  SQLite limitation), so equality/range queries on such huge integers may be
@@ -3,21 +3,123 @@
3
3
  **A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
4
4
 
5
5
  Reshape the data model as fast as your coding agent iterates — the frontend
6
- keeps calling the same small set of generic, deterministic endpoints.
6
+ keeps calling the same small set of generic, deterministic endpoints. One
7
+ process hosts many isolated apps (one per site), zero dependencies, backed by
8
+ SQLite.
7
9
 
10
+ 📖 **[Visual explainer → morphdb.pages.dev](https://morphdb.pages.dev)** — the whole idea (schema-fluid, API-stable), the agent/frontend split, relations, and how Claude plugs in over MCP, on one page.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install morphdb
8
16
  ```
9
- you (the coding agent) the frontend you build
10
- ────────────────────── ──────────────────────
11
- reshape the schema freely │ calls fixed generic endpoints
12
- PUT /schema/{type} │ POST /objects/{type}
13
- GET /schema │ GET /objects/{type}?field=…
14
- DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
15
- │ │
16
- └────────────── MorphDB ───────────┘
17
- (one process · many apps · SQLite)
18
- every call: X-App-Key: <app>
17
+
18
+ Manage the local server with the `morphdb` CLI:
19
+
20
+ ```bash
21
+ morphdb start # run in the background (default 127.0.0.1:8787)
22
+ morphdb status # running? where? how many apps?
23
+ morphdb stop # stop it
24
+ morphdb run # run in the foreground instead (blocking)
25
+ morphdb dashboard # read-only web view of every app + its tables
26
+ morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
27
+ ```
28
+
29
+ Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
30
+ `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
31
+ `--host`, `--port`, `--db`. From a source checkout with no install, the
32
+ foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
33
+
34
+ To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
35
+ to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
36
+
37
+ **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
38
+ `https://db.example.com`) and the schema CLI — plus any frontend that reads
39
+ `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
40
+ of localhost. It's a client-side setting that names a *backend*, not a database
41
+ connection string.
42
+
43
+ ## Use it
44
+
45
+ With the server running (`morphdb start`):
46
+
47
+ ```bash
48
+ BASE=http://127.0.0.1:8787
49
+
50
+ # 0. register an app; send its key as X-App-Key on every schema/object call
51
+ curl -X POST $BASE/app -d '{"key":"my-site"}'
52
+ H="X-App-Key: my-site"
53
+
54
+ # 1. define types + a relation
55
+ curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
56
+ curl -X PUT $BASE/schema/task -H "$H" -d '{
57
+ "fields": {"title":"string",
58
+ "done":{"type":"boolean","index":true},
59
+ "priority":{"type":"number","index":true}},
60
+ "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
61
+
62
+ # 2. create + read + query
63
+ U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
64
+ curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
65
+ curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
66
+ curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
67
+
68
+ # 3. morph the schema later — existing rows just gain the new field as null
69
+ curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
70
+ ```
71
+
72
+ See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
73
+
74
+ ## Command-line interface
75
+
76
+ `morphdb` runs the server as a **background service** — `start` launches it
77
+ detached and hands your terminal straight back; `status` / `stop` find it again
78
+ via a pid file under the state dir.
79
+
80
+ | Command | What it does |
81
+ | --- | --- |
82
+ | `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
83
+ | `morphdb status` | Is it running? URL, pid, health, and app count. |
84
+ | `morphdb stop` | Stop the background server. |
85
+ | `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
86
+ | `morphdb run` | Run in the **foreground** (blocking) instead. |
87
+ | `morphdb dashboard` | Open a read-only web view of every app and its tables. |
88
+ | `morphdb install-skill` | Install the bundled Claude Code skill (below). |
89
+ | `morphdb --version` | Print the version. |
90
+
91
+ `start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
92
+ and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
93
+ `dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
94
+ state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
95
+ `$MORPHDB_HOME`.
96
+
97
+ ```bash
98
+ morphdb start # background, default 127.0.0.1:8787
99
+ morphdb start --port 9000 --db ./my.sqlite3
100
+ morphdb status # -> running (pid …) at http://… [healthy]
101
+ morphdb dashboard # opens http://127.0.0.1:8788
102
+ morphdb stop
103
+ morphdb run # foreground instead (Ctrl-C to quit)
19
104
  ```
20
105
 
106
+ ### Install the Claude Code skill
107
+
108
+ `install-skill` writes the bundled MorphDB skill into a Claude skills directory,
109
+ so a coding agent automatically reaches for MorphDB when building a data-backed
110
+ site:
111
+
112
+ ```bash
113
+ morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
114
+ morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
115
+ morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
116
+ ```
117
+
118
+ It installs the skill **bundled in the installed package** (not live from
119
+ GitHub) and is **idempotent** — re-running overwrites with the current version.
120
+ To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
121
+ Claude Code afterward to pick it up.
122
+
21
123
  ## Why
22
124
 
23
125
  AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
@@ -31,6 +133,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
31
133
  **no migration, no row rewrite, no downtime** — regardless of how much data
32
134
  exists. Meanwhile the frontend talks to generic endpoints that never change.
33
135
 
136
+ ```
137
+ you (the coding agent) the frontend you build
138
+ ────────────────────── ──────────────────────
139
+ reshape the schema freely │ calls fixed generic endpoints
140
+ PUT /schema/{type} │ POST /objects/{type}
141
+ GET /schema │ GET /objects/{type}?field=…
142
+ DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
143
+ │ │
144
+ └────────────── MorphDB ───────────┘
145
+ (one process · many apps · SQLite)
146
+ every call: X-App-Key: <app>
147
+ ```
148
+
34
149
  ## The shape of it
35
150
 
36
151
  One MorphDB process hosts **many apps** (one per website), fully isolated from
@@ -99,63 +214,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
99
214
  > Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
100
215
  > horizontal scale, or production durability guarantees.
101
216
 
102
- ## Install / run
103
-
104
- ```bash
105
- pip install morphdb
106
- ```
107
-
108
- Manage the local server with the `morphdb` CLI:
109
-
110
- ```bash
111
- morphdb start # run in the background (default 127.0.0.1:8787)
112
- morphdb status # running? where? how many apps?
113
- morphdb stop # stop it
114
- morphdb run # run in the foreground instead (blocking)
115
- morphdb dashboard # read-only web view of every app + its tables
116
- morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
117
- ```
118
-
119
- Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
120
- `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
121
- `--host`, `--port`, `--db`. From a source checkout with no install, the
122
- foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
123
-
124
- Then: `curl http://127.0.0.1:8787/help` for a live reference.
125
-
126
- **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
127
- `https://db.example.com`) and the schema CLI — plus any frontend that reads
128
- `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
129
- of localhost. It's a client-side setting that names a *backend*, not a database
130
- connection string.
131
-
132
- ## Quickstart
133
-
134
- ```bash
135
- BASE=http://127.0.0.1:8787
136
-
137
- # 0. register an app; send its key as X-App-Key on every schema/object call
138
- curl -X POST $BASE/app -d '{"key":"my-site"}'
139
- H="X-App-Key: my-site"
140
-
141
- # 1. define types + a relation
142
- curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
143
- curl -X PUT $BASE/schema/task -H "$H" -d '{
144
- "fields": {"title":"string","done":"boolean","priority":"number"},
145
- "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
146
-
147
- # 2. create + read + query
148
- U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
149
- curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
150
- curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
151
- curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
152
-
153
- # 3. morph the schema later — existing rows just gain the new field as null
154
- curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
155
- ```
156
-
157
- See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
158
-
159
217
  ## Data model
160
218
 
161
219
  | Concept | What it is |
@@ -240,15 +298,58 @@ Every schema and object request must send the app key as the `X-App-Key` header
240
298
 
241
299
  ### Query operators
242
300
 
243
- Append `__op` to a field name: `eq` (default), `ne`, `gt`, `gte`, `lt`, `lte`,
244
- `contains` (substring), `in` (comma-separated), `exists` (`true`/`false`).
245
- Filtering is on **fields**, not relations.
301
+ A field is filterable/sortable only if its schema marks it **`"index": true`**
302
+ (opt-in, default off). Filtering or sorting an un-indexed field returns a 400
303
+ telling you to index it; turning the flag on backfills existing objects
304
+ automatically, turning it off is instant. `json` fields can't be indexed.
305
+
306
+ Append `__op` to an **indexed field** name: `eq` (default), `ne`, `gt`, `gte`,
307
+ `lt`, `lte`, `contains` (substring), `in` (comma-separated), `exists`
308
+ (`true`/`false`).
246
309
 
247
310
  ```
311
+ # priority, title, done, status all declared with "index": true
248
312
  GET /objects/task?priority__gte=3&title__contains=buy&done=false
249
- GET /objects/task?status__in=open,blocked&sort=_created_at&order=desc&limit=50
313
+ GET /objects/task?status__in=open,blocked&sort=priority&order=desc&limit=50
250
314
  ```
251
315
 
316
+ You can also filter by a **relation** — treat it like an ORM foreign key, not a
317
+ manual join. Filtering by a relation matches objects linked to a given neighbor,
318
+ and resolves through the indexed edge table (so it is index-backed):
319
+
320
+ | Query | Meaning |
321
+ | --- | --- |
322
+ | `?assignee=<guid>` | objects whose `assignee` is / includes that neighbor |
323
+ | `?assignee__in=<g1>,<g2>` | linked to any of those neighbors |
324
+ | `?assignee__ne=<guid>` | not linked to that neighbor (includes unlinked) |
325
+ | `?assignee__exists=true` | has any `assignee` (`false` → has none) |
326
+
327
+ ```
328
+ # "stages of business X that are still in build" — relation + field, one query
329
+ GET /objects/stage?business=<bizguid>&status=build&sort=_created_at
330
+ ```
331
+
332
+ Relation filters compose with field filters, `sort`, and pagination. Scalar
333
+ comparisons (`gt`/`lt`/`contains`) are field-only; relations support
334
+ `eq`/`ne`/`in`/`exists`. So **model a foreign key as a relation, not a string
335
+ field** — you keep one-read traversal *and* get filtering, indexed, for free.
336
+
337
+ ### Including related objects
338
+
339
+ By default a relation reads back as a guid (to-one) or list of guids (to-many).
340
+ Add `?include=` with comma-separated relation paths (dots nest) to hydrate them
341
+ into the full neighbor objects, nested Prisma-style:
342
+
343
+ ```
344
+ GET /objects/post?include=author,comments,comments.author
345
+ # each post.author becomes a full user; post.comments a list of full comments,
346
+ # and each comment.author a full user too.
347
+ ```
348
+
349
+ Works on the list endpoint and both single-object reads. Read-only, depth ≤ 4,
350
+ and batched (one query per relation per level — no N+1). Writes stay flat: create
351
+ and update with guids, never nested objects.
352
+
252
353
  ## Errors
253
354
 
254
355
  JSON shape: `{"error": {"code": "...", "message": "...", ...extra}}`.
@@ -283,8 +384,10 @@ allowed, `413` body too large, `500` internal.
283
384
  old type simply reads as unset (the field's default, or null) until it's
284
385
  written again; reads and queries apply this rule identically, so they always
285
386
  agree. Re-adding a dropped field at the same type recovers its values.
286
- - **Filtering is field-only.** Query operators apply to raw fields; relations
287
- are read/written on the object body but not filtered server-side (yet).
387
+ - **Filtering/sorting is opt-in per field.** Only a field marked `"index": true`
388
+ can be filtered or sorted (it gets a row in the indexed `field_index` table);
389
+ an un-indexed field is storage-only and a filter/sort on it is a 400. Relations
390
+ are always filterable via the indexed edge table — no flag needed.
288
391
  - **Integer magnitude.** Numbers are stored and read back exactly at any size.
289
392
  Filtering/sorting on integers beyond ±2⁶³ uses floating-point comparison (a
290
393
  SQLite limitation), so equality/range queries on such huge integers may be
@@ -5,4 +5,4 @@ coding agent iterates, while the frontend keeps calling the same small set of
5
5
  generic, deterministic endpoints.
6
6
  """
7
7
 
8
- __version__ = "0.1.2"
8
+ __version__ = "0.1.4"
@@ -0,0 +1,31 @@
1
+ """MorphDB command-line interface — process management + admin dashboard.
2
+
3
+ Intentionally separate from the core engine (db / schema / objects / server):
4
+ this package only *orchestrates* a server process and offers a read-only admin
5
+ view. It never changes how the core stores or serves data.
6
+
7
+ Commands (see :mod:`morphdb.cli.main`):
8
+
9
+ morphdb start the server in the background (alias of `start`)
10
+ morphdb start same, explicit
11
+ morphdb status is it running? where? how many apps?
12
+ morphdb stop stop the background server
13
+ morphdb logs show the server log (-f to follow)
14
+ morphdb run run in the foreground (blocking; for dev)
15
+ morphdb dashboard open a read-only web view of every app + its tables
16
+ morphdb mcp run the MCP server (stdio; spawned by Claude Code, not you)
17
+ morphdb install-skill install/update the bundled Claude Code skill
18
+
19
+ The ``mcp`` command is a thin HTTP *client* of the running backend daemon (it
20
+ auto-starts the daemon if needed). It exposes schema + app operations to a coding
21
+ agent as MCP tools, so the agent calls real tools instead of shelling out to the
22
+ bundled schema script. It is pure stdlib — no MCP SDK — so the package stays
23
+ dependency-free.
24
+
25
+ Storage: the local server keeps data in a per-user SQLite file at
26
+ ``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
27
+ dir with ``$MORPHDB_HOME``). To talk to a MorphDB hosted somewhere else instead
28
+ of a local one, point *clients* at it with ``$MORPHDB_HOST`` (a full URL) — that
29
+ is a client-side setting, not a database connection string; the engine is always
30
+ SQLite.
31
+ """