omgbase 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "66abf7c",
3
- "built": "2026-10-14T06:35:28.357Z"
2
+ "commit": "e2ab637",
3
+ "built": "2026-10-15T09:57:20.729Z"
4
4
  }
@@ -85,7 +85,7 @@ function runDoctor(cli, args) {
85
85
  usage: ["doctor [--json]", "doctor --size [--json]"],
86
86
  options: [
87
87
  ["--size", "instead of the checks: what the database is made of — bytes and rows per category (content, history, search, sync) and table, the file's pages, what VACUUM would reclaim, advice"],
88
- ["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, same_dispositions, advice }`"],
88
+ ["--json", "`{ ok, checks: [{ name, ok, detail }] }`; with --size `{ file, categories, same_dispositions, archived, advice }`"],
89
89
  ],
90
90
  });
91
91
  }
@@ -158,6 +158,8 @@ function runDoctorSize(cli, report) {
158
158
  ["size", `${humanBytes(f.bytes)} on disk, wal ${humanBytes(f.wal_bytes)}`],
159
159
  ["pages", `${f.page_count} x ${f.page_size} bytes, ${f.freelist_count} free`],
160
160
  ["reclaimable", humanBytes(f.reclaimable)],
161
+ // 15.0: the history that already moved to archive segments (spec/store §7 "Retention").
162
+ ["archived", `${report.archived.revisions} revisions in ${report.archived.segments} segments, ${humanBytes(report.archived.bytes)} on disk`],
161
163
  ];
162
164
  for (const line of columns(file))
163
165
  io.out(` ${line}`);
