@omgbase/core 0.1.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.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/cli/eval-matcher.d.ts +13 -0
  3. package/dist/src/cli/eval-matcher.js +36 -0
  4. package/dist/src/core/attach.d.ts +5 -0
  5. package/dist/src/core/attach.js +38 -0
  6. package/dist/src/core/cursor.d.ts +11 -0
  7. package/dist/src/core/cursor.js +35 -0
  8. package/dist/src/core/hash.d.ts +23 -0
  9. package/dist/src/core/hash.js +82 -0
  10. package/dist/src/core/ids.d.ts +6 -0
  11. package/dist/src/core/ids.js +26 -0
  12. package/dist/src/core/index.d.ts +1 -0
  13. package/dist/src/core/index.js +2 -0
  14. package/dist/src/core/ingest.d.ts +43 -0
  15. package/dist/src/core/ingest.js +287 -0
  16. package/dist/src/core/order-key.d.ts +8 -0
  17. package/dist/src/core/order-key.js +72 -0
  18. package/dist/src/core/parse/parse.d.ts +3 -0
  19. package/dist/src/core/parse/parse.js +104 -0
  20. package/dist/src/core/parse/render.d.ts +3 -0
  21. package/dist/src/core/parse/render.js +20 -0
  22. package/dist/src/core/parse/tree.d.ts +8 -0
  23. package/dist/src/core/parse/tree.js +43 -0
  24. package/dist/src/core/parse/types.d.ts +52 -0
  25. package/dist/src/core/parse/types.js +9 -0
  26. package/dist/src/core/read/document.d.ts +107 -0
  27. package/dist/src/core/read/document.js +191 -0
  28. package/dist/src/core/read/nodes.d.ts +47 -0
  29. package/dist/src/core/read/nodes.js +102 -0
  30. package/dist/src/core/read/outline.d.ts +12 -0
  31. package/dist/src/core/read/outline.js +76 -0
  32. package/dist/src/core/read/reader.d.ts +125 -0
  33. package/dist/src/core/read/reader.js +206 -0
  34. package/dist/src/core/read/refs.d.ts +15 -0
  35. package/dist/src/core/read/refs.js +51 -0
  36. package/dist/src/core/store/edges.d.ts +26 -0
  37. package/dist/src/core/store/edges.js +101 -0
  38. package/dist/src/core/store/fts.d.ts +10 -0
  39. package/dist/src/core/store/fts.js +22 -0
  40. package/dist/src/core/store/gc.d.ts +18 -0
  41. package/dist/src/core/store/gc.js +69 -0
  42. package/dist/src/core/store/nodes.d.ts +5 -0
  43. package/dist/src/core/store/nodes.js +57 -0
  44. package/dist/src/core/store/properties.d.ts +38 -0
  45. package/dist/src/core/store/properties.js +177 -0
  46. package/dist/src/core/store/rebuild.d.ts +3 -0
  47. package/dist/src/core/store/rebuild.js +29 -0
  48. package/dist/src/core/store/schema.d.ts +9 -0
  49. package/dist/src/core/store/schema.js +386 -0
  50. package/dist/src/core/store/sections.d.ts +2 -0
  51. package/dist/src/core/store/sections.js +29 -0
  52. package/dist/src/core/store/store.d.ts +24 -0
  53. package/dist/src/core/store/store.js +164 -0
  54. package/dist/src/core/store/writers.d.ts +52 -0
  55. package/dist/src/core/store/writers.js +89 -0
  56. package/dist/src/core/vec.d.ts +10 -0
  57. package/dist/src/core/vec.js +33 -0
  58. package/dist/src/format/adapter.d.ts +97 -0
  59. package/dist/src/format/adapter.js +14 -0
  60. package/dist/src/format/index.d.ts +6 -0
  61. package/dist/src/format/index.js +15 -0
  62. package/dist/src/format/json.d.ts +3 -0
  63. package/dist/src/format/json.js +324 -0
  64. package/dist/src/format/markdown.d.ts +5 -0
  65. package/dist/src/format/markdown.js +217 -0
  66. package/dist/src/format/registry.d.ts +6 -0
  67. package/dist/src/format/registry.js +24 -0
  68. package/dist/src/format/yaml.d.ts +5 -0
  69. package/dist/src/format/yaml.js +413 -0
  70. package/dist/src/graph/extract.d.ts +38 -0
  71. package/dist/src/graph/extract.js +242 -0
  72. package/dist/src/graph/history.d.ts +103 -0
  73. package/dist/src/graph/history.js +210 -0
  74. package/dist/src/graph/inbound-links.d.ts +33 -0
  75. package/dist/src/graph/inbound-links.js +130 -0
  76. package/dist/src/graph/link-destinations.d.ts +22 -0
  77. package/dist/src/graph/link-destinations.js +84 -0
  78. package/dist/src/graph/link-health.d.ts +79 -0
  79. package/dist/src/graph/link-health.js +146 -0
  80. package/dist/src/graph/links.d.ts +23 -0
  81. package/dist/src/graph/links.js +42 -0
  82. package/dist/src/index.d.ts +60 -0
  83. package/dist/src/index.js +70 -0
  84. package/dist/src/mcp/errors.d.ts +17 -0
  85. package/dist/src/mcp/errors.js +18 -0
  86. package/dist/src/mcp/graph.d.ts +63 -0
  87. package/dist/src/mcp/graph.js +212 -0
  88. package/dist/src/mcp/reference.d.ts +1 -0
  89. package/dist/src/mcp/reference.js +227 -0
  90. package/dist/src/mcp/server.d.ts +30 -0
  91. package/dist/src/mcp/server.js +1076 -0
  92. package/dist/src/mcp/stdio.d.ts +13 -0
  93. package/dist/src/mcp/stdio.js +24 -0
  94. package/dist/src/migrate/mrplex.d.ts +27 -0
  95. package/dist/src/migrate/mrplex.js +29 -0
  96. package/dist/src/mutate/apply.d.ts +83 -0
  97. package/dist/src/mutate/apply.js +302 -0
  98. package/dist/src/mutate/doc-store.d.ts +49 -0
  99. package/dist/src/mutate/doc-store.js +71 -0
  100. package/dist/src/mutate/docs.d.ts +60 -0
  101. package/dist/src/mutate/docs.js +232 -0
  102. package/dist/src/mutate/known-ids.d.ts +12 -0
  103. package/dist/src/mutate/known-ids.js +154 -0
  104. package/dist/src/mutate/load.d.ts +3 -0
  105. package/dist/src/mutate/load.js +66 -0
  106. package/dist/src/mutate/lower.d.ts +32 -0
  107. package/dist/src/mutate/lower.js +232 -0
  108. package/dist/src/mutate/macros.d.ts +91 -0
  109. package/dist/src/mutate/macros.js +230 -0
  110. package/dist/src/mutate/ops.d.ts +40 -0
  111. package/dist/src/mutate/ops.js +513 -0
  112. package/dist/src/mutate/opset.d.ts +92 -0
  113. package/dist/src/mutate/opset.js +79 -0
  114. package/dist/src/mutate/plan-update.d.ts +51 -0
  115. package/dist/src/mutate/plan-update.js +187 -0
  116. package/dist/src/mutate/tree.d.ts +46 -0
  117. package/dist/src/mutate/tree.js +167 -0
  118. package/dist/src/oqx/run.d.ts +1 -0
  119. package/dist/src/oqx/run.js +6 -0
  120. package/dist/src/oqx-js/context.d.ts +23 -0
  121. package/dist/src/oqx-js/context.js +564 -0
  122. package/dist/src/oqx-js/planner.d.ts +10 -0
  123. package/dist/src/oqx-js/planner.js +91 -0
  124. package/dist/src/oqx-js/run.d.ts +41 -0
  125. package/dist/src/oqx-js/run.js +332 -0
  126. package/dist/src/oqx-js/sql/translate.d.ts +27 -0
  127. package/dist/src/oqx-js/sql/translate.js +290 -0
  128. package/dist/src/reconcile/crossdoc.d.ts +34 -0
  129. package/dist/src/reconcile/crossdoc.js +76 -0
  130. package/dist/src/reconcile/eval/generator.d.ts +21 -0
  131. package/dist/src/reconcile/eval/generator.js +129 -0
  132. package/dist/src/reconcile/eval/metrics.d.ts +32 -0
  133. package/dist/src/reconcile/eval/metrics.js +74 -0
  134. package/dist/src/reconcile/flatten.d.ts +12 -0
  135. package/dist/src/reconcile/flatten.js +37 -0
  136. package/dist/src/reconcile/phase5.d.ts +8 -0
  137. package/dist/src/reconcile/phase5.js +167 -0
  138. package/dist/src/reconcile/phase6a.d.ts +2 -0
  139. package/dist/src/reconcile/phase6a.js +159 -0
  140. package/dist/src/reconcile/phases.d.ts +28 -0
  141. package/dist/src/reconcile/phases.js +0 -0
  142. package/dist/src/reconcile/reconcile.d.ts +21 -0
  143. package/dist/src/reconcile/reconcile.js +115 -0
  144. package/dist/src/reconcile/similarity.d.ts +7 -0
  145. package/dist/src/reconcile/similarity.js +41 -0
  146. package/dist/src/reconcile/types.d.ts +46 -0
  147. package/dist/src/reconcile/types.js +19 -0
  148. package/dist/src/search/cel/ast.d.ts +74 -0
  149. package/dist/src/search/cel/ast.js +3 -0
  150. package/dist/src/search/cel/compile.d.ts +60 -0
  151. package/dist/src/search/cel/compile.js +836 -0
  152. package/dist/src/search/cel/lexer.d.ts +9 -0
  153. package/dist/src/search/cel/lexer.js +139 -0
  154. package/dist/src/search/cel/parser.d.ts +7 -0
  155. package/dist/src/search/cel/parser.js +219 -0
  156. package/dist/src/search/drain.d.ts +39 -0
  157. package/dist/src/search/drain.js +97 -0
  158. package/dist/src/search/embeddings.d.ts +131 -0
  159. package/dist/src/search/embeddings.js +263 -0
  160. package/dist/src/search/external.d.ts +13 -0
  161. package/dist/src/search/external.js +177 -0
  162. package/dist/src/search/fts-query.d.ts +1 -0
  163. package/dist/src/search/fts-query.js +64 -0
  164. package/dist/src/search/provider.d.ts +17 -0
  165. package/dist/src/search/provider.js +13 -0
  166. package/dist/src/search/resolve.d.ts +18 -0
  167. package/dist/src/search/resolve.js +23 -0
  168. package/dist/src/search/rrf.d.ts +36 -0
  169. package/dist/src/search/rrf.js +84 -0
  170. package/dist/src/search/tasks.d.ts +12 -0
  171. package/dist/src/search/tasks.js +131 -0
  172. package/dist/src/search/text.d.ts +16 -0
  173. package/dist/src/search/text.js +21 -0
  174. package/dist/src/search/vector.d.ts +18 -0
  175. package/dist/src/search/vector.js +49 -0
  176. package/dist/src/sync/admin.d.ts +59 -0
  177. package/dist/src/sync/admin.js +53 -0
  178. package/dist/src/sync/attach.d.ts +13 -0
  179. package/dist/src/sync/attach.js +29 -0
  180. package/dist/src/sync/checkpoint.d.ts +21 -0
  181. package/dist/src/sync/checkpoint.js +52 -0
  182. package/dist/src/sync/driver.d.ts +25 -0
  183. package/dist/src/sync/driver.js +80 -0
  184. package/dist/src/sync/external-source.d.ts +14 -0
  185. package/dist/src/sync/external-source.js +137 -0
  186. package/dist/src/sync/freshness.d.ts +42 -0
  187. package/dist/src/sync/freshness.js +152 -0
  188. package/dist/src/sync/fs-util.d.ts +12 -0
  189. package/dist/src/sync/fs-util.js +55 -0
  190. package/dist/src/sync/git-heuristics.d.ts +6 -0
  191. package/dist/src/sync/git-heuristics.js +16 -0
  192. package/dist/src/sync/observe.d.ts +63 -0
  193. package/dist/src/sync/observe.js +84 -0
  194. package/dist/src/sync/plugin.d.ts +54 -0
  195. package/dist/src/sync/plugin.js +12 -0
  196. package/dist/src/sync/reconciling-ingest.d.ts +12 -0
  197. package/dist/src/sync/reconciling-ingest.js +209 -0
  198. package/dist/src/sync/recovery.d.ts +6 -0
  199. package/dist/src/sync/recovery.js +34 -0
  200. package/dist/src/sync/settings.d.ts +18 -0
  201. package/dist/src/sync/settings.js +58 -0
  202. package/dist/src/sync/sources.d.ts +37 -0
  203. package/dist/src/sync/sources.js +75 -0
  204. package/dist/src/sync/tombstone.d.ts +9 -0
  205. package/dist/src/sync/tombstone.js +55 -0
  206. package/dist/src/sync/watch-lease.d.ts +14 -0
  207. package/dist/src/sync/watch-lease.js +91 -0
  208. package/dist/src/sync/watcher.d.ts +19 -0
  209. package/dist/src/sync/watcher.js +34 -0
  210. package/dist/src/sync/workspace.d.ts +34 -0
  211. package/dist/src/sync/workspace.js +120 -0
  212. package/dist/src/sync/writer-lock.d.ts +16 -0
  213. package/dist/src/sync/writer-lock.js +121 -0
  214. package/package.json +80 -0
