morphdb 0.1.2__tar.gz → 0.1.3__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.
- {morphdb-0.1.2 → morphdb-0.1.3}/PKG-INFO +123 -69
- {morphdb-0.1.2 → morphdb-0.1.3}/README.md +122 -68
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/__init__.py +1 -1
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/__init__.py +8 -6
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/main.py +55 -15
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/skill.py +10 -8
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/skill/SKILL.md +3 -2
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/PKG-INFO +123 -69
- {morphdb-0.1.2 → morphdb-0.1.3}/pyproject.toml +1 -1
- {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_cli.py +40 -8
- {morphdb-0.1.2 → morphdb-0.1.3}/LICENSE +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/__main__.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/apps.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/associations.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/dashboard.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/service.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/db.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/errors.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/fieldtypes.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/objects.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/router.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/routes.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/schema.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/server.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/skill/scripts/morphdb_schema.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/util.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/SOURCES.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/dependency_links.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/entry_points.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/requires.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/top_level.txt +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/setup.cfg +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_apps.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_core.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_hardening.py +0 -0
- {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_relations.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphdb
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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,119 @@ 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
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install morphdb
|
|
35
41
|
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
|
|
43
|
+
Manage the local server with the `morphdb` CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
47
|
+
morphdb status # running? where? how many apps?
|
|
48
|
+
morphdb stop # stop it
|
|
49
|
+
morphdb run # run in the foreground instead (blocking)
|
|
50
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
51
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
55
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
56
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
57
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
58
|
+
|
|
59
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
60
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
61
|
+
|
|
62
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
63
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
64
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
65
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
66
|
+
connection string.
|
|
67
|
+
|
|
68
|
+
## Use it
|
|
69
|
+
|
|
70
|
+
With the server running (`morphdb start`):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
BASE=http://127.0.0.1:8787
|
|
74
|
+
|
|
75
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
76
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
77
|
+
H="X-App-Key: my-site"
|
|
78
|
+
|
|
79
|
+
# 1. define types + a relation
|
|
80
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
81
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
82
|
+
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
83
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
84
|
+
|
|
85
|
+
# 2. create + read + query
|
|
86
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
87
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
88
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
89
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
90
|
+
|
|
91
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
92
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
96
|
+
|
|
97
|
+
## Command-line interface
|
|
98
|
+
|
|
99
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
100
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
101
|
+
via a pid file under the state dir.
|
|
102
|
+
|
|
103
|
+
| Command | What it does |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
106
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
107
|
+
| `morphdb stop` | Stop the background server. |
|
|
108
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
109
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
110
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
111
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
112
|
+
| `morphdb --version` | Print the version. |
|
|
113
|
+
|
|
114
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
115
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
116
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
117
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
118
|
+
`$MORPHDB_HOME`.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
122
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
123
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
124
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
125
|
+
morphdb stop
|
|
126
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Install the Claude Code skill
|
|
130
|
+
|
|
131
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
132
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
133
|
+
site:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
137
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
138
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
46
139
|
```
|
|
47
140
|
|
|
141
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
142
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
143
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
144
|
+
Claude Code afterward to pick it up.
|
|
145
|
+
|
|
48
146
|
## Why
|
|
49
147
|
|
|
50
148
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -58,6 +156,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
58
156
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
59
157
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
60
158
|
|
|
159
|
+
```
|
|
160
|
+
you (the coding agent) the frontend you build
|
|
161
|
+
────────────────────── ──────────────────────
|
|
162
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
163
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
164
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
165
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
166
|
+
│ │
|
|
167
|
+
└────────────── MorphDB ───────────┘
|
|
168
|
+
(one process · many apps · SQLite)
|
|
169
|
+
every call: X-App-Key: <app>
|
|
170
|
+
```
|
|
171
|
+
|
|
61
172
|
## The shape of it
|
|
62
173
|
|
|
63
174
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -126,63 +237,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
126
237
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
127
238
|
> horizontal scale, or production durability guarantees.
|
|
128
239
|
|
|
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
240
|
## Data model
|
|
187
241
|
|
|
188
242
|
| Concept | What it is |
|
|
@@ -3,21 +3,119 @@
|
|
|
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
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install morphdb
|
|
8
14
|
```
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
|
|
16
|
+
Manage the local server with the `morphdb` CLI:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
20
|
+
morphdb status # running? where? how many apps?
|
|
21
|
+
morphdb stop # stop it
|
|
22
|
+
morphdb run # run in the foreground instead (blocking)
|
|
23
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
24
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
28
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
29
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
30
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
31
|
+
|
|
32
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
33
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
34
|
+
|
|
35
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
36
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
37
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
38
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
39
|
+
connection string.
|
|
40
|
+
|
|
41
|
+
## Use it
|
|
42
|
+
|
|
43
|
+
With the server running (`morphdb start`):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
BASE=http://127.0.0.1:8787
|
|
47
|
+
|
|
48
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
49
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
50
|
+
H="X-App-Key: my-site"
|
|
51
|
+
|
|
52
|
+
# 1. define types + a relation
|
|
53
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
54
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
55
|
+
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
56
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
57
|
+
|
|
58
|
+
# 2. create + read + query
|
|
59
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
60
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
61
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
62
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
63
|
+
|
|
64
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
65
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
69
|
+
|
|
70
|
+
## Command-line interface
|
|
71
|
+
|
|
72
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
73
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
74
|
+
via a pid file under the state dir.
|
|
75
|
+
|
|
76
|
+
| Command | What it does |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
79
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
80
|
+
| `morphdb stop` | Stop the background server. |
|
|
81
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
82
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
83
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
84
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
85
|
+
| `morphdb --version` | Print the version. |
|
|
86
|
+
|
|
87
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
88
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
89
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
90
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
91
|
+
`$MORPHDB_HOME`.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
95
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
96
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
97
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
98
|
+
morphdb stop
|
|
99
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Install the Claude Code skill
|
|
103
|
+
|
|
104
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
105
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
106
|
+
site:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
110
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
111
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
19
112
|
```
|
|
20
113
|
|
|
114
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
115
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
116
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
117
|
+
Claude Code afterward to pick it up.
|
|
118
|
+
|
|
21
119
|
## Why
|
|
22
120
|
|
|
23
121
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -31,6 +129,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
31
129
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
32
130
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
33
131
|
|
|
132
|
+
```
|
|
133
|
+
you (the coding agent) the frontend you build
|
|
134
|
+
────────────────────── ──────────────────────
|
|
135
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
136
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
137
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
138
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
139
|
+
│ │
|
|
140
|
+
└────────────── MorphDB ───────────┘
|
|
141
|
+
(one process · many apps · SQLite)
|
|
142
|
+
every call: X-App-Key: <app>
|
|
143
|
+
```
|
|
144
|
+
|
|
34
145
|
## The shape of it
|
|
35
146
|
|
|
36
147
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -99,63 +210,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
99
210
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
100
211
|
> horizontal scale, or production durability guarantees.
|
|
101
212
|
|
|
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
213
|
## Data model
|
|
160
214
|
|
|
161
215
|
| Concept | What it is |
|
|
@@ -6,12 +6,14 @@ view. It never changes how the core stores or serves data.
|
|
|
6
6
|
|
|
7
7
|
Commands (see :mod:`morphdb.cli.main`):
|
|
8
8
|
|
|
9
|
-
morphdb
|
|
10
|
-
morphdb start
|
|
11
|
-
morphdb status
|
|
12
|
-
morphdb stop
|
|
13
|
-
morphdb
|
|
14
|
-
morphdb
|
|
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 install-skill install/update the bundled Claude Code skill
|
|
15
17
|
|
|
16
18
|
Storage: the local server keeps data in a per-user SQLite file at
|
|
17
19
|
``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
"""``morphdb`` console-script entry point: a small process/admin CLI.
|
|
2
2
|
|
|
3
|
-
morphdb
|
|
4
|
-
morphdb start
|
|
5
|
-
morphdb status
|
|
6
|
-
morphdb stop
|
|
7
|
-
morphdb
|
|
8
|
-
morphdb
|
|
3
|
+
morphdb start the server in the background (alias of `start`)
|
|
4
|
+
morphdb start start the server in the background
|
|
5
|
+
morphdb status show whether it is running, where, and how many apps
|
|
6
|
+
morphdb stop stop the background server
|
|
7
|
+
morphdb logs show the background server's log (-f to follow)
|
|
8
|
+
morphdb run run the server in the foreground (blocking)
|
|
9
|
+
morphdb dashboard open the read-only admin dashboard
|
|
10
|
+
morphdb install-skill install the bundled Claude Code skill
|
|
9
11
|
|
|
10
12
|
``python -m morphdb`` remains the plain foreground server (what `start` and the
|
|
11
13
|
skill spawn under the hood); this CLI only wraps it.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
import argparse
|
|
17
|
+
import os
|
|
15
18
|
import sys
|
|
16
19
|
|
|
17
20
|
from . import dashboard, service
|
|
@@ -60,6 +63,39 @@ def cmd_stop(args):
|
|
|
60
63
|
return 0
|
|
61
64
|
|
|
62
65
|
|
|
66
|
+
def cmd_logs(args):
|
|
67
|
+
path = service.log_file()
|
|
68
|
+
if not os.path.exists(path):
|
|
69
|
+
print(f"No log yet at {path}. Has the server run? Try `morphdb start`.")
|
|
70
|
+
return 1
|
|
71
|
+
with open(path, "r", errors="replace") as f:
|
|
72
|
+
lines = f.readlines()
|
|
73
|
+
tail = lines[-args.lines:] if args.lines and args.lines > 0 else lines
|
|
74
|
+
sys.stdout.write("".join(tail))
|
|
75
|
+
if tail and not tail[-1].endswith("\n"):
|
|
76
|
+
sys.stdout.write("\n")
|
|
77
|
+
if args.follow:
|
|
78
|
+
_follow(path)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _follow(path):
|
|
83
|
+
"""Stream new lines appended to the log, like `tail -f`, until Ctrl-C."""
|
|
84
|
+
import time
|
|
85
|
+
with open(path, "r", errors="replace") as f:
|
|
86
|
+
f.seek(0, os.SEEK_END)
|
|
87
|
+
try:
|
|
88
|
+
while True:
|
|
89
|
+
line = f.readline()
|
|
90
|
+
if line:
|
|
91
|
+
sys.stdout.write(line)
|
|
92
|
+
sys.stdout.flush()
|
|
93
|
+
else:
|
|
94
|
+
time.sleep(0.3)
|
|
95
|
+
except KeyboardInterrupt:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
63
99
|
def cmd_dashboard(args):
|
|
64
100
|
dashboard.serve(args.db or service.default_db(), port=args.port,
|
|
65
101
|
open_browser=not args.no_open)
|
|
@@ -68,16 +104,14 @@ def cmd_dashboard(args):
|
|
|
68
104
|
|
|
69
105
|
def cmd_install_skill(args):
|
|
70
106
|
try:
|
|
71
|
-
dest = skill_mod.install_skill(project=args.project
|
|
72
|
-
except FileExistsError as e:
|
|
73
|
-
print(f"Skill already installed at {e}. Re-run with --force to overwrite.")
|
|
74
|
-
return 1
|
|
107
|
+
dest, existed = skill_mod.install_skill(project=args.project)
|
|
75
108
|
except (FileNotFoundError, OSError) as e:
|
|
76
109
|
print(f"Could not install skill: {e}")
|
|
77
110
|
return 1
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
verb = "Updated" if existed else "Installed"
|
|
112
|
+
where = "this project" if args.project else "~/.claude"
|
|
113
|
+
print(f"{verb} the 'morphdb' Claude skill at {dest} ({where}).\n"
|
|
114
|
+
f" Restart Claude Code (or reload skills) to pick it up.")
|
|
81
115
|
return 0
|
|
82
116
|
|
|
83
117
|
|
|
@@ -112,6 +146,13 @@ def build_parser():
|
|
|
112
146
|
sub.add_parser("stop", help="stop the background server"
|
|
113
147
|
).set_defaults(func=cmd_stop)
|
|
114
148
|
|
|
149
|
+
sp = sub.add_parser("logs", help="show the background server's log")
|
|
150
|
+
sp.add_argument("-n", "--lines", type=int, default=200,
|
|
151
|
+
help="number of trailing lines to show (default 200)")
|
|
152
|
+
sp.add_argument("-f", "--follow", action="store_true",
|
|
153
|
+
help="stream new log lines until Ctrl-C")
|
|
154
|
+
sp.set_defaults(func=cmd_logs)
|
|
155
|
+
|
|
115
156
|
sp = sub.add_parser("dashboard", help="open the read-only admin dashboard")
|
|
116
157
|
sp.add_argument("--port", type=int, default=8788, help="dashboard port (default 8788)")
|
|
117
158
|
sp.add_argument("--db", default=None, help="database to inspect (default the server's)")
|
|
@@ -119,12 +160,11 @@ def build_parser():
|
|
|
119
160
|
sp.set_defaults(func=cmd_dashboard)
|
|
120
161
|
|
|
121
162
|
sp = sub.add_parser("install-skill",
|
|
122
|
-
help="install the
|
|
163
|
+
help="install/update the bundled Claude Code skill")
|
|
123
164
|
sp.add_argument("--project", nargs="?", const=".", default=None,
|
|
124
165
|
metavar="DIR",
|
|
125
166
|
help="install into a project's .claude (DIR, default cwd) "
|
|
126
167
|
"instead of ~/.claude")
|
|
127
|
-
sp.add_argument("--force", action="store_true", help="overwrite if it exists")
|
|
128
168
|
sp.set_defaults(func=cmd_install_skill)
|
|
129
169
|
|
|
130
170
|
return p
|
|
@@ -25,30 +25,32 @@ def _copy_tree(src, dst):
|
|
|
25
25
|
f.write(src.read_bytes())
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def install_skill(claude_dir=None, project=None
|
|
28
|
+
def install_skill(claude_dir=None, project=None):
|
|
29
29
|
"""Copy the packaged skill into a `.claude/skills/morphdb` directory.
|
|
30
30
|
|
|
31
|
+
Idempotent: re-running overwrites the destination with the skill bundled in
|
|
32
|
+
the *currently installed* ``morphdb`` package, so it always lands the version
|
|
33
|
+
you have (run ``pip install -U morphdb`` first to refresh to the latest). It
|
|
34
|
+
reads from package data, not from GitHub.
|
|
35
|
+
|
|
31
36
|
``project`` (a path, or "." for cwd) installs into that project's
|
|
32
37
|
``.claude``; otherwise it installs into ``~/.claude`` (all projects).
|
|
33
38
|
``claude_dir`` overrides the `.claude` location outright (used by tests).
|
|
34
|
-
Returns
|
|
35
|
-
and ``force`` is false.
|
|
39
|
+
Returns ``(dest_path, existed_before)``.
|
|
36
40
|
"""
|
|
37
41
|
if claude_dir is None:
|
|
38
42
|
base = os.path.abspath(project) if project else os.path.expanduser("~")
|
|
39
43
|
claude_dir = os.path.join(base, ".claude")
|
|
40
44
|
dest = os.path.join(claude_dir, "skills", SKILL_NAME)
|
|
41
45
|
|
|
42
|
-
if os.path.exists(dest) and not force:
|
|
43
|
-
raise FileExistsError(dest)
|
|
44
|
-
|
|
45
46
|
src = resources.files("morphdb") / "skill"
|
|
46
47
|
if not src.is_dir():
|
|
47
48
|
raise FileNotFoundError(
|
|
48
49
|
"packaged skill not found (morphdb/skill missing from the install).")
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
existed = os.path.exists(dest)
|
|
52
|
+
if existed:
|
|
51
53
|
import shutil
|
|
52
54
|
shutil.rmtree(dest)
|
|
53
55
|
_copy_tree(src, dest)
|
|
54
|
-
return dest
|
|
56
|
+
return dest, existed
|
|
@@ -61,8 +61,9 @@ API).
|
|
|
61
61
|
|
|
62
62
|
**Debug tip:** if the frontend can't reach the backend (connection refused, a
|
|
63
63
|
`fetch` throws) and you're running locally, the server is probably down — run
|
|
64
|
-
`morphdb status`, then `morphdb start` if stopped
|
|
65
|
-
|
|
64
|
+
`morphdb status`, then `morphdb start` if stopped, and `morphdb logs` (add `-f`
|
|
65
|
+
to follow) to see errors. That's the first thing to check when a working app
|
|
66
|
+
suddenly stops loading or saving data.
|
|
66
67
|
|
|
67
68
|
## Mental model
|
|
68
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: morphdb
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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,119 @@ 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
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install morphdb
|
|
35
41
|
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
|
|
43
|
+
Manage the local server with the `morphdb` CLI:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
morphdb start # run in the background (default 127.0.0.1:8787)
|
|
47
|
+
morphdb status # running? where? how many apps?
|
|
48
|
+
morphdb stop # stop it
|
|
49
|
+
morphdb run # run in the foreground instead (blocking)
|
|
50
|
+
morphdb dashboard # read-only web view of every app + its tables
|
|
51
|
+
morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
|
|
55
|
+
`--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
|
|
56
|
+
`--host`, `--port`, `--db`. From a source checkout with no install, the
|
|
57
|
+
foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
|
|
58
|
+
|
|
59
|
+
To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
|
|
60
|
+
to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
|
|
61
|
+
|
|
62
|
+
**Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
|
|
63
|
+
`https://db.example.com`) and the schema CLI — plus any frontend that reads
|
|
64
|
+
`window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
|
|
65
|
+
of localhost. It's a client-side setting that names a *backend*, not a database
|
|
66
|
+
connection string.
|
|
67
|
+
|
|
68
|
+
## Use it
|
|
69
|
+
|
|
70
|
+
With the server running (`morphdb start`):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
BASE=http://127.0.0.1:8787
|
|
74
|
+
|
|
75
|
+
# 0. register an app; send its key as X-App-Key on every schema/object call
|
|
76
|
+
curl -X POST $BASE/app -d '{"key":"my-site"}'
|
|
77
|
+
H="X-App-Key: my-site"
|
|
78
|
+
|
|
79
|
+
# 1. define types + a relation
|
|
80
|
+
curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
|
|
81
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{
|
|
82
|
+
"fields": {"title":"string","done":"boolean","priority":"number"},
|
|
83
|
+
"relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
|
|
84
|
+
|
|
85
|
+
# 2. create + read + query
|
|
86
|
+
U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
|
|
87
|
+
curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
|
|
88
|
+
curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
|
|
89
|
+
curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
|
|
90
|
+
|
|
91
|
+
# 3. morph the schema later — existing rows just gain the new field as null
|
|
92
|
+
curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
|
|
96
|
+
|
|
97
|
+
## Command-line interface
|
|
98
|
+
|
|
99
|
+
`morphdb` runs the server as a **background service** — `start` launches it
|
|
100
|
+
detached and hands your terminal straight back; `status` / `stop` find it again
|
|
101
|
+
via a pid file under the state dir.
|
|
102
|
+
|
|
103
|
+
| Command | What it does |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
|
|
106
|
+
| `morphdb status` | Is it running? URL, pid, health, and app count. |
|
|
107
|
+
| `morphdb stop` | Stop the background server. |
|
|
108
|
+
| `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
|
|
109
|
+
| `morphdb run` | Run in the **foreground** (blocking) instead. |
|
|
110
|
+
| `morphdb dashboard` | Open a read-only web view of every app and its tables. |
|
|
111
|
+
| `morphdb install-skill` | Install the bundled Claude Code skill (below). |
|
|
112
|
+
| `morphdb --version` | Print the version. |
|
|
113
|
+
|
|
114
|
+
`start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
|
|
115
|
+
and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
|
|
116
|
+
`dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
|
|
117
|
+
state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
|
|
118
|
+
`$MORPHDB_HOME`.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
morphdb start # background, default 127.0.0.1:8787
|
|
122
|
+
morphdb start --port 9000 --db ./my.sqlite3
|
|
123
|
+
morphdb status # -> running (pid …) at http://… [healthy]
|
|
124
|
+
morphdb dashboard # opens http://127.0.0.1:8788
|
|
125
|
+
morphdb stop
|
|
126
|
+
morphdb run # foreground instead (Ctrl-C to quit)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Install the Claude Code skill
|
|
130
|
+
|
|
131
|
+
`install-skill` writes the bundled MorphDB skill into a Claude skills directory,
|
|
132
|
+
so a coding agent automatically reaches for MorphDB when building a data-backed
|
|
133
|
+
site:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
|
|
137
|
+
morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
|
|
138
|
+
morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
|
|
46
139
|
```
|
|
47
140
|
|
|
141
|
+
It installs the skill **bundled in the installed package** (not live from
|
|
142
|
+
GitHub) and is **idempotent** — re-running overwrites with the current version.
|
|
143
|
+
To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
|
|
144
|
+
Claude Code afterward to pick it up.
|
|
145
|
+
|
|
48
146
|
## Why
|
|
49
147
|
|
|
50
148
|
AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
|
|
@@ -58,6 +156,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
|
|
|
58
156
|
**no migration, no row rewrite, no downtime** — regardless of how much data
|
|
59
157
|
exists. Meanwhile the frontend talks to generic endpoints that never change.
|
|
60
158
|
|
|
159
|
+
```
|
|
160
|
+
you (the coding agent) the frontend you build
|
|
161
|
+
────────────────────── ──────────────────────
|
|
162
|
+
reshape the schema freely │ calls fixed generic endpoints
|
|
163
|
+
PUT /schema/{type} │ POST /objects/{type}
|
|
164
|
+
GET /schema │ GET /objects/{type}?field=…
|
|
165
|
+
DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
|
|
166
|
+
│ │
|
|
167
|
+
└────────────── MorphDB ───────────┘
|
|
168
|
+
(one process · many apps · SQLite)
|
|
169
|
+
every call: X-App-Key: <app>
|
|
170
|
+
```
|
|
171
|
+
|
|
61
172
|
## The shape of it
|
|
62
173
|
|
|
63
174
|
One MorphDB process hosts **many apps** (one per website), fully isolated from
|
|
@@ -126,63 +237,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
|
|
|
126
237
|
> Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
|
|
127
238
|
> horizontal scale, or production durability guarantees.
|
|
128
239
|
|
|
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
240
|
## Data model
|
|
187
241
|
|
|
188
242
|
| Concept | What it is |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "morphdb"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,6 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
13
13
|
|
|
14
14
|
from morphdb import apps, db, objects, schema # noqa: E402
|
|
15
15
|
from morphdb.cli import dashboard, service # noqa: E402
|
|
16
|
+
from morphdb.cli import main as cli_main # noqa: E402
|
|
16
17
|
from morphdb.cli import skill as skill_mod # noqa: E402
|
|
17
18
|
|
|
18
19
|
|
|
@@ -98,23 +99,54 @@ class TestDashboardGather(unittest.TestCase):
|
|
|
98
99
|
class TestInstallSkill(unittest.TestCase):
|
|
99
100
|
def test_install_copies_skill_files(self):
|
|
100
101
|
d = tempfile.mkdtemp()
|
|
101
|
-
dest = skill_mod.install_skill(claude_dir=d)
|
|
102
|
+
dest, existed = skill_mod.install_skill(claude_dir=d)
|
|
103
|
+
self.assertFalse(existed)
|
|
102
104
|
self.assertTrue(os.path.isfile(os.path.join(dest, "SKILL.md")))
|
|
103
105
|
self.assertTrue(os.path.isfile(
|
|
104
106
|
os.path.join(dest, "scripts", "morphdb_schema.py")))
|
|
105
|
-
# name + location
|
|
106
|
-
self.assertEqual(os.path.basename(dest), "morphdb")
|
|
107
107
|
self.assertEqual(dest, os.path.join(d, "skills", "morphdb"))
|
|
108
108
|
|
|
109
|
-
def
|
|
109
|
+
def test_reinstall_is_idempotent(self):
|
|
110
110
|
d = tempfile.mkdtemp()
|
|
111
111
|
skill_mod.install_skill(claude_dir=d)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
# force overwrites cleanly
|
|
115
|
-
dest = skill_mod.install_skill(claude_dir=d, force=True)
|
|
112
|
+
dest, existed = skill_mod.install_skill(claude_dir=d) # re-run overwrites
|
|
113
|
+
self.assertTrue(existed)
|
|
116
114
|
self.assertTrue(os.path.isfile(os.path.join(dest, "SKILL.md")))
|
|
117
115
|
|
|
118
116
|
|
|
117
|
+
class TestLogs(unittest.TestCase):
|
|
118
|
+
def setUp(self):
|
|
119
|
+
self._old = os.environ.get("MORPHDB_HOME")
|
|
120
|
+
self.tmp = tempfile.mkdtemp()
|
|
121
|
+
os.environ["MORPHDB_HOME"] = self.tmp
|
|
122
|
+
|
|
123
|
+
def tearDown(self):
|
|
124
|
+
if self._old is None:
|
|
125
|
+
os.environ.pop("MORPHDB_HOME", None)
|
|
126
|
+
else:
|
|
127
|
+
os.environ["MORPHDB_HOME"] = self._old
|
|
128
|
+
|
|
129
|
+
def _run(self, argv):
|
|
130
|
+
import contextlib
|
|
131
|
+
import io
|
|
132
|
+
buf = io.StringIO()
|
|
133
|
+
with contextlib.redirect_stdout(buf):
|
|
134
|
+
rc = cli_main.main(argv)
|
|
135
|
+
return rc, buf.getvalue()
|
|
136
|
+
|
|
137
|
+
def test_missing_log(self):
|
|
138
|
+
rc, out = self._run(["logs"])
|
|
139
|
+
self.assertEqual(rc, 1)
|
|
140
|
+
self.assertIn("No log yet", out)
|
|
141
|
+
|
|
142
|
+
def test_shows_tail(self):
|
|
143
|
+
with open(service.log_file(), "w") as f:
|
|
144
|
+
f.write("line1\nline2\nline3\n")
|
|
145
|
+
rc, out = self._run(["logs", "-n", "2"])
|
|
146
|
+
self.assertEqual(rc, 0)
|
|
147
|
+
self.assertIn("line3", out)
|
|
148
|
+
self.assertNotIn("line1", out)
|
|
149
|
+
|
|
150
|
+
|
|
119
151
|
if __name__ == "__main__":
|
|
120
152
|
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|