mdb-cli 0.1.0__py3-none-any.whl
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.
- mdb/__init__.py +0 -0
- mdb/atomic.py +25 -0
- mdb/data/SKILL.md +95 -0
- mdb/data/__init__.py +0 -0
- mdb/discovery.py +70 -0
- mdb/filelock.py +76 -0
- mdb/formatter.py +101 -0
- mdb/init.py +150 -0
- mdb/mdb.py +1214 -0
- mdb/models.py +81 -0
- mdb/parser.py +609 -0
- mdb/puller.py +212 -0
- mdb/validators.py +46 -0
- mdb_cli-0.1.0.dist-info/METADATA +220 -0
- mdb_cli-0.1.0.dist-info/RECORD +18 -0
- mdb_cli-0.1.0.dist-info/WHEEL +4 -0
- mdb_cli-0.1.0.dist-info/entry_points.txt +2 -0
- mdb_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
mdb/puller.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""SQLite pull-phase operations: query execution and feed ingestion."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
|
|
7
|
+
from mdb.models import FeedResult, TapMarker, TapResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute_tap_query(db_path: str, query: str) -> TapResult:
|
|
11
|
+
"""Execute a SELECT query against a SQLite database in read-only mode.
|
|
12
|
+
|
|
13
|
+
Opens the database via file: URI with mode=ro.
|
|
14
|
+
Returns TapResult with columns, rows, and success/error status.
|
|
15
|
+
Always closes the connection.
|
|
16
|
+
"""
|
|
17
|
+
# Create a dummy marker for the result
|
|
18
|
+
marker = TapMarker(line_number=0, scope=db_path, raw_query=query)
|
|
19
|
+
conn = None
|
|
20
|
+
try:
|
|
21
|
+
uri = f"file:{db_path}?mode=ro"
|
|
22
|
+
conn = sqlite3.connect(uri, uri=True)
|
|
23
|
+
cursor = conn.execute(query)
|
|
24
|
+
columns = [desc[0] for desc in cursor.description]
|
|
25
|
+
raw_rows = cursor.fetchall()
|
|
26
|
+
rows = [
|
|
27
|
+
[str(cell) if cell is not None else "" for cell in row]
|
|
28
|
+
for row in raw_rows
|
|
29
|
+
]
|
|
30
|
+
return TapResult(
|
|
31
|
+
marker=marker,
|
|
32
|
+
success=True,
|
|
33
|
+
columns=columns,
|
|
34
|
+
rows=rows,
|
|
35
|
+
)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return TapResult(
|
|
38
|
+
marker=marker,
|
|
39
|
+
success=False,
|
|
40
|
+
error=str(e),
|
|
41
|
+
)
|
|
42
|
+
finally:
|
|
43
|
+
if conn:
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_content_hash(
|
|
48
|
+
columns: list[str],
|
|
49
|
+
column_types: list[str],
|
|
50
|
+
rows: list[list[str]],
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Compute a deterministic SHA-256 hash from column names, types, and row values.
|
|
53
|
+
|
|
54
|
+
Returns a 64-character lowercase hexadecimal string.
|
|
55
|
+
"""
|
|
56
|
+
parts = []
|
|
57
|
+
# Column definitions: name:TYPE joined by \0
|
|
58
|
+
col_defs = "\0".join(f"{name}:{typ}" for name, typ in zip(columns, column_types))
|
|
59
|
+
parts.append(col_defs)
|
|
60
|
+
# Section separator
|
|
61
|
+
parts.append("\0\0")
|
|
62
|
+
# Row data: cells joined by \0, each row terminated by \n
|
|
63
|
+
for row in rows:
|
|
64
|
+
parts.append("\0".join(row))
|
|
65
|
+
parts.append("\n")
|
|
66
|
+
serialized = "".join(parts)
|
|
67
|
+
return hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ingest_feed_table(db_path: str, table_name: str, columns: list[str], rows: list[list[str]], column_types: list[str] | None = None) -> FeedResult:
|
|
71
|
+
"""Drop and recreate a table in the SQLite database, inserting all rows.
|
|
72
|
+
|
|
73
|
+
Creates the database file if it does not exist.
|
|
74
|
+
Wraps DROP + CREATE + INSERT in a single transaction.
|
|
75
|
+
When column_types is provided, uses specified types; otherwise defaults to TEXT.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
79
|
+
conn = sqlite3.connect(db_path)
|
|
80
|
+
try:
|
|
81
|
+
# Quote column names to handle spaces and reserved words
|
|
82
|
+
quoted_cols = [f'"{col}"' for col in columns]
|
|
83
|
+
types_for_hash = column_types if column_types is not None else ["TEXT"] * len(columns)
|
|
84
|
+
if column_types is not None:
|
|
85
|
+
col_defs = ", ".join(
|
|
86
|
+
f'{qc} {ct}' for qc, ct in zip(quoted_cols, column_types)
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
col_defs = ", ".join(f'{qc} TEXT' for qc in quoted_cols)
|
|
90
|
+
col_list = ", ".join(quoted_cols)
|
|
91
|
+
placeholders = ", ".join("?" for _ in columns)
|
|
92
|
+
|
|
93
|
+
conn.execute("BEGIN TRANSACTION")
|
|
94
|
+
|
|
95
|
+
# Ensure metadata table exists
|
|
96
|
+
conn.execute(
|
|
97
|
+
"CREATE TABLE IF NOT EXISTS _mdb_meta "
|
|
98
|
+
"(table_name TEXT PRIMARY KEY, content_hash TEXT NOT NULL)"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Compute content hash
|
|
102
|
+
computed_hash = compute_content_hash(columns, types_for_hash, rows)
|
|
103
|
+
|
|
104
|
+
# Look up stored hash
|
|
105
|
+
cursor = conn.execute(
|
|
106
|
+
"SELECT content_hash FROM _mdb_meta WHERE table_name = ?",
|
|
107
|
+
(table_name,),
|
|
108
|
+
)
|
|
109
|
+
row_result = cursor.fetchone()
|
|
110
|
+
|
|
111
|
+
# Skip if hash matches
|
|
112
|
+
if row_result is not None and row_result[0] == computed_hash:
|
|
113
|
+
return FeedResult(
|
|
114
|
+
task=None,
|
|
115
|
+
success=True,
|
|
116
|
+
skipped=True,
|
|
117
|
+
rows_written=0,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Hash mismatch or no stored hash: full write
|
|
121
|
+
conn.execute(f'DROP TABLE IF EXISTS "{table_name}"')
|
|
122
|
+
conn.execute(f'CREATE TABLE "{table_name}" ({col_defs})')
|
|
123
|
+
|
|
124
|
+
if rows:
|
|
125
|
+
conn.executemany(
|
|
126
|
+
f'INSERT INTO "{table_name}" ({col_list}) VALUES ({placeholders})',
|
|
127
|
+
rows,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Store hash
|
|
131
|
+
conn.execute(
|
|
132
|
+
"INSERT OR REPLACE INTO _mdb_meta (table_name, content_hash) VALUES (?, ?)",
|
|
133
|
+
(table_name, computed_hash),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
conn.commit()
|
|
137
|
+
|
|
138
|
+
return FeedResult(
|
|
139
|
+
task=None,
|
|
140
|
+
success=True,
|
|
141
|
+
rows_written=len(rows),
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
conn.rollback()
|
|
145
|
+
return FeedResult(
|
|
146
|
+
task=None,
|
|
147
|
+
success=False,
|
|
148
|
+
error=str(e),
|
|
149
|
+
rows_written=0,
|
|
150
|
+
)
|
|
151
|
+
finally:
|
|
152
|
+
conn.close()
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return FeedResult(
|
|
155
|
+
task=None,
|
|
156
|
+
success=False,
|
|
157
|
+
error=str(e),
|
|
158
|
+
rows_written=0,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cleanup_stale_tables(db_path: str, discovered_tables: set[str]) -> list[str]:
|
|
163
|
+
"""Remove orphaned tables and their metadata from a database.
|
|
164
|
+
|
|
165
|
+
Compares tracked tables in _mdb_meta against the set of table names
|
|
166
|
+
declared by markers in the known universe. Tables present in _mdb_meta
|
|
167
|
+
but not in discovered_tables are dropped and their metadata removed.
|
|
168
|
+
|
|
169
|
+
Returns sorted list of removed table names, or [] if no cleanup needed.
|
|
170
|
+
Never raises exceptions to caller.
|
|
171
|
+
"""
|
|
172
|
+
if not os.path.exists(db_path):
|
|
173
|
+
return []
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
conn = sqlite3.connect(db_path)
|
|
177
|
+
try:
|
|
178
|
+
# Check if _mdb_meta exists
|
|
179
|
+
cursor = conn.execute(
|
|
180
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='_mdb_meta'"
|
|
181
|
+
)
|
|
182
|
+
if cursor.fetchone() is None:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
# Get tracked table names
|
|
186
|
+
cursor = conn.execute("SELECT table_name FROM _mdb_meta")
|
|
187
|
+
tracked = set(row[0] for row in cursor.fetchall())
|
|
188
|
+
|
|
189
|
+
# Compute orphans
|
|
190
|
+
orphaned = tracked - discovered_tables
|
|
191
|
+
if not orphaned:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
# Remove orphans in a single transaction
|
|
195
|
+
removed = sorted(orphaned)
|
|
196
|
+
conn.execute("BEGIN TRANSACTION")
|
|
197
|
+
for name in removed:
|
|
198
|
+
conn.execute(f'DROP TABLE IF EXISTS "{name}"')
|
|
199
|
+
conn.execute("DELETE FROM _mdb_meta WHERE table_name = ?", (name,))
|
|
200
|
+
conn.commit()
|
|
201
|
+
|
|
202
|
+
return removed
|
|
203
|
+
except Exception:
|
|
204
|
+
try:
|
|
205
|
+
conn.rollback()
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
return []
|
|
209
|
+
finally:
|
|
210
|
+
conn.close()
|
|
211
|
+
except Exception:
|
|
212
|
+
return []
|
mdb/validators.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""SQL query validators for mdb."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Write keywords to reject (case-insensitive, word boundary match)
|
|
6
|
+
_WRITE_KEYWORDS_RE = re.compile(
|
|
7
|
+
r'\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b',
|
|
8
|
+
re.IGNORECASE,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_dql(query: str) -> str | None:
|
|
13
|
+
"""Validate that a SQL query contains only read-only (DQL) operations.
|
|
14
|
+
|
|
15
|
+
Returns None if valid (read-only), error message string if invalid.
|
|
16
|
+
"""
|
|
17
|
+
match = _WRITE_KEYWORDS_RE.search(query)
|
|
18
|
+
if match:
|
|
19
|
+
keyword = match.group(1).upper()
|
|
20
|
+
return f"Query contains write operation: {keyword}"
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_dml(query: str) -> str | None:
|
|
25
|
+
"""Validate that all statements in a query are DML.
|
|
26
|
+
|
|
27
|
+
Returns None if all statements are valid DML.
|
|
28
|
+
Error message string if any statement is invalid.
|
|
29
|
+
"""
|
|
30
|
+
_DML_KEYWORDS = {"INSERT", "UPDATE", "DELETE"}
|
|
31
|
+
_SELECT_KEYWORDS = {"SELECT"}
|
|
32
|
+
_DDL_KEYWORDS = {"CREATE", "DROP", "ALTER"}
|
|
33
|
+
|
|
34
|
+
statements = query.split(";")
|
|
35
|
+
for stmt in statements:
|
|
36
|
+
stripped = stmt.strip()
|
|
37
|
+
if not stripped:
|
|
38
|
+
continue
|
|
39
|
+
first_word = stripped.split()[0].upper()
|
|
40
|
+
if first_word in _SELECT_KEYWORDS:
|
|
41
|
+
return "Only mutative queries (INSERT, UPDATE, DELETE) are permitted with push. For SELECT queries, use mdb pull."
|
|
42
|
+
if first_word in _DDL_KEYWORDS:
|
|
43
|
+
return "Only DML statements are permitted. DDL (CREATE, DROP, ALTER) is not supported."
|
|
44
|
+
if first_word not in _DML_KEYWORDS:
|
|
45
|
+
return f"Unrecognized statement keyword: {first_word}. Only INSERT, UPDATE, DELETE are permitted."
|
|
46
|
+
return None
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdb-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markdown table workflows w/ SQLite powers ✨
|
|
5
|
+
Project-URL: Homepage, https://atomanoid.github.io/mdb/
|
|
6
|
+
Project-URL: Repository, https://github.com/atomanoid/mdb
|
|
7
|
+
Project-URL: Documentation, https://atomanoid.github.io/mdb/
|
|
8
|
+
Author: atomanoid
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
26
|
+
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<!-- <img src="https://raw.githubusercontent.com/atomanoid/mdb/main/docs/assets/floppy.svg" alt="floppy disk" width="128"> -->
|
|
31
|
+
<img src="https://raw.githubusercontent.com/gist/atomanoid/2e8135956d498969842907b1b149c72f/raw/f9d3aa40f4ebf7b347de617263ac1c7492bd6203/mdb-floppy.svg" alt="floppy disk" width="128">
|
|
32
|
+
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<h1 align="center"><code>mdb-cli</code></h1>
|
|
36
|
+
|
|
37
|
+
<h3 align="center">Markdown table workflows w/ SQLite powers ✨</h3>
|
|
38
|
+
|
|
39
|
+
`mdb-cli` provides the `mdb` command-line tool to make markdown tables data-driven and queryable via SQL, using specialized co-located markers `💾 ...` with some embedded inline SQL queries to **define and dynamically view** our project-level data.
|
|
40
|
+
|
|
41
|
+
- **Zero runtime dependencies** -- standard library only
|
|
42
|
+
- **Simple integration** -- add markers and get running
|
|
43
|
+
- **Python 3.11+** required
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
Install from [PyPI](https://pypi.org/project/mdb-cli/):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Using pipx (recommended)
|
|
51
|
+
pipx install mdb-cli
|
|
52
|
+
|
|
53
|
+
# Using uv (recommended)
|
|
54
|
+
uv tool install mdb-cli
|
|
55
|
+
|
|
56
|
+
# Using pip
|
|
57
|
+
pip install mdb-cli
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After installation, verify the tool is available:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mdb --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For development installation, see [CONTRIBUTING.md](https://github.com/atomanoid/mdb/blob/main/CONTRIBUTING.md).
|
|
67
|
+
|
|
68
|
+
## Basic Usage
|
|
69
|
+
|
|
70
|
+
### Marker Syntax
|
|
71
|
+
|
|
72
|
+
Place inline query markers above tables in your markdown file using the following format:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
`💾 [scope] <directive> <sql-query>`
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Component | Description |
|
|
79
|
+
| ------------- | ------------------------------------------------------- |
|
|
80
|
+
| `💾` | Marker prefix -- identifies the line as an `mdb` marker |
|
|
81
|
+
| `[scope]` | Optional named scope identifier of the dataset |
|
|
82
|
+
| `<directive>` | Directive: `🌀` (feed) or `💎` (tap) |
|
|
83
|
+
| `<sql-query>` | SQL query to execute against the dataset |
|
|
84
|
+
|
|
85
|
+
## Directives
|
|
86
|
+
|
|
87
|
+
| Directive | Name | Instruction |
|
|
88
|
+
| --------- | ---- | ----------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `🌀` | feed | Include the table below me as part of the dataset |
|
|
90
|
+
| `💎` | tap | Execute my SQL query on the dataset and render the results as the table below me (table auto-generated if absent) |
|
|
91
|
+
|
|
92
|
+
> Under the hood, datasets are ingested into SQLite backing databases on which SQL queries are actually run.
|
|
93
|
+
|
|
94
|
+
### Subcommands
|
|
95
|
+
|
|
96
|
+
| Subcommand | Behavior |
|
|
97
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `mdb pull <sql-query>` | process all `🌀` markers first (pulls), then outputs SQL query results in CSV format |
|
|
99
|
+
| `mdb push` | process all `🌀` markers first (pulls), then all `💎` markers after (pushes) |
|
|
100
|
+
| `mdb push <sql-mutation>` | process all `🌀` markers first (pulls), execute the mutation, then all `🌀` and `💎` markers after (pushes) |
|
|
101
|
+
|
|
102
|
+
### Example
|
|
103
|
+
|
|
104
|
+
Given a project file `snacks.md` with the following content:
|
|
105
|
+
|
|
106
|
+
```markdown
|
|
107
|
+
# Snacks
|
|
108
|
+
|
|
109
|
+
## Inventory
|
|
110
|
+
|
|
111
|
+
`💾 🌀 SELECT * FROM snacks`
|
|
112
|
+
|
|
113
|
+
| name | vibe | qty |
|
|
114
|
+
| ------------------ | ------------------ | --- |
|
|
115
|
+
| Hot Cheese Popcorn | chaos energy | 3 |
|
|
116
|
+
| Pocky | elegant simplicity | 12 |
|
|
117
|
+
| Takis | rolled-up danger | 7 |
|
|
118
|
+
| Goldfish | the people's snack | 41 |
|
|
119
|
+
|
|
120
|
+
## Low stock
|
|
121
|
+
|
|
122
|
+
`💾 💎 SELECT name, qty FROM snacks WHERE qty < 10 ORDER BY qty`
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
After running:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
mdb push
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
We'll have the `snacks.md` updated to the following content:
|
|
132
|
+
|
|
133
|
+
```markdown
|
|
134
|
+
# Snacks
|
|
135
|
+
|
|
136
|
+
## Inventory
|
|
137
|
+
|
|
138
|
+
`💾 🌀 SELECT * FROM snacks`
|
|
139
|
+
|
|
140
|
+
| name | vibe | qty |
|
|
141
|
+
| ------------------ | ------------------ | --- |
|
|
142
|
+
| Hot Cheese Popcorn | chaos energy | 3 |
|
|
143
|
+
| Pocky | elegant simplicity | 12 |
|
|
144
|
+
| Takis | rolled-up danger | 7 |
|
|
145
|
+
| Goldfish | the people's snack | 41 |
|
|
146
|
+
|
|
147
|
+
## Low stock
|
|
148
|
+
|
|
149
|
+
`💾 💎 SELECT name, qty FROM snacks WHERE qty < 10 ORDER BY qty`
|
|
150
|
+
|
|
151
|
+
| name | qty |
|
|
152
|
+
| ------------------ | --- |
|
|
153
|
+
| Hot Cheese Popcorn | 3 |
|
|
154
|
+
| Takis | 7 |
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The 🌀 (feed) marker ingests the snacks data into the (unscoped) dataset, then the 💎 (tap) marker executes the query and surfaces only the snacks running low in stock.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
Additionally, after running:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
mdb pull "SELECT SUM(qty) as total_snacks FROM snacks"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
We'll get the output:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
total_snacks
|
|
171
|
+
63
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The 🌀 (feed) marker again ingests the snacks data into the dataset, but the 💎 (tap) marker is not processed.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
We can also mutate data directly. Running:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
mdb push "UPDATE snacks SET qty = qty + 20 WHERE name = 'Takis'"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The 🌀 (feed) marker again ingests the snacks data into the dataset, applies the UPDATE, then both 🌀 (feed) and 💎 (tap) markers push fresh results back:
|
|
185
|
+
|
|
186
|
+
```markdown
|
|
187
|
+
## Inventory
|
|
188
|
+
|
|
189
|
+
`💾 🌀 SELECT * FROM snacks`
|
|
190
|
+
|
|
191
|
+
| name | vibe | qty |
|
|
192
|
+
| ------------------ | ------------------ | --- |
|
|
193
|
+
| Hot Cheese Popcorn | chaos energy | 3 |
|
|
194
|
+
| Pocky | elegant simplicity | 12 |
|
|
195
|
+
| Takis | rolled-up danger | 27 |
|
|
196
|
+
| Goldfish | the people's snack | 41 |
|
|
197
|
+
|
|
198
|
+
## Low stock
|
|
199
|
+
|
|
200
|
+
`💾 💎 SELECT name, qty FROM snacks WHERE qty < 10 ORDER BY qty`
|
|
201
|
+
|
|
202
|
+
| name | qty |
|
|
203
|
+
| ------------------ | --- |
|
|
204
|
+
| Hot Cheese Popcorn | 3 |
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Takis got restocked to 27 and dropped off the low-stock list — all from a single command.
|
|
208
|
+
|
|
209
|
+
## Agent Support
|
|
210
|
+
|
|
211
|
+
`mdb-cli` includes a built-in agent skill. Install it:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
mdb init # default: .mdb/skills/mdb/SKILL.md
|
|
215
|
+
mdb init .claude/skills # Claude Code: .claude/skills/mdb/SKILL.md
|
|
216
|
+
mdb init .opencode/skills # OpenCode: .opencode/skills/mdb/SKILL.md
|
|
217
|
+
mdb init path/to/agent/skills # any agent: path/to/agent/skills/mdb/SKILL.md
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Within a session, invoke the `/mdb` slash command to access a comprehensive reference covering marker syntax, subcommand workflows, templates for common operations, and an error reference guide. The skill provides everything an AI coding assistant needs to construct and debug mdb markers without leaving the editor.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mdb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mdb/atomic.py,sha256=xnmxwGPWdFGZQIMLZ9uuOS6tZCZb6YayaVLj1dzCcqo,719
|
|
3
|
+
mdb/discovery.py,sha256=Y7sD9Xvcga6RGsOKhptLOjM2Gsb3vRPVbWXD2LYvKhw,2139
|
|
4
|
+
mdb/filelock.py,sha256=Lp4WYbA6Vt_-7SUffSAXhNGeuALDCXMQPkAeiVpEpYQ,2159
|
|
5
|
+
mdb/formatter.py,sha256=pWpllujgZQy4JAkjbLP7EJJtVl2c9cKlye6lJEJc-70,3486
|
|
6
|
+
mdb/init.py,sha256=CqeF8pqjmZ363sgU6Cymmmd6jMqkb8mnpHzZuirQYiw,5352
|
|
7
|
+
mdb/mdb.py,sha256=Zd_W64ZP3FVHD0SIYMw0SkIRu5zFrNKeKx73QMzgsGU,46314
|
|
8
|
+
mdb/models.py,sha256=TBPVvA5IcWLWriX5klQdgaLlIZgZR2K1Jhc2qcOtKoo,1871
|
|
9
|
+
mdb/parser.py,sha256=jgwIYTCoBgDAkeBxLj7UyTm75U1K5aNzuULU71U90gI,21661
|
|
10
|
+
mdb/puller.py,sha256=AiW0vjl5Mm4YRGNhPZbD24M4IjikJQO1-48r_ZnJ9Ds,7058
|
|
11
|
+
mdb/validators.py,sha256=quvEieq1_LGbFzV4K4zxLrLLgEFeYLTHqDEwHHUF6dU,1619
|
|
12
|
+
mdb/data/SKILL.md,sha256=0R5afekElV7QDdS9AETKqjz1MzVzNkbFJ3sDKM8DsUA,4776
|
|
13
|
+
mdb/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
mdb_cli-0.1.0.dist-info/METADATA,sha256=Q6M7nrayK9j0aX1bPP9SJcOffUFv5-6SaPCLn69lOrk,7657
|
|
15
|
+
mdb_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
+
mdb_cli-0.1.0.dist-info/entry_points.txt,sha256=jpkNA_RSyT0mb_bbnqohVCrcm_ymAkFMRA6q2YqTkbA,37
|
|
17
|
+
mdb_cli-0.1.0.dist-info/licenses/LICENSE,sha256=0tfi6n8q7wQbUQ47en8pj6Kq4SLQwZ5qfNRbTg-mUyo,1077
|
|
18
|
+
mdb_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mdb-cli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|