omgbase 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -0
- package/dist/src/cmd/_remote.d.ts +7 -0
- package/dist/src/cmd/_remote.js +19 -12
- package/dist/src/cmd/cat.js +11 -2
- package/dist/src/cmd/oqx.js +22 -23
- package/dist/src/cmd/run.js +4 -5
- package/dist/src/cmd/show.js +1 -1
- package/dist/src/cmd/source.js +6 -3
- package/dist/src/cmd/sync.js +12 -7
- package/dist/src/output.d.ts +19 -1
- package/dist/src/output.js +60 -0
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# omgbase — the `omg` CLI
|
|
2
|
+
|
|
3
|
+
`omg` (also installed as `omgbase`) is the command-line client for **Open Markdown Graph Base**: a versioned, addressable graph layer over a directory of Markdown/YAML/JSON files. Your files stay the human source of truth; an embedded SQLite database under `.omgbase/` owns block identity, history, links, and the indexes you query. The CLI is the engine's second client — a thin adapter over [`@omgbase/core`](https://github.com/omgbase/omgbase/tree/main/packages/core), **embedded and daemonless**: every command opens the database directly, does its work in-process, and exits. Design notes live in the [monorepo README](https://github.com/omgbase/omgbase#readme) and [`docs/cli.md`](https://github.com/omgbase/omgbase/blob/main/docs/cli.md).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g omgbase # the `omg` and `omgbase` binaries (Node >= 22)
|
|
9
|
+
npm install -g @omgbase/embedder # optional: local semantic search
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`@omgbase/embedder` provides `omgbase-embedder`, a local transformers.js model (`Xenova/gte-base`, 768-dim) spoken to over stdio, so the CLI itself carries no ML dependency. Without it, full-text search, queries, and edits all work; `omg find` and OQX `semantic("…")` just have no embedding index to rank by. `omg init` offers to configure it when it is on `PATH`, or set it later with `omg config set embedding.provider omgbase-embedder --repo ""`.
|
|
13
|
+
|
|
14
|
+
## Five-minute tour
|
|
15
|
+
|
|
16
|
+
A throwaway directory with two Markdown files, `README.md` (a heading, a task list, a `[[decisions]]` link) and `decisions.md` (frontmatter plus two ADR headings). Every transcript below is real output.
|
|
17
|
+
|
|
18
|
+
```console
|
|
19
|
+
$ omg init
|
|
20
|
+
not inside a git repo — no .gitignore needed for .omgbase/
|
|
21
|
+
omgbase > initialized
|
|
22
|
+
----------------------------------------
|
|
23
|
+
ok workspace /private/tmp/omg-tour/notes
|
|
24
|
+
next: omg source add . to point a repo at a directory of files
|
|
25
|
+
|
|
26
|
+
$ omg source add . -y
|
|
27
|
+
* notes ← /private/tmp/omg-tour/notes 2 files
|
|
28
|
+
|
|
29
|
+
$ omg status
|
|
30
|
+
omgbase > notes
|
|
31
|
+
/private/tmp/omg-tour/notes
|
|
32
|
+
----------------------------------------
|
|
33
|
+
[D] docs 2 watcher o none
|
|
34
|
+
[B] blocks 13 synced ok converged
|
|
35
|
+
* commits 2 queue empty
|
|
36
|
+
> edges 1 commit# 2
|
|
37
|
+
|
|
38
|
+
$ omg ls
|
|
39
|
+
README.md 8 blocks 0s ago
|
|
40
|
+
decisions.md 5 blocks 0s ago
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`init` creates the workspace and ingests nothing; `source add` points a repo at a directory (the repo takes the directory's name) and runs the initial sync. Now find and query:
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
$ omg find sqlite
|
|
47
|
+
b_v32ar6f decisions.md#heading[3] ADR-002 Embedded SQLite
|
|
48
|
+
|
|
49
|
+
$ omg q 'from blocks where type == "task" && !attrs.checked'
|
|
50
|
+
b_24nr0y0 README.md
|
|
51
|
+
b_36p2zw1 README.md
|
|
52
|
+
|
|
53
|
+
$ omg q '$repo.blocks count { where type == "task" && !attrs.checked }'
|
|
54
|
+
2
|
|
55
|
+
|
|
56
|
+
$ omg outline README.md
|
|
57
|
+
omgbase > README.md
|
|
58
|
+
----------------------------------------
|
|
59
|
+
b_7046e9t h1 Project notes §
|
|
60
|
+
b_y9cqtgq p A small vault to try out omgbase.
|
|
61
|
+
b_e1x1y8p h2 Launch §
|
|
62
|
+
b_zysh41w ul
|
|
63
|
+
b_36p2zw1 li ☐ Write the announcement
|
|
64
|
+
b_24nr0y0 li ☐ Ship the release
|
|
65
|
+
b_1nrsgvf li ☑ Pick a date
|
|
66
|
+
b_1w4t737 p See [[decisions]] for the why.
|
|
67
|
+
|
|
68
|
+
$ omg cat b_v32ar6f --resolution text
|
|
69
|
+
ADR-002 Embedded SQLite
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`omg cat <path>` prints a document's exact bytes (frontmatter and all); given a block id it prints that block. Edits are structural mutations that update the file and the database together. The pipe is the changeset boundary — `--ids` emits block ids, `done -` reads them from stdin and completes them in one commit:
|
|
73
|
+
|
|
74
|
+
```console
|
|
75
|
+
$ omg q 'from blocks where type == "task" && !attrs.checked' --ids | omg done -
|
|
76
|
+
ok committed · 1 document touched
|
|
77
|
+
b_24nr0y0
|
|
78
|
+
b_36p2zw1
|
|
79
|
+
|
|
80
|
+
$ omg append b_e1x1y8p -m 'Shipped on time.'
|
|
81
|
+
ok committed · 1 document touched
|
|
82
|
+
b_qb861mj
|
|
83
|
+
|
|
84
|
+
$ omg cat README.md | tail -3
|
|
85
|
+
See [[decisions]] for the why.
|
|
86
|
+
|
|
87
|
+
Shipped on time.
|
|
88
|
+
|
|
89
|
+
$ omg log
|
|
90
|
+
#1 observed observed: README.md — 8 inserted
|
|
91
|
+
#2 observed observed: decisions.md — 5 inserted
|
|
92
|
+
#3 api api(human:alice): README.md — 8 edited
|
|
93
|
+
#4 api api(human:alice): README.md — 8 edited, 1 inserted
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Every edit is a commit; `omg hist <block>` is one block's biography and `omg diff <doc>` a unified diff. More worked examples (a larger corpus, the OQX tutorial, the interactive shell) are under [`examples/`](https://github.com/omgbase/omgbase/tree/main/examples).
|
|
97
|
+
|
|
98
|
+
## Command catalog
|
|
99
|
+
|
|
100
|
+
Grouped exactly as `omg --help` prints them; `omg <command> --help` gives one uniform card per command.
|
|
101
|
+
|
|
102
|
+
**bootstrap**
|
|
103
|
+
- `init` — Create a workspace (run `source add` to ingest files)
|
|
104
|
+
- `source` — Where a repo's bytes come from (add/list/attach/detach/rm)
|
|
105
|
+
- `repos` — List repos in this workspace
|
|
106
|
+
|
|
107
|
+
**orient & read**
|
|
108
|
+
- `status` — Where am I: repo, sync, watcher, queue
|
|
109
|
+
- `ls` — List live documents
|
|
110
|
+
- `outline`, `ol` — Document outline (frozen wire format)
|
|
111
|
+
- `cat` — Content bytes of a node (default raw)
|
|
112
|
+
- `show` — Metadata card for a node
|
|
113
|
+
- `find` — Ranked hybrid search for the id of a thing
|
|
114
|
+
|
|
115
|
+
**query**
|
|
116
|
+
- `query`, `q` — Composable query (OQX: from/where/select + collection ops)
|
|
117
|
+
- `run` — Evaluate an ```` ```omg ```` fence (OQX, inert, read-only)
|
|
118
|
+
|
|
119
|
+
**history & links**
|
|
120
|
+
- `log` — Commit digests (change feed)
|
|
121
|
+
- `hist` — A block's change biography
|
|
122
|
+
- `diff` — Unified diff of a document
|
|
123
|
+
- `links` — Open edges touching a node
|
|
124
|
+
|
|
125
|
+
**mutate**
|
|
126
|
+
- `apply` — Apply a raw changeset (the primitive)
|
|
127
|
+
- `insert` — Insert blocks under a parent
|
|
128
|
+
- `update` — Replace a block, or reconcile a whole document (identity-preserving)
|
|
129
|
+
- `edit` — Edit a block in `$EDITOR` (CAS pinned)
|
|
130
|
+
- `move` — Move blocks under a new parent
|
|
131
|
+
- `rm` — Remove blocks (or `--doc` a document)
|
|
132
|
+
- `done` — Check/uncheck task blocks
|
|
133
|
+
- `append` — Append into a heading's section
|
|
134
|
+
- `retarget` — Rewrite a link target (plan-by-default)
|
|
135
|
+
- `split` — Split a block at offsets
|
|
136
|
+
- `merge` — Merge adjacent blocks
|
|
137
|
+
- `node` — Edit a node's editable properties (surgical)
|
|
138
|
+
|
|
139
|
+
**documents**
|
|
140
|
+
- `new` — Create a document
|
|
141
|
+
- `mv` — Rename a document
|
|
142
|
+
- `meta` — Patch a document's frontmatter
|
|
143
|
+
|
|
144
|
+
**session**
|
|
145
|
+
- `shell` — Persistent session with typed bindings (`@1`/`@_`/`@name`)
|
|
146
|
+
|
|
147
|
+
**sync & serve**
|
|
148
|
+
- `sync` — Reconcile a repo with its filesystem source (`--watch` to stay live; `--server` for remote)
|
|
149
|
+
- `mcp` — Serve the MCP tool surface on stdio
|
|
150
|
+
|
|
151
|
+
**admin**
|
|
152
|
+
- `rebuild-index` — Rebuild derived tables
|
|
153
|
+
- `gc` — Mark-and-sweep (flag-gated)
|
|
154
|
+
- `doctor` — Invariant sweep (CI-able)
|
|
155
|
+
- `config` — Read/write repo settings
|
|
156
|
+
- `embed` — Embedding queue: status, or drain to embed
|
|
157
|
+
|
|
158
|
+
## MCP server
|
|
159
|
+
|
|
160
|
+
`omg mcp` serves the full engine tool surface over stdio with an in-process file watcher, so an agent session stays fresh; the host owns the process lifetime. Hosts launch servers from their own cwd, so point `-C` at an initialized workspace (a directory at or below one containing `.omgbase/`). Run outside a workspace it fails with `repo_not_found` and prints the `-C` form to use.
|
|
161
|
+
|
|
162
|
+
Claude Code:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
claude mcp add omg -- omg mcp -C /path/to/vault
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Claude Desktop (`claude_desktop_config.json`), Cursor (`.cursor/mcp.json`), and any host that takes an `mcpServers` map:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{ "mcpServers": { "omg": { "command": "omg", "args": ["mcp", "-C", "/path/to/vault"] } } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Add `--repo <slug>` to `args` when the workspace holds several repos, and `--no-watch` to skip the in-process watcher (it is auto-off when another live watcher already holds the lease).
|
|
175
|
+
|
|
176
|
+
## Remote mode
|
|
177
|
+
|
|
178
|
+
The global `--server <cmd|url>` flag runs the same command against a **remote engine over MCP** instead of the embedded local store: an `http(s)://` value connects over Streamable HTTP (add headers with repeatable `-H "Name: value"`), anything else is a command spawned and spoken to over stdio, e.g. `omg --server "omg mcp -C /path/to/vault" ls`. Implemented for the reads `sync`, `query`, `outline`, `hist`, `cat`, `ls`, `diff`, `find`, `log`; the doc-level mutators `new`, `mv`, `meta`, `rm`, `update`, `retarget`; the block-level sugar `apply`, `insert`, `move`, `split`, `merge`, `done`, `append`, `node`; and `shell`. Other commands (`status`, `edit`, `init`, `source`, admin) reject `--server` with a usage error rather than silently running locally.
|
|
179
|
+
|
|
180
|
+
## Output contract
|
|
181
|
+
|
|
182
|
+
Global flags are recognized anywhere on the line: `-C <dir>`, `--repo <slug>`, `--json`, `--jsonl`, `--ids`, `--stale`, `--no-color`, `--dry-run`, `--server <cmd|url>`, `-H <header>`, `--help`/`-h`, `--version`/`-V`.
|
|
183
|
+
|
|
184
|
+
- Human output (default) is colorized on a TTY; `--no-color`, `NO_COLOR`, or a pipe degrades to plain text.
|
|
185
|
+
- Query hits (`omg q`, `omg run`) print as `<id> <path>` lines. When the query projects (`select …`), they print as an aligned table instead: a dim header row, the id first, then the path (unless the projection itself selects `$path`), then the projected columns in `select` order. Strings and numbers are verbatim, an absent field is an empty cell, and a nested list/record is compact JSON clipped at 60 characters with `…`; `--jsonl` has the full values.
|
|
186
|
+
- `--json` prints the library's result object verbatim as one JSON document (the same shape the MCP tools return). `--jsonl` flattens list results to one object per line; `--ids` prints bare ids/paths, one per line — pipe fuel for `omg cat -`, `omg show -`, `omg done -`.
|
|
187
|
+
- `--dry-run` is global across every mutator: full validation and render, diffs printed, nothing committed.
|
|
188
|
+
- Errors always go to stderr: `error[<code>]: <message>` plus any conflict payload, or the typed error object as JSON with `--json`.
|
|
189
|
+
- Truncated list results print `… truncated; continue with --cursor <c>` on stderr and still exit 0.
|
|
190
|
+
|
|
191
|
+
Exit codes: `0` success (including truncated); `1` typed engine error or conflict; `2` CLI usage error (unknown command or flag, missing argument, `--server` on an unsupported command).
|
|
192
|
+
|
|
193
|
+
## Freshness
|
|
194
|
+
|
|
195
|
+
Reads are current by default: before running, a command sweeps the repo's files for changes since the last ingest and re-ingests what moved (tens of milliseconds when nothing changed). Skip the sweep with `--stale`, or run `omg sync --watch` — a live watcher holds a lease on `.omgbase/watch.lock`, and commands that see a live lease skip the sweep. `omg status` reports `watcher: live` or `none`.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { type EngineSpec } from "@omgbase/sync";
|
|
1
2
|
import type { Cli } from "../context.js";
|
|
3
|
+
/** Classify the global `--server` (+ `-H`) flags into an `EngineSpec`: an
|
|
4
|
+
* http(s) URL → Streamable HTTP with the headers attached; anything else → a
|
|
5
|
+
* command to spawn over stdio (a URL is never spawned). Usage errors — an empty
|
|
6
|
+
* value, `-H` with a stdio command — are `CliUsageError` (exit 2). Pure:
|
|
7
|
+
* nothing is connected or spawned here. */
|
|
8
|
+
export declare function remoteSpec(cli: Cli): EngineSpec;
|
|
2
9
|
/** Call an MCP tool on the remote engine, threading the global `--repo` slug.
|
|
3
10
|
* Returns the tool's parsed JSON result (same shape the local core fn returns,
|
|
4
11
|
* so the command's existing renderer works unchanged). */
|
package/dist/src/cmd/_remote.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { connectEngine, parseEngineSpec } from "@omgbase/sync";
|
|
2
2
|
import { CliUsageError } from "../output.js";
|
|
3
3
|
// Remote (`--server`) mode plumbing (ADR-014). When a command runs with the
|
|
4
4
|
// global `--server <cmd|url>` flag, it reaches a remote engine over MCP instead
|
|
@@ -8,7 +8,10 @@ import { CliUsageError } from "../output.js";
|
|
|
8
8
|
// closed via `closeRemote` at the end of a one-shot command or on shell exit.
|
|
9
9
|
//
|
|
10
10
|
// The flag is an http(s) URL to reach a remote server over Streamable HTTP, or
|
|
11
|
-
// otherwise a command to spawn and talk to over stdio.
|
|
11
|
+
// otherwise a command to spawn and talk to over stdio. That one rule lives in
|
|
12
|
+
// `@omgbase/sync`'s `parseEngineSpec`; `remoteSpec` below is its CLI face, and
|
|
13
|
+
// every `--server` consumer goes through it — including `sync`, which hands the
|
|
14
|
+
// spec to `runFsMirror` instead of calling tools itself.
|
|
12
15
|
let engine = null;
|
|
13
16
|
/** The process's remote engine, connecting (spawning the command or opening the
|
|
14
17
|
* HTTP transport) on first use. `--repo` is threaded into each tool call by
|
|
@@ -16,19 +19,23 @@ let engine = null;
|
|
|
16
19
|
async function remoteEngine(cli) {
|
|
17
20
|
if (engine)
|
|
18
21
|
return engine;
|
|
22
|
+
engine = await connectEngine(remoteSpec(cli));
|
|
23
|
+
return engine;
|
|
24
|
+
}
|
|
25
|
+
/** Classify the global `--server` (+ `-H`) flags into an `EngineSpec`: an
|
|
26
|
+
* http(s) URL → Streamable HTTP with the headers attached; anything else → a
|
|
27
|
+
* command to spawn over stdio (a URL is never spawned). Usage errors — an empty
|
|
28
|
+
* value, `-H` with a stdio command — are `CliUsageError` (exit 2). Pure:
|
|
29
|
+
* nothing is connected or spawned here. */
|
|
30
|
+
export function remoteSpec(cli) {
|
|
19
31
|
const spec = (cli.flags.server ?? "").trim();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return engine;
|
|
24
|
-
}
|
|
25
|
-
if (cli.flags.headers?.length)
|
|
32
|
+
const headers = parseHeaders(cli.flags.headers);
|
|
33
|
+
const isUrl = /^https?:\/\//i.test(spec);
|
|
34
|
+
if (!isUrl && Object.keys(headers).length > 0)
|
|
26
35
|
throw new CliUsageError("-H/--header only applies to an http(s) --server url");
|
|
27
|
-
|
|
28
|
-
if (argv.length === 0)
|
|
36
|
+
if (spec.length === 0)
|
|
29
37
|
throw new CliUsageError("--server requires a command to spawn (e.g. --server \"omg mcp -C /vault\") or an http(s) URL");
|
|
30
|
-
|
|
31
|
-
return engine;
|
|
38
|
+
return parseEngineSpec(spec, headers);
|
|
32
39
|
}
|
|
33
40
|
/** Parse repeatable `-H "Name: value"` flags into a header map. Splits on the
|
|
34
41
|
* first colon; later values win for a repeated name. */
|
package/dist/src/cmd/cat.js
CHANGED
|
@@ -3,11 +3,19 @@ import { resolveRef, nodesGet, docsRead } from "@omgbase/core";
|
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
4
|
import { remoteCall } from "./_remote.js";
|
|
5
5
|
import { expandBlockArgs } from "./_mutate.js";
|
|
6
|
+
/** `--resolution` only means something for a block; say so (stderr) rather than
|
|
7
|
+
* silently handing back raw bytes as if the flag had applied. */
|
|
8
|
+
function warnDocResolution(cli, ref, resolution) {
|
|
9
|
+
if (resolution === "raw")
|
|
10
|
+
return;
|
|
11
|
+
cli.io.err(cli.style.warn(` --resolution ${resolution} ignored for ${ref}: a document is always its exact bytes (resolutions apply to block refs)`));
|
|
12
|
+
}
|
|
6
13
|
async function catOne(cli, ref, resolution) {
|
|
7
14
|
// Remote: the polymorphic `read_ref` tool classifies + reads server-side.
|
|
8
15
|
if (cli.flags.server) {
|
|
9
16
|
const r = await remoteCall(cli, "read_ref", { ref, ...(resolution !== "raw" ? { resolution } : {}) });
|
|
10
17
|
if (r.kind === "document") {
|
|
18
|
+
warnDocResolution(cli, ref, resolution);
|
|
11
19
|
return { text: String(r.content ?? ""), json: { doc: r.docId, path: r.path, content: r.content }, capture: r.content };
|
|
12
20
|
}
|
|
13
21
|
return { text: String(r.raw ?? r.text ?? r.label ?? ""), json: r, capture: r };
|
|
@@ -26,6 +34,7 @@ async function catOne(cli, ref, resolution) {
|
|
|
26
34
|
const res = docsRead(ws.store, resolved.docId);
|
|
27
35
|
if (!res)
|
|
28
36
|
throw new EngineErrorLike("doc_missing", `no document ${ref}`);
|
|
37
|
+
warnDocResolution(cli, ref, resolution);
|
|
29
38
|
// shell: the bytes (a string; @_ only)
|
|
30
39
|
return { text: res.content, json: { doc: res.docId, path: res.path, content: res.content }, capture: res.content };
|
|
31
40
|
}
|
|
@@ -47,9 +56,9 @@ async function runCat(cli, args) {
|
|
|
47
56
|
summary: "Content bytes of a node — exact raw bytes by default, pipe-clean (`show` is the metadata card)",
|
|
48
57
|
usage: "cat <node…|-> [--resolution raw|text|outline|skeleton|full]",
|
|
49
58
|
options: [
|
|
50
|
-
["<node…>", "one or more refs: a
|
|
59
|
+
["<node…>", "one or more refs: a block id (`b_…`), a doc id (`d_…`), a node id (`n_…`), or a repo-relative doc path (`notes/x.md`)"],
|
|
51
60
|
["-", `read refs from stdin, one per line (\`${cli.prog} q … --ids | ${cli.prog} cat -\`)`],
|
|
52
|
-
["--resolution <r>", "raw (default: exact bytes) | text | outline | skeleton | full"],
|
|
61
|
+
["--resolution <r>", "block refs only: raw (default: exact bytes) | text | outline | skeleton | full; a document is always its exact bytes (warns if given)"],
|
|
53
62
|
],
|
|
54
63
|
});
|
|
55
64
|
}
|
package/dist/src/cmd/oqx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
|
|
4
|
-
import { truncationFooter, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
+
import { truncationFooter, EngineErrorLike, EXIT_OK, renderHelp, renderHits } from "../output.js";
|
|
5
5
|
import { loadEmbedding } from "./_embed.js";
|
|
6
6
|
import { remoteCall } from "./_remote.js";
|
|
7
7
|
import { readStdin } from "./_mutate.js";
|
|
@@ -10,12 +10,13 @@ import { readStdin } from "./_mutate.js";
|
|
|
10
10
|
// belong to OQX (`<receiver> collect|exists|count|first|single { <block> }`).
|
|
11
11
|
// `from E` selects + flattens a relation relative to the current source scope
|
|
12
12
|
// (top-level `from docs` = the repository's docs; `$repo.docs collect { from
|
|
13
|
-
// nodes … }` re-projects each doc through its nodes).
|
|
14
|
-
// `
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// nodes … }` re-projects each doc through its nodes). Clause order is fixed
|
|
14
|
+
// (ADR-020): `select … from … where … follow … order by … limit … offset …`,
|
|
15
|
+
// each at most once, at the top level and inside every block. Only `select` may
|
|
16
|
+
// drop its keyword, and only as the first clause (`nodes collect { attrs.text }`
|
|
17
|
+
// projects); a predicate always needs `where` (`nodes exists { where kind ==
|
|
18
|
+
// "md:task" }`), so a bare boolean property projects rather than filters.
|
|
19
|
+
// `where` may use the body's select aliases. Features: a boolean where
|
|
19
20
|
// tree over scalar predicates and consumer directives, count comparisons
|
|
20
21
|
// (`nodes count { … } >= 2`), nestable collect, one-scope lifts (^name:) that
|
|
21
22
|
// filter + capture, one-scope-outward references (^name) that correlate a nested
|
|
@@ -28,8 +29,9 @@ import { readStdin } from "./_mutate.js";
|
|
|
28
29
|
// `$repo.docs count { … }` / `$repo.docs exists { … }` reduce to a scalar,
|
|
29
30
|
// `$repo.docs first { … }` / `$repo.docs single { … }` to zero-or-one row (bare
|
|
30
31
|
// `from …` = collect). Coexists with `omg q` (CEL). Source is a positional
|
|
31
|
-
// string or -f file|-. Human output: one hit per line (id + path), or
|
|
32
|
-
//
|
|
32
|
+
// string or -f file|-. Human output: one hit per line (id + path), or an aligned
|
|
33
|
+
// id/path/columns table when the query projects (`select`), or the scalar for
|
|
34
|
+
// count/exists/none (see output.ts renderHits); --json/--jsonl/--ids.
|
|
33
35
|
async function runOqx(cli, args) {
|
|
34
36
|
const { values, positionals } = parseArgs({
|
|
35
37
|
args,
|
|
@@ -60,25 +62,24 @@ async function runOqx(cli, args) {
|
|
|
60
62
|
],
|
|
61
63
|
});
|
|
62
64
|
cli.io.out(" e.g. query 'from docs where nodes count { where kind == \"md:task\" } >= 2'");
|
|
63
|
-
cli.io.out(" query '
|
|
64
|
-
cli.io.out(" query 'from docs where nodes collect { ^open: value where kind == \"md:task\" && !attrs.checked }
|
|
65
|
-
cli.io.out(" query '
|
|
65
|
+
cli.io.out(" query 'select items: section.blocks collect { where type == \"list_item\" } from nodes where kind == \"md:section\"'");
|
|
66
|
+
cli.io.out(" query 'select $path, open from docs where nodes collect { ^open: value where kind == \"md:task\" && !attrs.checked }'");
|
|
67
|
+
cli.io.out(" query 'select owner_id, owner: $repo.nodes single { where kind == \"person\" && attrs.id == ^owner_id } from docs'");
|
|
66
68
|
cli.io.out(" query '$repo.docs count { where layer == \"canon\" }' # scalar; also $repo.<target> exists/none/first/single { … }");
|
|
67
69
|
cli.io.out(" query 'from docs where nodes none { where kind == \"md:task\" && !checked }' # none = zero rows (≡ !exists; \"every\" = none over the complement)");
|
|
68
70
|
cli.io.out(" query 'from docs where type == \"practitioner\" order by era desc limit 2 offset 1' # limit/offset bound the set (after order/distinct, before the consumer)");
|
|
69
|
-
cli.io.out(" query '
|
|
70
|
-
cli.io.out(" query 'from docs where layer == \"canon\"
|
|
71
|
-
cli.io.out(" query '
|
|
71
|
+
cli.io.out(" query 'select fm: entries(frontmatter) collect { k: $key, v: $value } from docs where $path == \"x.md\"' # a record as a collection ($key/$value); also entries(attrs), entries(inline)");
|
|
72
|
+
cli.io.out(" query 'select $path values from docs where layer == \"canon\"' # `values`: bare values, no {id,path} hits (one item only)");
|
|
73
|
+
cli.io.out(" query 'select $path, tags: tags collect { $value values where $value != \"draft\" } from docs' # $value = the current item (here: each tag)");
|
|
72
74
|
cli.io.out(" query '$repo.docs collect { from nodes where kind == \"md:task\" }' # `from E` re-projects the source (→ nodes)");
|
|
73
75
|
cli.io.out(" query 'from docs where text(\"philosophers stone\") && layer == \"canon\"' # full-text prune");
|
|
74
|
-
cli.io.out(" query '
|
|
76
|
+
cli.io.out(" query 'select s: semantic(\"the great work\") from blocks where semantic(\"the great work\") > 0.6' # embedding score (needs a provider)");
|
|
75
77
|
cli.io.out(" query 'from docs where type == \"practitioner\" order by era desc' # order by <expr> [asc|desc]");
|
|
76
|
-
cli.io.out(" query '
|
|
78
|
+
cli.io.out(" query 'select t: text, d: $depth, s: $stop from blocks where $id == \"b_x\" follow block.children' # recursive walk ($depth/$stop metadata)");
|
|
77
79
|
cli.io.out(" query 'from nodes where name == \"Overview\" follow section.subsections { depth 3 }' # follow [distinct] <rel> [{ where … frontier … depth n by … }]");
|
|
78
|
-
cli.io.out(" query '
|
|
80
|
+
cli.io.out(" query 'select p: $path, d: $depth, s: $stop from docs where $path == \"index.md\" follow doc.out' # citation graph (cyclic-safe: $stop=cycle)");
|
|
79
81
|
cli.io.out(" query 'from blocks where type == \"list_item\" && $leaf follow block.children' # $leaf/$depth/$stop filter the walk result post-walk");
|
|
80
|
-
cli.io.out(" query 'from edges where predicate == \"depends_on\"
|
|
81
|
-
cli.io.out(" query 'from docs where $path == \"a.md\" follow doc.out { via predicate == \"cites\" }' # predicate-filtered traversal (via = the licensing edge)");
|
|
82
|
+
cli.io.out(" query 'select src: $src, to: $dst_path from edges where predicate == \"depends_on\"' # the edges target: predicate/provenance/dst_kind + $src/$dst_path/$dst_uri");
|
|
82
83
|
return EXIT_OK;
|
|
83
84
|
}
|
|
84
85
|
const source = values.file
|
|
@@ -174,9 +175,7 @@ async function runOqx(cli, args) {
|
|
|
174
175
|
io.err(style.dim(" no hits"));
|
|
175
176
|
return EXIT_OK;
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
-
io.out(`${style.id(h.id)} ${style.accent(h.path)}`.trimEnd());
|
|
179
|
-
}
|
|
178
|
+
renderHits(cli, result.hits);
|
|
180
179
|
if (result.truncated)
|
|
181
180
|
truncationFooter(io, style, result.cursor ?? "");
|
|
182
181
|
return EXIT_OK;
|
package/dist/src/cmd/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { resolveRef, findDoc, loadDocBlocks, blockRaw, oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
|
|
3
|
-
import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
|
|
3
|
+
import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp, renderHits } from "../output.js";
|
|
4
4
|
import { loadEmbedding } from "./_embed.js";
|
|
5
5
|
// `omg run <locator|path>` (11 §5.3) — evaluate the first ```omg fence in a doc
|
|
6
6
|
// (or the fence at a block locator) and print its results. The fence body is an
|
|
@@ -123,10 +123,9 @@ async function runRun(cli, args) {
|
|
|
123
123
|
io.err(style.dim(" no hits"));
|
|
124
124
|
return EXIT_OK;
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
126
|
+
// Same renderer as `omg query`: `<id> <path>` lines, or a column table when
|
|
127
|
+
// the fence projects (a selected `text` shows as its first line).
|
|
128
|
+
renderHits(cli, result.hits);
|
|
130
129
|
if (result.truncated)
|
|
131
130
|
truncationFooter(io, style, result.cursor ?? "");
|
|
132
131
|
return EXIT_OK;
|
package/dist/src/cmd/show.js
CHANGED
|
@@ -33,7 +33,7 @@ function runShow(cli, args) {
|
|
|
33
33
|
summary: "Metadata card for a node: attrs, placement, open edges, last change (`cat` is the bytes)",
|
|
34
34
|
usage: "show <node…|-> [--include history]",
|
|
35
35
|
options: [
|
|
36
|
-
["<node…>", "one or more refs: a
|
|
36
|
+
["<node…>", "one or more refs: a block id (`b_…`), a doc id (`d_…`), a node id (`n_…`), or a repo-relative doc path"],
|
|
37
37
|
["-", `read refs from stdin, one per line (\`${cli.prog} q … --ids | ${cli.prog} show -\`)`],
|
|
38
38
|
["--include <list>", "comma-separated extras; `history` adds the block's last 5 changes"],
|
|
39
39
|
],
|
package/dist/src/cmd/source.js
CHANGED
|
@@ -19,7 +19,7 @@ function help(cli) {
|
|
|
19
19
|
summary: "Where a repo's bytes come from (a filesystem directory today; git/S3/… via adapters later)",
|
|
20
20
|
usage: "source <add|list|attach|detach|rm> …",
|
|
21
21
|
options: [
|
|
22
|
-
["add <dir> [--
|
|
22
|
+
["add <dir> [--repo <slug>] [--name <n>] [-y]", "point a repo at a directory: creates the repo (named after the dir unless --repo) + runs the initial sync (-y skips the consent prompt)"],
|
|
23
23
|
["list", "list sources and which repos they feed"],
|
|
24
24
|
["attach <name> [--repo <slug>]", "attach an existing source to a repo"],
|
|
25
25
|
["detach <name> [--repo <slug>]", "detach a source from a repo"],
|
|
@@ -82,7 +82,7 @@ async function runAdd(cli, args) {
|
|
|
82
82
|
const { values, positionals } = parseArgs({
|
|
83
83
|
args,
|
|
84
84
|
allowPositionals: true,
|
|
85
|
-
options: {
|
|
85
|
+
options: { name: { type: "string" }, yes: { type: "boolean", short: "y" }, help: { type: "boolean" } },
|
|
86
86
|
});
|
|
87
87
|
if (values.help)
|
|
88
88
|
return help(cli);
|
|
@@ -92,7 +92,10 @@ async function runAdd(cli, args) {
|
|
|
92
92
|
const abs = resolve(cli.cwd, dir);
|
|
93
93
|
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
94
94
|
throw new EngineErrorLike("target_missing", `no such directory: ${abs}`);
|
|
95
|
-
|
|
95
|
+
// `--repo <slug>` is the GLOBAL repo-selection flag (parsed in dispatch.ts, so it
|
|
96
|
+
// never reaches this parser); for `source add` the selected repo is the one being
|
|
97
|
+
// created/pointed, defaulting to the directory's name.
|
|
98
|
+
const slug = cli.flags.repo ?? (basename(abs) || "vault");
|
|
96
99
|
const sourceName = values.name ?? `${slug}-fs`;
|
|
97
100
|
const ws = cli.workspace();
|
|
98
101
|
if (getSourceByName(ws.store, sourceName))
|
package/dist/src/cmd/sync.js
CHANGED
|
@@ -4,14 +4,18 @@ import { Watcher, WatchLease, EmbedDrainer, freshnessSweep, EngineError } from "
|
|
|
4
4
|
import { runFsMirror } from "@omgbase/sync";
|
|
5
5
|
import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
6
6
|
import { loadEmbedding } from "./_embed.js";
|
|
7
|
+
import { remoteSpec } from "./_remote.js";
|
|
7
8
|
import { openRepoSource } from "./_source.js";
|
|
8
9
|
// `omg sync` — reconcile a repo with its filesystem source. One verb, three modes:
|
|
9
10
|
// omg sync one-shot local freshness sweep (fs → DB)
|
|
10
11
|
// omg sync --watch stay live locally (external fs-adapter watcher)
|
|
11
|
-
// omg sync --server <cmd>
|
|
12
|
+
// omg sync --server <cmd|url> the same, but against a remote engine over MCP
|
|
12
13
|
// (the coordinator — mirrors `omgbase-sync`)
|
|
13
|
-
// The `--server` form is the global remote flag (context REMOTE_OK)
|
|
14
|
-
//
|
|
14
|
+
// The `--server` form is the global remote flag (context REMOTE_OK) and obeys
|
|
15
|
+
// the same rule as every other remote command (`remoteSpec` in _remote.ts): an
|
|
16
|
+
// http(s) URL is reached over Streamable HTTP (with `-H` headers); anything else
|
|
17
|
+
// is a command spawned over stdio. It shares `runFsMirror` with the standalone
|
|
18
|
+
// `omgbase-sync` bin. There is no separate
|
|
15
19
|
// `omg watch` — watching is `omg sync --watch`. The local one-shot IS the
|
|
16
20
|
// explicit form of the freshness sweep every read runs by default.
|
|
17
21
|
function help(cli) {
|
|
@@ -21,8 +25,8 @@ function help(cli) {
|
|
|
21
25
|
usage: ["sync [--watch]", 'sync --server <cmd|url> [-H "Name: value"]… [--root <dir>] [--out]'],
|
|
22
26
|
options: [
|
|
23
27
|
["--watch", "stay live and reconcile edits as they land"],
|
|
24
|
-
["--server <cmd|url>", "mirror a local directory into a remote engine over MCP
|
|
25
|
-
['-H "Name: value"', "(--server url) extra HTTP header, repeatable"],
|
|
28
|
+
["--server <cmd|url>", "mirror a local directory into a remote engine over MCP: an http(s) url connects over Streamable HTTP; anything else is spawned as a stdio MCP server command"],
|
|
29
|
+
['-H "Name: value"', "(--server url only) extra HTTP header, repeatable"],
|
|
26
30
|
["--root <dir>", "(--server) the directory to mirror (default cwd)"],
|
|
27
31
|
["--out", "(--server) also export engine-authored changes back to disk"],
|
|
28
32
|
],
|
|
@@ -38,11 +42,12 @@ async function runSync(cli, args) {
|
|
|
38
42
|
return help(cli);
|
|
39
43
|
// Remote mode: run the coordinator against an MCP server (shared with the
|
|
40
44
|
// omgbase-sync bin). No local workspace needed — the fs dir is the source.
|
|
45
|
+
// `remoteSpec` classifies url-vs-command exactly as the other remote commands
|
|
46
|
+
// do, so an http(s) --server is connected to, never spawned as a command.
|
|
41
47
|
if (cli.flags.server !== undefined) {
|
|
42
48
|
const root = resolve(cli.cwd, values.root ?? ".");
|
|
43
|
-
const server = cli.flags.server.split(/\s+/).filter(Boolean);
|
|
44
49
|
await runFsMirror({
|
|
45
|
-
server,
|
|
50
|
+
server: remoteSpec(cli),
|
|
46
51
|
root,
|
|
47
52
|
...(values.watch ? { watch: true } : {}),
|
|
48
53
|
...(values.out ? { out: true } : {}),
|
package/dist/src/output.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IO } from "./render.js";
|
|
2
2
|
import type { Style } from "./style.js";
|
|
3
3
|
export declare const EXIT_OK = 0;
|
|
4
4
|
export declare const EXIT_ERROR = 1;
|
|
@@ -55,6 +55,24 @@ export declare class EngineErrorLike extends Error {
|
|
|
55
55
|
* current-truth payload pretty-printed.
|
|
56
56
|
*/
|
|
57
57
|
export declare function renderError(err: unknown, io: IO, style: Style, json: boolean): number;
|
|
58
|
+
export interface HitLike {
|
|
59
|
+
id: string;
|
|
60
|
+
path: string;
|
|
61
|
+
[k: string]: unknown;
|
|
62
|
+
}
|
|
63
|
+
export declare const HIT_CELL_MAX = 60;
|
|
64
|
+
/** Clip `s` to `max` visible characters, marking the cut with `…`. */
|
|
65
|
+
export declare function truncateCell(s: string, max?: number): string;
|
|
66
|
+
/** Render one projected value as a table cell (unstyled). */
|
|
67
|
+
export declare function hitCell(v: unknown): string;
|
|
68
|
+
/** The projected column names across `hits`: every key but the injected id/path,
|
|
69
|
+
* in first-seen order. Empty when nothing was projected. */
|
|
70
|
+
export declare function hitColumns(hits: HitLike[]): string[];
|
|
71
|
+
/** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected. */
|
|
72
|
+
export declare function renderHits(cli: {
|
|
73
|
+
io: IO;
|
|
74
|
+
style: Style;
|
|
75
|
+
}, hits: HitLike[]): void;
|
|
58
76
|
/** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
|
|
59
77
|
export declare function truncationFooter(io: IO, style: Style, cursor: number | string): void;
|
|
60
78
|
export {};
|
package/dist/src/output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { columns, visibleWidth } from "./render.js";
|
|
1
2
|
// Output contract + error rendering (11 §2.4, §4).
|
|
2
3
|
//
|
|
3
4
|
// Exit codes: 0 success (incl. truncated) · 1 typed engine error / conflict
|
|
@@ -124,6 +125,65 @@ function indent(s, pad) {
|
|
|
124
125
|
.map((l) => pad + l)
|
|
125
126
|
.join("\n");
|
|
126
127
|
}
|
|
128
|
+
export const HIT_CELL_MAX = 60;
|
|
129
|
+
/** Clip `s` to `max` visible characters, marking the cut with `…`. */
|
|
130
|
+
export function truncateCell(s, max = HIT_CELL_MAX) {
|
|
131
|
+
if (visibleWidth(s) <= max)
|
|
132
|
+
return s;
|
|
133
|
+
return [...s].slice(0, Math.max(max - 1, 0)).join("") + "…";
|
|
134
|
+
}
|
|
135
|
+
/** Render one projected value as a table cell (unstyled). */
|
|
136
|
+
export function hitCell(v) {
|
|
137
|
+
if (v === null || v === undefined)
|
|
138
|
+
return "";
|
|
139
|
+
if (typeof v === "string") {
|
|
140
|
+
const nl = v.indexOf("\n");
|
|
141
|
+
return truncateCell(nl === -1 ? v : `${v.slice(0, nl)}…`);
|
|
142
|
+
}
|
|
143
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint")
|
|
144
|
+
return String(v);
|
|
145
|
+
return truncateCell(JSON.stringify(v) ?? String(v));
|
|
146
|
+
}
|
|
147
|
+
/** The projected column names across `hits`: every key but the injected id/path,
|
|
148
|
+
* in first-seen order. Empty when nothing was projected. */
|
|
149
|
+
export function hitColumns(hits) {
|
|
150
|
+
const cols = [];
|
|
151
|
+
const seen = new Set(["id", "path"]);
|
|
152
|
+
for (const h of hits) {
|
|
153
|
+
for (const k of Object.keys(h)) {
|
|
154
|
+
if (seen.has(k))
|
|
155
|
+
continue;
|
|
156
|
+
seen.add(k);
|
|
157
|
+
cols.push(k);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return cols;
|
|
161
|
+
}
|
|
162
|
+
/** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected. */
|
|
163
|
+
export function renderHits(cli, hits) {
|
|
164
|
+
const { io, style } = cli;
|
|
165
|
+
const projected = hitColumns(hits);
|
|
166
|
+
if (projected.length === 0) {
|
|
167
|
+
for (const h of hits)
|
|
168
|
+
io.out(`${style.id(h.id)} ${style.accent(h.path)}`.trimEnd());
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const cols = projected.includes("$path") ? ["id", ...projected] : ["id", "path", ...projected];
|
|
172
|
+
const rows = [cols.map((c) => style.dim(c))];
|
|
173
|
+
for (const h of hits) {
|
|
174
|
+
rows.push(cols.map((c) => {
|
|
175
|
+
if (c === "id")
|
|
176
|
+
return style.id(h.id);
|
|
177
|
+
if (c === "path")
|
|
178
|
+
return style.accent(h.path);
|
|
179
|
+
if (c === "$path" && typeof h[c] === "string")
|
|
180
|
+
return style.accent(h[c]);
|
|
181
|
+
return hitCell(h[c]);
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
for (const line of columns(rows))
|
|
185
|
+
io.out(line);
|
|
186
|
+
}
|
|
127
187
|
/** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
|
|
128
188
|
export function truncationFooter(io, style, cursor) {
|
|
129
189
|
io.err(style.dim(`… truncated; continue with --cursor ${cursor}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omgbase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open Markdown Graph Base — CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
"omg": "./dist/src/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@omgbase/core": "^0.
|
|
48
|
+
"@omgbase/core": "^0.2.0",
|
|
49
49
|
"@omgbase/fs-adapter": "^0.1.0",
|
|
50
|
-
"@omgbase/sync": "^0.
|
|
50
|
+
"@omgbase/sync": "^0.2.0",
|
|
51
51
|
"yaml": "^2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@simplebrains/recital": "^0.8.
|
|
54
|
+
"@simplebrains/recital": "^0.8.1"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsc -p tsconfig.json",
|