@usegraft/compiler 0.1.1 → 0.2.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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @usegraft/compiler
2
+
3
+ > Validate authored MDX against your collections and project it into the content index. The bridge that makes file-authored content queryable.
4
+
5
+ Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @usegraft/compiler
11
+ ```
12
+
13
+ Most projects use `graft compile` from [`@usegraft/cli`](https://www.npmjs.com/package/@usegraft/cli) rather than calling this directly.
14
+
15
+ ## Compile
16
+
17
+ ```ts
18
+ import { compile, compileStatic } from "@usegraft/compiler";
19
+
20
+ // into Postgres
21
+ const result = await compile({ contentDir, collections, db, branchId: "main" });
22
+
23
+ // or into a SQLite artifact, with no database at all
24
+ const result = await compileStatic({ contentDir, collections, indexPath: ".graft/index.db" });
25
+ ```
26
+
27
+ Both return a `ChangeSet`: what was added, changed and removed, plus the git SHA the tree was compiled from. Projection is a content-hash diff, not a delete-and-rebuild, so unchanged rows are untouched and removals are a soft delete.
28
+
29
+ ## What it refuses, and why
30
+
31
+ `readDocs` validates every authored file before anything reaches the index:
32
+
33
+ - Frontmatter against the collection schema.
34
+ - Slug uniqueness within a collection.
35
+ - Files sitting outside a registered collection folder.
36
+ - **Executable MDX.** `{…}` expressions, `import`, `export` and spread attributes are refused, because rendering evaluates them as JavaScript on the server.
37
+
38
+ Every offending document is reported at once rather than the first found, because an author fixing them one at a time learns the rule slowly.
39
+
40
+ Set `export const mdxTrust = "full"` in `graft.config.ts` to allow full MDX. That is correct only where every author of the repository has commit access, since code review then really is the control. It defaults to `"restricted"`, matching `MdxBody`, so a document that compiles is a document that renders.
41
+
42
+ ---
43
+
44
+ MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/feat/core/packages/compiler/CHANGELOG.md)
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { AnyCollection } from '@usegraft/core';
2
+ import { MdxTrust } from '@usegraft/mdx-safety';
2
3
  import { Database, ChangeSet } from '@usegraft/db';
3
4
 
5
+ /** Kebab-case: URL-safe, unambiguous in paths, and stable as a primary-key part. */
6
+ /** URL-safe kebab-case: the only shape a slug may take. */
7
+ declare const SLUG_RE: RegExp;
4
8
  /** One asset reference found in a document's frontmatter. */
5
9
  interface DocAsset {
6
10
  /** The field the reference came from. */
@@ -21,6 +25,51 @@ interface ProjectedDoc {
21
25
  }
22
26
  declare function parseDocument(raw: string, collection: AnyCollection, sourcePath: string): ProjectedDoc;
23
27
 
28
+ /** How THIS surface tells a caller to do the thing the error is about. */
29
+ interface ContentHints {
30
+ /** How to see what collections exist, e.g. "see list_collections". */
31
+ listCollections?: string;
32
+ /** How to create a document, e.g. "author it with write_content". */
33
+ authorDocument?: string;
34
+ }
35
+ /** Every `.md`/`.mdx` file under `dir`, recursively, in stable order. */
36
+ declare function walkContentFiles(dir: string): string[];
37
+ /** Resolve a registered collection, or throw a miss the caller can act on. */
38
+ declare function requireCollection(collections: Record<string, AnyCollection>, name: string, hints?: ContentHints): AnyCollection;
39
+ /** Parse every document in one collection's folder. Empty if it does not exist. */
40
+ declare function readCollectionDocs(contentDir: string, collectionName: string, collection: AnyCollection): ProjectedDoc[];
41
+ /** Find one document by slug, or throw DOCUMENT_NOT_FOUND listing the real ones. */
42
+ declare function findDoc(contentDir: string, collectionName: string, collection: AnyCollection, slug: string, hints?: ContentHints): ProjectedDoc;
43
+
44
+ interface ContainOptions {
45
+ /** What the path names, for the error message (default: "path"). */
46
+ label?: string;
47
+ /**
48
+ * Follow symlinks instead of refusing them. Only for paths the operator
49
+ * typed, never for anything a request or a manifest supplied.
50
+ */
51
+ allowSymlinks?: boolean;
52
+ /**
53
+ * Accept an absolute path, provided it still resolves inside the root.
54
+ *
55
+ * For a local agent naming a file on its own machine an absolute path is the
56
+ * natural form, and containment is what matters, not the spelling. Leave this
57
+ * off for anything arriving in a request field, where "relative to the root"
58
+ * is the contract and an absolute path means the caller misunderstood — or is
59
+ * probing.
60
+ */
61
+ allowAbsolute?: boolean;
62
+ }
63
+ /**
64
+ * Resolve `path` against `root` and return it only if it genuinely lives there.
65
+ *
66
+ * Refuses absolute paths, NUL bytes, anything that escapes the root once
67
+ * normalised, and — unless `allowSymlinks` — any segment that is a symbolic
68
+ * link, whether or not its target escapes. A symlink inside the tree is a
69
+ * redirection the caller did not get to choose, so the honest answer is no.
70
+ */
71
+ declare function resolveContained(root: string, path: string, options?: ContainOptions): string;
72
+
24
73
  interface CompileOptions {
25
74
  contentDir: string;
26
75
  collections: Record<string, AnyCollection>;
@@ -38,6 +87,12 @@ interface CompileOptions {
38
87
  * unless this is set (legitimate after a collection rename/delete).
39
88
  */
40
89
  pruneUnknown?: boolean;
90
+ /**
91
+ * How much of MDX authored bodies may be. Defaults to "restricted", matching
92
+ * MdxBody, so a document that compiles is a document that renders. From
93
+ * `export const mdxTrust` in graft.config.ts.
94
+ */
95
+ mdxTrust?: MdxTrust;
41
96
  }
42
97
  interface CompileResult {
43
98
  count: number;
@@ -47,8 +102,16 @@ interface CompileResult {
47
102
  /** The commit the content tree was compiled from, when resolvable. */
48
103
  gitSha: string | null;
49
104
  }
105
+ interface ReadDocsOptions {
106
+ /**
107
+ * How much of MDX authored bodies may be. Defaults to "restricted", which is
108
+ * also MdxBody's default, so a document that compiles is a document that
109
+ * renders. Set "full" only where every author has commit access.
110
+ */
111
+ mdxTrust?: MdxTrust;
112
+ }
50
113
  /** Read + validate the content tree into normalized docs. Pure of the database. */
51
- declare function readDocs(contentDir: string, collections: Record<string, AnyCollection>): ProjectedDoc[];
114
+ declare function readDocs(contentDir: string, collections: Record<string, AnyCollection>, options?: ReadDocsOptions): ProjectedDoc[];
52
115
  declare function compile(options: CompileOptions): Promise<CompileResult>;
53
116
  interface CompileStaticOptions {
54
117
  contentDir: string;
@@ -57,6 +120,12 @@ interface CompileStaticOptions {
57
120
  indexPath: string;
58
121
  /** As in CompileOptions: omit to auto-resolve, null to skip. */
59
122
  gitSha?: string | null;
123
+ /**
124
+ * How much of MDX authored bodies may be. Defaults to "restricted", matching
125
+ * MdxBody, so a document that compiles is a document that renders. From
126
+ * `export const mdxTrust` in graft.config.ts.
127
+ */
128
+ mdxTrust?: MdxTrust;
60
129
  }
61
130
  /**
62
131
  * The static-mode sibling of `compile`: same validate step (readDocs), but the
@@ -85,4 +154,4 @@ declare function writeDocumentFile(fullPath: string, raw: string): void;
85
154
  */
86
155
  declare function composeDocument(existingRaw: string | undefined, data: Record<string, unknown>, body: string): string;
87
156
 
88
- export { type CompileOptions, type CompileResult, type CompileStaticOptions, type DocAsset, type ProjectedDoc, compile, compileStatic, composeDocument, parseDocument, readDocs, resolveGitSha, writeDocumentFile };
157
+ export { type CompileOptions, type CompileResult, type CompileStaticOptions, type ContainOptions, type ContentHints, type DocAsset, type ProjectedDoc, type ReadDocsOptions, SLUG_RE, compile, compileStatic, composeDocument, findDoc, parseDocument, readCollectionDocs, readDocs, requireCollection, resolveContained, resolveGitSha, walkContentFiles, writeDocumentFile };
package/dist/index.js CHANGED
@@ -49,18 +49,121 @@ function collectAssets(collection, data) {
49
49
  return assets;
50
50
  }
51
51
 
52
- // src/compile.ts
53
- import { execFileSync } from "child_process";
52
+ // src/content-files.ts
54
53
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
55
54
  import { join, relative, sep } from "path";
56
55
  import { GraftError as GraftError2 } from "@usegraft/contracts";
56
+ function walkContentFiles(dir) {
57
+ const out = [];
58
+ for (const name of readdirSync(dir)) {
59
+ const full = join(dir, name);
60
+ if (statSync(full).isDirectory()) out.push(...walkContentFiles(full));
61
+ else if (/\.mdx?$/.test(name)) out.push(full);
62
+ }
63
+ return out.sort();
64
+ }
65
+ function requireCollection(collections, name, hints = {}) {
66
+ const collection = collections[name];
67
+ if (collection) return collection;
68
+ const registered = Object.keys(collections);
69
+ const known = registered.join(", ") || "(none registered)";
70
+ const where = hints.listCollections ? ` (${hints.listCollections})` : "";
71
+ throw new GraftError2({
72
+ code: "COLLECTION_NOT_FOUND",
73
+ message: `No collection named "${name}" is registered`,
74
+ fix: `Use one of the registered collections: ${known}${where}, or add defineCollection({ name: "${name}", \u2026 }) to the schema.`,
75
+ details: { collection: name, registered }
76
+ });
77
+ }
78
+ function readCollectionDocs(contentDir, collectionName, collection) {
79
+ const dir = join(contentDir, collectionName);
80
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
81
+ const docs = [];
82
+ for (const file of walkContentFiles(dir)) {
83
+ const sourcePath = relative(contentDir, file).split(sep).join("/");
84
+ docs.push(parseDocument(readFileSync(file, "utf8"), collection, sourcePath));
85
+ }
86
+ return docs.sort((a, b) => a.slug.localeCompare(b.slug));
87
+ }
88
+ function findDoc(contentDir, collectionName, collection, slug, hints = {}) {
89
+ const docs = readCollectionDocs(contentDir, collectionName, collection);
90
+ const doc = docs.find((candidate) => candidate.slug === slug);
91
+ if (doc) return doc;
92
+ const slugs = docs.map((candidate) => candidate.slug);
93
+ const author = hints.authorDocument ? ` ${hints.authorDocument}` : "";
94
+ throw new GraftError2({
95
+ code: "DOCUMENT_NOT_FOUND",
96
+ message: `No document "${slug}" in collection "${collectionName}"`,
97
+ fix: slugs.length > 0 ? `Existing slugs: ${slugs.join(", ")}. Use one of those,${author || " or create it."}` : `Collection "${collectionName}" has no documents yet \u2014${author || " create the first one."}`,
98
+ details: { collection: collectionName, slug, existing: slugs }
99
+ });
100
+ }
101
+
102
+ // src/paths.ts
103
+ import { existsSync as existsSync2, lstatSync } from "fs";
104
+ import { isAbsolute, normalize, resolve, sep as sep2 } from "path";
105
+ import { GraftError as GraftError3 } from "@usegraft/contracts";
106
+ function refuse(label, path, why, fix) {
107
+ throw new GraftError3({
108
+ code: "INPUT_VALIDATION_FAILED",
109
+ message: `${label} "${path}" ${why}.`,
110
+ fix,
111
+ details: { path }
112
+ });
113
+ }
114
+ function resolveContained(root, path, options = {}) {
115
+ const label = options.label ?? "path";
116
+ const rootAbs = resolve(root);
117
+ if (!path || path.includes("\0")) {
118
+ refuse(
119
+ label,
120
+ path,
121
+ "is empty or contains a NUL byte",
122
+ "Pass a relative path with no NUL bytes."
123
+ );
124
+ }
125
+ if (isAbsolute(path) && options.allowAbsolute !== true) {
126
+ refuse(label, path, "is absolute", `Pass a path relative to ${rootAbs}.`);
127
+ }
128
+ const candidate = isAbsolute(path) ? resolve(normalize(path)) : resolve(rootAbs, normalize(path));
129
+ if (candidate !== rootAbs && !candidate.startsWith(rootAbs + sep2)) {
130
+ refuse(
131
+ label,
132
+ path,
133
+ "resolves outside the permitted directory",
134
+ `Pass a path inside ${rootAbs}.`
135
+ );
136
+ }
137
+ if (options.allowSymlinks !== true) {
138
+ let cursor = rootAbs;
139
+ for (const segment of candidate.slice(rootAbs.length).split(sep2).filter(Boolean)) {
140
+ cursor = resolve(cursor, segment);
141
+ if (existsSync2(cursor) && lstatSync(cursor).isSymbolicLink()) {
142
+ refuse(
143
+ label,
144
+ path,
145
+ "passes through a symbolic link",
146
+ "Symbolic links are refused because they redirect a path somewhere the caller did not name. Replace the link with a real file, or read the target directly."
147
+ );
148
+ }
149
+ }
150
+ }
151
+ return candidate;
152
+ }
153
+
154
+ // src/compile.ts
155
+ import { execFileSync } from "child_process";
156
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
157
+ import { relative as relative2, sep as sep3 } from "path";
158
+ import { GraftError as GraftError4 } from "@usegraft/contracts";
159
+ import { findExecutableMdx, UncheckableMdxError } from "@usegraft/mdx-safety";
57
160
  import {
58
161
  projectBranchContent,
59
162
  projectStaticContent
60
163
  } from "@usegraft/db";
61
- function readDocs(contentDir, collections) {
62
- if (!existsSync(contentDir) || !statSync(contentDir).isDirectory()) {
63
- throw new GraftError2({
164
+ function readDocs(contentDir, collections, options = {}) {
165
+ if (!existsSync3(contentDir) || !statSync2(contentDir).isDirectory()) {
166
+ throw new GraftError4({
64
167
  code: "CONTENT_DIR_NOT_FOUND",
65
168
  message: `Content directory not found: ${contentDir}`,
66
169
  fix: `Create it (documents live at <contentDir>/<collection>/<slug>.mdx, e.g. content/pages/home.mdx) or pass the correct \`contentDir\` to compile().`,
