omgbase 0.10.1 → 0.11.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 CHANGED
@@ -182,7 +182,7 @@ The global `--server <cmd|url>` flag runs the same command against a **remote en
182
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
183
 
184
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.
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
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
187
  - `--dry-run` is global across every mutator: full validation and render, diffs printed, nothing committed.
188
188
  - Errors always go to stderr: `error[<code>]: <message>` plus any conflict payload, or the typed error object as JSON with `--json`.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "449eaff",
3
- "built": "2026-10-10T13:16:55.528Z"
2
+ "commit": "0145153",
3
+ "built": "2026-10-12T05:05:42.833Z"
4
4
  }
@@ -38,7 +38,7 @@ export interface RunOpsOptions {
38
38
  export declare function runOps(cli: Cli, ws: Workspace, repo: RepoRow, ops: Op[], opts?: RunOpsOptions): number;
39
39
  /**
40
40
  * Remote peer of runOps: call the changeset-producing tool that mirrors this
41
- * command (blocks_insert, tasks_complete, …) against the `--server` engine — the
41
+ * command (blocks_insert, blocks_check, …) against the `--server` engine — the
42
42
  * server resolves refs + pins CAS + applies — then render the returned
43
43
  * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
44
44
  * tool's dry_run so the preview comes back without committing.
@@ -142,7 +142,7 @@ export function runOps(cli, ws, repo, ops, opts = {}) {
142
142
  }
143
143
  /**
144
144
  * Remote peer of runOps: call the changeset-producing tool that mirrors this
145
- * command (blocks_insert, tasks_complete, …) against the `--server` engine — the
145
+ * command (blocks_insert, blocks_check, …) against the `--server` engine — the
146
146
  * server resolves refs + pins CAS + applies — then render the returned
147
147
  * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
148
148
  * tool's dry_run so the preview comes back without committing.
@@ -1,4 +1,5 @@
1
1
  import { type EngineSpec } from "@omgbase/sync";
2
+ import { type Store } from "@omgbase/core";
2
3
  import type { Cli } from "../context.js";
3
4
  /** Classify the global `--server` (+ `-H`) flags into an `EngineSpec`: an
4
5
  * http(s) URL → Streamable HTTP with the headers attached; anything else → a
@@ -10,6 +11,21 @@ export declare function remoteSpec(cli: Cli): EngineSpec;
10
11
  * Returns the tool's parsed JSON result (same shape the local core fn returns,
11
12
  * so the command's existing renderer works unchanged). */
12
13
  export declare function remoteCall<T>(cli: Cli, tool: string, args: Record<string, unknown>): Promise<T>;
14
+ /** A document ref as the surface's locator (spec/surface §4 "Arguments", 3.0):
15
+ * a `d_` id is the `id` argument, anything else the `path` — the two names a
16
+ * `docs_*` tool takes (one of them required). */
17
+ export declare function docLocator(ref: string): {
18
+ id: string;
19
+ } | {
20
+ path: string;
21
+ };
22
+ /** The live document a ref names, for a LOCAL verb, or the surface's own
23
+ * `doc_missing` ("no document with id or path "<ref>"", `data` the locator) —
24
+ * the same wording the `docs_*` tools give a remote caller (spec/surface §4
25
+ * "Arguments"), so `omg mv nope.md …` reads alike in both modes. */
26
+ export declare function requireDoc(store: Store, repoId: string, ref: string): string;
27
+ /** The live node a ref names, or the surface's `block_missing` ("no node with id "<ref>""). */
28
+ export declare function requireNode(store: Store, repoId: string, nodeId: string): string;
13
29
  /** Close the remote connection if one was opened (idempotent). Called after a
14
30
  * one-shot command and on shell exit. */
15
31
  export declare function closeRemote(): Promise<void>;
@@ -1,5 +1,6 @@
1
1
  import { connectEngine, parseEngineSpec } from "@omgbase/sync";
2
- import { CliUsageError } from "../output.js";
2
+ import { isValidId, findDocByRef } from "@omgbase/core";
3
+ import { CliUsageError, EngineErrorLike } from "../output.js";
3
4
  // Remote (`--server`) mode plumbing (ADR-014). When a command runs with the
4
5
  // global `--server <cmd|url>` flag, it reaches a remote engine over MCP instead
5
6
  // of the embedded local store: it calls the MCP tool that mirrors its local
@@ -56,6 +57,29 @@ export async function remoteCall(cli, tool, args) {
56
57
  const eng = await remoteEngine(cli);
57
58
  return eng.callTool(tool, { ...args, ...(cli.flags.repo ? { repo: cli.flags.repo } : {}) });
58
59
  }
60
+ /** A document ref as the surface's locator (spec/surface §4 "Arguments", 3.0):
61
+ * a `d_` id is the `id` argument, anything else the `path` — the two names a
62
+ * `docs_*` tool takes (one of them required). */
63
+ export function docLocator(ref) {
64
+ return isValidId(ref, "d") ? { id: ref } : { path: ref };
65
+ }
66
+ /** The live document a ref names, for a LOCAL verb, or the surface's own
67
+ * `doc_missing` ("no document with id or path "<ref>"", `data` the locator) —
68
+ * the same wording the `docs_*` tools give a remote caller (spec/surface §4
69
+ * "Arguments"), so `omg mv nope.md …` reads alike in both modes. */
70
+ export function requireDoc(store, repoId, ref) {
71
+ const info = findDocByRef(store, repoId, ref);
72
+ if (!info)
73
+ throw new EngineErrorLike("doc_missing", `no document with id or path ${JSON.stringify(ref)}`, { data: docLocator(ref) });
74
+ return info.docId;
75
+ }
76
+ /** The live node a ref names, or the surface's `block_missing` ("no node with id "<ref>""). */
77
+ export function requireNode(store, repoId, nodeId) {
78
+ const row = store.db.prepare("SELECT 1 FROM nodes WHERE node_id = ? AND repo_id = ?").get(nodeId, repoId);
79
+ if (!row)
80
+ throw new EngineErrorLike("block_missing", `no node with id ${JSON.stringify(nodeId)}`, { data: { id: nodeId } });
81
+ return nodeId;
82
+ }
59
83
  /** Close the remote connection if one was opened (idempotent). Called after a
60
84
  * one-shot command and on shell exit. */
61
85
  export async function closeRemote() {
@@ -1,2 +1,11 @@
1
1
  import type { Command } from "../commands.js";
2
+ /** A ref that names a block: a `b_` block id, an `n_` node id (its block), or a
3
+ * `path#type[n]` locator — everything else is a document (a `d_` id or a path). */
4
+ export declare function isBlockRef(ref: string): boolean;
5
+ /** The remote tool that reads the block a ref names, by the ref's SHAPE (ids
6
+ * carry their kind, spec/surface §4 "Names and arguments"): an `n_` node id is
7
+ * `nodes_block_read` (the block the node was projected from — `blocks_read`'s
8
+ * shape; `blocks_read` itself takes a block id only), a `b_` id or a
9
+ * `path#type[n]` locator is `blocks_read`, anything else is a document (null). */
10
+ export declare function blockReadTool(ref: string): "nodes_block_read" | "blocks_read" | null;
2
11
  export declare const cmdCat: Command;
@@ -1,8 +1,37 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { resolveRef, nodesGet, docsRead } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
- import { remoteCall } from "./_remote.js";
4
+ import { remoteCall, docLocator } from "./_remote.js";
5
5
  import { expandBlockArgs } from "./_mutate.js";
6
+ // `omg cat <node…|->` (11 §5.2) — content only, default raw exact bytes, pipe-clean.
7
+ // For a document ref, cats the whole rendered document; for a block, the block
8
+ // (subtree at raw). No decoration on stdout: cat is the bytes. Several refs (or
9
+ // `-` = refs from stdin, one per line — the counterpart of `--ids`, same
10
+ // convention as the mutators) are cat'ed in order, like unix cat. `--json` is
11
+ // the mapped tool's result: `docs_read` for a document, `blocks_read` for a block,
12
+ // `nodes_block_read` for a node (the block it was projected from — the same shape).
13
+ //
14
+ // Refs are what `resolveRef` accepts: a block/doc/node id or a document path
15
+ // in either form (`/notes/x.md`, `notes/x.md`). `--resolution` is a block-level notion (blocks_read); a document
16
+ // is always its exact bytes (docs_read has no resolution ladder), so asking for
17
+ // another resolution on a document ref is warned about, not silently ignored.
18
+ /** A ref that names a block: a `b_` block id, an `n_` node id (its block), or a
19
+ * `path#type[n]` locator — everything else is a document (a `d_` id or a path). */
20
+ export function isBlockRef(ref) {
21
+ return blockReadTool(ref) !== null;
22
+ }
23
+ /** The remote tool that reads the block a ref names, by the ref's SHAPE (ids
24
+ * carry their kind, spec/surface §4 "Names and arguments"): an `n_` node id is
25
+ * `nodes_block_read` (the block the node was projected from — `blocks_read`'s
26
+ * shape; `blocks_read` itself takes a block id only), a `b_` id or a
27
+ * `path#type[n]` locator is `blocks_read`, anything else is a document (null). */
28
+ export function blockReadTool(ref) {
29
+ if (ref.startsWith("n_"))
30
+ return "nodes_block_read";
31
+ if (ref.startsWith("b_") || ref.includes("#"))
32
+ return "blocks_read";
33
+ return null;
34
+ }
6
35
  /** `--resolution` only means something for a block; say so (stderr) rather than
7
36
  * silently handing back raw bytes as if the flag had applied. */
8
37
  function warnDocResolution(cli, ref, resolution) {
@@ -11,17 +40,21 @@ function warnDocResolution(cli, ref, resolution) {
11
40
  cli.io.err(cli.style.warn(` --resolution ${resolution} ignored for ${ref}: a document is always its exact bytes (resolutions apply to block refs)`));
12
41
  }
13
42
  async function catOne(cli, ref, resolution) {
14
- // Remote: the polymorphic `read_ref` tool classifies + reads server-side.
43
+ // Remote: the ref's SHAPE picks the tool (ids carry their kind, a path is a
44
+ // document): an `n_` node id is `nodes_block_read` (the block it was projected
45
+ // from), a `b_` id or a `path#type[n]` locator is `blocks_read` (a block id
46
+ // only — an `n_` id there is `block_missing`), a `d_` id or a path is
47
+ // `docs_read` — the same results the local run renders (`read_ref` classified
48
+ // server-side before surface 3.0).
15
49
  if (cli.flags.server) {
16
- const r = await remoteCall(cli, "read_ref", { ref, ...(resolution !== "raw" ? { resolution } : {}) });
17
- if (r.kind === "document") {
18
- warnDocResolution(cli, ref, resolution);
19
- // The `docs_read` result is the `read_ref` document result without `kind`.
20
- const { kind: _kind, ...doc } = r;
21
- void _kind;
22
- return { text: String(r.content ?? ""), json: doc, capture: r.content };
50
+ const tool = blockReadTool(ref);
51
+ if (tool) {
52
+ const r = await remoteCall(cli, tool, { id: ref, resolution });
53
+ return { text: String(r.raw ?? r.text ?? r.label ?? ""), json: r, capture: r };
23
54
  }
24
- return { text: String(r.raw ?? r.text ?? r.label ?? ""), json: r, capture: r };
55
+ const r = await remoteCall(cli, "docs_read", docLocator(ref));
56
+ warnDocResolution(cli, ref, resolution);
57
+ return { text: String(r.raw ?? ""), json: r, capture: r.raw };
25
58
  }
26
59
  const ws = cli.workspace();
27
60
  const repo = cli.repo(ws);
@@ -39,7 +72,7 @@ async function catOne(cli, ref, resolution) {
39
72
  throw new EngineErrorLike("doc_missing", `no document ${ref}`);
40
73
  warnDocResolution(cli, ref, resolution);
41
74
  // shell: the bytes (a string; @_ only). `--json` is the `docs_read` result.
42
- return { text: res.content, json: res, capture: res.content };
75
+ return { text: res.raw, json: res, capture: res.raw };
43
76
  }
44
77
  const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution });
45
78
  if (!node)
@@ -1,8 +1,8 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { findDoc, docDiffUnified, diffBlocks } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
- import { remoteCall } from "./_remote.js";
5
- // `omg diff <doc>` (11 §5.5) — `diff_unified`: the unified diff, `--json` the
4
+ import { remoteCall, docLocator } from "./_remote.js";
5
+ // `omg diff <doc>` (11 §5.5) — `docs_diff_unified`: the unified diff, `--json` the
6
6
  // tool's `{ doc, path, from, to, diff }`. With no revisions: current vs previous
7
7
  // ("what did the last commit do here"). `--blocks` is the block-grain `diff`
8
8
  // tool: added/removed/changed blocks between the same two revisions.
@@ -20,7 +20,7 @@ async function runDiff(cli, args) {
20
20
  options: [
21
21
  ["--from <rev>", "older revision id (default: the one before --to)"],
22
22
  ["--to <rev>", "newer revision id (default: current)"],
23
- ["--blocks", "block-grain: which blocks were added, removed or changed (the `diff` tool)"],
23
+ ["--blocks", "block-grain: which blocks were added, removed or changed (the `docs_diff` tool)"],
24
24
  ],
25
25
  });
26
26
  }
@@ -29,8 +29,8 @@ async function runDiff(cli, args) {
29
29
  throw new CliUsageError("diff requires a <doc>");
30
30
  let result;
31
31
  if (cli.flags.server) {
32
- result = await remoteCall(cli, "diff_unified", {
33
- doc: ref,
32
+ result = await remoteCall(cli, "docs_diff_unified", {
33
+ ...docLocator(ref),
34
34
  ...(values.from ? { from_rev: values.from } : {}),
35
35
  ...(values.to ? { to_rev: values.to } : {}),
36
36
  });
@@ -52,11 +52,11 @@ async function runDiff(cli, args) {
52
52
  // Block-grain over the same resolved revision pair.
53
53
  let entries;
54
54
  if (cli.flags.server)
55
- entries = await remoteCall(cli, "diff", { doc: ref, from_rev: result.from, to_rev: result.to });
55
+ entries = await remoteCall(cli, "docs_diff", { ...docLocator(ref), from_rev: result.from, to_rev: result.to });
56
56
  else
57
- entries = diffBlocks(cli.workspace().store, result.doc, result.from, result.to);
57
+ entries = diffBlocks(cli.workspace().store, result.id, result.from, result.to);
58
58
  if (cli.flags.mode !== "human")
59
- return emitMachine(cli, entries, { items: entries, ids: entries.map((e) => e.blockId) });
59
+ return emitMachine(cli, entries, { items: entries, ids: entries.map((e) => e.id) });
60
60
  if (entries.length === 0) {
61
61
  io.err(style.dim(" no changes"));
62
62
  return EXIT_OK;
@@ -64,7 +64,7 @@ async function runDiff(cli, args) {
64
64
  for (const e of entries) {
65
65
  const mark = e.kind === "added" ? style.ok("+") : e.kind === "removed" ? style.err("-") : style.warn("~");
66
66
  const preview = (e.after ?? e.before ?? "").split("\n")[0] ?? "";
67
- io.out(`${mark} ${style.id(e.blockId)} ${preview}`.trimEnd());
67
+ io.out(`${mark} ${style.id(e.id)} ${preview}`.trimEnd());
68
68
  }
69
69
  return EXIT_OK;
70
70
  }
@@ -3,14 +3,15 @@ import { parse as parseYamlScalar } from "yaml";
3
3
  import { docsCreate, docsMove, docsDelete, docsSetMeta, surfaceDocOpResult, surfaceDocMoveResult, referencePath } from "@omgbase/core";
4
4
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
5
5
  import { readContent, extractContentOpts, renderDiffs } from "./_mutate.js";
6
- import { remoteCall } from "./_remote.js";
6
+ import { remoteCall, docLocator, requireDoc } from "./_remote.js";
7
7
  // Document-level commands (11 §5.6): new / mv / rm --doc / meta. Thin wrappers
8
8
  // over the core doc ops, which own the file-write + commit + flock protocol.
9
9
  // The global --dry-run rides as the op's `dryRun` (spec/cli §3.6): the op
10
10
  // validates and returns the per-file diffs it would make, committing nothing.
11
- // Every path printed is the reference form (spec/surface §1 "Paths"): the
12
- // library's result is re-shaped through `surfaceDocOpResult` — the same
13
- // function the MCP server applies — before it is rendered.
11
+ // Every path printed is the reference form (spec/surface §1 "Paths") and the
12
+ // result's identity is `id`/`path` (3.0): the library's result is re-shaped
13
+ // through `surfaceDocOpResult` — the same function the MCP server applies —
14
+ // before it is rendered, so the CLI and the tool print one shape.
14
15
  function ctxOf(cli, ws, repoId, rootPath, actor) {
15
16
  return {
16
17
  repoId,
@@ -30,7 +31,7 @@ function report(cli, verb, res) {
30
31
  return EXIT_OK;
31
32
  }
32
33
  cli.io.err(cli.style.dim(` ${cli.style.ok(cli.render.g.ok)} ${verb} ${cli.style.accent(res.path)}`));
33
- cli.io.out(res.docId);
34
+ cli.io.out(res.id);
34
35
  return EXIT_OK;
35
36
  }
36
37
  // `mv` rewrites the inbound links by default (spec/mutate 1.3; `--no-retarget`
@@ -51,14 +52,14 @@ function reportMove(cli, from, res) {
51
52
  }
52
53
  if (res.dangling.length === 0)
53
54
  return code;
54
- const where = res.dangling.map((l) => (l.block ? `${l.path} ${l.block}` : `${l.path} (frontmatter)`)).join(", ");
55
+ const where = res.dangling.map((l) => (l.block_id ? `${l.path} ${l.block_id}` : `${l.path} (frontmatter)`)).join(", ");
55
56
  const n = res.dangling.length;
56
57
  io.err(style.warn(` ${style.warn(cli.render.g.warn)} ${n} inbound link${n === 1 ? "" : "s"} still name${n === 1 ? "s" : ""} the old path: ${where}`));
57
- if (res.dangling.some((l) => l.block))
58
+ if (res.dangling.some((l) => l.block_id))
58
59
  io.err(style.dim(` fix: ${cli.prog} retarget ${from} ${res.path} --apply`));
59
60
  const seen = new Set();
60
61
  for (const l of res.dangling) {
61
- if (l.block || !l.field)
62
+ if (l.block_id || !l.field)
62
63
  continue;
63
64
  const key = `${l.path}\u0000${l.field}`;
64
65
  if (seen.has(key))
@@ -125,22 +126,22 @@ async function runMv(cli, args) {
125
126
  throw new CliUsageError("mv requires <doc> and <new-path>");
126
127
  const noRetarget = values["no-retarget"] === true;
127
128
  if (cli.flags.server) {
128
- const res = await remoteCall(cli, "docs_move", { doc, to_path: toPath, ...(noRetarget ? { retarget_inbound: false } : {}), ...dryRunArg(cli) });
129
+ const res = await remoteCall(cli, "docs_move", { ...docLocator(doc), to_path: toPath, ...(noRetarget ? { retarget_inbound: false } : {}), ...dryRunArg(cli) });
129
130
  return reportMove(cli, fromPathOf(res, doc), res);
130
131
  }
131
132
  const ws = cli.workspace();
132
133
  const repo = cli.repo(ws);
133
- const res = surfaceDocMoveResult(docsMove(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, values.actor), doc, toPath, noRetarget ? { retargetInbound: false } : {}));
134
+ const res = surfaceDocMoveResult(docsMove(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, values.actor), requireDoc(ws.store, repo.repoId, doc), toPath, noRetarget ? { retargetInbound: false } : {}));
134
135
  return reportMove(cli, fromPathOf(res, doc), res);
135
136
  }
136
137
  // ---- rm --doc (block rm lives in mutate.ts; this handles the --doc branch) --
137
138
  export async function runRmDoc(cli, doc, actor) {
138
139
  if (cli.flags.server) {
139
- return report(cli, "deleted", await remoteCall(cli, "docs_delete", { doc, ...dryRunArg(cli) }));
140
+ return report(cli, "deleted", await remoteCall(cli, "docs_delete", { ...docLocator(doc), ...dryRunArg(cli) }));
140
141
  }
141
142
  const ws = cli.workspace();
142
143
  const repo = cli.repo(ws);
143
- const res = surfaceDocOpResult(docsDelete(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, actor), doc));
144
+ const res = surfaceDocOpResult(docsDelete(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, actor), requireDoc(ws.store, repo.repoId, doc)));
144
145
  return report(cli, "deleted", res);
145
146
  }
146
147
  // ---- meta -------------------------------------------------------------------
@@ -193,7 +194,7 @@ async function runMeta(cli, args) {
193
194
  throw new CliUsageError("meta requires --set or --unset");
194
195
  if (cli.flags.server) {
195
196
  return report(cli, "patched", await remoteCall(cli, "docs_set_meta", {
196
- doc,
197
+ ...docLocator(doc),
197
198
  ...(Object.keys(set).length ? { set } : {}),
198
199
  ...(unset.length ? { unset } : {}),
199
200
  ...dryRunArg(cli),
@@ -201,7 +202,7 @@ async function runMeta(cli, args) {
201
202
  }
202
203
  const ws = cli.workspace();
203
204
  const repo = cli.repo(ws);
204
- const res = surfaceDocOpResult(docsSetMeta(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, values.actor), doc, { set, unset }));
205
+ const res = surfaceDocOpResult(docsSetMeta(ws.store, ctxOf(cli, ws, repo.repoId, repo.rootPath, values.actor), requireDoc(ws.store, repo.repoId, doc), { set, unset }));
205
206
  return report(cli, "patched", res);
206
207
  }
207
208
  function dryRunArg(cli) {
@@ -37,7 +37,7 @@ function runEdit(cli, args) {
37
37
  const repo = cli.repo(ws);
38
38
  const resolved = resolveRef(ws.store, repo.repoId, ref);
39
39
  if (!resolved || resolved.kind !== "block")
40
- throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
40
+ throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
41
41
  const blockId = resolved.blockId;
42
42
  const node = nodesGet(ws.store, resolved.docId, blockId, { resolution: "raw" });
43
43
  const before = node?.raw ?? "";
@@ -36,9 +36,9 @@ async function runFind(cli, args) {
36
36
  const limit = values.one ? 1 : values.n ? Number(values.n) : 10;
37
37
  let hits;
38
38
  if (cli.flags.server) {
39
- // Remote: the `resolve` tool does the hybrid ranking server-side (using the
39
+ // Remote: the `blocks_resolve` tool does the hybrid ranking server-side (using the
40
40
  // server's embedder if configured). Same ranked-hit shape → render unchanged.
41
- hits = await remoteCall(cli, "resolve", { query: text, limit });
41
+ hits = await remoteCall(cli, "blocks_resolve", { query: text, limit });
42
42
  }
43
43
  else {
44
44
  const ws = cli.workspace();
@@ -2,7 +2,7 @@ import { parseArgs } from "node:util";
2
2
  import { resolveRef, historyNode } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
- // `omg hist <node>` (11 §5.5) — history_node: a block's biography.
5
+ // `omg hist <node>` (11 §5.5) — blocks_history: a block's biography.
6
6
  async function runHist(cli, args) {
7
7
  const { values, positionals } = parseArgs({
8
8
  args,
@@ -22,9 +22,9 @@ async function runHist(cli, args) {
22
22
  throw new CliUsageError("hist requires a <node>");
23
23
  let changes;
24
24
  if (cli.flags.server) {
25
- // Remote: history_node keys on a block id (globally unique). Pass the ref
25
+ // Remote: blocks_history keys on a block id (globally unique). Pass the ref
26
26
  // through — a locator would need a local resolve, so remote hist wants an id.
27
- changes = await remoteCall(cli, "history_node", { id: ref, ...(values.n ? { limit: Number(values.n) } : {}) });
27
+ changes = await remoteCall(cli, "blocks_history", { id: ref, ...(values.n ? { limit: Number(values.n) } : {}) });
28
28
  }
29
29
  else {
30
30
  const ws = cli.workspace();
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { resolveRef, docLinks } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
- // `omg links <node>` (11 §5.4) — open edges touching the node; default both
4
+ // `omg links <node>` (11 §5.4) — open links touching the node; default both
5
5
  // directions, grouped; doc-grain by default, --blocks for block-grain (one row
6
6
  // per edge, naming the source block). Backlinks = --in.
7
7
  function runLinks(cli, args) {
@@ -19,13 +19,13 @@ function runLinks(cli, args) {
19
19
  if (values.help) {
20
20
  return renderHelp(cli, {
21
21
  name: "links",
22
- summary: "Open edges touching a node, grouped by predicate (both directions by default)",
22
+ summary: "Open links touching a node, grouped by predicate (both directions by default)",
23
23
  usage: "links <node> [--in|--out] [--pred <p,p>] [--blocks]",
24
24
  options: [
25
- ["--in", "incoming edges only (backlinks)"],
26
- ["--out", "outgoing edges only"],
25
+ ["--in", "incoming links only (backlinks)"],
26
+ ["--out", "outgoing links only"],
27
27
  ["--pred <p,p>", "keep only these predicates"],
28
- ["--blocks", "block-grain edges (default: doc-grain)"],
28
+ ["--blocks", "block-grain links (default: doc-grain)"],
29
29
  ],
30
30
  });
31
31
  }
@@ -47,7 +47,7 @@ function runLinks(cli, args) {
47
47
  cli.capture?.(result); // shell: edges (out+in) become the addressable frame
48
48
  // `{ out, in }` is not a list: `--jsonl` prints the document; `--ids` the far nodes.
49
49
  if (cli.flags.mode !== "human")
50
- return emitMachine(cli, result, { ids: [...result.out, ...result.in].map((e) => e.node) });
50
+ return emitMachine(cli, result, { ids: [...result.out, ...result.in].map((e) => e.doc_id) });
51
51
  const { render, style, io } = cli;
52
52
  const g = render.g;
53
53
  if (result.out.length === 0 && result.in.length === 0) {
@@ -56,26 +56,26 @@ function runLinks(cli, args) {
56
56
  }
57
57
  // Doc-grain: one row per (predicate, node) with its rollup count. Block-grain:
58
58
  // one row per edge, the source block beside the far node.
59
- const src = (e) => style.dim(`(${e.block ?? "frontmatter"})`);
59
+ const src = (e) => style.dim(`(${e.block_id ?? "frontmatter"})`);
60
60
  if (result.out.length > 0) {
61
61
  io.out(style.dim(" out"));
62
62
  for (const e of result.out) {
63
63
  if (values.blocks)
64
- io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${src(e)}`);
64
+ io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.doc_id)} ${src(e)}`);
65
65
  else
66
- io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
66
+ io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.doc_id)} ${style.dim(`×${e.count}`)}`);
67
67
  }
68
68
  }
69
69
  if (result.in.length > 0) {
70
70
  io.out(style.dim(" in (backlinks)"));
71
71
  for (const e of result.in) {
72
72
  if (values.blocks)
73
- io.out(` ${style.id(e.node)} ${src(e)} ${style.dim(g.arrow)} ${style.accent(e.predicate)}`);
73
+ io.out(` ${style.id(e.doc_id)} ${src(e)} ${style.dim(g.arrow)} ${style.accent(e.predicate)}`);
74
74
  else
75
- io.out(` ${style.id(e.node)} ${style.dim(g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
75
+ io.out(` ${style.id(e.doc_id)} ${style.dim(g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
76
76
  }
77
77
  }
78
78
  return EXIT_OK;
79
79
  }
80
- export const cmdLinks = { name: "links", summary: "Open edges touching a node", run: (cli, a) => runLinks(cli, a) };
80
+ export const cmdLinks = { name: "links", summary: "Open links touching a node", run: (cli, a) => runLinks(cli, a) };
81
81
  //# sourceMappingURL=links.js.map
@@ -38,9 +38,11 @@ async function runLs(cli, args) {
38
38
  rows.push(...page.items);
39
39
  cursor = page.truncated ? page.cursor : null;
40
40
  } while (cursor);
41
- cli.capture?.(rows); // shell: docs become the addressable frame (ref = path)
41
+ cli.capture?.(rows); // shell: docs become the addressable frame (ref = id, label path)
42
+ // `--ids` prints the documents' `id`s (spec/cli 3.0; the paths before, a
43
+ // stand-in for an identity the row did not carry).
42
44
  if (cli.flags.mode !== "human")
43
- return emitMachine(cli, rows, { items: rows, ids: rows.map((r) => r.path) });
45
+ return emitMachine(cli, rows, { items: rows, ids: rows.map((r) => r.id) });
44
46
  const { style, io } = cli;
45
47
  if (rows.length === 0) {
46
48
  io.err(style.dim(" no documents"));
@@ -49,7 +51,7 @@ async function runLs(cli, args) {
49
51
  const table = rows.map((r) => [
50
52
  style.accent(r.path),
51
53
  style.dim(`${r.blocks} blocks`),
52
- style.dim(relTime(r.ts)),
54
+ style.dim(relTime(r.updated_at)),
53
55
  ]);
54
56
  for (const line of columns(table, [{}, { align: "right" }, { align: "right" }]))
55
57
  io.out(line);
@@ -65,7 +65,7 @@ async function runMcp(cli, args) {
65
65
  const repo = cli.repo(ws);
66
66
  // Connect the configured embedder once (if any) so the query tool's
67
67
  // `semantic` mode has a vectorizer for the session. Unset ⇒ semantic queries
68
- // return semantic_unavailable. The spawned process lives for the session and
68
+ // return provider_unavailable. The spawned process lives for the session and
69
69
  // is released on shutdown.
70
70
  //
71
71
  // A CONFIGURED-but-broken embedder must never be mistaken for "no embedder":
@@ -151,7 +151,7 @@ async function runMcp(cli, args) {
151
151
  // The query surface's `embedQuery`: a live vectorizer when the embedder loaded,
152
152
  // or — when it's configured-but-broken — a hook that throws `embedder_failed`
153
153
  // so semantic access fails loudly and specifically (not the misleading
154
- // `semantic_unavailable`, which the server emits only when embedQuery is
154
+ // `provider_unavailable`, which the server emits only when embedQuery is
155
155
  // absent, i.e. genuinely no provider configured). `const` captures satisfy
156
156
  // closure narrowing over the `let`-bound state above.
157
157
  const loaded = embedding;
@@ -167,6 +167,7 @@ async function runMcp(cli, args) {
167
167
  ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
168
168
  ...(drainer ? { onMutation: () => drainer.schedule() } : {}),
169
169
  ...(embedQuery ? { embedQuery } : {}),
170
+ ...(loaded ? { embedProvider: loaded.providerName } : {}),
170
171
  // The `version` tool's view of this binary (spec/surface §4).
171
172
  host: hostInfo(),
172
173
  });
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { readFileSync } from "node:fs";
3
- import { resolveRef, tasksComplete, sectionsAppend, } from "@omgbase/core";
3
+ import { resolveRef, tasksComplete, sectionsAppend, isContainerBlock, } from "@omgbase/core";
4
4
  import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
5
5
  import { runOps, runOpsRemote, readContent, readStdin, expandBlockArgs, extractContentOpts } from "./_mutate.js";
6
6
  import { runRmDoc } from "./docs.js";
@@ -10,7 +10,7 @@ import { runRmDoc } from "./docs.js";
10
10
  function block(cli, ws, repoId, ref) {
11
11
  const r = resolveRef(ws.store, repoId, ref);
12
12
  if (!r || r.kind !== "block")
13
- throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
13
+ throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
14
14
  return r.blockId;
15
15
  }
16
16
  // `done` addresses task blocks only (spec/cli §6): any other live block type is
@@ -21,7 +21,7 @@ function taskBlock(ws, ref, blockId) {
21
21
  .prepare("SELECT type FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
22
22
  .get(blockId);
23
23
  if (!row)
24
- throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
24
+ throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
25
25
  if (row.type !== "task") {
26
26
  throw new EngineErrorLike("type_mismatch", `not a task: ${ref} is a ${row.type}`, { data: { block: blockId, type: row.type } });
27
27
  }
@@ -141,7 +141,7 @@ async function runUpdate(cli, args) {
141
141
  const markdown = readContent(content);
142
142
  // Remote: the server pins CAS from current bytes when --expect is absent.
143
143
  if (cli.flags.server)
144
- return runOpsRemote(cli, "blocks_update", { block: ref, markdown, ...(values.expect ? { expect: { content_hash: values.expect } } : {}) });
144
+ return runOpsRemote(cli, "blocks_update", { id: ref, markdown, ...(values.expect ? { expect: { content_hash: values.expect } } : {}) });
145
145
  const ws = cli.workspace();
146
146
  const repo = cli.repo(ws);
147
147
  const blockId = block(cli, ws, repo.repoId, ref);
@@ -226,7 +226,7 @@ async function runRm(cli, args) {
226
226
  const op = { op: "remove", blocks };
227
227
  return runOps(cli, ws, repo, [op], actorOf(values));
228
228
  }
229
- // ---- done (tasks_complete) --------------------------------------------------
229
+ // ---- done (blocks_check) --------------------------------------------------
230
230
  async function runDone(cli, args) {
231
231
  const { values, positionals } = parseArgs({
232
232
  args,
@@ -249,7 +249,7 @@ async function runDone(cli, args) {
249
249
  if (refs.length === 0)
250
250
  throw new CliUsageError("done requires one or more task blocks (or - for stdin)");
251
251
  if (cli.flags.server)
252
- return runOpsRemote(cli, "tasks_complete", { blocks: refs, ...(values.undo ? { checked: false } : {}) });
252
+ return runOpsRemote(cli, "blocks_check", { ids: refs, ...(values.undo ? { checked: false } : {}) });
253
253
  const ws = cli.workspace();
254
254
  const repo = cli.repo(ws);
255
255
  const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
@@ -258,7 +258,7 @@ async function runDone(cli, args) {
258
258
  : tasksComplete(ws.store, blocks);
259
259
  return runOps(cli, ws, repo, ops, actorOf(values));
260
260
  }
261
- // ---- append (sections_append) -----------------------------------------------
261
+ // ---- append (blocks_append) --------------------------------------------------
262
262
  async function runAppend(cli, args) {
263
263
  const content = extractContentOpts(args);
264
264
  const { values, positionals } = parseArgs({
@@ -269,10 +269,10 @@ async function runAppend(cli, args) {
269
269
  if (values.help) {
270
270
  return renderHelp(cli, {
271
271
  name: "append",
272
- summary: "Append markdown at the end of a heading's section",
273
- usage: "append <heading> (-m <markdown> | -f <file> | -) [--actor <s>] [--dry-run]",
272
+ summary: "Append markdown into a block: a heading's section or a container's children",
273
+ usage: "append <block> (-m <markdown> | -f <file> | -) [--actor <s>] [--dry-run]",
274
274
  options: [
275
- ["<heading>", "the heading block (id or locator) whose section receives the content"],
275
+ ["<block>", "a heading (its section receives the content) or a container — list, item, quote, table (its children do)"],
276
276
  ["-m <markdown>", "content inline"],
277
277
  ["-f <file>", "content from a file"],
278
278
  ["-", "content from stdin"],
@@ -280,17 +280,34 @@ async function runAppend(cli, args) {
280
280
  ],
281
281
  });
282
282
  }
283
- const heading = positionals[0];
284
- if (!heading)
285
- throw new CliUsageError("append requires a <heading> block");
283
+ const target = positionals[0];
284
+ if (!target)
285
+ throw new CliUsageError("append requires a <block>");
286
286
  const markdown = readContent(content);
287
- // Remote: sections_append resolves the heading ref (id OR text) server-side.
287
+ // Remote: blocks_append resolves the ref and decides by the block's kind
288
+ // server-side (spec/surface §4).
288
289
  if (cli.flags.server)
289
- return runOpsRemote(cli, "sections_append", { heading, markdown });
290
+ return runOpsRemote(cli, "blocks_append", { id: target, text: markdown });
290
291
  const ws = cli.workspace();
291
292
  const repo = cli.repo(ws);
292
- const headingId = block(cli, ws, repo.repoId, heading);
293
- return runOps(cli, ws, repo, sectionsAppend(headingId, markdown), actorOf(values));
293
+ const blockId = block(cli, ws, repo.repoId, target);
294
+ return runOps(cli, ws, repo, appendOps(ws.store, blockId, markdown), actorOf(values));
295
+ }
296
+ /** The ops `blocks_append` expands to, by what the block is: a heading → the end
297
+ * of its section (`sectionsAppend`); a container → the end of its children; a
298
+ * leaf → `block_not_container` (append means into, never a sibling insert). */
299
+ function appendOps(store, blockId, markdown) {
300
+ const row = store.db
301
+ .prepare("SELECT b.type AS type, d.format AS format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.deleted_commit IS NULL) AS children FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_commit IS NULL")
302
+ .get(blockId);
303
+ if (!row)
304
+ throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(blockId)}`, { data: { id: blockId } });
305
+ if (row.type === "heading")
306
+ return sectionsAppend(blockId, markdown);
307
+ if (isContainerBlock(row.format ?? "markdown", { type: row.type, children: new Array(row.children) })) {
308
+ return [{ op: "insert", to: { parent: blockId, at: "end" }, markdown }];
309
+ }
310
+ throw new EngineErrorLike("block_not_container", `block ${blockId} is a ${row.type}, not a heading or a container; append means into — insert after it with blocks_insert { to: <its parent>, at: { after: ${JSON.stringify(blockId)} } }`, { data: { id: blockId, type: row.type } });
294
311
  }
295
312
  // ---- split / merge ----------------------------------------------------------
296
313
  async function runSplit(cli, args) {
@@ -324,7 +341,7 @@ async function runSplit(cli, args) {
324
341
  return n;
325
342
  });
326
343
  if (cli.flags.server)
327
- return runOpsRemote(cli, "blocks_split", { block: ref, at });
344
+ return runOpsRemote(cli, "blocks_split", { id: ref, at });
328
345
  const ws = cli.workspace();
329
346
  const repo = cli.repo(ws);
330
347
  const blockId = block(cli, ws, repo.repoId, ref);
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { nodeSet, editablePropsFor } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
+ import { requireNode } from "./_remote.js";
4
5
  import { runOps, runOpsRemote } from "./_mutate.js";
5
6
  // `omg node set <nodeId> <prop> <value>` (node-editability) — surgically set an
6
7
  // editable property of a projected node (e.g. a link's text/target, a task's
@@ -37,13 +38,13 @@ async function runNode(cli, args) {
37
38
  if (!nodeId || !prop || valueParts.length === 0)
38
39
  throw new CliUsageError("node set <nodeId> <prop> <value>");
39
40
  const value = valueParts.join(" ");
40
- // Remote: node_set resolves node → block + rewrites the span server-side.
41
+ // Remote: nodes_set resolves node → block + rewrites the span server-side.
41
42
  if (cli.flags.server)
42
- return runOpsRemote(cli, "node_set", { node: nodeId, prop, value });
43
+ return runOpsRemote(cli, "nodes_set", { id: nodeId, prop, value });
43
44
  const ws = cli.workspace();
44
45
  const repo = cli.repo(ws);
45
- const ops = nodeSet(ws.store, nodeId, prop, value);
46
- return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
46
+ const ops = nodeSet(ws.store, requireNode(ws.store, repo.repoId, nodeId), prop, value);
47
+ return runOps(cli, ws, repo, ops, { reason: `nodes_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
47
48
  }
48
49
  if (sub === "props") {
49
50
  // `props` is a local editability lookup with no remote tool (a niche read).
@@ -23,7 +23,7 @@ import { readStdin } from "./_mutate.js";
23
23
  // query to a parent binding, the root row's collections ^docs/^nodes/^blocks for
24
24
  // join-equivalents, first/single lookups, full-text text(...) + embedding-score
25
25
  // semantic(...) predicates, `order by <expr> [asc|desc]` ranking, and recursive
26
- // `follow [distinct] <rel> [{ where … frontier … depth n by … }]` traversal over
26
+ // `follow [distinct] <rel> [{ where … frontier … depth n }]` traversal (a brace after a destination is its block, oqx 0.20) over
27
27
  // a type-preserving relation with $depth/$stop/$ordinal recursion metadata +
28
28
  // post-walk filtering. A top-level consumer directive changes the result shape:
29
29
  // `docs count { … }` / `docs exists { … }` reduce to a scalar,
@@ -46,7 +46,7 @@ async function runOqx(cli, args) {
46
46
  if (values.help) {
47
47
  renderHelp(cli, {
48
48
  name: "query",
49
- summary: "Composable OQX query: from <docs|blocks|nodes|edges> where … select … [order by …] [follow …] (alias: q)",
49
+ summary: "Composable OQX query: from <docs|blocks|nodes|links> where … select … [order by …] [follow …] (alias: q)",
50
50
  usage: ["query <source> [-n <N>] [--cursor <c>] [--ids|--json|--jsonl]", "query -f <file|-> [-n <N>] [--cursor <c>]"],
51
51
  options: [
52
52
  ["<source>", "the OQX query text (quote it); `from` may be omitted inside blocks"],
@@ -63,28 +63,28 @@ async function runOqx(cli, args) {
63
63
  });
64
64
  cli.io.out(" e.g. query 'from docs where nodes count { where kind == \"md:task\" } >= 2'");
65
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\" and not attrs.checked }'");
67
- cli.io.out(" query 'select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } from docs' # ^docs/^nodes/^blocks/^edges: the repository (the root row) from inside a row; 0^docs from any depth; ^$id / 0^$id = the repository id");
66
+ cli.io.out(" query 'select path, open from docs where nodes collect { ^open: value where kind == \"md:task\" and not attrs.checked }'");
67
+ cli.io.out(" query 'select owner_id, owner: ^nodes single { where kind == \"person\" and attrs.id == ^owner_id } from docs' # ^docs/^nodes/^blocks/^links: the repository (the root row) from inside a row; 0^docs from any depth; ^id / 0^id = the repository id");
68
68
  cli.io.out(" query 'docs count { where layer == \"canon\" }' # scalar; also <target> exists/none/first/single { … } at the top level");
69
69
  cli.io.out(" query 'from docs where nodes none { where kind == \"md:task\" and not checked }' # none = zero rows (≡ not … exists; \"every\" = none over the complement)");
70
70
  cli.io.out(" query 'from docs where era is not null and (layer == \"canon\" or layer == \"working\")' # and / or / not are the connectives (&& || ! accepted); x is y / x is not y = identity, x is null = absent");
71
- cli.io.out(" query 'docs { type == \"substance\" and not verified }' # sugar: a block without a consumer is collect; a leading predicate drops `where` (bare names still project — filter a bare field with `is x`); not after follow, where { … } is options");
72
- cli.io.out(" query 'select subject, process: ^docs[slug == ^subject].$path from docs where type == \"lab-note\"' # x[p] = first match or absent; x[p]! = exactly one, required; x[n] = positional; title! = the value or a loud error; a select item may use the items to its left");
73
- cli.io.out(" query 'select $path, $depth from docs where $path == \"/timeline/review.md\" follow refs(before), refs(after)' # refs(field): the live documents a property's references name (either path form or a d_ id; dangling dropped) — a receiver, a source or a follow destination");
74
- cli.io.out(" query 'select $path, next: ^docs { ^$path in list(before) } from docs where type == \"milestone\"' # paths are /-rooted on the surface ($path == \"/x.md\"; inputs accept both forms), so a property's reference compares with ^$path directly");
71
+ cli.io.out(" query 'docs { type == \"substance\" and not verified }' # sugar: a block without a consumer is collect; a leading predicate drops `where` (bare names still project — filter a bare field with `is x`); after a follow destination too: follow children { depth 2 } is its block");
72
+ cli.io.out(" query 'select subject, process: ^docs[slug == ^subject].path from docs where type == \"lab-note\"' # x[p] = first match or absent; x[p]! = exactly one, required; x[n] = positional; title! = the value or a loud error; a select item may use the items to its left");
73
+ cli.io.out(" query 'select path, $depth from docs where path == \"/timeline/review.md\" follow refs(before), refs(after)' # refs(field): the live documents a property's references name (either path form or a d_ id; dangling dropped) — a receiver, a source or a follow destination");
74
+ cli.io.out(" query 'select path, next: ^docs { ^path in list(before) } from docs where type == \"milestone\"' # paths are /-rooted on the surface (path == \"/x.md\"; inputs accept both forms), so a property's reference compares with ^path directly");
75
75
  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)");
76
- cli.io.out(" query 'select fm: entries(frontmatter) collect { k: $key, v: $it } from docs where $path == \"/x.md\"' # a record as a collection ($key/$it); also entries(attrs), entries(inline)");
77
- cli.io.out(" query 'select $path values from docs where layer == \"canon\"' # `values`: bare values, no {id,path} hits (one item only)");
78
- cli.io.out(" query 'select $path, tags: tags collect { $it values where $it != \"draft\" } from docs' # $it = the current item (here: each tag)");
76
+ cli.io.out(" query 'select fm: entries(frontmatter) collect { k: $key, v: $it } from docs where path == \"/x.md\"' # a record as a collection ($key/$it); also entries(attrs), entries(inline)");
77
+ cli.io.out(" query 'select path values from docs where layer == \"canon\"' # `values`: bare values, no {id,path} hits (one item only)");
78
+ cli.io.out(" query 'select path, tags: tags collect { $it values where $it != \"draft\" } from docs' # $it = the current item (here: each tag)");
79
79
  cli.io.out(" query 'docs collect { from nodes where kind == \"md:task\" }' # `from E` re-projects the source (→ nodes)");
80
80
  cli.io.out(" query 'from docs where text(\"philosophers stone\") and layer == \"canon\"' # full-text prune");
81
81
  cli.io.out(" query 'select s: semantic(\"the great work\") from blocks where semantic(\"the great work\") > 0.6' # embedding score (needs a provider)");
82
82
  cli.io.out(" query 'from docs where type == \"practitioner\" order by era desc' # order by <expr> [asc|desc]");
83
- 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)");
84
- cli.io.out(" query 'from nodes where name == \"Overview\" follow section.subsections { depth 3 }' # follow [distinct] <rel> [{ where … frontier … depth n by … }]");
85
- 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)");
83
+ 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)");
84
+ cli.io.out(" query 'from nodes where name == \"Overview\" follow section.subsections { depth 3 }' # follow [distinct] <dest> [{ where … frontier … depth n }] — a brace after any destination is that destination's block (identity is $key: project $key: <expr> to rekey a walk)");
85
+ cli.io.out(" query 'select p: path, d: $depth, s: $stop from docs where path == \"/index.md\" follow out_links collect { from dst }' # citation graph (cyclic-safe: $stop=cycle)");
86
86
  cli.io.out(" query 'from blocks where type == \"list_item\" and $leaf follow block.children' # $leaf/$depth/$stop filter the walk result post-walk");
87
- 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");
87
+ cli.io.out(" query 'select src: doc_id, to: dst_path from links where predicate == \"depends_on\"' # the links target: predicate/provenance/dst_kind + doc_id/dst_path/uri");
88
88
  return EXIT_OK;
89
89
  }
90
90
  const source = values.file
@@ -116,7 +116,7 @@ async function runOqx(cli, args) {
116
116
  if (phrases.length > 0) {
117
117
  const loaded = await loadEmbedding(ws, repo.repoId);
118
118
  if (!loaded) {
119
- throw new EngineErrorLike("semantic_unavailable", "semantic(...) needs an embedding provider", {
119
+ throw new EngineErrorLike("provider_unavailable", "semantic(...) needs an embedding provider", {
120
120
  hint: "omg config set embedding.provider <command|url>",
121
121
  });
122
122
  }
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { findDoc, docsOutline, referencePath } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
- import { remoteCall } from "./_remote.js";
4
+ import { remoteCall, docLocator } from "./_remote.js";
5
5
  // `omg outline <doc|path>` (alias ol) — the wire format (06 §6). Human output
6
6
  // prints the outline text with full block ids inline; --json emits the
7
7
  // OutlineResult verbatim.
@@ -32,11 +32,11 @@ async function runOutline(cli, args) {
32
32
  let result;
33
33
  let header;
34
34
  if (cli.flags.server) {
35
- // Remote: the docs_outline tool resolves the ref + returns the same
36
- // OutlineResult. The header shows the ref you passed (no local path lookup):
35
+ // Remote: the docs_outline tool resolves the locator (`id` for a `d_` id,
36
+ // else `path`) + returns the same OutlineResult. The header shows the ref you passed (no local path lookup):
37
37
  // a path ref rooted (spec/surface §1 "Paths"), a `d_` id verbatim.
38
38
  result = await remoteCall(cli, "docs_outline", {
39
- doc: ref,
39
+ ...docLocator(ref),
40
40
  ...(values.depth ? { depth: Number(values.depth) } : {}),
41
41
  ...(values.skeleton ? { resolution: "skeleton" } : {}),
42
42
  });
@@ -66,7 +66,7 @@ function renderPlan(cli, from, to, hits) {
66
66
  }
67
67
  io.err(style.dim(` plan — ${hits.length} block(s) would change; re-run with --apply to commit`));
68
68
  for (const h of hits) {
69
- io.out(style.id(h.block));
69
+ io.out(style.id(h.id));
70
70
  for (const line of unifiedDiffLines(cli, h.oldRaw, h.newRaw))
71
71
  io.out(line);
72
72
  io.out("");
@@ -84,7 +84,7 @@ async function runRun(cli, args) {
84
84
  if (phrases.length > 0) {
85
85
  const loaded = await loadEmbedding(ws, repo.repoId);
86
86
  if (!loaded) {
87
- throw new EngineErrorLike("semantic_unavailable", "semantic(...) needs an embedding provider", {
87
+ throw new EngineErrorLike("provider_unavailable", "semantic(...) needs an embedding provider", {
88
88
  hint: "omg config set embedding.provider <command|url>",
89
89
  });
90
90
  }
@@ -9,15 +9,20 @@ function showOne(cli, ref, include) {
9
9
  if (!resolved)
10
10
  throw new EngineErrorLike("doc_missing", `no node ${ref}`);
11
11
  if (resolved.kind === "document") {
12
- // The card reads the merged properties + open edges; `--json` is the
13
- // `read_ref` result for a document (`{ kind: "document", ...docs_read }`).
12
+ // The card reads the merged properties + open links; `--json` is the
13
+ // `docs_read` result tagged with its kind (`{ kind: "document", ...docs_read }`).
14
14
  const res = docsRead(ws.store, resolved.docId);
15
15
  if (!res)
16
16
  throw new EngineErrorLike("doc_missing", `no document ${ref}`);
17
17
  const links = docLinks(ws.store, resolved.docId, { direction: "both" });
18
- const properties = docPropertiesMerged(ws.store.db, resolved.docId);
18
+ // The authored keys, merged (frontmatter + inline — the `data` view); the
19
+ // computed rows are rendered under their surface names (`title`/`tags`,
20
+ // spec/surface §1.2 — the store's `$title`/`$tags` keys never reach the
21
+ // terminal) from the re-keyed `properties.computed` the read carries.
22
+ const properties = Object.fromEntries(Object.entries(docPropertiesMerged(ws.store.db, resolved.docId)).filter(([k]) => !k.startsWith("$")));
23
+ const computed = res.properties.computed ?? {};
19
24
  const payload = { kind: "document", ...res };
20
- return { payload, render: () => renderDocCard(cli, res.path, properties, links) };
25
+ return { payload, render: () => renderDocCard(cli, res.path, properties, computed, links) };
21
26
  }
22
27
  const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution: "full" });
23
28
  if (!node)
@@ -34,7 +39,7 @@ function runShow(cli, args) {
34
39
  if (values.help) {
35
40
  return renderHelp(cli, {
36
41
  name: "show",
37
- summary: "Metadata card for a node: attrs, placement, open edges, last change (`cat` is the bytes)",
42
+ summary: "Metadata card for a node: attrs, placement, open links, last change (`cat` is the bytes)",
38
43
  usage: "show <node…|-> [--include history]",
39
44
  options: [
40
45
  ["<node…>", "one or more refs: a block id (`b_…`), a doc id (`d_…`), a node id (`n_…`), or a doc path (`/notes/x.md`; the leading `/` is optional)"],
@@ -59,7 +64,7 @@ function runShow(cli, args) {
59
64
  c.render();
60
65
  return EXIT_OK;
61
66
  }
62
- function renderDocCard(cli, path, fm, links) {
67
+ function renderDocCard(cli, path, fm, computed, links) {
63
68
  const { render, style, io } = cli;
64
69
  io.out(render.wordmark(path));
65
70
  io.out(render.rule(40));
@@ -69,23 +74,32 @@ function renderDocCard(cli, path, fm, links) {
69
74
  for (const k of keys)
70
75
  io.out(` ${style.accent(k)} ${style.dim("=")} ${fmt(fm[k])}`);
71
76
  }
77
+ // The computed rows under their surface names (`title`, `tags` — spec/surface
78
+ // §1.2), their own section so a computed title never masquerades as authored.
79
+ const computedKeys = Object.keys(computed);
80
+ if (computedKeys.length > 0) {
81
+ io.out(` ${style.dim("computed")}`);
82
+ for (const k of computedKeys)
83
+ io.out(` ${style.accent(k)} ${style.dim("=")} ${fmt(computed[k])}`);
84
+ }
72
85
  if (links.out.length > 0) {
73
- io.out(` ${style.dim("out edges")}`);
86
+ io.out(` ${style.dim("out links")}`);
74
87
  for (const e of links.out)
75
- io.out(` ${style.accent(e.predicate)} ${style.dim(render.g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
88
+ io.out(` ${style.accent(e.predicate)} ${style.dim(render.g.arrow)} ${style.id(e.doc_id)} ${style.dim(`×${e.count}`)}`);
76
89
  }
77
90
  if (links.in.length > 0) {
78
91
  io.out(` ${style.dim("backlinks")}`);
79
92
  for (const e of links.in)
80
- io.out(` ${style.id(e.node)} ${style.dim(render.g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
93
+ io.out(` ${style.id(e.doc_id)} ${style.dim(render.g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
81
94
  }
82
95
  }
83
96
  function renderBlockCard(cli, node, history) {
84
97
  const { render, style, io } = cli;
85
98
  io.out(` ${render.typeGlyph(node.type)} ${style.bold(node.type)} ${style.id(node.id)}`);
86
99
  io.out(render.rule(40));
87
- if (node.placement)
88
- io.out(` ${style.dim("placement")} parent=${style.id(node.placement.parent ?? "—")} ordinal=${node.placement.ordinal} depth=${node.placement.depth}`);
100
+ // The placement is flattened onto the row (3.0: `parent_id`/`ordinal`/`depth`); the line reads the same.
101
+ if (node.ordinal !== undefined)
102
+ io.out(` ${style.dim("placement")} parent=${style.id(node.parent_id ?? "—")} ordinal=${node.ordinal} depth=${node.depth}`);
89
103
  if (node.attrs && Object.keys(node.attrs).length > 0) {
90
104
  io.out(` ${style.dim("attrs")}`);
91
105
  for (const [k, v] of Object.entries(node.attrs))
@@ -3,7 +3,7 @@ import { docsUpdate, renderOpsetPlan, surfaceDocsUpdate } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
4
  import { readContent, extractContentOpts } from "./_mutate.js";
5
5
  import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
6
- import { remoteCall } from "./_remote.js";
6
+ import { remoteCall, docLocator, requireDoc } from "./_remote.js";
7
7
  // `omg update <target> (-f file | -)`: a polymorphic-target command (the omgbase
8
8
  // north-star — every command accepts any entity ref). A block id (b_…) replaces
9
9
  // that block's markdown (CAS-pinned, the low-level op). A doc id or path is the
@@ -57,7 +57,7 @@ async function runDocUpdate(cli, args) {
57
57
  // Remote: docs_update reconciles + commits server-side, returning the same
58
58
  // { opset, result } — render unchanged.
59
59
  const r = await remoteCall(cli, "docs_update", {
60
- doc,
60
+ ...docLocator(doc),
61
61
  content: bytes,
62
62
  ...(values.reason ? { reason: values.reason } : {}),
63
63
  ...(dryRun ? { dry_run: true } : {}),
@@ -76,7 +76,7 @@ async function runDocUpdate(cli, args) {
76
76
  ...(ws.omgbaseDir ? { omgbaseDir: ws.omgbaseDir } : {}),
77
77
  ...(values.actor ? { actor: values.actor } : {}),
78
78
  };
79
- ({ opset, result } = surfaceDocsUpdate(docsUpdate(ws.store, ctx, doc, bytes, {
79
+ ({ opset, result } = surfaceDocsUpdate(docsUpdate(ws.store, ctx, requireDoc(ws.store, repo.repoId, doc), bytes, {
80
80
  dryRun,
81
81
  ...(values.reason ? { reason: values.reason } : {}),
82
82
  })));
@@ -85,7 +85,7 @@ export const REMOTE_OK = new Set([
85
85
  // doc-level mutators
86
86
  "new", "mv", "meta", "rm", "update", "retarget",
87
87
  // block-level sugar (server-side ref resolution + CAS via the blocks_* /
88
- // tasks_complete / sections_append / node_set / apply tools)
88
+ // blocks_check / blocks_append / nodes_set / apply tools)
89
89
  "apply", "insert", "move", "split", "merge", "done", "append", "node",
90
90
  // shell accepts --server itself, then threads it into every line it runs.
91
91
  "shell",
@@ -92,10 +92,13 @@ export declare const HIT_CELL_MAX = 60;
92
92
  export declare function truncateCell(s: string, max?: number): string;
93
93
  /** Render one projected value as a table cell (unstyled). */
94
94
  export declare function hitCell(v: unknown): string;
95
- /** The projected column names across `hits`: every key but the injected id/path,
96
- * in first-seen order. Empty when nothing was projected. */
95
+ /** The projected column names across `hits`: every key but the envelope's
96
+ * `id`/`path` (spec/surface §1.4, 3.0 — a `path` projection collapses onto
97
+ * the envelope key, so it is never a column), in first-seen order. Empty when
98
+ * nothing was projected. */
97
99
  export declare function hitColumns(hits: HitLike[]): string[];
98
- /** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected. */
100
+ /** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected
101
+ * (header `id`, `path`, then the projected keys — spec/cli §4, 3.0). */
99
102
  export declare function renderHits(cli: {
100
103
  io: IO;
101
104
  style: Style;
@@ -190,8 +190,10 @@ export function hitCell(v) {
190
190
  return String(v);
191
191
  return truncateCell(JSON.stringify(v) ?? String(v));
192
192
  }
193
- /** The projected column names across `hits`: every key but the injected id/path,
194
- * in first-seen order. Empty when nothing was projected. */
193
+ /** The projected column names across `hits`: every key but the envelope's
194
+ * `id`/`path` (spec/surface §1.4, 3.0 — a `path` projection collapses onto
195
+ * the envelope key, so it is never a column), in first-seen order. Empty when
196
+ * nothing was projected. */
195
197
  export function hitColumns(hits) {
196
198
  const cols = [];
197
199
  const seen = new Set(["id", "path"]);
@@ -205,7 +207,8 @@ export function hitColumns(hits) {
205
207
  }
206
208
  return cols;
207
209
  }
208
- /** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected. */
210
+ /** Human-tier hit list: `<id> <path>` lines, or an aligned table when projected
211
+ * (header `id`, `path`, then the projected keys — spec/cli §4, 3.0). */
209
212
  export function renderHits(cli, hits) {
210
213
  const { io, style } = cli;
211
214
  const projected = hitColumns(hits);
@@ -214,7 +217,7 @@ export function renderHits(cli, hits) {
214
217
  io.out(`${style.id(h.id)} ${style.accent(h.path)}`.trimEnd());
215
218
  return;
216
219
  }
217
- const cols = projected.includes("$path") ? ["id", ...projected] : ["id", "path", ...projected];
220
+ const cols = ["id", "path", ...projected];
218
221
  const rows = [cols.map((c) => style.dim(c))];
219
222
  for (const h of hits) {
220
223
  rows.push(cols.map((c) => {
@@ -222,8 +225,6 @@ export function renderHits(cli, hits) {
222
225
  return style.id(h.id);
223
226
  if (c === "path")
224
227
  return style.accent(h.path);
225
- if (c === "$path" && typeof h[c] === "string")
226
- return style.accent(h[c]);
227
228
  return hitCell(h[c]);
228
229
  }));
229
230
  }
@@ -10,7 +10,10 @@
10
10
  // signal to use OQX instead.
11
11
  export class RefError extends Error {
12
12
  }
13
- const REF_RE = /^@(\d+|_|[A-Za-z][\w-]*)(?:\[(\d+)\])?(?:\.([A-Za-z_]\w*))?$/;
13
+ // `.field` reads any projected key (`@1.id`, `@t[1].path`, `@1.layer` —
14
+ // spec/surface §1.4, 3.0 spells the envelope as the bare attributes); a leading
15
+ // `$` is admitted for an OQX-named projection (`$depth`).
16
+ const REF_RE = /^@(\d+|_|[A-Za-z][\w-]*)(?:\[(\d+)\])?(?:\.(\$?[A-Za-z_]\w*))?$/;
14
17
  /** Parse a whole-token reference; null if the token isn't a reference. */
15
18
  export function parseRef(token) {
16
19
  if (!token.startsWith("@"))
@@ -64,13 +67,20 @@ function rowOf(el) {
64
67
  return { ref: el, label: el, value: el };
65
68
  if (el && typeof el === "object") {
66
69
  const o = el;
67
- const ref = firstString(o, ["id", "node", "block", "locator", "path"]);
68
- const label = firstString(o, ["path", "locator", "name", "id", "node"]) ?? ref ?? preview(o);
70
+ const ref = firstString(o, REF_KEYS);
71
+ const label = firstString(o, ["path", "locator", "name", "id", "node", "doc_id"]) ?? ref ?? preview(o);
69
72
  return { ref: ref ?? label ?? "", label: label ?? "", value: el };
70
73
  }
71
74
  const s = String(el);
72
75
  return { ref: s, label: s, value: el };
73
76
  }
77
+ // The keys that make an object an entity ref, in precedence order: a row's
78
+ // `id` (spec/surface §1.2, 3.0 — every tool result spells a row's identity as
79
+ // the attribute; a 2.x server's `id` is the same key), a `links` row's far node
80
+ // (`doc_id`) and an edge's `block_id`, then the pre-3.0 envelopes (`node`,
81
+ // `block`) — kept so a remote 2.x server's results still address — then
82
+ // `locator` and a path.
83
+ const REF_KEYS = ["id", "doc_id", "block_id", "node", "block", "locator", "path"];
74
84
  function firstString(o, keys) {
75
85
  for (const k of keys)
76
86
  if (typeof o[k] === "string")
@@ -98,7 +108,7 @@ export function coerce(value) {
98
108
  }
99
109
  if (typeof value === "object") {
100
110
  const o = value;
101
- const ref = firstString(o, ["id", "node", "block", "locator", "path"]);
111
+ const ref = firstString(o, REF_KEYS);
102
112
  if (ref)
103
113
  return ref;
104
114
  // A collection-shaped result object (e.g. {hits:[…]}) can't be one arg.
@@ -295,7 +295,7 @@ export class ShellSession {
295
295
  return String(v);
296
296
  if (v && typeof v === "object") {
297
297
  const o = v;
298
- const id = o.id ?? o.node ?? o.path ?? o.locator;
298
+ const id = o.id ?? o.id ?? o.node ?? o.path ?? o.path ?? o.locator; // a query hit leads with id/path (surface 3.0)
299
299
  return id ? String(id) : "record";
300
300
  }
301
301
  return "empty";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omgbase",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "Open Markdown Graph Base — CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,9 +45,9 @@
45
45
  "omg": "./dist/src/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@omgbase/core": "^0.10.1",
48
+ "@omgbase/core": "^0.11.0",
49
49
  "@omgbase/fs-adapter": "^0.2.0",
50
- "@omgbase/sync": "^0.4.11",
50
+ "@omgbase/sync": "^0.5.0",
51
51
  "yaml": "^2.9.0"
52
52
  },
53
53
  "devDependencies": {