omgbase 0.3.0 → 0.4.1

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,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { resolveRef, historyNode } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
5
  // `omg hist <node>` (11 §5.5) — history_node: a block's biography.
6
6
  async function runHist(cli, args) {
@@ -34,15 +34,9 @@ async function runHist(cli, args) {
34
34
  throw new EngineErrorLike("block_missing", `hist needs a block id; got ${ref}`);
35
35
  changes = historyNode(ws.store, resolved.blockId, values.n ? { limit: Number(values.n) } : {});
36
36
  }
37
- if (cli.flags.mode === "json") {
38
- cli.io.out(JSON.stringify(changes));
39
- return EXIT_OK;
40
- }
41
- if (cli.flags.mode === "jsonl") {
42
- for (const c of changes)
43
- cli.io.out(JSON.stringify(c));
44
- return EXIT_OK;
45
- }
37
+ // A change carries no id of its own, so `--ids` is the `--json` document.
38
+ if (cli.flags.mode !== "human")
39
+ return emitMachine(cli, changes, { items: changes });
46
40
  const { style, io } = cli;
47
41
  if (changes.length === 0) {
48
42
  io.err(style.dim(" no history"));
@@ -1,9 +1,9 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { resolveRef, docLinks } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  // `omg links <node>` (11 §5.4) — open edges touching the node; default both
5
- // directions, grouped; doc-grain by default, --blocks for block-grain.
6
- // Backlinks = --in.
5
+ // directions, grouped; doc-grain by default, --blocks for block-grain (one row
6
+ // per edge, naming the source block). Backlinks = --in.
7
7
  function runLinks(cli, args) {
8
8
  const { values, positionals } = parseArgs({
9
9
  args,
@@ -45,30 +45,35 @@ function runLinks(cli, args) {
45
45
  opts.blocks = true;
46
46
  const result = docLinks(ws.store, resolved.docId, opts);
47
47
  cli.capture?.(result); // shell: edges (out+in) become the addressable frame
48
- if (cli.flags.mode === "json") {
49
- cli.io.out(JSON.stringify(result));
50
- return EXIT_OK;
51
- }
52
- if (cli.flags.mode === "ids") {
53
- for (const e of [...result.out, ...result.in])
54
- cli.io.out(e.node);
55
- return EXIT_OK;
56
- }
48
+ // `{ out, in }` is not a list: `--jsonl` prints the document; `--ids` the far nodes.
49
+ if (cli.flags.mode !== "human")
50
+ return emitMachine(cli, result, { ids: [...result.out, ...result.in].map((e) => e.node) });
57
51
  const { render, style, io } = cli;
58
52
  const g = render.g;
59
53
  if (result.out.length === 0 && result.in.length === 0) {
60
54
  io.err(style.dim(" no links"));
61
55
  return EXIT_OK;
62
56
  }
57
+ // Doc-grain: one row per (predicate, node) with its rollup count. Block-grain:
58
+ // one row per edge, the source block beside the far node.
59
+ const src = (e) => style.dim(`(${e.block ?? "frontmatter"})`);
63
60
  if (result.out.length > 0) {
64
61
  io.out(style.dim(" out"));
65
- for (const e of result.out)
66
- io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
62
+ for (const e of result.out) {
63
+ if (values.blocks)
64
+ io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${src(e)}`);
65
+ else
66
+ io.out(` ${style.accent(e.predicate)} ${style.dim(g.arrow)} ${style.id(e.node)} ${style.dim(`×${e.count}`)}`);
67
+ }
67
68
  }
68
69
  if (result.in.length > 0) {
69
70
  io.out(style.dim(" in (backlinks)"));
70
- for (const e of result.in)
71
- io.out(` ${style.id(e.node)} ${style.dim(g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
71
+ for (const e of result.in) {
72
+ if (values.blocks)
73
+ io.out(` ${style.id(e.node)} ${src(e)} ${style.dim(g.arrow)} ${style.accent(e.predicate)}`);
74
+ else
75
+ io.out(` ${style.id(e.node)} ${style.dim(g.arrow)} ${style.accent(e.predicate)} ${style.dim(`×${e.count}`)}`);
76
+ }
72
77
  }
73
78
  return EXIT_OK;
74
79
  }
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { changesSince } from "@omgbase/core";
3
- import { CliUsageError, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, truncationFooter, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
5
  // `omg log` (11 §5.5) — changes_since, one digest summary per line. Relative
6
6
  // --since (24h/7d) is resolved to a literal ISO timestamp client-side, then to
@@ -23,9 +23,12 @@ function relativeToIso(since) {
23
23
  const ms = unit === "h" ? 3600e3 : unit === "d" ? 86400e3 : unit === "w" ? 7 * 86400e3 : 30 * 86400e3;
24
24
  return new Date(Date.now() - n * ms).toISOString();
25
25
  }
26
- // Absolute timestamp: pass through (Date normalizes).
26
+ // Absolute timestamp: pass through (Date normalizes). Anything else is the
27
+ // user's mistake, not "the epoch" (spec/cli §6 `log`).
27
28
  const t = Date.parse(since);
28
- return Number.isNaN(t) ? new Date(0).toISOString() : new Date(t).toISOString();
29
+ if (Number.isNaN(t))
30
+ throw new CliUsageError(`bad --since '${since}' (use a relative age like 24h or 7d, or an ISO timestamp)`);
31
+ return new Date(t).toISOString();
29
32
  }
30
33
  async function runLog(cli, args) {
31
34
  const { values } = parseArgs({
@@ -76,21 +79,8 @@ async function runLog(cli, args) {
76
79
  opts.origin = values.origin;
77
80
  result = changesSince(ws.store, repo.repoId, opts);
78
81
  }
79
- if (cli.flags.mode === "json") {
80
- cli.io.out(JSON.stringify(result));
81
- return EXIT_OK;
82
- }
83
- if (cli.flags.mode === "jsonl") {
84
- for (const d of result.digests)
85
- cli.io.out(JSON.stringify(d));
86
- if (result.truncated)
87
- truncationFooter(cli.io, cli.style, result.cursor);
88
- return EXIT_OK;
89
- }
90
- if (cli.flags.mode === "ids") {
91
- for (const d of result.digests)
92
- cli.io.out(d.commit);
93
- return EXIT_OK;
82
+ if (cli.flags.mode !== "human") {
83
+ return emitMachine(cli, result, { items: result.digests, ids: result.digests.map((d) => d.commit), cursor: result.truncated ? result.cursor : null });
94
84
  }
95
85
  const { style, io } = cli;
96
86
  if (result.digests.length === 0) {
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { docsList } from "@omgbase/core";
3
3
  import { columns } from "../render.js";
4
- import { EXIT_OK, renderHelp } from "../output.js";
4
+ import { EXIT_OK, emitMachine, renderHelp } from "../output.js";
5
5
  import { remoteCall } from "./_remote.js";
6
6
  // `omg ls [glob]` (11 §5.2) — live documents: path, block count, last-commit
7
7
  // time. glob is a simple prefix/suffix match on the path (SQL LIKE with * → %).
@@ -39,20 +39,8 @@ async function runLs(cli, args) {
39
39
  cursor = page.truncated ? page.cursor : null;
40
40
  } while (cursor);
41
41
  cli.capture?.(rows); // shell: docs become the addressable frame (ref = path)
42
- if (cli.flags.mode === "ids") {
43
- for (const r of rows)
44
- cli.io.out(r.path);
45
- return EXIT_OK;
46
- }
47
- if (cli.flags.mode === "json") {
48
- cli.io.out(JSON.stringify(rows));
49
- return EXIT_OK;
50
- }
51
- if (cli.flags.mode === "jsonl") {
52
- for (const r of rows)
53
- cli.io.out(JSON.stringify(r));
54
- return EXIT_OK;
55
- }
42
+ if (cli.flags.mode !== "human")
43
+ return emitMachine(cli, rows, { items: rows, ids: rows.map((r) => r.path) });
56
44
  const { style, io } = cli;
57
45
  if (rows.length === 0) {
58
46
  io.err(style.dim(" no documents"));
@@ -1,6 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, EmbedDrainer, EngineError, pinClock, sequentialMinter, setIdMinter } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
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";
4
5
  import { loadEmbedding } from "./_embed.js";
5
6
  import { openRepoSource } from "./_source.js";
6
7
  // `omg mcp [--no-watch]` (11 §5.8) — MCP server on stdio; the host owns the
@@ -11,38 +12,18 @@ import { openRepoSource } from "./_source.js";
11
12
  // CRITICAL: on stdio, stdout is the MCP protocol channel. Nothing may write to
12
13
  // stdout here — every diagnostic goes to stderr (cli.io.err).
13
14
  /**
14
- * The two conformance seams of spec/surface §7.1, read from the environment
15
- * BEFORE the workspace opens (so no id is minted and no timestamp read without
16
- * them). For conformance runs only — never set in production:
17
- *
18
- * OMGBASE_SPEC_MINTER=sequential the fixture minter of spec/store §2.2 for
19
- * the whole process (`d_0, d_1, …`, each
20
- * prefix from 0); any other value is a
21
- * startup error
22
- * OMGBASE_SPEC_CLOCK=<RFC 3339> "now" is that instant for the whole
23
- * process: every commit a tool stamps, every
24
- * `ts` reported as current
25
- *
26
- * Both are announced on stderr (stdout is the protocol channel).
15
+ * Announce the two conformance seams of spec/surface §7.1 on stderr (stdout is
16
+ * the protocol channel). They are installed for every verb at the entry point
17
+ * (`seams.ts`, spec/cli §2.6) — `mcp` only reports them, so a conformance run
18
+ * is visibly not a production one.
27
19
  */
28
- function installSpecSeams(cli) {
29
- const minter = process.env.OMGBASE_SPEC_MINTER;
30
- if (minter !== undefined && minter !== "") {
31
- if (minter !== "sequential") {
32
- throw new CliUsageError(`OMGBASE_SPEC_MINTER=${JSON.stringify(minter)}: the only value is "sequential" (spec/surface §7.1)`, "unset it, or set OMGBASE_SPEC_MINTER=sequential for a conformance run");
33
- }
34
- setIdMinter(sequentialMinter());
20
+ function announceSpecSeams(cli) {
21
+ const seams = installSpecSeams();
22
+ if (seams.minter) {
35
23
  cli.io.err(cli.style.dim("[mcp] spec seam: sequential id minter (OMGBASE_SPEC_MINTER=sequential) — conformance run, not for production"));
36
24
  }
37
- const clock = process.env.OMGBASE_SPEC_CLOCK;
38
- if (clock !== undefined && clock !== "") {
39
- try {
40
- pinClock(clock);
41
- }
42
- catch {
43
- throw new CliUsageError(`OMGBASE_SPEC_CLOCK=${JSON.stringify(clock)}: not an RFC 3339 instant (spec/surface §7.1)`, "e.g. OMGBASE_SPEC_CLOCK=2026-09-27T00:00:00.000Z");
44
- }
45
- cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${new Date().toISOString()} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
25
+ if (seams.clock !== null) {
26
+ cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${seams.clock} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
46
27
  }
47
28
  }
48
29
  async function runMcp(cli, args) {
@@ -64,7 +45,7 @@ async function runMcp(cli, args) {
64
45
  notes: [`host config: {"command": "${cli.prog}", "args": ["mcp", "-C", "/path/to/notes"]}`],
65
46
  });
66
47
  }
67
- installSpecSeams(cli);
48
+ announceSpecSeams(cli);
68
49
  // The host launches us from its own cwd, so a missing workspace here almost
69
50
  // always means the host config lacks `-C <dir>` — say so, rather than the
70
51
  // generic "run init" hint (which would create an empty workspace in the wrong place).
@@ -127,10 +108,10 @@ async function runMcp(cli, args) {
127
108
  if (wantWatch) {
128
109
  lease = WatchLease.tryAcquire(ws.omgbaseDir);
129
110
  if (lease) {
130
- // Prime with a one-shot sweep so the session starts fresh, then watch via
131
- // the external fs-adapter process (chokidar lives there, not in-engine).
132
- if (repo.rootPath)
133
- freshnessSweep(ws.store, repo.repoId, repo.rootPath);
111
+ // spec/sync §5 order: open the source (the external fs-adapter process —
112
+ // chokidar lives there, not in-engine), `watch`, wait for `ready` (bounded),
113
+ // THEN the priming sweep, then report live. An edit landing before the feed
114
+ // is primed is caught by the sweep, one after it by the feed.
134
115
  source = await openRepoSource(ws.store, repo);
135
116
  if (source) {
136
117
  watcher = new Watcher(ws.store, repo.repoId, source, {
@@ -144,6 +125,11 @@ async function runMcp(cli, args) {
144
125
  onError: (err) => cli.io.err(cli.style.dim(`[watch] error: ${String(err)}`)),
145
126
  });
146
127
  await watcher.start();
128
+ if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
129
+ 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`));
130
+ }
131
+ if (repo.rootPath)
132
+ freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
147
133
  cli.io.err(cli.style.dim(`[mcp] serving ${repo.slug} on stdio · watcher live`));
148
134
  }
149
135
  else {
@@ -13,6 +13,20 @@ function block(cli, ws, repoId, ref) {
13
13
  throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
14
14
  return r.blockId;
15
15
  }
16
+ // `done` addresses task blocks only (spec/cli §6): any other live block type is
17
+ // a type_mismatch before an op is built — the macro itself would record a no-op
18
+ // `edited` commit (its `attrs.checked` only rewrites a task's marker).
19
+ function taskBlock(ws, ref, blockId) {
20
+ const row = ws.store.db
21
+ .prepare("SELECT type FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
22
+ .get(blockId);
23
+ if (!row)
24
+ throw new EngineErrorLike("block_missing", `not a block: ${ref}`);
25
+ if (row.type !== "task") {
26
+ throw new EngineErrorLike("type_mismatch", `not a task: ${ref} is a ${row.type}`, { data: { block: blockId, type: row.type } });
27
+ }
28
+ return blockId;
29
+ }
16
30
  function rawHashOf(ws, blockId) {
17
31
  const row = ws.store.db
18
32
  .prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
@@ -67,33 +81,36 @@ async function runInsert(cli, args) {
67
81
  const { values, positionals } = parseArgs({
68
82
  args: content.rest,
69
83
  allowPositionals: true,
70
- options: { at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
84
+ options: { at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
71
85
  });
72
86
  if (values.help) {
73
87
  return renderHelp(cli, {
74
88
  name: "insert",
75
89
  summary: "Insert markdown as new block(s) under a parent block or heading",
76
- usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
90
+ usage: "insert <to> (-m <markdown> | -f <file> | -) [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
77
91
  options: [
78
92
  ["<to>", "parent block id, or a heading block id to append into its section"],
79
93
  ["-m <markdown>", "content inline"],
80
94
  ["-f <file>", "content from a file"],
81
95
  ["-", "content from stdin"],
82
96
  ["--at <pos>", "end (default) | start | before <id> | after <id>"],
97
+ ["--expect <hash>", "destination CAS: fail with stale_expectation unless the parent's direct child ids (joined by \",\", sha256 hex) still hash to this"],
83
98
  ["--actor <s>", "commit actor (default human:$USER)"],
84
99
  ],
85
100
  });
86
101
  }
87
102
  const to = positionals[0];
88
103
  if (!to)
89
- throw new CliUsageError("insert requires a <to> parent (block id, or a heading id for section append)");
104
+ throw new CliUsageError("insert requires a <to> parent (a container block id, or a doc id/path for the top level)");
90
105
  const markdown = readContent(content);
106
+ // The destination-parent CAS (spec/mutate §1.2) rides as the op-level expect.
107
+ const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
91
108
  if (cli.flags.server)
92
- return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values) });
109
+ return runOpsRemote(cli, "blocks_insert", { to, markdown, at: parseAt(values), ...expect });
93
110
  const ws = cli.workspace();
94
111
  const repo = cli.repo(ws);
95
112
  const parent = block(cli, ws, repo.repoId, to);
96
- const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown };
113
+ const op = { op: "insert", to: { parent, at: parseAt(values) }, markdown, ...expect };
97
114
  return runOps(cli, ws, repo, [op], actorOf(values));
98
115
  }
99
116
  // ---- update -----------------------------------------------------------------
@@ -143,17 +160,18 @@ async function runMove(cli, args) {
143
160
  const { values, positionals } = parseArgs({
144
161
  args,
145
162
  allowPositionals: true,
146
- options: { to: { type: "string" }, at: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
163
+ options: { to: { type: "string" }, at: { type: "string" }, expect: { type: "string" }, actor: { type: "string" }, help: { type: "boolean" } },
147
164
  });
148
165
  if (values.help) {
149
166
  return renderHelp(cli, {
150
167
  name: "move",
151
168
  summary: "Move block(s) under a new parent, identity preserved",
152
- usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--actor <s>] [--dry-run]",
169
+ usage: "move <blocks…|-> --to <parent> [--at end|start|before <id>|after <id>] [--expect <parent_children_hash>] [--actor <s>] [--dry-run]",
153
170
  options: [
154
171
  ["<blocks…>", "block ids; `-` reads them from stdin, one per line"],
155
172
  ["--to <parent>", "destination parent block"],
156
173
  ["--at <pos>", "end (default) | start | before <id> | after <id>"],
174
+ ["--expect <hash>", "destination CAS: fail with stale_expectation unless the destination's direct child ids (joined by \",\", sha256 hex) still hash to this"],
157
175
  ["--actor <s>", "commit actor (default human:$USER)"],
158
176
  ],
159
177
  });
@@ -163,13 +181,15 @@ async function runMove(cli, args) {
163
181
  const refs = expandBlockArgs(positionals);
164
182
  if (refs.length === 0)
165
183
  throw new CliUsageError("move requires one or more blocks (or - for stdin)");
184
+ // The destination-parent CAS (spec/mutate §1.2), checked once for the run.
185
+ const expect = values.expect ? { expect: { parent_children_hash: values.expect } } : {};
166
186
  if (cli.flags.server)
167
- return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values) });
187
+ return runOpsRemote(cli, "blocks_move", { blocks: refs, to: values.to, at: parseAt(values), ...expect });
168
188
  const ws = cli.workspace();
169
189
  const repo = cli.repo(ws);
170
190
  const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
171
191
  const parent = block(cli, ws, repo.repoId, values.to);
172
- const op = { op: "move", blocks, to: { parent, at: parseAt(values) } };
192
+ const op = { op: "move", blocks, to: { parent, at: parseAt(values) }, ...expect };
173
193
  return runOps(cli, ws, repo, [op], actorOf(values));
174
194
  }
175
195
  // ---- rm (blocks; --doc handled by the doc-level command) --------------------
@@ -232,7 +252,7 @@ async function runDone(cli, args) {
232
252
  return runOpsRemote(cli, "tasks_complete", { blocks: refs, ...(values.undo ? { checked: false } : {}) });
233
253
  const ws = cli.workspace();
234
254
  const repo = cli.repo(ws);
235
- const blocks = refs.map((r) => block(cli, ws, repo.repoId, r));
255
+ const blocks = refs.map((r) => taskBlock(ws, r, block(cli, ws, repo.repoId, r)));
236
256
  const ops = values.undo
237
257
  ? blocks.map((b) => ({ op: "update", block: b, attrs: { checked: false }, expect: pinned(ws, b) }))
238
258
  : tasksComplete(ws.store, blocks);
@@ -295,7 +315,14 @@ async function runSplit(cli, args) {
295
315
  throw new CliUsageError("split requires a <block>");
296
316
  if (!values.at)
297
317
  throw new CliUsageError("split requires --at n[,n…]");
298
- const at = values.at.split(",").map((s) => Number(s.trim()));
318
+ // Offsets are integers (spec/mutate §2.5); anything else is a usage error
319
+ // here, not a NaN that reaches the kernel.
320
+ const at = values.at.split(",").map((s) => {
321
+ const n = Number(s.trim());
322
+ if (s.trim() === "" || !Number.isInteger(n))
323
+ throw new CliUsageError(`bad --at '${values.at}' (offsets must be integers: n[,n…])`);
324
+ return n;
325
+ });
299
326
  if (cli.flags.server)
300
327
  return runOpsRemote(cli, "blocks_split", { block: ref, at });
301
328
  const ws = cli.workspace();
@@ -28,8 +28,12 @@ async function runNode(cli, args) {
28
28
  });
29
29
  }
30
30
  if (sub === "set") {
31
- parseArgs({ args: rest, allowPositionals: true, options: { actor: { type: "string" } } });
32
- const [nodeId, prop, ...valueParts] = rest.filter((a) => !a.startsWith("--"));
31
+ // The value is every positional after <prop>, joined by a space; --actor
32
+ // (an option WITH a value) is taken out by the parser, never by a prefix
33
+ // filter — that filter used to leave the actor's value inside the property
34
+ // value (`checked "true human:spec"`), which the task editor read as false.
35
+ const { values, positionals } = parseArgs({ args: rest, allowPositionals: true, options: { actor: { type: "string" } } });
36
+ const [nodeId, prop, ...valueParts] = positionals;
33
37
  if (!nodeId || !prop || valueParts.length === 0)
34
38
  throw new CliUsageError("node set <nodeId> <prop> <value>");
35
39
  const value = valueParts.join(" ");
@@ -39,7 +43,7 @@ async function runNode(cli, args) {
39
43
  const ws = cli.workspace();
40
44
  const repo = cli.repo(ws);
41
45
  const ops = nodeSet(ws.store, nodeId, prop, value);
42
- return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}` });
46
+ return runOps(cli, ws, repo, ops, { reason: `node_set ${prop}`, ...(values.actor ? { actor: values.actor } : {}) });
43
47
  }
44
48
  if (sub === "props") {
45
49
  // `props` is a local editability lookup with no remote tool (a niche read).
@@ -1,2 +1,12 @@
1
+ import { type OqxResult } from "@omgbase/core";
1
2
  import type { Command } from "../commands.js";
3
+ import type { Cli } from "../context.js";
4
+ /**
5
+ * Render an `OqxResult` in the current mode (shared with `run`). `--json` is the
6
+ * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
7
+ * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
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`.
10
+ */
11
+ export declare function renderOqxResult(cli: Cli, result: OqxResult): number;
2
12
  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, EngineErrorLike, EXIT_OK, renderHelp, renderHits } from "../output.js";
4
+ import { truncationFooter, CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp, renderHits } from "../output.js";
5
5
  import { loadEmbedding } from "./_embed.js";
6
6
  import { remoteCall } from "./_remote.js";
7
7
  import { readStdin } from "./_mutate.js";
@@ -85,10 +85,8 @@ async function runOqx(cli, args) {
85
85
  const source = values.file
86
86
  ? (values.file === "-" ? readStdin() : readFileSync(values.file, "utf8"))
87
87
  : positionals.join(" ").trim();
88
- if (!source) {
89
- cli.io.err(cli.style.dim(" usage: query <source> (or -f file|-)"));
90
- return EXIT_OK;
91
- }
88
+ if (!source)
89
+ throw new CliUsageError("query requires a <source> (or -f file|-)");
92
90
  const opts = {};
93
91
  if (values.n)
94
92
  opts.limit = Number(values.n);
@@ -135,11 +133,24 @@ async function runOqx(cli, args) {
135
133
  : result.consumer === "exists" ? result.exists
136
134
  : result.consumer === "none" ? result.none
137
135
  : result.values ?? result);
138
- // JSON emits the whole result verbatim (incl. consumer + any scalar), so
139
- // count/exists round-trip without special-casing.
140
- if (cli.flags.mode === "json") {
141
- cli.io.out(JSON.stringify(result));
142
- return EXIT_OK;
136
+ return renderOqxResult(cli, result);
137
+ }
138
+ /**
139
+ * Render an `OqxResult` in the current mode (shared with `run`). `--json` is the
140
+ * whole result verbatim (incl. consumer + any scalar), so count/exists round-trip
141
+ * without special-casing; `--jsonl` streams the hits (or the values, as JSON);
142
+ * `--ids` the hit ids — a scalar result has neither, so those modes print the
143
+ * document. The truncation footer prints in every mode but `--json`.
144
+ */
145
+ export function renderOqxResult(cli, result) {
146
+ const cursor = result.truncated ? result.cursor ?? "" : null;
147
+ if (cli.flags.mode !== "human") {
148
+ const list = result.values ?? (result.consumer === "collect" || result.consumer === "first" || result.consumer === "single" ? result.hits : undefined);
149
+ return emitMachine(cli, result, {
150
+ ...(list ? { items: list } : {}),
151
+ ...(list && !result.values ? { ids: result.hits.map((h) => h.id) } : {}),
152
+ cursor,
153
+ });
143
154
  }
144
155
  // Scalar consumers (count/exists/none) have no hits — render the reduction itself.
145
156
  if (result.consumer === "count" || result.consumer === "exists" || result.consumer === "none") {
@@ -148,26 +159,12 @@ async function runOqx(cli, args) {
148
159
  return EXIT_OK;
149
160
  }
150
161
  // A `values` projection has no hits — one bare value per line (strings
151
- // verbatim, anything else as JSON; `--jsonl` is JSON throughout).
162
+ // verbatim, anything else as JSON).
152
163
  if (result.values) {
153
164
  for (const v of result.values)
154
- cli.io.out(typeof v === "string" && cli.flags.mode !== "jsonl" ? v : JSON.stringify(v));
155
- if (result.truncated)
156
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
157
- return EXIT_OK;
158
- }
159
- if (cli.flags.mode === "ids") {
160
- for (const h of result.hits)
161
- cli.io.out(h.id);
162
- if (result.truncated)
163
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
164
- return EXIT_OK;
165
- }
166
- if (cli.flags.mode === "jsonl") {
167
- for (const h of result.hits)
168
- cli.io.out(JSON.stringify(h));
169
- if (result.truncated)
170
- truncationFooter(cli.io, cli.style, result.cursor ?? "");
165
+ cli.io.out(typeof v === "string" ? v : JSON.stringify(v));
166
+ if (cursor !== null)
167
+ truncationFooter(cli.io, cli.style, cursor);
171
168
  return EXIT_OK;
172
169
  }
173
170
  const { style, io } = cli;
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { findDoc, docsOutline } from "@omgbase/core";
3
- import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, renderHelp } from "../output.js";
3
+ import { CliUsageError, EngineErrorLike, truncationFooter, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { remoteCall } from "./_remote.js";
5
5
  // `omg outline <doc|path>` (alias ol) — the wire format (06 §6). Human output
6
6
  // prints the outline text with full block ids inline; --json emits the
@@ -11,7 +11,6 @@ async function runOutline(cli, args) {
11
11
  allowPositionals: true,
12
12
  options: {
13
13
  depth: { type: "string" },
14
- section: { type: "string" },
15
14
  skeleton: { type: "boolean" },
16
15
  help: { type: "boolean" },
17
16
  },
@@ -20,10 +19,9 @@ async function runOutline(cli, args) {
20
19
  return renderHelp(cli, {
21
20
  name: "outline",
22
21
  summary: "A document's outline with block ids inline (frozen wire format) — the orientation view",
23
- usage: "outline <doc|path> [--depth <n>] [--section <locator>] [--skeleton]",
22
+ usage: "outline <doc|path> [--depth <n>] [--skeleton]",
24
23
  options: [
25
24
  ["--depth <n>", "limit heading depth"],
26
- ["--section <locator>", "outline only the section at a locator"],
27
25
  ["--skeleton", "structure only, no text"],
28
26
  ],
29
27
  });
@@ -57,10 +55,9 @@ async function runOutline(cli, args) {
57
55
  result = docsOutline(ws.store, info.docId, opts);
58
56
  header = info.path;
59
57
  }
60
- if (cli.flags.mode !== "human") {
61
- cli.io.out(JSON.stringify(result));
62
- return EXIT_OK;
63
- }
58
+ // `{ text, truncated }` carries neither a list nor ids: every machine mode is the document.
59
+ if (cli.flags.mode !== "human")
60
+ return emitMachine(cli, result, { cursor: result.truncated ? "budget" : null });
64
61
  const { render, style, io } = cli;
65
62
  io.out(render.wordmark(header));
66
63
  io.out(render.rule(40));
@@ -1,11 +1,12 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { linksRetarget } from "@omgbase/core";
3
3
  import { CliUsageError, EXIT_OK, renderHelp } from "../output.js";
4
- import { runOps } from "./_mutate.js";
4
+ import { runOps, unifiedDiffLines } from "./_mutate.js";
5
5
  import { remoteCall } from "./_remote.js";
6
6
  // `omg retarget <from> <to> [--scope glob] [--apply]` (11 §5.6) — plan-by-default.
7
- // Without --apply it runs the dry-run and prints the per-block diffs (the 06 §4
8
- // "always dry-run first" contract, encoded as the default). --apply commits.
7
+ // Without --apply it runs the dry-run and prints, per hit block, the unified
8
+ // diff of its raw (the same diff every other preview prints — the 06 §4 "always
9
+ // dry-run first" contract, encoded as the default). --apply commits.
9
10
  async function runRetarget(cli, args) {
10
11
  const { values, positionals } = parseArgs({
11
12
  args,
@@ -65,27 +66,11 @@ function renderPlan(cli, from, to, hits) {
65
66
  io.err(style.dim(` plan — ${hits.length} block(s) would change; re-run with --apply to commit`));
66
67
  for (const h of hits) {
67
68
  io.out(style.id(h.block));
68
- for (const line of lineDiff(h.oldRaw, h.newRaw)) {
69
- io.out(line.startsWith("+") ? style.ok(line) : line.startsWith("-") ? style.err(line) : style.dim(line));
70
- }
69
+ for (const line of unifiedDiffLines(cli, h.oldRaw, h.newRaw))
70
+ io.out(line);
71
71
  io.out("");
72
72
  }
73
73
  return EXIT_OK;
74
74
  }
75
- function lineDiff(before, after) {
76
- const a = before.split("\n");
77
- const b = after.split("\n");
78
- const out = [];
79
- const max = Math.max(a.length, b.length);
80
- for (let i = 0; i < max; i++) {
81
- if (a[i] === b[i])
82
- continue;
83
- if (a[i] !== undefined)
84
- out.push(`- ${a[i]}`);
85
- if (b[i] !== undefined)
86
- out.push(`+ ${b[i]}`);
87
- }
88
- return out;
89
- }
90
75
  export const cmdRetarget = { name: "retarget", summary: "Rewrite a link target (plan-by-default)", run: (c, a) => runRetarget(c, a) };
91
76
  //# sourceMappingURL=retarget.js.map