@@ -69,14 +172,17 @@ function readDocs(contentDir, collections) {
69
172
  }
70
173
  const docs = [];
71
174
  const seen = /* @__PURE__ */ new Map();
72
- for (const file of walk(contentDir)) {
73
- const sourcePath = relative(contentDir, file).split(sep).join("/");
175
+ const executable = [];
176
+ const unreadable = [];
177
+ const mdxTrust = options.mdxTrust ?? "restricted";
178
+ for (const file of walkContentFiles(contentDir)) {
179
+ const sourcePath = relative2(contentDir, file).split(sep3).join("/");
74
180
  const segments = sourcePath.split("/");
75
181
  const collectionName = segments.length > 1 ? segments[0] ?? "" : "";
76
182
  const collection = collections[collectionName];
77
183
  if (!collection) {
78
184
  const known = Object.keys(collections).join(", ") || "(none registered)";
79
- throw new GraftError2({
185
+ throw new GraftError4({
80
186
  code: "COLLECTION_NOT_FOUND",
81
187
  message: segments.length > 1 ? `No collection registered for "${collectionName}" (${sourcePath})` : `${sourcePath} sits at the content root; documents must live in a collection folder`,
82
188
  fix: segments.length > 1 ? `Register it \u2014 defineCollection({ name: "${collectionName}", fields: { \u2026 } }) and pass it to compile() \u2014 or move the file into one of: ${known}.` : `Move it to <contentDir>/<collection>/${sourcePath}. Registered collections: ${known}.`,
@@ -84,18 +190,34 @@ function readDocs(contentDir, collections) {
84
190
  });
85
191
  }
86
192
  if (collection.authority === "db-authoritative") {
87
- throw new GraftError2({
193
+ throw new GraftError4({
88
194
  code: "AUTHORITY_MISMATCH",
89
195
  message: `${sourcePath} is a file, but collection "${collectionName}" is db-authoritative \u2014 its records live in Postgres, not in the content tree.`,
90
196
  fix: `Remove the file and write the data through the collection's functions instead (insertRecord via a defineFunction mutation). Files are only for file-authoritative collections.`,
91
197
  details: { sourcePath, collection: collectionName, authority: collection.authority }
92
198
  });
93
199
  }
94
- const doc = parseDocument(readFileSync(file, "utf8"), collection, sourcePath);
200
+ const doc = parseDocument(readFileSync2(file, "utf8"), collection, sourcePath);
201
+ if (mdxTrust !== "full") {
202
+ try {
203
+ const found = findExecutableMdx(doc.body);
204
+ if (found.length > 0) {
205
+ const first = found[0];
206
+ executable.push({
207
+ sourcePath,
208
+ found: found.length,
209
+ first: first?.line === void 0 ? "" : `line ${first.line}`
210
+ });
211
+ }
212
+ } catch (error) {
213
+ if (!(error instanceof UncheckableMdxError)) throw error;
214
+ unreadable.push({ sourcePath, reason: error.message });
215
+ }
216
+ }
95
217
  const key = `${doc.collection}/${doc.slug}`;
96
218
  const existing = seen.get(key);
97
219
  if (existing) {
98
- throw new GraftError2({
220
+ throw new GraftError4({
99
221
  code: "SLUG_NOT_UNIQUE",
100
222
  message: `Slug "${doc.slug}" in collection "${doc.collection}" is used by both ${existing} and ${sourcePath}`,
101
223
  fix: `Give one a unique \`slug\` in frontmatter, or rename the file.`,
@@ -105,12 +227,33 @@ function readDocs(contentDir, collections) {
105
227
  seen.set(key, sourcePath);
106
228
  docs.push(doc);
107
229
  }
230
+ if (unreadable.length > 0) {
231
+ const listed = unreadable.map((u) => `${u.sourcePath} (${u.reason})`).join(", ");
232
+ throw new GraftError4({
233
+ code: "INPUT_VALIDATION_FAILED",
234
+ message: `${unreadable.length} document(s) could not be checked for executable MDX: ${listed}.`,
235
+ fix: "Fix the MDX syntax so it parses. Unparseable source is refused rather than indexed, because the renderer's parser is not this one and the difference between them is where executable source would hide.",
236
+ details: { unreadable }
237
+ });
238
+ }
239
+ if (executable.length > 0) {
240
+ const listed = executable.slice(0, 10).map((e) => e.first === "" ? e.sourcePath : `${e.sourcePath} (${e.first})`).join(", ");
241
+ const more = executable.length > 10 ? ` \u2026and ${executable.length - 10} more` : "";
242
+ throw new GraftError4({
243
+ code: "INPUT_VALIDATION_FAILED",
244
+ message: `Executable MDX in ${executable.length} document(s): ${listed}${more}.`,
245
+ fix: 'Rendering evaluates `{\u2026}` and `import` as JavaScript on the server, and MdxBody refuses them by default, so these documents would fail per-request rather than here. Write prose, Markdown and components with literal attributes. If every author of this repository has commit access, set `export const mdxTrust = "full"` in graft.config.ts and pass trust="full" to MdxBody. Code review is the control in that case, which is what ADR 0004 assumes.',
246
+ details: { documents: executable.length, offenders: executable.slice(0, 20) }
247
+ });
248
+ }
108
249
  return docs.sort(
109
250
  (a, b) => a.collection === b.collection ? a.slug.localeCompare(b.slug) : a.collection.localeCompare(b.collection)
110
251
  );
111
252
  }
112
253
  async function compile(options) {
113
- const docs = readDocs(options.contentDir, options.collections);
254
+ const docs = readDocs(options.contentDir, options.collections, {
255
+ mdxTrust: options.mdxTrust
256
+ });
114
257
  const gitSha = options.gitSha === void 0 ? resolveGitSha(options.contentDir) : options.gitSha;
115
258
  const changes = await projectBranchContent(
116
259
  options.db,
@@ -132,7 +275,9 @@ async function compile(options) {
132
275
  return { count: docs.length, docs, changes, gitSha };
133
276
  }
134
277
  async function compileStatic(options) {
135
- const docs = readDocs(options.contentDir, options.collections);
278
+ const docs = readDocs(options.contentDir, options.collections, {
279
+ mdxTrust: options.mdxTrust
280
+ });
136
281
  const gitSha = options.gitSha === void 0 ? resolveGitSha(options.contentDir) : options.gitSha;
137
282
  const changes = await projectStaticContent(
138
283
  docs.map((doc) => ({
@@ -158,20 +303,11 @@ function resolveGitSha(cwd) {
158
303
  return null;
159
304
  }
160
305
  }
161
- function walk(dir) {
162
- const out = [];
163
- for (const name of readdirSync(dir)) {
164
- const full = join(dir, name);
165
- if (statSync(full).isDirectory()) out.push(...walk(full));
166
- else if (/\.mdx?$/.test(name)) out.push(full);
167
- }
168
- return out.sort();
169
- }
170
306
 
171
307
  // src/serialize.ts
172
308
  import { mkdirSync, writeFileSync } from "fs";
173
309
  import { dirname } from "path";
174
- import { GraftError as GraftError3 } from "@usegraft/contracts";
310
+ import { GraftError as GraftError5 } from "@usegraft/contracts";
175
311
  import matter2 from "gray-matter";
176
312
  var READ_ONLY_CODES = /* @__PURE__ */ new Set(["EROFS", "EACCES", "EPERM"]);
177
313
  function writeDocumentFile(fullPath, raw) {
@@ -181,7 +317,7 @@ function writeDocumentFile(fullPath, raw) {
181
317
  } catch (error) {
182
318
  const code = error.code ?? "";
183
319
  if (!READ_ONLY_CODES.has(code)) throw error;
184
- throw new GraftError3({
320
+ throw new GraftError5({
185
321
  code: "CONTENT_TREE_READ_ONLY",
186
322
  message: `Cannot write ${fullPath}: the filesystem refused it (${code}).`,
187
323
  fix: "Authored content lives in files, so writing needs a writable checkout. Run this surface locally (`graft studio` / `graft mcp`) or in a container with the project mounted read-write; a serverless deployment's filesystem is read-only and should serve reads only.",
@@ -280,11 +416,17 @@ function composeDocument(existingRaw, data, body) {
280
416
  return frontmatter + separator + body + (needsNewline ? newline : "");
281
417
  }
282
418
  export {
419
+ SLUG_RE,
283
420
  compile,
284
421
  compileStatic,
285
422
  composeDocument,
423
+ findDoc,
286
424
  parseDocument,
425
+ readCollectionDocs,
287
426
  readDocs,
427
+ requireCollection,
428
+ resolveContained,
288
429
  resolveGitSha,
430
+ walkContentFiles,
289
431
  writeDocumentFile
290
432
  };
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@usegraft/compiler",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
+ "description": "Validate authored MDX against your collections and project it into the content index.",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "cms",
9
+ "compiler",
10
+ "content-index",
11
+ "graft",
12
+ "headless-cms",
13
+ "mcp",
14
+ "mdx",
15
+ "typescript",
16
+ "validation"
17
+ ],
18
+ "homepage": "https://github.com/AndersonDesign1/graft#readme",
4
19
  "license": "MIT",
5
20
  "repository": {
6
21
  "type": "git",
@@ -24,9 +39,10 @@
24
39
  "access": "public"
25
40
  },
26
41
  "dependencies": {
27
- "@usegraft/contracts": "0.1.1",
28
- "@usegraft/core": "0.1.1",
29
- "@usegraft/db": "0.1.1",
42
+ "@usegraft/contracts": "0.2.0",
43
+ "@usegraft/core": "0.2.0",
44
+ "@usegraft/db": "0.2.0",
45
+ "@usegraft/mdx-safety": "0.2.0",
30
46
  "gray-matter": "^4.0.3"
31
47
  },
32
48
  "engines": {
@@ -36,7 +52,6 @@
36
52
  "build": "tsup src/index.ts --format esm --dts --clean",
37
53
  "dev": "tsup src/index.ts --format esm --watch",
38
54
  "typecheck": "tsc --noEmit",
39
- "test": "vitest run --passWithNoTests",
40
- "lint": "oxlint ."
55
+ "test": "vitest run --passWithNoTests"
41
56
  }
42
57
  }