@@ -0,0 +1,91 @@
1
+ import { openSync, closeSync, writeFileSync, readFileSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ // Watch lease (11 §3.4). `omg watch` / `omg mcp`'s in-process watcher holds an
4
+ // advisory lock on <workspace>/.omgbase/watch.lock for its lifetime. Liveness is
5
+ // the lock itself: try-acquire non-blocking; acquirable ⇒ no live watcher. No
6
+ // heartbeats, no PID files beyond this record, no stale-lease sweeper. Commands
7
+ // probe it to skip the freshness sweep; `omg status` reports live/none.
8
+ //
9
+ // Implemented with the same O_EXCL + pid-liveness substitution as the writer
10
+ // lock (Node exposes no flock; no new deps — 11 §8). Held open until release().
11
+ const LEASE_NAME = "watch.lock";
12
+ function pidAlive(pid) {
13
+ try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ }
17
+ catch (err) {
18
+ return err.code === "EPERM";
19
+ }
20
+ }
21
+ function readHolderPid(leasePath) {
22
+ try {
23
+ return JSON.parse(readFileSync(leasePath, "utf8")).pid ?? null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /** True if a live watcher currently holds the lease. */
30
+ export function watchLeaseLive(omgbaseDir) {
31
+ const leasePath = join(omgbaseDir, LEASE_NAME);
32
+ if (!existsSync(leasePath))
33
+ return false;
34
+ const pid = readHolderPid(leasePath);
35
+ return pid != null && pidAlive(pid);
36
+ }
37
+ export class WatchLease {
38
+ held = false;
39
+ leasePath;
40
+ constructor(omgbaseDir) {
41
+ this.leasePath = join(omgbaseDir, LEASE_NAME);
42
+ }
43
+ /**
44
+ * Try to take the lease. Returns a held lease, or null if a live watcher
45
+ * already holds it. A lease whose holder pid is dead is stolen.
46
+ */
47
+ static tryAcquire(omgbaseDir) {
48
+ mkdirSync(omgbaseDir, { recursive: true });
49
+ const lease = new WatchLease(omgbaseDir);
50
+ if (lease.take())
51
+ return lease;
52
+ return null;
53
+ }
54
+ take() {
55
+ try {
56
+ const fd = openSync(this.leasePath, "wx");
57
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
58
+ closeSync(fd);
59
+ this.held = true;
60
+ return true;
61
+ }
62
+ catch (err) {
63
+ if (err.code !== "EEXIST")
64
+ throw err;
65
+ const pid = readHolderPid(this.leasePath);
66
+ if (pid == null || !pidAlive(pid)) {
67
+ try {
68
+ unlinkSync(this.leasePath);
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ return this.take();
74
+ }
75
+ return false;
76
+ }
77
+ }
78
+ release() {
79
+ if (!this.held)
80
+ return;
81
+ this.held = false;
82
+ try {
83
+ if (existsSync(this.leasePath))
84
+ unlinkSync(this.leasePath);
85
+ }
86
+ catch {
87
+ /* best-effort */
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=watch-lease.js.map
@@ -0,0 +1,19 @@
1
+ import type { Store } from "../core/store/store.js";
2
+ import { type CheckpointResult } from "./driver.js";
3
+ import type { SyncSource } from "./plugin.js";
4
+ export interface WatcherOptions {
5
+ onCheckpoint?: (result: CheckpointResult) => void;
6
+ /** Surface a reconcile error from a background batch (the watch stays live). */
7
+ onError?: (err: unknown) => void;
8
+ }
9
+ export declare class Watcher {
10
+ private store;
11
+ private repoId;
12
+ private source;
13
+ private opts;
14
+ private sub;
15
+ constructor(store: Store, repoId: string, source: SyncSource, opts?: WatcherOptions);
16
+ start(): Promise<void>;
17
+ private ingest;
18
+ stop(): Promise<void>;
19
+ }
@@ -0,0 +1,34 @@
1
+ import { reconcileChanges } from "./driver.js";
2
+ export class Watcher {
3
+ store;
4
+ repoId;
5
+ source;
6
+ opts;
7
+ sub = null;
8
+ constructor(store, repoId, source, opts = {}) {
9
+ this.store = store;
10
+ this.repoId = repoId;
11
+ this.source = source;
12
+ this.opts = opts;
13
+ }
14
+ async start() {
15
+ if (!this.source.watch)
16
+ throw new Error("source does not support watch()");
17
+ this.sub = await this.source.watch((paths) => {
18
+ // A batch arrives from the adapter's stream; reconcile it off the callback.
19
+ void this.ingest(paths).catch((err) => this.opts.onError?.(err));
20
+ });
21
+ }
22
+ async ingest(paths) {
23
+ if (paths.length === 0)
24
+ return null;
25
+ const result = await reconcileChanges(this.store, this.repoId, this.source, paths.map((path) => ({ path })));
26
+ this.opts.onCheckpoint?.(result);
27
+ return result;
28
+ }
29
+ async stop() {
30
+ await this.sub?.stop();
31
+ this.sub = null;
32
+ }
33
+ }
34
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1,34 @@
1
+ import { Store } from "../core/store/store.js";
2
+ export interface RepoRow {
3
+ repoId: string;
4
+ slug: string;
5
+ /** The repo's filesystem root, DERIVED from its attached `fs` source's
6
+ * `config.root` (ADR-014; the `repos.root_path` column was removed in v13).
7
+ * null for a sourceless/headless repo (its `sync`/`watch` are no-ops). */
8
+ rootPath: string | null;
9
+ }
10
+ export declare class Workspace {
11
+ readonly root: string;
12
+ readonly omgbaseDir: string;
13
+ readonly dbPath: string;
14
+ readonly store: Store;
15
+ private constructor();
16
+ /** Find the workspace containing `startDir` (walk up); null if none. */
17
+ static find(startDir?: string): Workspace | null;
18
+ /** Open the workspace rooted exactly at `root` (creating .omgbase/ + db). */
19
+ static open(root: string): Workspace;
20
+ repos(): RepoRow[];
21
+ repoBySlug(slug: string): RepoRow | null;
22
+ /**
23
+ * Select the active repo. With an explicit slug, that repo (or throws
24
+ * repo_not_found). Otherwise the repo whose root_path contains `cwd`; if none
25
+ * or several match, throws AmbiguousRepo with the candidate slugs.
26
+ */
27
+ selectRepo(cwd: string, slug?: string): RepoRow;
28
+ close(): void;
29
+ }
30
+ export declare class RepoSelectionError extends Error {
31
+ readonly code: "repo_not_found";
32
+ readonly candidates: string[];
33
+ constructor(code: "repo_not_found", message: string, candidates: string[]);
34
+ }
@@ -0,0 +1,120 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { dirname, join, resolve, relative, isAbsolute } from "node:path";
3
+ import { Store } from "../core/store/store.js";
4
+ /** Extract an fs source's root from its stored `config` JSON, or null. */
5
+ function fsRootFromConfig(config) {
6
+ if (!config)
7
+ return null;
8
+ try {
9
+ const root = JSON.parse(config).root;
10
+ return typeof root === "string" && root !== "" ? root : null;
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export class Workspace {
17
+ root;
18
+ omgbaseDir;
19
+ dbPath;
20
+ store;
21
+ constructor(root) {
22
+ this.root = root;
23
+ this.omgbaseDir = join(root, ".omgbase");
24
+ this.dbPath = join(this.omgbaseDir, "omgbase.db");
25
+ this.store = new Store({ path: this.dbPath });
26
+ }
27
+ /** Find the workspace containing `startDir` (walk up); null if none. */
28
+ static find(startDir = process.cwd()) {
29
+ let dir = resolve(startDir);
30
+ for (;;) {
31
+ const candidate = join(dir, ".omgbase");
32
+ if (existsSync(candidate) && statSync(candidate).isDirectory()) {
33
+ return new Workspace(dir);
34
+ }
35
+ const parent = dirname(dir);
36
+ if (parent === dir)
37
+ return null;
38
+ dir = parent;
39
+ }
40
+ }
41
+ /** Open the workspace rooted exactly at `root` (creating .omgbase/ + db). */
42
+ static open(root) {
43
+ return new Workspace(resolve(root));
44
+ }
45
+ repos() {
46
+ // rootPath is derived from the repo's attached `fs` source (ADR-014). A repo
47
+ // may have several attachments, so the LEFT JOIN can yield multiple rows per
48
+ // repo; dedup by repo and take the first fs source's root.
49
+ const rows = this.store.db
50
+ .prepare(`SELECT r.repo_id AS repo_id, r.slug AS slug, s.config AS fs_config
51
+ FROM repos r
52
+ LEFT JOIN attachments a ON a.repo_id = r.repo_id
53
+ LEFT JOIN sources s ON s.source_id = a.source_id AND s.adapter = 'fs'
54
+ ORDER BY r.slug`)
55
+ .all();
56
+ const byId = new Map();
57
+ for (const r of rows) {
58
+ const existing = byId.get(r.repo_id);
59
+ if (existing) {
60
+ if (!existing.rootPath)
61
+ existing.rootPath = fsRootFromConfig(r.fs_config);
62
+ }
63
+ else {
64
+ byId.set(r.repo_id, { repoId: r.repo_id, slug: r.slug, rootPath: fsRootFromConfig(r.fs_config) });
65
+ }
66
+ }
67
+ return [...byId.values()];
68
+ }
69
+ repoBySlug(slug) {
70
+ return this.repos().find((r) => r.slug === slug) ?? null;
71
+ }
72
+ /**
73
+ * Select the active repo. With an explicit slug, that repo (or throws
74
+ * repo_not_found). Otherwise the repo whose root_path contains `cwd`; if none
75
+ * or several match, throws AmbiguousRepo with the candidate slugs.
76
+ */
77
+ selectRepo(cwd, slug) {
78
+ const repos = this.repos();
79
+ if (slug) {
80
+ const found = repos.find((r) => r.slug === slug);
81
+ if (!found) {
82
+ throw new RepoSelectionError("repo_not_found", `no repo with slug '${slug}'`, repos.map((r) => r.slug));
83
+ }
84
+ return found;
85
+ }
86
+ if (repos.length === 1)
87
+ return repos[0];
88
+ const here = resolve(cwd);
89
+ // Only filesystem-backed repos (a derived rootPath) can contain the cwd; a
90
+ // sourceless/headless repo has no location, so it never matches by cwd.
91
+ const containing = repos.filter((r) => {
92
+ if (r.rootPath == null)
93
+ return false;
94
+ const rel = relative(resolve(r.rootPath), here);
95
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
96
+ });
97
+ if (containing.length === 1)
98
+ return containing[0];
99
+ if (containing.length === 0) {
100
+ throw new RepoSelectionError("repo_not_found", `no repo contains ${here}; select one with --repo`, repos.map((r) => r.slug));
101
+ }
102
+ // Several contain cwd (nested roots): pick the deepest (longest root path).
103
+ containing.sort((a, b) => resolve(b.rootPath).length - resolve(a.rootPath).length);
104
+ return containing[0];
105
+ }
106
+ close() {
107
+ this.store.close();
108
+ }
109
+ }
110
+ export class RepoSelectionError extends Error {
111
+ code;
112
+ candidates;
113
+ constructor(code, message, candidates) {
114
+ super(message);
115
+ this.code = code;
116
+ this.candidates = candidates;
117
+ this.name = "RepoSelectionError";
118
+ }
119
+ }
120
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1,16 @@
1
+ export interface WriterLockOptions {
2
+ /** Max time to wait for the lock before throwing (ms). Default 5000. */
3
+ timeoutMs?: number;
4
+ /** Poll interval while waiting (ms). Default 25. */
5
+ pollMs?: number;
6
+ }
7
+ export declare class WriterLockTimeout extends Error {
8
+ constructor(lockPath: string, holderPid: number | null);
9
+ }
10
+ /**
11
+ * Run fn while holding the workspace writer lock. Blocks (polling) until the
12
+ * lock is free or the timeout elapses, then releases the lock even if fn throws.
13
+ */
14
+ export declare function withWriterLock<T>(omgbaseDir: string, fn: () => T, opts?: WriterLockOptions): T;
15
+ /** True if no live writer holds the lock (probe without acquiring). */
16
+ export declare function writerLockFree(omgbaseDir: string): boolean;
@@ -0,0 +1,121 @@
1
+ import { openSync, closeSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ // Cross-process writer lock (11 §3.2). The write protocol's per-repo writer
4
+ // mutex (04 §6 step 1) generalizes to an advisory lock on
5
+ // <workspace>/.omgbase/writer.lock, held for steps 2–7. All writers — one-shot
6
+ // CLI mutations, watcher checkpoint ingests, `omg mcp` applies — acquire it.
7
+ //
8
+ // Node core exposes no flock(2), and the design forbids new runtime deps (11
9
+ // §8). We implement the same advisory contract with an O_EXCL lockfile: exclusive
10
+ // creation is atomic on local filesystems, the holder writes its pid for
11
+ // liveness, and a lock whose pid is dead is stolen (no daemon, no lease sweeper —
12
+ // the same "probe by trying" spirit as the watch lease in §3.4). This is an
13
+ // as-built substitution for flock; the file-CAS + ingest-and-replay behavior in
14
+ // the write protocol is unchanged.
15
+ const LOCK_NAME = "writer.lock";
16
+ const STALE_MS = 30_000;
17
+ function pidAlive(pid) {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ }
22
+ catch (err) {
23
+ // ESRCH = no such process; EPERM = alive but not ours (still alive).
24
+ return err.code === "EPERM";
25
+ }
26
+ }
27
+ export class WriterLockTimeout extends Error {
28
+ constructor(lockPath, holderPid) {
29
+ super(`could not acquire writer lock ${lockPath} (held by pid ${holderPid ?? "?"})`);
30
+ this.name = "WriterLockTimeout";
31
+ }
32
+ }
33
+ function tryAcquire(lockPath) {
34
+ try {
35
+ const fd = openSync(lockPath, "wx"); // O_CREAT | O_EXCL
36
+ writeFileSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
37
+ closeSync(fd);
38
+ return true;
39
+ }
40
+ catch (err) {
41
+ if (err.code !== "EEXIST")
42
+ throw err;
43
+ // Lock exists: steal it if the holder is dead or the record is stale.
44
+ let holderPid = null;
45
+ let ts = 0;
46
+ try {
47
+ const rec = JSON.parse(readFileSync(lockPath, "utf8"));
48
+ holderPid = rec.pid ?? null;
49
+ ts = rec.ts ?? 0;
50
+ }
51
+ catch {
52
+ // Unreadable/partial lock record — treat as stale.
53
+ }
54
+ const dead = holderPid != null && !pidAlive(holderPid);
55
+ const stale = Date.now() - ts > STALE_MS;
56
+ if (dead || stale) {
57
+ try {
58
+ unlinkSync(lockPath);
59
+ }
60
+ catch {
61
+ /* someone else stole it first; fall through to retry */
62
+ }
63
+ return tryAcquire(lockPath);
64
+ }
65
+ return false;
66
+ }
67
+ }
68
+ function readHolderPid(lockPath) {
69
+ try {
70
+ return JSON.parse(readFileSync(lockPath, "utf8")).pid ?? null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ // Busy-wait with a blocking sleep between polls. Writers hold the lock only for
77
+ // the brief file-CAS + ingest window, so contention waits are short; a sync
78
+ // spin keeps the lock API synchronous like the rest of the write path.
79
+ function sleepSync(ms) {
80
+ const until = Date.now() + ms;
81
+ while (Date.now() < until) {
82
+ /* spin */
83
+ }
84
+ }
85
+ /**
86
+ * Run fn while holding the workspace writer lock. Blocks (polling) until the
87
+ * lock is free or the timeout elapses, then releases the lock even if fn throws.
88
+ */
89
+ export function withWriterLock(omgbaseDir, fn, opts = {}) {
90
+ const timeoutMs = opts.timeoutMs ?? 5000;
91
+ const pollMs = opts.pollMs ?? 25;
92
+ mkdirSync(omgbaseDir, { recursive: true });
93
+ const lockPath = join(omgbaseDir, LOCK_NAME);
94
+ const deadline = Date.now() + timeoutMs;
95
+ while (!tryAcquire(lockPath)) {
96
+ if (Date.now() >= deadline)
97
+ throw new WriterLockTimeout(lockPath, readHolderPid(lockPath));
98
+ sleepSync(pollMs);
99
+ }
100
+ try {
101
+ return fn();
102
+ }
103
+ finally {
104
+ try {
105
+ if (existsSync(lockPath))
106
+ unlinkSync(lockPath);
107
+ }
108
+ catch {
109
+ /* best-effort release */
110
+ }
111
+ }
112
+ }
113
+ /** True if no live writer holds the lock (probe without acquiring). */
114
+ export function writerLockFree(omgbaseDir) {
115
+ const lockPath = join(omgbaseDir, LOCK_NAME);
116
+ if (!existsSync(lockPath))
117
+ return true;
118
+ const pid = readHolderPid(lockPath);
119
+ return pid == null || !pidAlive(pid);
120
+ }
121
+ //# sourceMappingURL=writer-lock.js.map
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@omgbase/core",
3
+ "version": "0.1.0",
4
+ "description": "Open Markdown Graph Base — embedded engine",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/omgbase/omgbase.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/omgbase/omgbase#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/omgbase/omgbase/issues"
14
+ },
15
+ "keywords": [
16
+ "markdown",
17
+ "knowledge-graph",
18
+ "sqlite",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "obsidian",
22
+ "agents",
23
+ "omgbase",
24
+ "engine",
25
+ "oqx",
26
+ "query",
27
+ "embedded"
28
+ ],
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "!dist/**/*.test.*",
38
+ "!dist/corpus/**",
39
+ "!dist/**/*.map",
40
+ "!dist/**/*.tsbuildinfo",
41
+ "LICENSE"
42
+ ],
43
+ "type": "module",
44
+ "main": "./dist/src/index.js",
45
+ "types": "./dist/src/index.d.ts",
46
+ "exports": {
47
+ ".": {
48
+ "import": "./dist/src/index.js",
49
+ "types": "./dist/src/index.d.ts"
50
+ },
51
+ "./*": {
52
+ "import": "./dist/src/*.js",
53
+ "types": "./dist/src/*.d.ts"
54
+ }
55
+ },
56
+ "dependencies": {
57
+ "@modelcontextprotocol/sdk": "^1.30.0",
58
+ "@omgbase/oqx": "^0.10.2",
59
+ "better-sqlite3": "^13.0.3",
60
+ "mdast-util-from-markdown": "^2.0.2",
61
+ "mdast-util-frontmatter": "^2.0.1",
62
+ "mdast-util-gfm": "^3.0.0",
63
+ "micromark-extension-frontmatter": "^2.0.0",
64
+ "micromark-extension-gfm": "^3.0.0",
65
+ "yaml": "^2.9.0",
66
+ "zod": "^4.5.4"
67
+ },
68
+ "devDependencies": {
69
+ "@types/better-sqlite3": "^9.6.0",
70
+ "@types/mdast": "^4.0.4",
71
+ "fast-check": "^4.9.0"
72
+ },
73
+ "scripts": {
74
+ "build": "tsc -p tsconfig.json",
75
+ "typecheck": "tsc -p tsconfig.json --noEmit",
76
+ "test": "vitest run",
77
+ "lint": "eslint .",
78
+ "lint:fix": "eslint . --fix"
79
+ }
80
+ }