omgbase 0.13.0 → 0.18.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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "48b355b",
3
- "built": "2026-10-12T21:39:26.285Z"
2
+ "commit": "66abf7c",
3
+ "built": "2026-10-14T06:35:28.357Z"
4
4
  }
@@ -46,9 +46,12 @@ export declare function runSugar(cli: Cli, ws: Workspace, repo: RepoRow, sugar:
46
46
  * SAME renderers as local. The global --dry-run is the `plan` tool over the
47
47
  * effects the verb would build (`effects` composes them from the refs as given:
48
48
  * an id names its row, anything else a document path — the server resolves);
49
- * no tool takes `dry_run` since surface 4.0.
49
+ * no tool takes `dry_run` since surface 4.0. A server that does not list the
50
+ * tool (spec/surface §4 "Profiles": `--tools minimal`) gets `apply { effects,
51
+ * reason: <tool> }` from the same closure (`remoteSugar`, spec/cli §2.3);
52
+ * `refs` are the refs the verb received, refused there when one is a locator.
50
53
  */
51
- export declare function runSugarRemote(cli: Cli, tool: string, toolArgs: Record<string, unknown>, effects: () => unknown[] | Promise<unknown[]>): Promise<number>;
54
+ export declare function runSugarRemote(cli: Cli, tool: string, toolArgs: Record<string, unknown>, effects: () => unknown[] | Promise<unknown[]>, refs?: string[]): Promise<number>;
52
55
  /** A ref as an effect target for a remote `plan`: a `b_` id is the block, a `d_` id the document, anything else a document path. */
53
56
  export declare function targetOf(ref: string): EffectTarget;
54
57
  /**
@@ -70,7 +73,8 @@ export declare function renderPlan(cli: Cli, plan: Plan): number;
70
73
  /**
71
74
  * A Receipt (any write tool's result since 4.0 — a `links_*` result carries its
72
75
  * `hits`/`pairs` beside the receipt's keys): `--json` verbatim; human the
73
- * commit confirmation of §4 (one revision per touched document) and, on
76
+ * commit confirmation of §4 (one revision per touched document), the plan's
77
+ * summary lines under it (4.3: what was committed, in words) and, on
74
78
  * stdout, the ids the write names — the plan's `preview.results[].ids` with
75
79
  * every `$new.n` realized from `new_ids` when the plan is at hand with its
76
80
  * preview (a local write, any `execute`); else (a plan echoed without
@@ -79,6 +83,8 @@ export declare function renderPlan(cli: Cli, plan: Plan): number;
79
83
  * ids that name no row.
80
84
  */
81
85
  export declare function renderReceipt(cli: Cli, receipt: Receipt, plan?: unknown): number;
86
+ /** The Receipt's `summary` (spec/cli §4, 4.3): what was committed in words, one line per effect, indented under the confirmation on stderr. */
87
+ export declare function renderSummary(cli: Cli, summary: readonly string[] | undefined): void;
82
88
  export declare function receiptIds(receipt: Receipt, plan: unknown): string[];
83
89
  /**
84
90
  * The dry-run rendering every write verb shares (spec/cli §3.6): the note on
@@ -2,7 +2,7 @@ import { readFileSync, readSync } from "node:fs";
2
2
  import { userInfo } from "node:os";
3
3
  import { applyEffectsDetailed, applySugar, executePlan, isValidId, substituteSymbols, unifiedDiff, surfacePlan, surfaceReceipt } from "@omgbase/core";
4
4
  import { CliUsageError, EXIT_OK, EXIT_ERROR } from "../output.js";
5
- import { remoteCall } from "./_remote.js";
5
+ import { remoteCall, remoteSugar } from "./_remote.js";
6
6
  // Shared plumbing for every write command (spec/cli §3.6, §4; ADR-021's
7
7
  // retirement release, cli 4.0). The sugar verbs build their effects with the
8
8
  // shared builders of `@omgbase/core` (`mutate/sugar.ts` — the same table the
@@ -152,12 +152,15 @@ export function runSugar(cli, ws, repo, sugar, opts = {}) {
152
152
  * SAME renderers as local. The global --dry-run is the `plan` tool over the
153
153
  * effects the verb would build (`effects` composes them from the refs as given:
154
154
  * an id names its row, anything else a document path — the server resolves);
155
- * no tool takes `dry_run` since surface 4.0.
155
+ * no tool takes `dry_run` since surface 4.0. A server that does not list the
156
+ * tool (spec/surface §4 "Profiles": `--tools minimal`) gets `apply { effects,
157
+ * reason: <tool> }` from the same closure (`remoteSugar`, spec/cli §2.3);
158
+ * `refs` are the refs the verb received, refused there when one is a locator.
156
159
  */
157
- export async function runSugarRemote(cli, tool, toolArgs, effects) {
160
+ export async function runSugarRemote(cli, tool, toolArgs, effects, refs = []) {
158
161
  if (cli.flags.dryRun)
159
- return renderPlan(cli, await remoteCall(cli, "plan", { effects: await effects() }));
160
- return renderReceipt(cli, await remoteCall(cli, tool, toolArgs));
162
+ return renderPlan(cli, await remoteCall(cli, "plan", { effects: await effects(), diffs: true }));
163
+ return renderReceipt(cli, await remoteSugar(cli, tool, toolArgs, effects, refs));
161
164
  }
162
165
  /** A ref as an effect target for a remote `plan`: a `b_` id is the block, a `d_` id the document, anything else a document path. */
163
166
  export function targetOf(ref) {
@@ -195,7 +198,7 @@ export function runEffects(cli, ws, repo, effects, opts = {}) {
195
198
  /** Remote peer of `runEffects`: the `apply` tool with `effects` (the Receipt), or on a dry run the `plan` tool (the Plan). */
196
199
  export async function runEffectsRemote(cli, effects, reason) {
197
200
  if (cli.flags.dryRun)
198
- return renderPlan(cli, await remoteCall(cli, "plan", { effects }));
201
+ return renderPlan(cli, await remoteCall(cli, "plan", { effects, diffs: true }));
199
202
  return renderReceipt(cli, await remoteCall(cli, "apply", { effects, ...(reason ? { reason } : {}) }));
200
203
  }
201
204
  /** `execute`: commit exactly a Plan (the plan form, spec/mutate §11.7 — admission, then the one transaction), rendered as a Receipt. */
@@ -226,7 +229,8 @@ export function renderPlan(cli, plan) {
226
229
  /**
227
230
  * A Receipt (any write tool's result since 4.0 — a `links_*` result carries its
228
231
  * `hits`/`pairs` beside the receipt's keys): `--json` verbatim; human the
229
- * commit confirmation of §4 (one revision per touched document) and, on
232
+ * commit confirmation of §4 (one revision per touched document), the plan's
233
+ * summary lines under it (4.3: what was committed, in words) and, on
230
234
  * stdout, the ids the write names — the plan's `preview.results[].ids` with
231
235
  * every `$new.n` realized from `new_ids` when the plan is at hand with its
232
236
  * preview (a local write, any `execute`); else (a plan echoed without
@@ -244,10 +248,16 @@ export function renderReceipt(cli, receipt, plan) {
244
248
  const { render, style, io } = cli;
245
249
  const n = receipt.revisions.length;
246
250
  io.err(style.dim(` ${style.ok(render.g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
251
+ renderSummary(cli, receipt.summary);
247
252
  for (const id of ids)
248
253
  io.out(id);
249
254
  return EXIT_OK;
250
255
  }
256
+ /** The Receipt's `summary` (spec/cli §4, 4.3): what was committed in words, one line per effect, indented under the confirmation on stderr. */
257
+ export function renderSummary(cli, summary) {
258
+ for (const line of summary ?? [])
259
+ cli.io.err(cli.style.dim(` ${line}`));
260
+ }
251
261
  export function receiptIds(receipt, plan) {
252
262
  const results = plan?.preview?.results;
253
263
  if (!Array.isArray(results))
@@ -11,6 +11,20 @@ export declare function remoteSpec(cli: Cli): EngineSpec;
11
11
  * Returns the tool's parsed JSON result (same shape the local core fn returns,
12
12
  * so the command's existing renderer works unchanged). */
13
13
  export declare function remoteCall<T>(cli: Cli, tool: string, args: Record<string, unknown>): Promise<T>;
14
+ /** Whether the remote engine lists `tool` (spec/surface §4 "Profiles": a `--tools minimal` server lists ten). */
15
+ export declare function remoteHasTool(cli: Cli, tool: string): Promise<boolean>;
16
+ /** A `#` locator (`path#type[n]`, `#^anchor`): a ref only a server with the sugar tools resolves. */
17
+ export declare function isLocator(ref: string): boolean;
18
+ /**
19
+ * The remote sugar call (spec/cli §2.3, 4.2): call `tool` with `toolArgs` when
20
+ * the server lists it — the server resolves refs and pins CAS — else send
21
+ * `apply { effects, reason: <tool> }` with the effects the local builders make
22
+ * from the refs as given (the closure a dry run already uses: an id names its
23
+ * row, anything else a document path). Server-side locator resolution is the
24
+ * one thing a minimal server cannot do, so a `#` locator among `refs` is a
25
+ * usage error there rather than a bogus document path.
26
+ */
27
+ export declare function remoteSugar<T>(cli: Cli, tool: string, toolArgs: Record<string, unknown>, effects: () => unknown[] | Promise<unknown[]>, refs?: string[]): Promise<T>;
14
28
  /** A document ref as the surface's locator (spec/surface §4 "Arguments", 3.0):
15
29
  * a `d_` id is the `id` argument, anything else the `path` — the two names a
16
30
  * `docs_*` tool takes (one of them required). */
@@ -57,6 +57,34 @@ export async function remoteCall(cli, tool, args) {
57
57
  const eng = await remoteEngine(cli);
58
58
  return eng.callTool(tool, { ...args, ...(cli.flags.repo ? { repo: cli.flags.repo } : {}) });
59
59
  }
60
+ /** Whether the remote engine lists `tool` (spec/surface §4 "Profiles": a `--tools minimal` server lists ten). */
61
+ export async function remoteHasTool(cli, tool) {
62
+ return (await remoteEngine(cli)).hasTool(tool);
63
+ }
64
+ /** A `#` locator (`path#type[n]`, `#^anchor`): a ref only a server with the sugar tools resolves. */
65
+ export function isLocator(ref) {
66
+ return ref.includes("#") && !isValidId(ref, "b") && !isValidId(ref, "d");
67
+ }
68
+ /**
69
+ * The remote sugar call (spec/cli §2.3, 4.2): call `tool` with `toolArgs` when
70
+ * the server lists it — the server resolves refs and pins CAS — else send
71
+ * `apply { effects, reason: <tool> }` with the effects the local builders make
72
+ * from the refs as given (the closure a dry run already uses: an id names its
73
+ * row, anything else a document path). Server-side locator resolution is the
74
+ * one thing a minimal server cannot do, so a `#` locator among `refs` is a
75
+ * usage error there rather than a bogus document path.
76
+ */
77
+ export async function remoteSugar(cli, tool, toolArgs, effects, refs = []) {
78
+ if (await remoteHasTool(cli, tool))
79
+ return remoteCall(cli, tool, toolArgs);
80
+ const locator = refs.find(isLocator);
81
+ if (locator !== undefined) {
82
+ throw new CliUsageError(`${JSON.stringify(locator)} is a locator, and this server does not list ${tool} (a minimal tools profile) — refs are not resolved server-side there; pass a block or document id instead`);
83
+ }
84
+ // The commit reason: the caller's own `reason` argument when the tool takes one, else the tool's name.
85
+ const reason = typeof toolArgs.reason === "string" ? toolArgs.reason : tool;
86
+ return remoteCall(cli, "apply", { effects: await effects(), reason });
87
+ }
60
88
  /** A document ref as the surface's locator (spec/surface §4 "Arguments", 3.0):
61
89
  * a `d_` id is the `id` argument, anything else the `path` — the two names a
62
90
  * `docs_*` tool takes (one of them required). */
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
- import { rebuildIndex, runGc, reposStatus, buildEmbedTasks, buildDocEmbedTasks, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, ftsIndexedRowCount, liveLeafCount, } from "@omgbase/core";
2
+ import { rebuildIndex, runGc, reposStatus, buildEmbedTasks, buildDocEmbedTasks, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, ftsIndexedRowCount, liveLeafCount, sizeReport, vacuum, reclaimableBytes, humanBytes, formatShare, } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, EXIT_ERROR, emitMachine, renderHelp } from "../output.js";
4
+ import { columns } from "../render.js";
4
5
  import { loadEmbedding, drainEmbeddings } from "./_embed.js";
5
6
  // admin/maintenance (11 §5.8–5.9). The live watcher moved to `omg sync --watch`.
6
7
  // ---- rebuild-index ----------------------------------------------------------
@@ -35,13 +36,16 @@ function runRebuild(cli, args) {
35
36
  }
36
37
  // ---- gc ---------------------------------------------------------------------
37
38
  function runGcCmd(cli, args) {
38
- const { values } = parseArgs({ args, allowPositionals: true, options: { "dry-run": { type: "boolean" }, help: { type: "boolean" } } });
39
+ const { values } = parseArgs({ args, allowPositionals: true, options: { "dry-run": { type: "boolean" }, vacuum: { type: "boolean" }, help: { type: "boolean" } } });
39
40
  if (values.help) {
40
41
  return renderHelp(cli, {
41
42
  name: "gc",
42
- summary: "Mark-and-sweep unreferenced blobs and tree nodes (refuses unless gc.enabled is set in repo settings)",
43
- usage: "gc [--dry-run]",
44
- options: [["--dry-run", "report what would be swept, sweep nothing (allowed even when gc is disabled)"]],
43
+ summary: "Mark-and-sweep unreferenced blobs and tree nodes, and the `same` disposition rows a pre-14.2 store wrote (refuses unless gc.enabled is set in repo settings)",
44
+ usage: "gc [--dry-run] [--vacuum]",
45
+ options: [
46
+ ["--dry-run", "report what would be swept, sweep nothing (allowed even when gc is disabled)"],
47
+ ["--vacuum", "after the sweep, VACUUM the database to return its free pages to the filesystem (`doctor --size` shows how many)"],
48
+ ],
45
49
  });
46
50
  }
47
51
  // `--dry-run` is also a global flag (parseGlobals consumes it) — honor either spelling.
@@ -53,24 +57,41 @@ function runGcCmd(cli, args) {
53
57
  if (!enabled && !dryRun) {
54
58
  throw new EngineErrorLike("target_missing", "gc is disabled; set gc.enabled=true (or use --dry-run)");
55
59
  }
56
- const result = runGc(ws.store, { enabled: enabled || dryRun });
60
+ // A dry run counts every sweep — blobs, tree nodes and the `same` disposition
61
+ // rows a 14.1 store wrote (spec/store §7, 14.2) — and deletes nothing.
62
+ const swept = runGc(ws.store, { enabled: enabled || dryRun, dryRun });
63
+ // `--vacuum` runs after the sweep (the sweep's freed pages are what it returns);
64
+ // a dry run measures the free pages and vacuums nothing (spec/store §7).
65
+ const vac = values.vacuum
66
+ ? dryRun
67
+ ? { reclaimable: reclaimableBytes(ws.store), applied: false }
68
+ : { ...vacuum(ws.store), applied: true }
69
+ : undefined;
70
+ const result = vac ? { ...swept, vacuum: vac } : swept;
57
71
  if (cli.flags.mode !== "human")
58
72
  return emitMachine(cli, result);
59
- cli.io.err(cli.style.dim(` swept ${result.blobsSwept} blobs, ${result.treeNodesSwept} tree nodes`));
73
+ cli.io.err(cli.style.dim(` swept ${swept.blobsSwept} blobs, ${swept.treeNodesSwept} tree nodes, ${swept.sameDispositionsSwept} same dispositions`));
74
+ if (vac)
75
+ cli.io.err(cli.style.dim(vac.applied ? ` vacuumed, reclaimed ${humanBytes(vac.reclaimable)}` : ` would vacuum ${humanBytes(vac.reclaimable)}`));
60
76
  return EXIT_OK;
61
77
  }
62
78
  // ---- doctor -----------------------------------------------------------------
63
79
  function runDoctor(cli, args) {
64
- const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
80
+ const { values } = parseArgs({ args, allowPositionals: true, options: { size: { type: "boolean" }, help: { type: "boolean" } } });
65
81
  if (values.help) {
66
82
  return renderHelp(cli, {
67
83
  name: "doctor",
68
84
  summary: "Invariant sweep: convergence, FTS rows vs live leaf blocks, dangling revisions, SQLite integrity — exit 1 on any failure (CI-able)",
69
- usage: "doctor [--json]",
70
- options: [["--json", "`{ ok, checks: [{ name, ok, detail }] }`"]],
85
+ usage: ["doctor [--json]", "doctor --size [--json]"],
86
+ options: [
87
+ ["--size", "instead of the checks: what the database is made of — bytes and rows per category (content, history, search, sync) and table, the file's pages, what VACUUM would reclaim, advice"],
88
+ ["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, same_dispositions, advice }`"],
89
+ ],
71
90
  });
72
91
  }
73
92
  const ws = cli.workspace();
93
+ if (values.size)
94
+ return runDoctorSize(cli, sizeReport(ws.store));
74
95
  const repo = cli.repo(ws);
75
96
  const db = ws.store.db;
76
97
  const checks = [];
@@ -98,6 +119,52 @@ function runDoctor(cli, args) {
98
119
  }
99
120
  return allOk ? EXIT_OK : EXIT_ERROR;
100
121
  }
122
+ // `doctor --size` (spec/cli §6, cli 4.3): the store's size report (spec/store
123
+ // §7 "Size") as one aligned table — each category, then its three biggest
124
+ // tables indented — the file's page accounting, and the advice lines. Exit 0:
125
+ // a size report is information, not a check. The bytes and share cells are
126
+ // right-aligned in fixed widths (the widest humanized size, `1023.9 KB`, is 9;
127
+ // the widest share, `100.0%`, 6) so the layout is the same whatever the
128
+ // numbers — which is what lets the fixtures pin it by shape (§8).
129
+ const TOP_TABLES = 3;
130
+ const NO_BYTES = "—";
131
+ const BYTES_CELL = 9;
132
+ const SHARE_CELL = 6;
133
+ function runDoctorSize(cli, report) {
134
+ if (cli.flags.mode !== "human")
135
+ return emitMachine(cli, report);
136
+ const { style, io } = cli;
137
+ const bytes = (n) => (n === null ? NO_BYTES : humanBytes(n)).padStart(BYTES_CELL);
138
+ const share = (n) => (n === null ? NO_BYTES : formatShare(n)).padStart(SHARE_CELL);
139
+ // The last column is never padded by `columns`, so the row counts are
140
+ // right-aligned here (to the widest count, or the header).
141
+ const shown = report.categories.flatMap((c) => [c.rows, ...c.tables.slice(0, TOP_TABLES).map((t) => t.rows)]);
142
+ const rowsWidth = Math.max(4, ...shown.map((n) => String(n).length));
143
+ const count = (n) => String(n).padStart(rowsWidth);
144
+ const rows = [[style.dim("category"), style.dim("bytes".padStart(BYTES_CELL)), style.dim("share".padStart(SHARE_CELL)), style.dim("rows".padStart(rowsWidth))]];
145
+ for (const c of report.categories) {
146
+ rows.push([style.accent(c.name), bytes(c.bytes), share(c.share), count(c.rows)]);
147
+ for (const t of c.tables.slice(0, TOP_TABLES))
148
+ rows.push([` ${t.name}`, bytes(t.bytes), share(t.share), count(t.rows)]);
149
+ }
150
+ for (const line of columns(rows, [{}, { align: "right" }, { align: "right" }]))
151
+ io.out(` ${line}`);
152
+ if (report.detail)
153
+ io.out(style.dim(` ${report.detail}: row counts only`));
154
+ io.out("");
155
+ const f = report.file;
156
+ const file = [
157
+ ["file", style.path(f.path)],
158
+ ["size", `${humanBytes(f.bytes)} on disk, wal ${humanBytes(f.wal_bytes)}`],
159
+ ["pages", `${f.page_count} x ${f.page_size} bytes, ${f.freelist_count} free`],
160
+ ["reclaimable", humanBytes(f.reclaimable)],
161
+ ];
162
+ for (const line of columns(file))
163
+ io.out(` ${line}`);
164
+ for (const a of report.advice)
165
+ io.out(` ${style.dim("advice:")} ${a}`);
166
+ return EXIT_OK;
167
+ }
101
168
  function resolveConfigScope(cli, ws) {
102
169
  if (cli.flags.repo === "")
103
170
  return { kind: "workspace" };
Binary file
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError } from "@omgbase/core";
3
- import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
2
+ import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError, resolveSettings, parseToolsProfile, TOOLS_PROFILES, } from "@omgbase/core";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
4
  import { installSpecSeams, WRITER_LOCK_TIMEOUT_ENV } from "../seams.js";
5
5
  import { loadEmbedding } from "./_embed.js";
6
6
  import { openRepoSource } from "./_source.js";
@@ -34,21 +34,28 @@ async function runMcp(cli, args) {
34
34
  const { values } = parseArgs({
35
35
  args,
36
36
  allowPositionals: true,
37
- options: { "no-watch": { type: "boolean" }, help: { type: "boolean" } },
37
+ options: { "no-watch": { type: "boolean" }, tools: { type: "string" }, help: { type: "boolean" } },
38
38
  });
39
39
  if (values.help) {
40
40
  return renderHelp(cli, {
41
41
  name: "mcp",
42
42
  summary: "Serve the MCP tool surface on stdio for an MCP host (Claude Code, Cursor, …); the host owns the process lifetime",
43
- usage: "mcp [-C <workspace-dir>] [--repo <slug>] [--no-watch]",
43
+ usage: "mcp [-C <workspace-dir>] [--repo <slug>] [--no-watch] [--tools full|minimal]",
44
44
  options: [
45
45
  ["-C <dir>", "the workspace to serve — a directory at or below one containing .omgbase/ (the host rarely starts you inside it)"],
46
46
  ["--repo <slug>", "which repo, when the workspace has several"],
47
47
  ["--no-watch", "don't run the in-process file watcher (it is auto-off when another live watcher holds the lease)"],
48
+ ["--tools <profile>", "the catalog profile to serve: full (default — every tool) or minimal (ten tools: query, query_syntax, docs_read, docs_tree, blocks_resolve, changes_since, plan, execute, apply, version); overrides the mcp.tools setting"],
48
49
  ],
49
50
  notes: [`host config: {"command": "${cli.prog}", "args": ["mcp", "-C", "/path/to/notes"]}`],
50
51
  });
51
52
  }
53
+ // The profile flag is checked before anything is opened: a bad value is the
54
+ // usage error (exit 2) whatever the workspace holds.
55
+ const flagProfile = values.tools === undefined ? undefined : parseToolsProfile(values.tools);
56
+ if (values.tools !== undefined && flagProfile === null) {
57
+ throw new CliUsageError(`--tools must be one of ${TOOLS_PROFILES.join("|")} (got ${JSON.stringify(values.tools)})`);
58
+ }
52
59
  announceSpecSeams(cli);
53
60
  // The host launches us from its own cwd, so a missing workspace here almost
54
61
  // always means the host config lacks `-C <dir>` — say so, rather than the
@@ -66,6 +73,21 @@ async function runMcp(cli, args) {
66
73
  throw err;
67
74
  }
68
75
  const repo = cli.repo(ws);
76
+ // The catalog profile (spec/surface §4 "Profiles", spec/cli `mcp` 4.2): the
77
+ // flag wins over the workspace's `mcp.tools` setting (repo overrides
78
+ // inherit, as every setting does); an unrecognized setting value is reported
79
+ // and ignored — the server comes up `full`, never silently reduced.
80
+ const settingProfile = resolveSettings(ws.store, repo.repoId).mcp?.tools;
81
+ let tools = "full";
82
+ if (flagProfile !== undefined && flagProfile !== null)
83
+ tools = flagProfile;
84
+ else if (settingProfile !== undefined) {
85
+ const parsed = parseToolsProfile(settingProfile);
86
+ if (parsed === null)
87
+ cli.io.err(cli.style.warn(`[mcp] ignoring setting mcp.tools = ${JSON.stringify(settingProfile)} (expected ${TOOLS_PROFILES.join("|")}); serving the full catalog`));
88
+ else
89
+ tools = parsed;
90
+ }
69
91
  // Connect the configured embedder once (if any) so the query tool's
70
92
  // `semantic` mode has a vectorizer for the session. Unset ⇒ semantic queries
71
93
  // return provider_unavailable. The spawned process lives for the session and
@@ -156,6 +178,8 @@ async function runMcp(cli, args) {
156
178
  }
157
179
  if (drainer)
158
180
  cli.io.err(cli.style.dim(`[mcp] auto-embed on mutation enabled`));
181
+ if (tools !== "full")
182
+ cli.io.err(cli.style.dim(`[mcp] tools profile: ${tools}`));
159
183
  // The query surface's `embedQuery`: a live vectorizer when the embedder loaded,
160
184
  // or — when it's configured-but-broken — a hook that throws `embedder_failed`
161
185
  // so semantic access fails loudly and specifically (not the misleading
@@ -182,6 +206,7 @@ async function runMcp(cli, args) {
182
206
  ...(loaded ? { embedProvider: loaded.providerName } : {}),
183
207
  // The `version` tool's view of this binary (spec/surface §4).
184
208
  host: hostInfo(),
209
+ tools,
185
210
  });
186
211
  let shuttingDown = false;
187
212
  const shutdown = async () => {
@@ -4,7 +4,7 @@ import { resolveRef, isContainerBlock, appendEffects, checkEffects, insertEffect
4
4
  import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
5
5
  import { runSugar, runSugarRemote, runEffects, runEffectsRemote, runExecutePlan, runExecutePlanRemote, readContent, readStdin, expandBlockArgs, extractContentOpts, targetOf } from "./_mutate.js";
6
6
  import { runRmDoc } from "./docs.js";
7
- import { remoteCall } from "./_remote.js";
7
+ import { remoteCall, remoteHasTool } from "./_remote.js";
8
8
  // Write sugar (11 §5.6; cli 4.0): each command resolves its refs, builds its
9
9
  // effects with the shared builders of `@omgbase/core` (the same table the MCP
10
10
  // tools use — ADR-012, the CLI holds no business logic) and applies them as one
@@ -43,6 +43,12 @@ function taskBlock(ws, ref, blockId) {
43
43
  }
44
44
  return blockId;
45
45
  }
46
+ /** The block ref an `--at` anchor names (none for start/end) — a ref the remote fallback must be able to send as an id. */
47
+ function anchorOf(at) {
48
+ if (typeof at === "string")
49
+ return [];
50
+ return "before" in at ? [at.before] : [at.after];
51
+ }
46
52
  // Parse an `--at` spec: end|start|before X|after X (default end).
47
53
  function parseAt(values) {
48
54
  const at = values.at?.trim();
@@ -157,7 +163,7 @@ async function runInsert(cli, args) {
157
163
  const expect = values.expect ? { parent_children_hash: values.expect } : undefined;
158
164
  const at = parseAt(values);
159
165
  if (cli.flags.server) {
160
- return runSugarRemote(cli, "blocks_insert", { to, markdown, at, ...(expect ? { expect } : {}) }, () => insertEffects(targetOf(to), at, markdown, expect).effects);
166
+ return runSugarRemote(cli, "blocks_insert", { to, markdown, at, ...(expect ? { expect } : {}) }, () => insertEffects(targetOf(to), at, markdown, expect).effects, [to, ...anchorOf(at)]);
161
167
  }
162
168
  const ws = cli.workspace();
163
169
  const repo = cli.repo(ws);
@@ -192,7 +198,7 @@ async function runUpdate(cli, args) {
192
198
  const expect = values.expect ? { content_hash: values.expect } : undefined;
193
199
  // Remote: the server pins CAS from current bytes when --expect is absent.
194
200
  if (cli.flags.server) {
195
- return runSugarRemote(cli, "blocks_update", { id: ref, markdown, ...(expect ? { expect } : {}) }, () => updateBlockEffects(ref, { markdown, expect }).effects);
201
+ return runSugarRemote(cli, "blocks_update", { id: ref, markdown, ...(expect ? { expect } : {}) }, () => updateBlockEffects(ref, { markdown, expect }).effects, [ref]);
196
202
  }
197
203
  const ws = cli.workspace();
198
204
  const repo = cli.repo(ws);
@@ -236,7 +242,7 @@ async function runMove(cli, args) {
236
242
  const at = parseAt(values);
237
243
  const to = values.to;
238
244
  if (cli.flags.server) {
239
- return runSugarRemote(cli, "blocks_move", { blocks: refs, to, at, ...(expect ? { expect } : {}) }, () => moveBlocksEffects(refs, targetOf(to), at, expect).effects);
245
+ return runSugarRemote(cli, "blocks_move", { blocks: refs, to, at, ...(expect ? { expect } : {}) }, () => moveBlocksEffects(refs, targetOf(to), at, expect).effects, [...refs, to, ...anchorOf(at)]);
240
246
  }
241
247
  const ws = cli.workspace();
242
248
  const repo = cli.repo(ws);
@@ -270,7 +276,7 @@ async function runRm(cli, args) {
270
276
  if (refs.length === 0)
271
277
  throw new CliUsageError("rm requires one or more blocks (or - for stdin), or --doc");
272
278
  if (cli.flags.server)
273
- return runSugarRemote(cli, "blocks_remove", { blocks: refs }, () => removeBlocksEffects(refs).effects);
279
+ return runSugarRemote(cli, "blocks_remove", { blocks: refs }, () => removeBlocksEffects(refs).effects, refs);
274
280
  const ws = cli.workspace();
275
281
  const repo = cli.repo(ws);
276
282
  const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
@@ -300,7 +306,7 @@ async function runDone(cli, args) {
300
306
  throw new CliUsageError("done requires one or more task blocks (or - for stdin)");
301
307
  const checked = !values.undo;
302
308
  if (cli.flags.server)
303
- return runSugarRemote(cli, "blocks_check", { ids: refs, ...(values.undo ? { checked: false } : {}) }, () => checkEffects(refs, checked).effects);
309
+ return runSugarRemote(cli, "blocks_check", { ids: refs, ...(values.undo ? { checked: false } : {}) }, () => checkEffects(refs, checked).effects, refs);
304
310
  const ws = cli.workspace();
305
311
  const repo = cli.repo(ws);
306
312
  const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
@@ -336,9 +342,13 @@ async function runAppend(cli, args) {
336
342
  // server-side (spec/surface §4); a remote dry run reads the kind first.
337
343
  if (cli.flags.server) {
338
344
  return runSugarRemote(cli, "blocks_append", { id: target, text: markdown }, async () => {
339
- const { type } = await remoteCall(cli, "blocks_read", { id: target, resolution: "skeleton" });
345
+ // The block's type decides the effect; `blocks_read` reads it on a full
346
+ // server, a `query` over the blocks target on a minimal one (§2.3).
347
+ const type = (await remoteHasTool(cli, "blocks_read"))
348
+ ? (await remoteCall(cli, "blocks_read", { id: target, resolution: "skeleton" })).type
349
+ : (await remoteCall(cli, "query", { query: `select type from blocks where id == ${JSON.stringify(target)}` })).hits[0]?.type;
340
350
  return appendEffects(type === "heading" ? { kind: "section", heading_id: target } : { kind: "block", id: target }, markdown).effects;
341
- });
351
+ }, [target]);
342
352
  }
343
353
  const ws = cli.workspace();
344
354
  const repo = cli.repo(ws);
@@ -393,7 +403,7 @@ async function runSplit(cli, args) {
393
403
  return n;
394
404
  });
395
405
  if (cli.flags.server)
396
- return runSugarRemote(cli, "blocks_split", { id: ref, at }, () => splitEffects(ref, at).effects);
406
+ return runSugarRemote(cli, "blocks_split", { id: ref, at }, () => splitEffects(ref, at).effects, [ref]);
397
407
  const ws = cli.workspace();
398
408
  const repo = cli.repo(ws);
399
409
  const blockId = block(cli, ws, repo.repoId, ref);
@@ -422,7 +432,7 @@ async function runMerge(cli, args) {
422
432
  if (refs.length < 2)
423
433
  throw new CliUsageError("merge requires at least two blocks");
424
434
  if (cli.flags.server)
425
- return runSugarRemote(cli, "blocks_merge", { blocks: refs, ...(values.sep !== undefined ? { separator: values.sep } : {}) }, () => mergeEffects(refs, values.sep).effects);
435
+ return runSugarRemote(cli, "blocks_merge", { blocks: refs, ...(values.sep !== undefined ? { separator: values.sep } : {}) }, () => mergeEffects(refs, values.sep).effects, refs);
426
436
  const ws = cli.workspace();
427
437
  const repo = cli.repo(ws);
428
438
  const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
@@ -41,7 +41,7 @@ async function runNode(cli, args) {
41
41
  const value = valueParts.join(" ");
42
42
  // Remote: nodes_set resolves node → block + rewrites the span server-side.
43
43
  if (cli.flags.server)
44
- return runSugarRemote(cli, "nodes_set", { id: nodeId, prop, value }, () => nodeSetEffects(nodeId, prop, value).effects);
44
+ return runSugarRemote(cli, "nodes_set", { id: nodeId, prop, value }, () => nodeSetEffects(nodeId, prop, value).effects, [nodeId]);
45
45
  const ws = cli.workspace();
46
46
  const repo = cli.repo(ws);
47
47
  return runSugar(cli, ws, repo, nodeSetEffects(requireNode(ws.store, repo.repoId, nodeId), prop, value), values.actor ? { actor: values.actor } : {});
@@ -6,7 +6,8 @@ import type { Cli } from "../context.js";
6
6
  * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
7
7
  * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
8
8
  * `--ids` the hit ids — a scalar result has neither, so those modes print the
9
- * document. The truncation footer prints in every mode but `--json`.
9
+ * document, and so does a `bare` result (its hits carry no envelope). The
10
+ * truncation footer prints in every mode but `--json`.
10
11
  */
11
- export declare function renderOqxResult(cli: Cli, result: OqxResult): number;
12
+ export declare function renderOqxResult(cli: Cli, result: OqxResult, bare?: boolean): number;
12
13
  export declare const cmdOqx: Command;
@@ -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, CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp, renderHits } from "../output.js";
4
+ import { truncationFooter, CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp, renderHits, renderBareHits } from "../output.js";
5
5
  import { loadEmbedding } from "./_embed.js";
6
6
  import { remoteCall } from "./_remote.js";
7
7
  import { readStdin } from "./_mutate.js";
@@ -31,7 +31,11 @@ import { readStdin } from "./_mutate.js";
31
31
  // `from …` = collect). Coexists with `omg q` (CEL). Source is a positional
32
32
  // string or -f file|-. Human output: one hit per line (id + path), or an aligned
33
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.
34
+ // count/exists/none (see output.ts renderHits); --json/--jsonl/--ids. `--bare`
35
+ // is the tool's `bare` passed through (ADR-012: a thin client; spec/surface
36
+ // §1.4, 4.2): every hit is its projection alone, rendered as a table without
37
+ // the id/path columns (`--json`/`--jsonl` the bare hits; `--ids` has no id to
38
+ // print and falls back to the document, like a scalar result).
35
39
  async function runOqx(cli, args) {
36
40
  const { values, positionals } = parseArgs({
37
41
  args,
@@ -39,6 +43,7 @@ async function runOqx(cli, args) {
39
43
  options: {
40
44
  n: { type: "string", short: "n" },
41
45
  cursor: { type: "string" },
46
+ bare: { type: "boolean" },
42
47
  file: { type: "string", short: "f" },
43
48
  help: { type: "boolean" },
44
49
  },
@@ -47,12 +52,13 @@ async function runOqx(cli, args) {
47
52
  renderHelp(cli, {
48
53
  name: "query",
49
54
  summary: "Composable OQX query: [select …] from <docs|blocks|nodes|links> [where …] [follow …] [order by …] [limit N] (alias: q)",
50
- usage: ["query <source> [-n <N>] [--cursor <c>] [--ids|--json|--jsonl]", "query -f <file|-> [-n <N>] [--cursor <c>]"],
55
+ usage: ["query <source> [-n <N>] [--cursor <c>] [--bare] [--ids|--json|--jsonl]", "query -f <file|-> [-n <N>] [--cursor <c>] [--bare]"],
51
56
  options: [
52
57
  ["<source>", "the OQX query text (quote it); `from` may be omitted inside blocks"],
53
58
  ["-f <file|->", "read the query from a file, or stdin with `-`"],
54
59
  ["-n <N>", "page size"],
55
60
  ["--cursor <c>", "continue a truncated result"],
61
+ ["--bare", "hits as the projection only — no automatic id/path columns (needs a `select`; `values` is the bare-values form)"],
56
62
  ["--ids", `bare ids, one per line — pipe fuel (\`… --ids | ${cli.prog} cat -\`)`],
57
63
  ],
58
64
  notes: [
@@ -97,6 +103,8 @@ async function runOqx(cli, args) {
97
103
  opts.limit = Number(values.n);
98
104
  if (values.cursor)
99
105
  opts.cursor = values.cursor;
106
+ if (values.bare)
107
+ opts.bare = true;
100
108
  let result;
101
109
  if (cli.flags.server) {
102
110
  // Remote: the `query` MCP tool returns the same OqxResult; render unchanged.
@@ -104,6 +112,7 @@ async function runOqx(cli, args) {
104
112
  query: source,
105
113
  ...(opts.limit !== undefined ? { limit: opts.limit } : {}),
106
114
  ...(opts.cursor !== undefined ? { cursor: opts.cursor } : {}),
115
+ ...(opts.bare !== undefined ? { bare: opts.bare } : {}),
107
116
  });
108
117
  }
109
118
  else {
@@ -138,29 +147,40 @@ async function runOqx(cli, args) {
138
147
  : result.consumer === "exists" ? result.exists
139
148
  : result.consumer === "none" ? result.none
140
149
  : result.values ?? result);
141
- return renderOqxResult(cli, result);
150
+ return renderOqxResult(cli, result, opts.bare === true);
142
151
  }
143
152
  /**
144
153
  * Render an `OqxResult` in the current mode (shared with `run`). `--json` is the
145
154
  * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
146
155
  * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
147
156
  * `--ids` the hit ids — a scalar result has neither, so those modes print the
148
- * document. The truncation footer prints in every mode but `--json`.
157
+ * document, and so does a `bare` result (its hits carry no envelope). The
158
+ * truncation footer prints in every mode but `--json`.
149
159
  */
150
- export function renderOqxResult(cli, result) {
160
+ export function renderOqxResult(cli, result, bare = false) {
151
161
  const cursor = result.truncated ? result.cursor ?? "" : null;
152
162
  if (cli.flags.mode !== "human") {
153
163
  const list = result.values ?? (result.consumer === "collect" || result.consumer === "first" || result.consumer === "single" ? result.hits : undefined);
154
164
  return emitMachine(cli, result, {
155
165
  ...(list ? { items: list } : {}),
156
- ...(list && !result.values ? { ids: result.hits.map((h) => h.id) } : {}),
166
+ ...(list && !result.values && !bare ? { ids: result.hits.map((h) => h.id) } : {}),
157
167
  cursor,
158
168
  });
159
169
  }
170
+ // Unknown names (spec/surface §1.5, spec/cli §6 `query`): the result's `hints`
171
+ // — names the query reads that no row of their kind carries — print LAST on
172
+ // stderr, one dim ` hint: …` line each, in every human rendering (the
173
+ // machine modes carry the member itself).
174
+ const { style, io } = cli;
175
+ const hints = () => {
176
+ for (const h of result.hints ?? [])
177
+ io.err(style.dim(` hint: ${h}`));
178
+ };
160
179
  // Scalar consumers (count/exists/none) have no hits — render the reduction itself.
161
180
  if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
162
181
  const scalar = result.consumer === "count" ? String(result.count) : result.consumer === "exists" ? String(result.exists) : String(result.none);
163
182
  cli.io.out(scalar);
183
+ hints();
164
184
  return EXIT_OK;
165
185
  }
166
186
  // A `values` projection has no hits — one bare value per line (strings
@@ -170,16 +190,21 @@ export function renderOqxResult(cli, result) {
170
190
  cli.io.out(typeof v === "string" ? v : JSON.stringify(v));
171
191
  if (cursor !== null)
172
192
  truncationFooter(cli.io, cli.style, cursor);
193
+ hints();
173
194
  return EXIT_OK;
174
195
  }
175
- const { style, io } = cli;
176
196
  if (result.hits.length === 0) {
177
197
  io.err(style.dim(" no hits"));
198
+ hints();
178
199
  return EXIT_OK;
179
200
  }
180
- renderHits(cli, result.hits);
201
+ if (bare)
202
+ renderBareHits(cli, result.hits);
203
+ else
204
+ renderHits(cli, result.hits);
181
205
  if (result.truncated)
182
206
  truncationFooter(io, style, result.cursor ?? "");
207
+ hints();
183
208
  return EXIT_OK;
184
209
  }
185
210
  // `query` (alias `q`) is the single, default query surface — the OQX engine.
@@ -2,7 +2,7 @@ import { parseArgs } from "node:util";
2
2
  import { applySugar, planEffects, retargetEffects, surfacePlan, surfaceReceipt } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
4
  import { defaultActor, renderPlan as renderPlanDiffs, renderReceipt, unifiedDiffLines } from "./_mutate.js";
5
- import { remoteCall } from "./_remote.js";
5
+ import { remoteCall, remoteSugar } from "./_remote.js";
6
6
  async function runRetarget(cli, args) {
7
7
  const { values, positionals } = parseArgs({
8
8
  args,
@@ -29,10 +29,11 @@ async function runRetarget(cli, args) {
29
29
  // Remote: the `plan` tool previews, `links_retarget` commits server-side.
30
30
  if (cli.flags.server) {
31
31
  if (!values.apply || cli.flags.dryRun) {
32
- const plan = await remoteCall(cli, "plan", { effects: sugar.effects });
32
+ const plan = await remoteCall(cli, "plan", { effects: sugar.effects, diffs: true });
33
33
  return values.apply ? renderPlanDiffs(cli, plan) : renderHits(cli, from, to, retargetOf(plan).hits);
34
34
  }
35
- const r = await remoteCall(cli, "links_retarget", { from_target: from, to_target: to, ...(values.scope ? { path_glob: values.scope } : {}) });
35
+ // `links_retarget` where listed (its result adds `hits`/`pairs`), else `apply { retarget }` (spec/cli §2.3, 4.2).
36
+ const r = await remoteSugar(cli, "links_retarget", { from_target: from, to_target: to, ...(values.scope ? { path_glob: values.scope } : {}) }, () => sugar.effects);
36
37
  return renderReceipt(cli, r);
37
38
  }
38
39
  const ws = cli.workspace();
@@ -1,9 +1,9 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { applyEffectsDetailed, planEffects, referencePath, renderIdentityPlan, surfacePlan, surfaceReceipt } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
- import { readContent, extractContentOpts, receiptIds, defaultActor } from "./_mutate.js";
4
+ import { readContent, extractContentOpts, receiptIds, defaultActor, renderSummary } from "./_mutate.js";
5
5
  import { cmdUpdate as cmdUpdateBlock } from "./mutate.js";
6
- import { remoteCall, docLocator, requireDoc } from "./_remote.js";
6
+ import { remoteCall, remoteSugar, 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
@@ -59,10 +59,12 @@ async function runDocUpdate(cli, args) {
59
59
  // Remote: the `docs_update` tool commits the `set content` server-side and
60
60
  // returns the Receipt (the confirmation then lists the ids it minted, as a
61
61
  // remote `apply` does); the dry run is the `plan` tool over the same effect.
62
+ // A server without `docs_update` (a minimal profile) gets `apply` over the same effect (spec/cli §2.3, 4.2).
63
+ const setContent = { op: "set", target: { kind: "document", ...docLocator(doc) }, field: "content", value: bytes };
62
64
  if (dryRun)
63
- plan = await remoteCall(cli, "plan", { effects: [{ op: "set", target: { kind: "document", ...docLocator(doc) }, field: "content", value: bytes }] });
65
+ plan = await remoteCall(cli, "plan", { effects: [setContent], diffs: true });
64
66
  else
65
- receipt = await remoteCall(cli, "docs_update", { ...docLocator(doc), content: bytes, ...(values.reason ? { reason: values.reason } : {}) });
67
+ receipt = await remoteSugar(cli, "docs_update", { ...docLocator(doc), content: bytes, ...(values.reason ? { reason: values.reason } : {}) }, () => [setContent]);
66
68
  }
67
69
  else {
68
70
  const ws = cli.workspace();
@@ -108,6 +110,7 @@ async function runDocUpdate(cli, args) {
108
110
  const g = cli.render.g;
109
111
  const n = receipt.revisions.length;
110
112
  cli.io.err(cli.style.dim(` ${cli.style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
113
+ renderSummary(cli, receipt.summary);
111
114
  for (const id of new Set(receiptIds(receipt, plan)))
112
115
  cli.io.out(id);
113
116
  return EXIT_OK;
@@ -41,7 +41,7 @@ async function runVersion(cli, args) {
41
41
  return emitMachine(cli, info);
42
42
  const { io, style } = cli;
43
43
  const dash = "—";
44
- const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built"];
44
+ const keys = ["engine", "version", "specs", "schema", "mcp", "runtime", "commit", "built", "profile"];
45
45
  const width = Math.max(...keys.map((k) => k.length));
46
46
  const line = (key, value) => {
47
47
  io.out(`${style.dim(key.padEnd(width))} ${value === null ? style.dim(dash) : String(value)}`);
@@ -58,6 +58,9 @@ async function runVersion(cli, args) {
58
58
  line("runtime", info.runtime);
59
59
  line("commit", info.commit);
60
60
  line("built", info.built);
61
+ // The catalog profile served (spec/surface §4 "Profiles", 4.3): `full` for
62
+ // the library itself; over --server, whatever the remote serves.
63
+ line("profile", info.profile);
61
64
  return EXIT_OK;
62
65
  }
63
66
  export const cmdVersion = {
@@ -103,6 +103,15 @@ export declare function renderHits(cli: {
103
103
  io: IO;
104
104
  style: Style;
105
105
  }, hits: HitLike[]): void;
106
+ /** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
107
+ * projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
108
+ * first-seen order across the hits — a projected `id`/`path` is an ordinary
109
+ * column, rendered as any other cell — with no envelope columns before them.
110
+ * Header row, then one row per hit; cells as the enveloped table's. */
111
+ export declare function renderBareHits(cli: {
112
+ io: IO;
113
+ style: Style;
114
+ }, hits: Record<string, unknown>[]): void;
106
115
  /** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
107
116
  export declare function truncationFooter(io: IO, style: Style, cursor: number | string): void;
108
117
  export {};
@@ -231,6 +231,29 @@ export function renderHits(cli, hits) {
231
231
  for (const line of columns(rows))
232
232
  io.out(line);
233
233
  }
234
+ /** The bare hit table (`query --bare`, spec/cli §4, 4.1): a hit is its
235
+ * projection alone (`spec/surface` §1.4, 4.2), so the columns are every key in
236
+ * first-seen order across the hits — a projected `id`/`path` is an ordinary
237
+ * column, rendered as any other cell — with no envelope columns before them.
238
+ * Header row, then one row per hit; cells as the enveloped table's. */
239
+ export function renderBareHits(cli, hits) {
240
+ const { io, style } = cli;
241
+ const cols = [];
242
+ const seen = new Set();
243
+ for (const h of hits) {
244
+ for (const k of Object.keys(h)) {
245
+ if (seen.has(k))
246
+ continue;
247
+ seen.add(k);
248
+ cols.push(k);
249
+ }
250
+ }
251
+ const rows = [cols.map((c) => style.dim(c))];
252
+ for (const h of hits)
253
+ rows.push(cols.map((c) => hitCell(h[c])));
254
+ for (const line of columns(rows))
255
+ io.out(line);
256
+ }
234
257
  /** Loud truncation footer to stderr (§4.4). Exit code stays 0. */
235
258
  export function truncationFooter(io, style, cursor) {
236
259
  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.13.0",
3
+ "version": "0.18.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.15.0",
48
+ "@omgbase/core": "^0.21.0",
49
49
  "@omgbase/fs-adapter": "^0.2.0",
50
- "@omgbase/sync": "^0.5.8",
50
+ "@omgbase/sync": "^0.5.14",
51
51
  "yaml": "^2.9.0"
52
52
  },
53
53
  "devDependencies": {