omgbase 0.11.1 → 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 +57 -16
- package/dist/src/cmd/_mutate.js +144 -53
- 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/edit.js +7 -7
- package/dist/src/cmd/help.js +1 -1
- package/dist/src/cmd/mcp.js +44 -7
- package/dist/src/cmd/mutate.d.ts +1 -0
- package/dist/src/cmd/mutate.js +123 -64
- package/dist/src/cmd/node.js +7 -7
- package/dist/src/cmd/oqx.d.ts +3 -2
- package/dist/src/cmd/oqx.js +21 -8
- package/dist/src/cmd/retarget.js +29 -25
- package/dist/src/cmd/source.js +4 -2
- package/dist/src/cmd/sync.js +12 -6
- package/dist/src/cmd/update.js +55 -41
- package/dist/src/cmd/version.js +4 -1
- package/dist/src/commands.js +2 -1
- package/dist/src/context.js +3 -2
- package/dist/src/dispatch.js +9 -4
- package/dist/src/output.d.ts +9 -0
- package/dist/src/output.js +23 -0
- package/dist/src/seams.d.ts +3 -0
- package/dist/src/seams.js +39 -3
- package/dist/src/shell/refs.js +3 -10
- package/package.json +3 -3
package/dist/src/cmd/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError } from "@omgbase/core";
|
|
3
|
-
import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
|
-
import { installSpecSeams } from "../seams.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
|
+
import { installSpecSeams, WRITER_LOCK_TIMEOUT_ENV } from "../seams.js";
|
|
5
5
|
import { loadEmbedding } from "./_embed.js";
|
|
6
6
|
import { openRepoSource } from "./_source.js";
|
|
7
7
|
import { hostInfo } from "../build.js";
|
|
@@ -26,26 +26,36 @@ function announceSpecSeams(cli) {
|
|
|
26
26
|
if (seams.clock !== null) {
|
|
27
27
|
cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${seams.clock} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
|
|
28
28
|
}
|
|
29
|
+
if (seams.writerLockTimeoutMs !== null) {
|
|
30
|
+
cli.io.err(cli.style.dim(`[mcp] test seam: writer lock waits ${seams.writerLockTimeoutMs} ms (${WRITER_LOCK_TIMEOUT_ENV}) — not for production`));
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
async function runMcp(cli, args) {
|
|
31
34
|
const { values } = parseArgs({
|
|
32
35
|
args,
|
|
33
36
|
allowPositionals: true,
|
|
34
|
-
options: { "no-watch": { type: "boolean" }, help: { type: "boolean" } },
|
|
37
|
+
options: { "no-watch": { type: "boolean" }, tools: { type: "string" }, help: { type: "boolean" } },
|
|
35
38
|
});
|
|
36
39
|
if (values.help) {
|
|
37
40
|
return renderHelp(cli, {
|
|
38
41
|
name: "mcp",
|
|
39
42
|
summary: "Serve the MCP tool surface on stdio for an MCP host (Claude Code, Cursor, …); the host owns the process lifetime",
|
|
40
|
-
usage: "mcp [-C <workspace-dir>] [--repo <slug>] [--no-watch]",
|
|
43
|
+
usage: "mcp [-C <workspace-dir>] [--repo <slug>] [--no-watch] [--tools full|minimal]",
|
|
41
44
|
options: [
|
|
42
45
|
["-C <dir>", "the workspace to serve — a directory at or below one containing .omgbase/ (the host rarely starts you inside it)"],
|
|
43
46
|
["--repo <slug>", "which repo, when the workspace has several"],
|
|
44
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"],
|
|
45
49
|
],
|
|
46
50
|
notes: [`host config: {"command": "${cli.prog}", "args": ["mcp", "-C", "/path/to/notes"]}`],
|
|
47
51
|
});
|
|
48
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
|
+
}
|
|
49
59
|
announceSpecSeams(cli);
|
|
50
60
|
// The host launches us from its own cwd, so a missing workspace here almost
|
|
51
61
|
// always means the host config lacks `-C <dir>` — say so, rather than the
|
|
@@ -63,6 +73,21 @@ async function runMcp(cli, args) {
|
|
|
63
73
|
throw err;
|
|
64
74
|
}
|
|
65
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
|
+
}
|
|
66
91
|
// Connect the configured embedder once (if any) so the query tool's
|
|
67
92
|
// `semantic` mode has a vectorizer for the session. Unset ⇒ semantic queries
|
|
68
93
|
// return provider_unavailable. The spawned process lives for the session and
|
|
@@ -115,7 +140,11 @@ async function runMcp(cli, args) {
|
|
|
115
140
|
// is primed is caught by the sweep, one after it by the feed.
|
|
116
141
|
source = await openRepoSource(ws.store, repo);
|
|
117
142
|
if (source) {
|
|
143
|
+
// Every checkpoint and the priming sweep run under the workspace writer
|
|
144
|
+
// lock (spec/sync §7), as the Rust watcher's do: a verb in another
|
|
145
|
+
// process waits for the ingest instead of racing its file-CAS.
|
|
118
146
|
watcher = new Watcher(ws.store, repo.repoId, source, {
|
|
147
|
+
omgbaseDir: ws.omgbaseDir,
|
|
119
148
|
onCheckpoint: (r) => {
|
|
120
149
|
if (r.ingested.length > 0 || r.deleted.length > 0) {
|
|
121
150
|
cli.io.err(cli.style.dim(`[watch] checkpoint: +${r.ingested.length} -${r.deleted.length}`));
|
|
@@ -129,8 +158,9 @@ async function runMcp(cli, args) {
|
|
|
129
158
|
if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
|
|
130
159
|
cli.io.err(cli.style.dim(`[watch] the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready`));
|
|
131
160
|
}
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
const root = repo.rootPath;
|
|
162
|
+
if (root)
|
|
163
|
+
withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)); // prime: anything that landed before the feed was ready
|
|
134
164
|
cli.io.err(cli.style.dim(`[mcp] serving ${repo.slug} on stdio · watcher live`));
|
|
135
165
|
}
|
|
136
166
|
else {
|
|
@@ -148,6 +178,8 @@ async function runMcp(cli, args) {
|
|
|
148
178
|
}
|
|
149
179
|
if (drainer)
|
|
150
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}`));
|
|
151
183
|
// The query surface's `embedQuery`: a live vectorizer when the embedder loaded,
|
|
152
184
|
// or — when it's configured-but-broken — a hook that throws `embedder_failed`
|
|
153
185
|
// so semantic access fails loudly and specifically (not the misleading
|
|
@@ -161,15 +193,20 @@ async function runMcp(cli, args) {
|
|
|
161
193
|
: failed
|
|
162
194
|
? (_text) => Promise.reject(failed)
|
|
163
195
|
: undefined;
|
|
196
|
+
// The workspace dir makes every write tool take the writer lock for its whole
|
|
197
|
+
// call (spec/sync §7) — `omg` verbs, the Rust `omgbase mcp` and the watcher
|
|
198
|
+
// all lock the same file, so the server must too.
|
|
164
199
|
const handle = await serveStdio({
|
|
165
200
|
store: ws.store,
|
|
166
201
|
repoId: repo.repoId,
|
|
202
|
+
omgbaseDir: ws.omgbaseDir,
|
|
167
203
|
...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
|
|
168
204
|
...(drainer ? { onMutation: () => drainer.schedule() } : {}),
|
|
169
205
|
...(embedQuery ? { embedQuery } : {}),
|
|
170
206
|
...(loaded ? { embedProvider: loaded.providerName } : {}),
|
|
171
207
|
// The `version` tool's view of this binary (spec/surface §4).
|
|
172
208
|
host: hostInfo(),
|
|
209
|
+
tools,
|
|
173
210
|
});
|
|
174
211
|
let shuttingDown = false;
|
|
175
212
|
const shutdown = async () => {
|
package/dist/src/cmd/mutate.d.ts
CHANGED
package/dist/src/cmd/mutate.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { resolveRef,
|
|
3
|
+
import { resolveRef, isContainerBlock, appendEffects, checkEffects, insertEffects, mergeEffects, moveBlocksEffects, removeBlocksEffects, splitEffects, updateBlockEffects, } from "@omgbase/core";
|
|
4
4
|
import { CliUsageError, EngineErrorLike, renderHelp } from "../output.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runSugar, runSugarRemote, runEffects, runEffectsRemote, runExecutePlan, runExecutePlanRemote, readContent, readStdin, expandBlockArgs, extractContentOpts, targetOf } from "./_mutate.js";
|
|
6
6
|
import { runRmDoc } from "./docs.js";
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
import { remoteCall, remoteHasTool } from "./_remote.js";
|
|
8
|
+
// Write sugar (11 §5.6; cli 4.0): each command resolves its refs, builds its
|
|
9
|
+
// effects with the shared builders of `@omgbase/core` (the same table the MCP
|
|
10
|
+
// tools use — ADR-012, the CLI holds no business logic) and applies them as one
|
|
11
|
+
// request via `runSugar` (writer flock, actor default, `--dry-run` as a plan,
|
|
12
|
+
// conflict rendering). `apply` is the primitive — a changeset of `effects`
|
|
13
|
+
// (ADR-021, the intentional-write vocabulary; the kernel `ops` input is retired
|
|
14
|
+
// at cli 4.0) — and `execute` commits a frozen plan; the rest are sugar.
|
|
10
15
|
function block(cli, ws, repoId, ref) {
|
|
11
16
|
const r = resolveRef(ws.store, repoId, ref);
|
|
12
17
|
if (!r || r.kind !== "block")
|
|
13
18
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
|
|
14
19
|
return r.blockId;
|
|
15
20
|
}
|
|
21
|
+
// A parent ref for insert/move: a block ref names that block as the parent; a
|
|
22
|
+
// document ref (id or path) names the document's top level — the same reading
|
|
23
|
+
// the server gives `blocks_insert.to` / `blocks_move.to`.
|
|
24
|
+
function parent(ws, repoId, ref) {
|
|
25
|
+
const r = resolveRef(ws.store, repoId, ref);
|
|
26
|
+
if (!r)
|
|
27
|
+
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(ref)}`, { data: { id: ref } });
|
|
28
|
+
if (r.kind === "document")
|
|
29
|
+
return { kind: "document", id: r.docId };
|
|
30
|
+
return { kind: "block", id: r.blockId };
|
|
31
|
+
}
|
|
16
32
|
// `done` addresses task blocks only (spec/cli §6): any other live block type is
|
|
17
33
|
// a type_mismatch before an op is built — the macro itself would record a no-op
|
|
18
34
|
// `edited` commit (its `attrs.checked` only rewrites a task's marker).
|
|
@@ -27,11 +43,11 @@ function taskBlock(ws, ref, blockId) {
|
|
|
27
43
|
}
|
|
28
44
|
return blockId;
|
|
29
45
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return
|
|
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];
|
|
35
51
|
}
|
|
36
52
|
// Parse an `--at` spec: end|start|before X|after X (default end).
|
|
37
53
|
function parseAt(values) {
|
|
@@ -55,25 +71,65 @@ async function runApply(cli, args) {
|
|
|
55
71
|
if (values.help) {
|
|
56
72
|
return renderHelp(cli, {
|
|
57
73
|
name: "apply",
|
|
58
|
-
summary: "Apply a
|
|
74
|
+
summary: "Apply a changeset — `{ effects: [...] }`, the intentional writes (create/insert/set/unset/move/remove/split/merge/retarget) planned and committed as one transaction — the primitive every other mutator expands to",
|
|
59
75
|
usage: "apply [-f <changeset.json|->] [--reason <s>] [--actor <s>] [--dry-run]",
|
|
60
76
|
options: [
|
|
61
77
|
["-f <file|->", "changeset JSON from a file or stdin (default: stdin)"],
|
|
62
78
|
["--reason <s>", "commit reason recorded in history"],
|
|
63
79
|
["--actor <s>", "commit actor (default human:$USER)"],
|
|
64
|
-
["--dry-run", "
|
|
80
|
+
["--dry-run", "plan only: render the diff, commit nothing (--json prints the Plan for `execute`)"],
|
|
65
81
|
],
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
const raw = values.f ? (values.f === "-" ? readStdin() : readFileSync(values.f, "utf8")) : readStdin();
|
|
69
85
|
const parsed = JSON.parse(raw);
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
// The effects are the one input (spec/cli §6 `apply`, 4.0): the kernel changeset
|
|
87
|
+
// `{ ops }` of 3.x is refused by name — the six ops are a library/fixture contract.
|
|
88
|
+
if (parsed.ops !== undefined)
|
|
89
|
+
throw new CliUsageError("`ops` is no longer an input — apply takes `effects` (the kernel ops are a library/fixture contract since spec/mutate 3.0)");
|
|
90
|
+
if (parsed.effects === undefined)
|
|
91
|
+
throw new CliUsageError("changeset must have an `effects` array");
|
|
92
|
+
if (!Array.isArray(parsed.effects))
|
|
93
|
+
throw new CliUsageError("changeset `effects` must be an array");
|
|
94
|
+
const opts = { ...(values.reason ? { reason: values.reason } : {}), ...(values.actor ? { actor: values.actor } : {}) };
|
|
72
95
|
if (cli.flags.server)
|
|
73
|
-
return
|
|
96
|
+
return runEffectsRemote(cli, parsed.effects, values.reason);
|
|
74
97
|
const ws = cli.workspace();
|
|
75
98
|
const repo = cli.repo(ws);
|
|
76
|
-
return
|
|
99
|
+
return runEffects(cli, ws, repo, parsed.effects, opts);
|
|
100
|
+
}
|
|
101
|
+
// ---- execute (the plan form of the Effect layer) ------------------------------
|
|
102
|
+
async function runExecute(cli, args) {
|
|
103
|
+
const { values, positionals } = parseArgs({
|
|
104
|
+
args,
|
|
105
|
+
allowPositionals: true,
|
|
106
|
+
options: { f: { type: "string", short: "f" }, reason: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
|
|
107
|
+
});
|
|
108
|
+
if (values.help) {
|
|
109
|
+
return renderHelp(cli, {
|
|
110
|
+
name: "execute",
|
|
111
|
+
summary: "Commit exactly a frozen Plan (what `apply -f <effects> --dry-run --json` printed) — one transaction, one commit; stale_plan if a document moved on",
|
|
112
|
+
usage: "execute (-f <plan.json> | -) [--reason <s>] [--actor <s>]",
|
|
113
|
+
options: [
|
|
114
|
+
["-f <file|->", "the Plan JSON from a file or stdin"],
|
|
115
|
+
["-", "the Plan JSON from stdin"],
|
|
116
|
+
["--reason <s>", "commit reason recorded in history"],
|
|
117
|
+
["--actor <s>", "commit actor (default human:$USER)"],
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// The plan IS the preview: a dry run of `execute` would be `apply --dry-run` again.
|
|
122
|
+
if (cli.flags.dryRun)
|
|
123
|
+
throw new CliUsageError("execute has no dry run — the plan is the preview (re-plan with apply --dry-run)");
|
|
124
|
+
const source = values.f ?? (positionals[0] === "-" ? "-" : undefined);
|
|
125
|
+
if (source === undefined)
|
|
126
|
+
throw new CliUsageError("execute requires -f <plan.json> (or - for stdin)");
|
|
127
|
+
const plan = JSON.parse(source === "-" ? readStdin() : readFileSync(source, "utf8"));
|
|
128
|
+
if (cli.flags.server)
|
|
129
|
+
return runExecutePlanRemote(cli, plan, values.reason);
|
|
130
|
+
const ws = cli.workspace();
|
|
131
|
+
const repo = cli.repo(ws);
|
|
132
|
+
return runExecutePlan(cli, ws, repo, plan, { ...(values.reason ? { reason: values.reason } : {}), ...(values.actor ? { actor: values.actor } : {}) });
|
|
77
133
|
}
|
|
78
134
|
// ---- insert -----------------------------------------------------------------
|
|
79
135
|
async function runInsert(cli, args) {
|
|
@@ -103,15 +159,15 @@ async function runInsert(cli, args) {
|
|
|
103
159
|
if (!to)
|
|
104
160
|
throw new CliUsageError("insert requires a <to> parent (a container block id, or a doc id/path for the top level)");
|
|
105
161
|
const markdown = readContent(content);
|
|
106
|
-
// The destination-parent CAS (spec/mutate §1.2) rides as the
|
|
107
|
-
const expect = values.expect ? {
|
|
108
|
-
|
|
109
|
-
|
|
162
|
+
// The destination-parent CAS (spec/mutate §1.2) rides as the effect's expect.
|
|
163
|
+
const expect = values.expect ? { parent_children_hash: values.expect } : undefined;
|
|
164
|
+
const at = parseAt(values);
|
|
165
|
+
if (cli.flags.server) {
|
|
166
|
+
return runSugarRemote(cli, "blocks_insert", { to, markdown, at, ...(expect ? { expect } : {}) }, () => insertEffects(targetOf(to), at, markdown, expect).effects, [to, ...anchorOf(at)]);
|
|
167
|
+
}
|
|
110
168
|
const ws = cli.workspace();
|
|
111
169
|
const repo = cli.repo(ws);
|
|
112
|
-
|
|
113
|
-
const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown, ...expect };
|
|
114
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
170
|
+
return runSugar(cli, ws, repo, insertEffects(parent(ws, repo.repoId, to), at, markdown, expect), actorOf(values));
|
|
115
171
|
}
|
|
116
172
|
// ---- update -----------------------------------------------------------------
|
|
117
173
|
async function runUpdate(cli, args) {
|
|
@@ -139,21 +195,21 @@ async function runUpdate(cli, args) {
|
|
|
139
195
|
if (!ref)
|
|
140
196
|
throw new CliUsageError("update requires a <block>");
|
|
141
197
|
const markdown = readContent(content);
|
|
198
|
+
const expect = values.expect ? { content_hash: values.expect } : undefined;
|
|
142
199
|
// Remote: the server pins CAS from current bytes when --expect is absent.
|
|
143
|
-
if (cli.flags.server)
|
|
144
|
-
return
|
|
200
|
+
if (cli.flags.server) {
|
|
201
|
+
return runSugarRemote(cli, "blocks_update", { id: ref, markdown, ...(expect ? { expect } : {}) }, () => updateBlockEffects(ref, { markdown, expect }).effects, [ref]);
|
|
202
|
+
}
|
|
145
203
|
const ws = cli.workspace();
|
|
146
204
|
const repo = cli.repo(ws);
|
|
147
205
|
const blockId = block(cli, ws, repo.repoId, ref);
|
|
148
206
|
// CAS (§5.7): scripted callers pass --expect; interactive callers omit it and
|
|
149
|
-
//
|
|
150
|
-
// edits since a read the caller never made).
|
|
151
|
-
const expect = values.expect ?? rawHashOf(ws, blockId);
|
|
207
|
+
// the planner pins the live hash (protects against concurrent edits, not
|
|
208
|
+
// against edits since a read the caller never made).
|
|
152
209
|
if (!values.expect) {
|
|
153
210
|
cli.io.err(cli.style.dim(` updating ${ref} (CAS pinned from current bytes)`));
|
|
154
211
|
}
|
|
155
|
-
|
|
156
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
212
|
+
return runSugar(cli, ws, repo, updateBlockEffects(blockId, { markdown, expect }), actorOf(values));
|
|
157
213
|
}
|
|
158
214
|
// ---- move -------------------------------------------------------------------
|
|
159
215
|
async function runMove(cli, args) {
|
|
@@ -182,15 +238,16 @@ async function runMove(cli, args) {
|
|
|
182
238
|
if (refs.length === 0)
|
|
183
239
|
throw new CliUsageError("move requires one or more blocks (or - for stdin)");
|
|
184
240
|
// The destination-parent CAS (spec/mutate §1.2), checked once for the run.
|
|
185
|
-
const expect = values.expect ? {
|
|
186
|
-
|
|
187
|
-
|
|
241
|
+
const expect = values.expect ? { parent_children_hash: values.expect } : undefined;
|
|
242
|
+
const at = parseAt(values);
|
|
243
|
+
const to = values.to;
|
|
244
|
+
if (cli.flags.server) {
|
|
245
|
+
return runSugarRemote(cli, "blocks_move", { blocks: refs, to, at, ...(expect ? { expect } : {}) }, () => moveBlocksEffects(refs, targetOf(to), at, expect).effects, [...refs, to, ...anchorOf(at)]);
|
|
246
|
+
}
|
|
188
247
|
const ws = cli.workspace();
|
|
189
248
|
const repo = cli.repo(ws);
|
|
190
249
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
191
|
-
|
|
192
|
-
const op = { op: "move", blocks, to: { parent, at: parseAt(values) }, ...expect };
|
|
193
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
250
|
+
return runSugar(cli, ws, repo, moveBlocksEffects(blocks, parent(ws, repo.repoId, to), at, expect), actorOf(values));
|
|
194
251
|
}
|
|
195
252
|
// ---- rm (blocks; --doc handled by the doc-level command) --------------------
|
|
196
253
|
async function runRm(cli, args) {
|
|
@@ -219,12 +276,11 @@ async function runRm(cli, args) {
|
|
|
219
276
|
if (refs.length === 0)
|
|
220
277
|
throw new CliUsageError("rm requires one or more blocks (or - for stdin), or --doc");
|
|
221
278
|
if (cli.flags.server)
|
|
222
|
-
return
|
|
279
|
+
return runSugarRemote(cli, "blocks_remove", { blocks: refs }, () => removeBlocksEffects(refs).effects, refs);
|
|
223
280
|
const ws = cli.workspace();
|
|
224
281
|
const repo = cli.repo(ws);
|
|
225
282
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
226
|
-
|
|
227
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
283
|
+
return runSugar(cli, ws, repo, removeBlocksEffects(blocks), actorOf(values));
|
|
228
284
|
}
|
|
229
285
|
// ---- done (blocks_check) --------------------------------------------------
|
|
230
286
|
async function runDone(cli, args) {
|
|
@@ -248,15 +304,13 @@ async function runDone(cli, args) {
|
|
|
248
304
|
const refs = expandBlockArgs(positionals);
|
|
249
305
|
if (refs.length === 0)
|
|
250
306
|
throw new CliUsageError("done requires one or more task blocks (or - for stdin)");
|
|
307
|
+
const checked = !values.undo;
|
|
251
308
|
if (cli.flags.server)
|
|
252
|
-
return
|
|
309
|
+
return runSugarRemote(cli, "blocks_check", { ids: refs, ...(values.undo ? { checked: false } : {}) }, () => checkEffects(refs, checked).effects, refs);
|
|
253
310
|
const ws = cli.workspace();
|
|
254
311
|
const repo = cli.repo(ws);
|
|
255
312
|
const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
|
|
256
|
-
|
|
257
|
-
? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
|
|
258
|
-
: tasksComplete(ws.store, blocks);
|
|
259
|
-
return runOps(cli, ws, repo, ops, actorOf(values));
|
|
313
|
+
return runSugar(cli, ws, repo, checkEffects(blocks, checked), actorOf(values));
|
|
260
314
|
}
|
|
261
315
|
// ---- append (blocks_append) --------------------------------------------------
|
|
262
316
|
async function runAppend(cli, args) {
|
|
@@ -285,27 +339,35 @@ async function runAppend(cli, args) {
|
|
|
285
339
|
throw new CliUsageError("append requires a <block>");
|
|
286
340
|
const markdown = readContent(content);
|
|
287
341
|
// Remote: blocks_append resolves the ref and decides by the block's kind
|
|
288
|
-
// server-side (spec/surface §4).
|
|
289
|
-
if (cli.flags.server)
|
|
290
|
-
return
|
|
342
|
+
// server-side (spec/surface §4); a remote dry run reads the kind first.
|
|
343
|
+
if (cli.flags.server) {
|
|
344
|
+
return runSugarRemote(cli, "blocks_append", { id: target, text: markdown }, async () => {
|
|
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;
|
|
350
|
+
return appendEffects(type === "heading" ? { kind: "section", heading_id: target } : { kind: "block", id: target }, markdown).effects;
|
|
351
|
+
}, [target]);
|
|
352
|
+
}
|
|
291
353
|
const ws = cli.workspace();
|
|
292
354
|
const repo = cli.repo(ws);
|
|
293
355
|
const blockId = block(cli, ws, repo.repoId, target);
|
|
294
|
-
return
|
|
356
|
+
return runSugar(cli, ws, repo, appendSugar(ws.store, blockId, markdown), actorOf(values));
|
|
295
357
|
}
|
|
296
|
-
/** The
|
|
297
|
-
* of its section
|
|
298
|
-
*
|
|
299
|
-
function
|
|
358
|
+
/** The effect `blocks_append` expands to, by what the block is: a heading → the
|
|
359
|
+
* end of its section; a container → the end of its children; a leaf →
|
|
360
|
+
* `block_not_container` (append means into, never a sibling insert). */
|
|
361
|
+
function appendSugar(store, blockId, markdown) {
|
|
300
362
|
const row = store.db
|
|
301
363
|
.prepare("SELECT b.type AS type, d.format AS format, (SELECT count(*) FROM blocks c WHERE c.parent_block = b.block_id AND c.deleted_commit IS NULL) AS children FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ? AND b.deleted_commit IS NULL")
|
|
302
364
|
.get(blockId);
|
|
303
365
|
if (!row)
|
|
304
366
|
throw new EngineErrorLike("block_missing", `no block with id ${JSON.stringify(blockId)}`, { data: { id: blockId } });
|
|
305
367
|
if (row.type === "heading")
|
|
306
|
-
return
|
|
368
|
+
return appendEffects({ kind: "section", heading_id: blockId }, markdown);
|
|
307
369
|
if (isContainerBlock(row.format ?? "markdown", { type: row.type, children: new Array(row.children) })) {
|
|
308
|
-
return
|
|
370
|
+
return appendEffects({ kind: "block", id: blockId }, markdown);
|
|
309
371
|
}
|
|
310
372
|
throw new EngineErrorLike("block_not_container", `block ${blockId} is a ${row.type}, not a heading or a container; append means into — insert after it with blocks_insert { to: <its parent>, at: { after: ${JSON.stringify(blockId)} } }`, { data: { id: blockId, type: row.type } });
|
|
311
373
|
}
|
|
@@ -341,12 +403,12 @@ async function runSplit(cli, args) {
|
|
|
341
403
|
return n;
|
|
342
404
|
});
|
|
343
405
|
if (cli.flags.server)
|
|
344
|
-
return
|
|
406
|
+
return runSugarRemote(cli, "blocks_split", { id: ref, at }, () => splitEffects(ref, at).effects, [ref]);
|
|
345
407
|
const ws = cli.workspace();
|
|
346
408
|
const repo = cli.repo(ws);
|
|
347
409
|
const blockId = block(cli, ws, repo.repoId, ref);
|
|
348
|
-
|
|
349
|
-
return
|
|
410
|
+
// The planner pins the content CAS from the live row.
|
|
411
|
+
return runSugar(cli, ws, repo, splitEffects(blockId, at), actorOf(values));
|
|
350
412
|
}
|
|
351
413
|
async function runMerge(cli, args) {
|
|
352
414
|
const { values, positionals } = parseArgs({
|
|
@@ -370,21 +432,18 @@ async function runMerge(cli, args) {
|
|
|
370
432
|
if (refs.length < 2)
|
|
371
433
|
throw new CliUsageError("merge requires at least two blocks");
|
|
372
434
|
if (cli.flags.server)
|
|
373
|
-
return
|
|
435
|
+
return runSugarRemote(cli, "blocks_merge", { blocks: refs, ...(values.sep !== undefined ? { separator: values.sep } : {}) }, () => mergeEffects(refs, values.sep).effects, refs);
|
|
374
436
|
const ws = cli.workspace();
|
|
375
437
|
const repo = cli.repo(ws);
|
|
376
438
|
const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
|
|
377
|
-
|
|
378
|
-
return runOps(cli, ws, repo, [op], actorOf(values));
|
|
439
|
+
return runSugar(cli, ws, repo, mergeEffects(blocks, values.sep), actorOf(values));
|
|
379
440
|
}
|
|
380
441
|
// helpers ---------------------------------------------------------------------
|
|
381
|
-
function pinned(ws, blockId) {
|
|
382
|
-
return { content_hash: rawHashOf(ws, blockId) ?? "" };
|
|
383
|
-
}
|
|
384
442
|
function actorOf(values) {
|
|
385
443
|
return values.actor ? { actor: values.actor } : {};
|
|
386
444
|
}
|
|
387
|
-
export const cmdApply = { name: "apply", summary: "Apply a
|
|
445
|
+
export const cmdApply = { name: "apply", summary: "Apply a changeset of effects (the primitive)", run: (c, a) => runApply(c, a) };
|
|
446
|
+
export const cmdExecute = { name: "execute", summary: "Commit a frozen plan (from apply --dry-run --json)", run: (c, a) => runExecute(c, a) };
|
|
388
447
|
export const cmdInsert = { name: "insert", summary: "Insert blocks under a parent", run: (c, a) => runInsert(c, a) };
|
|
389
448
|
export const cmdUpdate = { name: "update", summary: "Replace a block's markdown (CAS)", run: (c, a) => runUpdate(c, a) };
|
|
390
449
|
export const cmdMove = { name: "move", summary: "Move blocks under a new parent", run: (c, a) => runMove(c, a) };
|
package/dist/src/cmd/node.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import { nodeSetEffects, editablePropsFor } from "@omgbase/core";
|
|
3
3
|
import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
|
|
4
4
|
import { requireNode } from "./_remote.js";
|
|
5
|
-
import {
|
|
5
|
+
import { runSugar, runSugarRemote } from "./_mutate.js";
|
|
6
6
|
// `omg node set <nodeId> <prop> <value>` (node-editability) — surgically set an
|
|
7
7
|
// editable property of a projected node (e.g. a link's text/target, a task's
|
|
8
|
-
// checked)
|
|
9
|
-
//
|
|
8
|
+
// checked): one `set { node, field: prop, value }` effect (the `nodes_set`
|
|
9
|
+
// builder; cli 4.0), lowered by the planner through the adapter's registered
|
|
10
|
+
// editor. `omg node props <nodeId>` reports what's editable.
|
|
10
11
|
function nodeKindFormat(cli, nodeId) {
|
|
11
12
|
const ws = cli.workspace();
|
|
12
13
|
const row = ws.store.db.prepare("SELECT n.kind AS kind, d.format AS format FROM nodes n JOIN docs d ON d.doc_id = n.doc_id WHERE n.node_id = ?").get(nodeId);
|
|
@@ -40,11 +41,10 @@ async function runNode(cli, args) {
|
|
|
40
41
|
const value = valueParts.join(" ");
|
|
41
42
|
// Remote: nodes_set resolves node → block + rewrites the span server-side.
|
|
42
43
|
if (cli.flags.server)
|
|
43
|
-
return
|
|
44
|
+
return runSugarRemote(cli, "nodes_set", { id: nodeId, prop, value }, () => nodeSetEffects(nodeId, prop, value).effects, [nodeId]);
|
|
44
45
|
const ws = cli.workspace();
|
|
45
46
|
const repo = cli.repo(ws);
|
|
46
|
-
|
|
47
|
-
return runOps(cli, ws, repo, ops, { reason: `nodes_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
|
|
47
|
+
return runSugar(cli, ws, repo, nodeSetEffects(requireNode(ws.store, repo.repoId, nodeId), prop, value), values.actor ? { actor: values.actor } : {});
|
|
48
48
|
}
|
|
49
49
|
if (sub === "props") {
|
|
50
50
|
// `props` is a local editability lookup with no remote tool (a niche read).
|
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;
|