omgbase 0.13.0 → 0.16.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/dist/build-info.json +2 -2
- package/dist/src/cmd/_mutate.d.ts +9 -3
- package/dist/src/cmd/_mutate.js +15 -5
- package/dist/src/cmd/_remote.d.ts +14 -0
- package/dist/src/cmd/_remote.js +28 -0
- package/dist/src/cmd/admin.js +74 -9
- package/dist/src/cmd/docs.js +0 -0
- package/dist/src/cmd/mcp.js +29 -4
- package/dist/src/cmd/mutate.js +20 -10
- package/dist/src/cmd/node.js +1 -1
- package/dist/src/cmd/oqx.d.ts +3 -2
- package/dist/src/cmd/oqx.js +21 -8
- package/dist/src/cmd/retarget.js +3 -2
- package/dist/src/cmd/update.js +7 -4
- package/dist/src/cmd/version.js +4 -1
- package/dist/src/output.d.ts +9 -0
- package/dist/src/output.js +23 -0
- package/package.json +3 -3
package/dist/build-info.json
CHANGED
|
@@ -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[]
|
|
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)
|
|
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
|
package/dist/src/cmd/_mutate.js
CHANGED
|
@@ -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
162
|
return renderPlan(cli, await remoteCall(cli, "plan", { effects: await effects() }));
|
|
160
|
-
return renderReceipt(cli, await
|
|
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) {
|
|
@@ -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)
|
|
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). */
|
package/dist/src/cmd/_remote.js
CHANGED
|
@@ -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). */
|
package/dist/src/cmd/admin.js
CHANGED
|
@@ -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
43
|
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: [
|
|
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,39 @@ 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
|
|
60
|
+
const swept = runGc(ws.store, { enabled: enabled || dryRun });
|
|
61
|
+
// `--vacuum` runs after the sweep (the sweep's freed pages are what it returns);
|
|
62
|
+
// a dry run measures the free pages and vacuums nothing (spec/store §7).
|
|
63
|
+
const vac = values.vacuum
|
|
64
|
+
? dryRun
|
|
65
|
+
? { reclaimable: reclaimableBytes(ws.store), applied: false }
|
|
66
|
+
: { ...vacuum(ws.store), applied: true }
|
|
67
|
+
: undefined;
|
|
68
|
+
const result = vac ? { ...swept, vacuum: vac } : swept;
|
|
57
69
|
if (cli.flags.mode !== "human")
|
|
58
70
|
return emitMachine(cli, result);
|
|
59
|
-
cli.io.err(cli.style.dim(` swept ${
|
|
71
|
+
cli.io.err(cli.style.dim(` swept ${swept.blobsSwept} blobs, ${swept.treeNodesSwept} tree nodes`));
|
|
72
|
+
if (vac)
|
|
73
|
+
cli.io.err(cli.style.dim(vac.applied ? ` vacuumed, reclaimed ${humanBytes(vac.reclaimable)}` : ` would vacuum ${humanBytes(vac.reclaimable)}`));
|
|
60
74
|
return EXIT_OK;
|
|
61
75
|
}
|
|
62
76
|
// ---- doctor -----------------------------------------------------------------
|
|
63
77
|
function runDoctor(cli, args) {
|
|
64
|
-
const { values } = parseArgs({ args, allowPositionals: true, options: { help: { type: "boolean" } } });
|
|
78
|
+
const { values } = parseArgs({ args, allowPositionals: true, options: { size: { type: "boolean" }, help: { type: "boolean" } } });
|
|
65
79
|
if (values.help) {
|
|
66
80
|
return renderHelp(cli, {
|
|
67
81
|
name: "doctor",
|
|
68
82
|
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: [
|
|
83
|
+
usage: ["doctor [--json]", "doctor --size [--json]"],
|
|
84
|
+
options: [
|
|
85
|
+
["--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"],
|
|
86
|
+
["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, advice }`"],
|
|
87
|
+
],
|
|
71
88
|
});
|
|
72
89
|
}
|
|
73
90
|
const ws = cli.workspace();
|
|
91
|
+
if (values.size)
|
|
92
|
+
return runDoctorSize(cli, sizeReport(ws.store));
|
|
74
93
|
const repo = cli.repo(ws);
|
|
75
94
|
const db = ws.store.db;
|
|
76
95
|
const checks = [];
|
|
@@ -98,6 +117,52 @@ function runDoctor(cli, args) {
|
|
|
98
117
|
}
|
|
99
118
|
return allOk ? EXIT_OK : EXIT_ERROR;
|
|
100
119
|
}
|
|
120
|
+
// `doctor --size` (spec/cli §6, cli 4.3): the store's size report (spec/store
|
|
121
|
+
// §7 "Size") as one aligned table — each category, then its three biggest
|
|
122
|
+
// tables indented — the file's page accounting, and the advice lines. Exit 0:
|
|
123
|
+
// a size report is information, not a check. The bytes and share cells are
|
|
124
|
+
// right-aligned in fixed widths (the widest humanized size, `1023.9 KB`, is 9;
|
|
125
|
+
// the widest share, `100.0%`, 6) so the layout is the same whatever the
|
|
126
|
+
// numbers — which is what lets the fixtures pin it by shape (§8).
|
|
127
|
+
const TOP_TABLES = 3;
|
|
128
|
+
const NO_BYTES = "—";
|
|
129
|
+
const BYTES_CELL = 9;
|
|
130
|
+
const SHARE_CELL = 6;
|
|
131
|
+
function runDoctorSize(cli, report) {
|
|
132
|
+
if (cli.flags.mode !== "human")
|
|
133
|
+
return emitMachine(cli, report);
|
|
134
|
+
const { style, io } = cli;
|
|
135
|
+
const bytes = (n) => (n === null ? NO_BYTES : humanBytes(n)).padStart(BYTES_CELL);
|
|
136
|
+
const share = (n) => (n === null ? NO_BYTES : formatShare(n)).padStart(SHARE_CELL);
|
|
137
|
+
// The last column is never padded by `columns`, so the row counts are
|
|
138
|
+
// right-aligned here (to the widest count, or the header).
|
|
139
|
+
const shown = report.categories.flatMap((c) => [c.rows, ...c.tables.slice(0, TOP_TABLES).map((t) => t.rows)]);
|
|
140
|
+
const rowsWidth = Math.max(4, ...shown.map((n) => String(n).length));
|
|
141
|
+
const count = (n) => String(n).padStart(rowsWidth);
|
|
142
|
+
const rows = [[style.dim("category"), style.dim("bytes".padStart(BYTES_CELL)), style.dim("share".padStart(SHARE_CELL)), style.dim("rows".padStart(rowsWidth))]];
|
|
143
|
+
for (const c of report.categories) {
|
|
144
|
+
rows.push([style.accent(c.name), bytes(c.bytes), share(c.share), count(c.rows)]);
|
|
145
|
+
for (const t of c.tables.slice(0, TOP_TABLES))
|
|
146
|
+
rows.push([` ${t.name}`, bytes(t.bytes), share(t.share), count(t.rows)]);
|
|
147
|
+
}
|
|
148
|
+
for (const line of columns(rows, [{}, { align: "right" }, { align: "right" }]))
|
|
149
|
+
io.out(` ${line}`);
|
|
150
|
+
if (report.detail)
|
|
151
|
+
io.out(style.dim(` ${report.detail}: row counts only`));
|
|
152
|
+
io.out("");
|
|
153
|
+
const f = report.file;
|
|
154
|
+
const file = [
|
|
155
|
+
["file", style.path(f.path)],
|
|
156
|
+
["size", `${humanBytes(f.bytes)} on disk, wal ${humanBytes(f.wal_bytes)}`],
|
|
157
|
+
["pages", `${f.page_count} x ${f.page_size} bytes, ${f.freelist_count} free`],
|
|
158
|
+
["reclaimable", humanBytes(f.reclaimable)],
|
|
159
|
+
];
|
|
160
|
+
for (const line of columns(file))
|
|
161
|
+
io.out(` ${line}`);
|
|
162
|
+
for (const a of report.advice)
|
|
163
|
+
io.out(` ${style.dim("advice:")} ${a}`);
|
|
164
|
+
return EXIT_OK;
|
|
165
|
+
}
|
|
101
166
|
function resolveConfigScope(cli, ws) {
|
|
102
167
|
if (cli.flags.repo === "")
|
|
103
168
|
return { kind: "workspace" };
|
package/dist/src/cmd/docs.js
CHANGED
|
Binary file
|
package/dist/src/cmd/mcp.js
CHANGED
|
@@ -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 () => {
|
package/dist/src/cmd/mutate.js
CHANGED
|
@@ -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
|
-
|
|
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));
|
package/dist/src/cmd/node.js
CHANGED
|
@@ -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 } : {});
|
package/dist/src/cmd/oqx.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/src/cmd/oqx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { oqxRun, oqxRunAsync, collectSemanticPhrases } from "@omgbase/core";
|
|
4
|
-
import { truncationFooter, 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,22 +147,23 @@ 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
|
|
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
|
}
|
|
@@ -177,7 +187,10 @@ export function renderOqxResult(cli, result) {
|
|
|
177
187
|
io.err(style.dim(" no hits"));
|
|
178
188
|
return EXIT_OK;
|
|
179
189
|
}
|
|
180
|
-
|
|
190
|
+
if (bare)
|
|
191
|
+
renderBareHits(cli, result.hits);
|
|
192
|
+
else
|
|
193
|
+
renderHits(cli, result.hits);
|
|
181
194
|
if (result.truncated)
|
|
182
195
|
truncationFooter(io, style, result.cursor ?? "");
|
|
183
196
|
return EXIT_OK;
|
package/dist/src/cmd/retarget.js
CHANGED
|
@@ -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,
|
|
@@ -32,7 +32,8 @@ async function runRetarget(cli, args) {
|
|
|
32
32
|
const plan = await remoteCall(cli, "plan", { effects: sugar.effects });
|
|
33
33
|
return values.apply ? renderPlanDiffs(cli, plan) : renderHits(cli, from, to, retargetOf(plan).hits);
|
|
34
34
|
}
|
|
35
|
-
|
|
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();
|
package/dist/src/cmd/update.js
CHANGED
|
@@ -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: [
|
|
65
|
+
plan = await remoteCall(cli, "plan", { effects: [setContent] });
|
|
64
66
|
else
|
|
65
|
-
receipt = await
|
|
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;
|
package/dist/src/cmd/version.js
CHANGED
|
@@ -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 = {
|
package/dist/src/output.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/src/output.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.16.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.
|
|
48
|
+
"@omgbase/core": "^0.19.0",
|
|
49
49
|
"@omgbase/fs-adapter": "^0.2.0",
|
|
50
|
-
"@omgbase/sync": "^0.5.
|
|
50
|
+
"@omgbase/sync": "^0.5.12",
|
|
51
51
|
"yaml": "^2.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|