@@ -6,7 +6,7 @@ const GROUPS = [
6
6
  { title: "bootstrap", names: ["init", "source", "repos"] },
7
7
  { title: "orient & read", names: ["status", "ls", "outline", "cat", "show", "find"] },
8
8
  { title: "query", names: ["query", "run"] },
9
- { title: "history & links", names: ["log", "hist", "diff", "links"] },
9
+ { title: "history & links", names: ["log", "hist", "diff", "links", "history"] },
10
10
  { title: "mutate", names: ["apply", "execute", "insert", "update", "edit", "move", "rm", "done", "append", "retarget", "split", "merge", "node"] },
11
11
  { title: "documents", names: ["new", "mv", "meta"] },
12
12
  { title: "session", names: ["shell"] },
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../commands.js";
2
+ export declare const cmdHistory: Command;
@@ -0,0 +1,232 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { evaluateRetention, retentionPolicy, exportHistory, attachArchive, detachArchive, listArchives, humanBytes, withWriterLock, RetentionPolicyError, NotASegment, StaleSegment, ARCHIVE_DIR, } from "@omgbase/core";
5
+ import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
6
+ import { columns } from "../render.js";
7
+ // `omg history <plan|export|attach|detach|list>` (spec/cli §6 `history`, cli
8
+ // 4.6): the operator surface of history retention (spec/store §7 "Retention",
9
+ // 15.0). `plan` evaluates the policy and prints what would move; `export`
10
+ // writes one archive segment (and with `--prune` squashes the store); `attach`
11
+ // / `detach` / `list` manage the segment registry. No MCP tool: the catalog's
12
+ // only trace is the `revisions.archived` attribute and `revision_archived`.
13
+ const SUBCOMMANDS = ["plan", "export", "attach", "detach", "list"];
14
+ function renderCard(cli) {
15
+ return renderHelp(cli, {
16
+ name: "history",
17
+ summary: "History retention: plan what the policy would archive, export it to a segment, attach/detach/list segments",
18
+ usage: [
19
+ "history plan [--before <ts>] [--policy <json|@file>]",
20
+ "history export [--prune] [--dest <dir>] [--before <ts>] [--policy <json|@file>] [--dry-run]",
21
+ "history attach <file|dir>",
22
+ "history detach <segment_id|path>",
23
+ "history list",
24
+ ],
25
+ options: [
26
+ ["plan", "per document: revisions kept / to archive and the bytes only they reach; the policy in effect"],
27
+ ["export", "write ONE new segment file (default dest .omgbase/archives/) holding the archived revisions' bodies; --prune then squashes the store and records the segment"],
28
+ ["attach <path>", "record a segment file (or every segment of a directory that belongs to this repo) so reads at its revisions work again"],
29
+ ["detach <ref>", "forget a segment by id or path (its revisions stay listed; reading one is revision_archived)"],
30
+ ["list", "the repo's segments: commit range, revisions, bytes, state (attached, missing, pending, detached, stale)"],
31
+ ["--before <ts>", "only revisions committed before this RFC 3339 instant are candidates"],
32
+ ["--policy <p>", "a JSON object (or @file) merged over the effective history.retention settings for this run"],
33
+ ["--prune", "(export) after the segment is durable: NULL the archived bodies, drop their change rows, sweep, record"],
34
+ ["--dest <dir>", "(export) where the segment file goes"],
35
+ ["--dry-run", "(export) the plan, nothing written"],
36
+ ],
37
+ notes: [
38
+ "policy keys: keep_days, ladder [{ after_days, keep: \"one per day|week|month|year\" }], per_doc_max, keep_origins, overrides [{ glob, … }]",
39
+ `e.g. ${cli.prog} history export --prune --policy '{"keep_days":90}'`,
40
+ ],
41
+ });
42
+ }
43
+ function parsePolicy(raw) {
44
+ if (raw === undefined)
45
+ return undefined;
46
+ const text = raw.startsWith("@") ? readFileSync(raw.slice(1), "utf8") : raw;
47
+ try {
48
+ return JSON.parse(text);
49
+ }
50
+ catch {
51
+ throw new CliUsageError(`--policy: not JSON (${raw.startsWith("@") ? raw : "pass an object, or @file"})`);
52
+ }
53
+ }
54
+ function parseBefore(raw) {
55
+ if (raw === undefined)
56
+ return undefined;
57
+ const t = Date.parse(raw);
58
+ if (Number.isNaN(t))
59
+ throw new CliUsageError(`bad --before '${raw}' (use an RFC 3339 instant)`);
60
+ return new Date(t).toISOString();
61
+ }
62
+ /** The repo's effective policy with the run's `--policy` over it; a malformed policy is a usage error. */
63
+ function policyOf(ws, repoId, raw) {
64
+ try {
65
+ return retentionPolicy(ws.store, repoId, parsePolicy(raw));
66
+ }
67
+ catch (e) {
68
+ if (e instanceof RetentionPolicyError)
69
+ throw new CliUsageError(`--policy: ${e.message}`);
70
+ throw e;
71
+ }
72
+ }
73
+ function plural(n, one, many = `${one}s`) {
74
+ return `${n} ${n === 1 ? one : many}`;
75
+ }
76
+ /** The plan rendering (`history plan`, `history export --dry-run`). */
77
+ function renderPlan(cli, plan) {
78
+ if (cli.flags.mode !== "human")
79
+ return emitMachine(cli, plan, { items: plan.docs, ids: plan.docs.map((d) => d.id) });
80
+ const { io, style } = cli;
81
+ if (plan.docs.length > 0) {
82
+ const rows = [[style.dim("path"), style.dim("revisions"), style.dim("keep"), style.dim("archive"), style.dim("bytes")]];
83
+ for (const d of plan.docs)
84
+ rows.push([style.accent(d.path), String(d.revisions), String(d.keep.length), String(d.archive.length), humanBytes(d.bytes)]);
85
+ for (const line of columns(rows, [{}, { align: "right" }, { align: "right" }, { align: "right" }, { align: "right" }]))
86
+ io.out(` ${line}`);
87
+ }
88
+ const t = plan.totals;
89
+ if (t.archive === 0)
90
+ io.out(` nothing to archive: ${plural(t.revisions, "revision")}, all kept`);
91
+ else
92
+ io.out(` ${t.archive} of ${plural(t.revisions, "revision")} in ${plural(t.docs, "document")} would archive ${humanBytes(t.bytes)}`);
93
+ io.out(` ${style.dim("policy")} ${JSON.stringify(plan.policy)}`);
94
+ if (plan.before !== null)
95
+ io.out(` ${style.dim("before")} ${plan.before}`);
96
+ return EXIT_OK;
97
+ }
98
+ function commitRange(lo, hi) {
99
+ return lo === hi ? `commit #${lo}` : `commits #${lo}–#${hi}`;
100
+ }
101
+ function runPlan(cli, args) {
102
+ const { values } = parseArgs({ args, allowPositionals: false, options: { before: { type: "string" }, policy: { type: "string" } } });
103
+ const ws = cli.workspace();
104
+ const repo = cli.repo(ws);
105
+ const before = parseBefore(values.before);
106
+ const plan = evaluateRetention(ws.store, repo.repoId, { policy: policyOf(ws, repo.repoId, values.policy), ...(before !== undefined ? { before } : {}) });
107
+ return renderPlan(cli, plan);
108
+ }
109
+ function renderExport(cli, r) {
110
+ if (cli.flags.mode !== "human")
111
+ return emitMachine(cli, { segment: r.segment, reused: r.reused, squashed: r.squashed, gc: r.gc, totals: r.plan.totals });
112
+ const { io, style } = cli;
113
+ if (!r.segment) {
114
+ io.err(style.dim(" nothing to archive"));
115
+ return EXIT_OK;
116
+ }
117
+ const s = r.segment;
118
+ io.out(s.path);
119
+ io.err(style.dim(` ${r.reused ? `finished the unrecorded segment ${s.id}` : `wrote segment ${s.id}`}: ${plural(s.revision_count, "revision")} (${commitRange(s.seq_lo, s.seq_hi)}), ${humanBytes(s.bytes)}`));
120
+ if (r.squashed && r.gc) {
121
+ const q = r.squashed;
122
+ io.err(style.dim(` squashed ${plural(q.revisions, "revision")}, ${plural(q.commits, "commit")}, ${plural(q.dispositions, "disposition")}, ${plural(q.block_changes, "block change")}; swept ${plural(r.gc.blobs, "blob")}, ${plural(r.gc.tree_nodes, "tree node")}`));
123
+ }
124
+ return EXIT_OK;
125
+ }
126
+ function defaultDest(ws) {
127
+ return join(ws.omgbaseDir, ARCHIVE_DIR);
128
+ }
129
+ function runExport(cli, args) {
130
+ const { values } = parseArgs({
131
+ args,
132
+ allowPositionals: false,
133
+ options: { prune: { type: "boolean" }, dest: { type: "string" }, before: { type: "string" }, policy: { type: "string" }, "dry-run": { type: "boolean" } },
134
+ });
135
+ const ws = cli.workspace();
136
+ const repo = cli.repo(ws);
137
+ const before = parseBefore(values.before);
138
+ const policy = policyOf(ws, repo.repoId, values.policy);
139
+ const opts = { policy, ...(before !== undefined ? { before } : {}) };
140
+ if (values["dry-run"] || cli.flags.dryRun)
141
+ return renderPlan(cli, evaluateRetention(ws.store, repo.repoId, opts));
142
+ const dest = values.dest ? resolve(cli.cwd, values.dest) : defaultDest(ws);
143
+ const run = () => exportHistory(ws.store, repo.repoId, { ...opts, dest, prune: Boolean(values.prune) });
144
+ try {
145
+ // The squash is a write: under the workspace writer lock like every other.
146
+ const result = values.prune ? withWriterLock(ws.omgbaseDir, run) : run();
147
+ return renderExport(cli, result);
148
+ }
149
+ catch (e) {
150
+ if (e instanceof StaleSegment)
151
+ throw new EngineErrorLike("target_missing", e.message, { data: { path: e.path, rev: e.rev } });
152
+ throw e;
153
+ }
154
+ }
155
+ function runAttach(cli, args) {
156
+ const { positionals } = parseArgs({ args, allowPositionals: true, options: {} });
157
+ const target = positionals[0];
158
+ if (!target)
159
+ throw new CliUsageError("history attach requires a <file|dir>");
160
+ const ws = cli.workspace();
161
+ const repo = cli.repo(ws);
162
+ let result;
163
+ try {
164
+ result = withWriterLock(ws.omgbaseDir, () => attachArchive(ws.store, repo.repoId, resolve(cli.cwd, target)));
165
+ }
166
+ catch (e) {
167
+ if (e instanceof NotASegment)
168
+ throw new EngineErrorLike("target_missing", e.message, { data: { path: e.path } });
169
+ throw e;
170
+ }
171
+ if (cli.flags.mode !== "human")
172
+ return emitMachine(cli, result, { items: result.attached, ids: result.attached.map((a) => a.segment_id) });
173
+ const { io, style } = cli;
174
+ if (result.attached.length === 0)
175
+ io.err(style.dim(" nothing to attach"));
176
+ for (const a of result.attached)
177
+ io.err(style.dim(` attached ${a.segment_id}: ${plural(a.revision_count, "revision")} (${commitRange(a.seq_lo, a.seq_hi)}) ${a.path}`));
178
+ if (result.skipped > 0)
179
+ io.err(style.dim(` skipped ${plural(result.skipped, "file")} that ${result.skipped === 1 ? "is" : "are"} not this repository's segments`));
180
+ return EXIT_OK;
181
+ }
182
+ function runDetach(cli, args) {
183
+ const { positionals } = parseArgs({ args, allowPositionals: true, options: {} });
184
+ const ref = positionals[0];
185
+ if (!ref)
186
+ throw new CliUsageError("history detach requires a <segment_id|path>");
187
+ const ws = cli.workspace();
188
+ const repo = cli.repo(ws);
189
+ const detached = withWriterLock(ws.omgbaseDir, () => detachArchive(ws.store, repo.repoId, ref.includes("/") || ref.endsWith(".db") ? resolve(cli.cwd, ref) : ref));
190
+ if (detached === null)
191
+ throw new EngineErrorLike("target_missing", `no archive segment ${JSON.stringify(ref)}`, { data: { segment: ref } });
192
+ if (cli.flags.mode !== "human")
193
+ return emitMachine(cli, { detached });
194
+ cli.io.err(cli.style.dim(` detached ${detached}`));
195
+ return EXIT_OK;
196
+ }
197
+ function runList(cli, args) {
198
+ parseArgs({ args, allowPositionals: false, options: {} });
199
+ const ws = cli.workspace();
200
+ const repo = cli.repo(ws);
201
+ const segments = listArchives(ws.store, repo.repoId, { dest: defaultDest(ws) });
202
+ if (cli.flags.mode !== "human")
203
+ return emitMachine(cli, segments, { items: segments, ids: segments.map((s) => s.segment_id) });
204
+ const { io, style } = cli;
205
+ if (segments.length === 0) {
206
+ io.err(style.dim(" no segments"));
207
+ return EXIT_OK;
208
+ }
209
+ const rows = [[style.dim("segment"), style.dim("commits"), style.dim("revisions"), style.dim("bytes"), style.dim("state"), style.dim("path")]];
210
+ for (const s of segments)
211
+ rows.push([style.id(s.segment_id), s.seq_lo === s.seq_hi ? `#${s.seq_lo}` : `#${s.seq_lo}–#${s.seq_hi}`, String(s.revision_count), humanBytes(s.bytes), s.state, style.path(s.path)]);
212
+ for (const line of columns(rows, [{}, {}, { align: "right" }, { align: "right" }]))
213
+ io.out(` ${line}`);
214
+ return EXIT_OK;
215
+ }
216
+ function runHistory(cli, args) {
217
+ const [sub, ...rest] = args;
218
+ if (sub === undefined || sub === "--help" || sub === "-h" || sub === "help" || rest.includes("--help") || rest.includes("-h"))
219
+ return renderCard(cli);
220
+ if (!SUBCOMMANDS.includes(sub)) {
221
+ throw new CliUsageError(`unknown history subcommand '${sub}' (${SUBCOMMANDS.join("|")})`, `run '${cli.prog} history --help'`);
222
+ }
223
+ switch (sub) {
224
+ case "plan": return runPlan(cli, rest);
225
+ case "export": return runExport(cli, rest);
226
+ case "attach": return runAttach(cli, rest);
227
+ case "detach": return runDetach(cli, rest);
228
+ case "list": return runList(cli, rest);
229
+ }
230
+ }
231
+ export const cmdHistory = { name: "history", summary: "Retention: plan/export archive segments, attach/detach/list", run: (cli, a) => runHistory(cli, a) };
232
+ //# sourceMappingURL=history.js.map
@@ -11,6 +11,7 @@ import { cmdLog } from "./cmd/log.js";
11
11
  import { cmdHist } from "./cmd/hist.js";
12
12
  import { cmdDiff } from "./cmd/diff.js";
13
13
  import { cmdLinks } from "./cmd/links.js";
14
+ import { cmdHistory } from "./cmd/history.js";
14
15
  import { cmdSync } from "./cmd/sync.js";
15
16
  import { cmdMcp } from "./cmd/mcp.js";
16
17
  import { cmdApply, cmdExecute, cmdInsert, cmdMove, cmdRm, cmdDone, cmdAppend, cmdSplit, cmdMerge } from "./cmd/mutate.js";
@@ -39,6 +40,7 @@ export const COMMANDS = [
39
40
  cmdHist,
40
41
  cmdDiff,
41
42
  cmdLinks,
43
+ cmdHistory,
42
44
  cmdApply,
43
45
  cmdExecute,
44
46
  cmdInsert,
@@ -61,8 +61,8 @@ export const NO_WORKSPACE_OK = new Set(["init", "help", "version"]);
61
61
  // whose card is also their bare invocation (spec/cli §5): help is documentation,
62
62
  // not work, so these route to the card before any workspace is opened or swept —
63
63
  // exactly like the `--help` flag.
64
- export const HELP_WORD_OK = new Set(["config", "embed", "node", "source"]);
65
- export const BARE_IS_HELP = new Set(["node", "source"]);
64
+ export const HELP_WORD_OK = new Set(["config", "embed", "node", "source", "history"]);
65
+ export const BARE_IS_HELP = new Set(["node", "source", "history"]);
66
66
  /** Does this invocation ask for a command's help card? (the flag, the word, or a bare card-only command) */
67
67
  export function asksForHelp(flags, name, args) {
68
68
  if (flags.help)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omgbase",
3
- "version": "0.18.0",
3
+ "version": "0.20.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.21.0",
48
+ "@omgbase/core": "^0.23.0",
49
49
  "@omgbase/fs-adapter": "^0.2.0",
50
- "@omgbase/sync": "^0.5.14",
50
+ "@omgbase/sync": "^0.5.16",
51
51
  "yaml": "^2.9.0"
52
52
  },
53
53
  "devDependencies": {