@usegraft/cli 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,60 @@
1
+ # @usegraft/cli
2
+
3
+ > The `graft` command. Scaffold a project, compile authored content, serve the runtime, split your database credentials.
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 -D @usegraft/cli
11
+ # or run it without installing
12
+ npx graft init
13
+ ```
14
+
15
+ ## Quickstart, with no database
16
+
17
+ A content project needs no services at all. `graft compile` writes the index to a SQLite artifact your app reads embedded.
18
+
19
+ ```bash
20
+ npx graft init # scaffolds graft.config.ts, content/, llms.txt
21
+ npx graft compile # → .graft/index.db
22
+ ```
23
+
24
+ Move to Postgres by setting `DATABASE_URL` and changing one line in `graft.config.ts`:
25
+
26
+ ```ts
27
+ export const index = "postgres";
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ | Command | Does |
33
+ | ------------------------------ | ---------------------------------------------------------------------------------------------- |
34
+ | `graft init` | Scaffold a project |
35
+ | `graft compile` | Validate authored MDX and project it into the index |
36
+ | `graft dev` | Recompile on save |
37
+ | `graft serve` | Run the headless runtime: typed functions, MCP over HTTP, `/healthz` |
38
+ | `graft studio` | Open the optional editing UI |
39
+ | `graft mcp` | Serve MCP on stdio, for `.mcp.json` |
40
+ | `graft add <item>` | Copy a registry primitive into your repo, shadcn-style |
41
+ | `graft approve <id>` | Decide a pending approval. Operator credential only |
42
+ | `graft harden <role>` | Grant a Postgres role exactly what a deployment needs, and nothing that can decide an approval |
43
+ | `graft branch` / `graft merge` | Copy-on-write content branches |
44
+ | `graft migrate` | Run content codemods when the schema changes |
45
+
46
+ Run `graft <command> --help` for flags.
47
+
48
+ ## Two credentials
49
+
50
+ `graft harden` splits the operator credential from the runtime one. The runtime can serve, project content, and request approvals. It can never decide one, and Postgres enforces that rather than the application.
51
+
52
+ ```bash
53
+ graft harden graft_runtime
54
+ # deploy with the runtime role's URL as DATABASE_URL
55
+ # keep the operator URL for compile, migrate, merge and approve
56
+ ```
57
+
58
+ ---
59
+
60
+ MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/feat/core/packages/cli/CHANGELOG.md) · [Security policy](https://github.com/AndersonDesign1/graft/blob/feat/core/SECURITY.md)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findConfig
4
- } from "./chunk-BUPFXTUG.js";
4
+ } from "./chunk-2NH6KYTL.js";
5
5
 
6
6
  // src/commands/add.ts
7
7
  import { dirname } from "path";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadProjectEnv,
4
4
  requireDatabaseUrl
5
- } from "./chunk-BUPFXTUG.js";
5
+ } from "./chunk-2NH6KYTL.js";
6
6
 
7
7
  // src/commands/approvals.ts
8
8
  import { userInfo } from "os";
@@ -33,12 +33,10 @@ async function decideCommand(options) {
33
33
  ]);
34
34
  const handle = createDb(url);
35
35
  try {
36
- const row = await decideApproval(
37
- handle.db,
38
- options.id,
39
- options.decision,
40
- options.decidedBy ?? operatorName()
41
- );
36
+ const row = await decideApproval(handle.db, options.id, options.decision, {
37
+ kind: "human",
38
+ id: operatorName()
39
+ });
42
40
  if (!row) {
43
41
  throw new GraftError({
44
42
  code: "APPROVAL_INVALID",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadProjectEnv
4
- } from "./chunk-BUPFXTUG.js";
4
+ } from "./chunk-2NH6KYTL.js";
5
5
 
6
6
  // src/commands/asset.ts
7
7
  import { readFileSync, statSync } from "fs";
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadProjectEnv,
4
4
  requireDatabaseUrl
5
- } from "./chunk-BUPFXTUG.js";
5
+ } from "./chunk-2NH6KYTL.js";
6
6
 
7
7
  // src/commands/branch.ts
8
8
  import { GraftError } from "@usegraft/contracts";
@@ -3,7 +3,7 @@
3
3
  // src/config.ts
4
4
  import { existsSync } from "fs";
5
5
  import { dirname, join, resolve } from "path";
6
- import { GraftError } from "@usegraft/contracts";
6
+ import { GraftError, STATIC_INDEX_DEFAULT_PATH } from "@usegraft/contracts";
7
7
  import { createJiti } from "jiti";
8
8
  var CONFIG_FILENAMES = [
9
9
  "graft.config.ts",
@@ -97,6 +97,7 @@ async function loadConfig(configPath) {
97
97
  const projectDir = dirname(configPath);
98
98
  const index = parseIndexConfig(mod.index, projectDir, configPath);
99
99
  assertStaticSupports(index, collections, functions, configPath);
100
+ const mdxTrust = parseMdxTrust(mod.mdxTrust, configPath);
100
101
  const contentDirSetting = typeof mod.contentDir === "string" ? mod.contentDir : "content";
101
102
  const migrationsDirSetting = typeof mod.migrationsDir === "string" ? mod.migrationsDir : "migrations";
102
103
  return {
@@ -106,9 +107,20 @@ async function loadConfig(configPath) {
106
107
  migrationsDir: resolve(projectDir, migrationsDirSetting),
107
108
  index,
108
109
  collections,
109
- functions
110
+ functions,
111
+ mdxTrust
110
112
  };
111
113
  }
114
+ function parseMdxTrust(value, configPath) {
115
+ if (value === void 0) return "restricted";
116
+ if (value === "restricted" || value === "full") return value;
117
+ throw new GraftError({
118
+ code: "CONFIG_INVALID",
119
+ message: `${configPath} exports \`mdxTrust\` as ${JSON.stringify(value)}, which is not "restricted" or "full".`,
120
+ fix: 'Export `mdxTrust = "restricted"` (the default: `{\u2026}` expressions and `import` are refused in authored bodies) or `mdxTrust = "full"` (every author has commit access, so code review is the control). Omit the export to keep the default.',
121
+ details: { mdxTrust: value }
122
+ });
123
+ }
112
124
  function assertStaticSupports(index, collections, functions, configPath) {
113
125
  if (index.driver !== "static") return;
114
126
  const dbCollections = Object.values(collections).filter((collection) => collection.authority === "db-authoritative").map((collection) => collection.name);
@@ -125,18 +137,17 @@ function assertStaticSupports(index, collections, functions, configPath) {
125
137
  details: { dbCollections, functions: functionNames }
126
138
  });
127
139
  }
128
- var STATIC_INDEX_DEFAULT = ".graft/index.db";
129
140
  function parseIndexConfig(value, projectDir, configPath) {
130
141
  if (value === void 0 || value === "postgres") return { driver: "postgres" };
131
142
  if (value === "static") {
132
- return { driver: "static", path: resolve(projectDir, STATIC_INDEX_DEFAULT) };
143
+ return { driver: "static", path: resolve(projectDir, STATIC_INDEX_DEFAULT_PATH) };
133
144
  }
134
145
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
135
146
  const driver = value.driver;
136
147
  const path = value.path;
137
148
  if (driver === "postgres") return { driver: "postgres" };
138
149
  if (driver === "static" && (path === void 0 || typeof path === "string")) {
139
- return { driver: "static", path: resolve(projectDir, path ?? STATIC_INDEX_DEFAULT) };
150
+ return { driver: "static", path: resolve(projectDir, path ?? STATIC_INDEX_DEFAULT_PATH) };
140
151
  }
141
152
  }
142
153
  throw new GraftError({
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  loadProjectEnv,
6
6
  requireDatabaseUrl
7
- } from "./chunk-BUPFXTUG.js";
7
+ } from "./chunk-2NH6KYTL.js";
8
8
 
9
9
  // src/commands/migrate.ts
10
10
  import { existsSync, readdirSync, statSync } from "fs";
@@ -99,6 +99,7 @@ Author one as migrations/0001-<name>.ts default-exporting defineContentMigration
99
99
  db: branch.db,
100
100
  contentDir: config.contentDir,
101
101
  collections: config.collections,
102
+ mdxTrust: config.mdxTrust,
102
103
  branchId: writeBranch,
103
104
  gitSha
104
105
  });
@@ -7,7 +7,7 @@ import {
7
7
  loadConfig,
8
8
  loadProjectEnv,
9
9
  requireDatabaseUrl
10
- } from "./chunk-BUPFXTUG.js";
10
+ } from "./chunk-2NH6KYTL.js";
11
11
 
12
12
  // src/commands/compile.ts
13
13
  import { GraftError } from "@usegraft/contracts";
@@ -29,6 +29,7 @@ async function compileCommand(options) {
29
29
  const result = await compileStatic({
30
30
  contentDir: config.contentDir,
31
31
  collections: config.collections,
32
+ mdxTrust: config.mdxTrust,
32
33
  indexPath: config.index.path
33
34
  });
34
35
  console.log(formatCompileResult(result));
@@ -52,6 +53,7 @@ async function compileCommand(options) {
52
53
  db: branch.db,
53
54
  contentDir: config.contentDir,
54
55
  collections: config.collections,
56
+ mdxTrust: config.mdxTrust,
55
57
  branchId: scopeWriteBranch(branch.scope),
56
58
  pruneUnknown: options.pruneUnknown
57
59
  });
@@ -4,13 +4,14 @@ import {
4
4
  loadConfig,
5
5
  loadProjectEnv,
6
6
  requireDatabaseUrl
7
- } from "./chunk-BUPFXTUG.js";
7
+ } from "./chunk-2NH6KYTL.js";
8
8
 
9
9
  // src/commands/serve.ts
10
10
  import { execFileSync } from "child_process";
11
11
  import { createServer } from "http";
12
12
  import { createActorResolver } from "@usegraft/auth";
13
13
  import { GraftError } from "@usegraft/contracts";
14
+ import { setRequestPeer } from "@usegraft/core";
14
15
  function createServeRouter(routes) {
15
16
  return async (request) => {
16
17
  const { pathname } = new URL(request.url);
@@ -41,9 +42,34 @@ var HOP_BY_HOP = /* @__PURE__ */ new Set([
41
42
  "upgrade",
42
43
  "proxy-connection"
43
44
  ]);
44
- function createNodeListener(handler) {
45
+ function allowedHostsFor(host) {
46
+ if (host === "0.0.0.0" || host === "::" || host === "") return void 0;
47
+ if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
48
+ return ["127.0.0.1", "localhost", "[::1]"];
49
+ }
50
+ return [host];
51
+ }
52
+ function hostname(host) {
53
+ const trimmed = host.trim().toLowerCase();
54
+ if (trimmed.startsWith("[")) return trimmed.slice(0, trimmed.indexOf("]") + 1);
55
+ const colon = trimmed.lastIndexOf(":");
56
+ return colon === -1 ? trimmed : trimmed.slice(0, colon);
57
+ }
58
+ function createNodeListener(handler, options = {}) {
59
+ const allowed = options.allowedHosts?.map(hostname);
45
60
  return async (req, res) => {
46
61
  try {
62
+ if (allowed && !allowed.includes(hostname(req.headers.host ?? ""))) {
63
+ res.writeHead(400, { "content-type": "application/json" });
64
+ res.end(
65
+ JSON.stringify({
66
+ error: "INPUT_VALIDATION_FAILED",
67
+ message: `This server does not answer to Host "${req.headers.host ?? ""}".`,
68
+ fix: `Reach it at one of: ${allowed.join(", ")}. A mismatched Host is either a misconfigured proxy or a DNS-rebinding attempt.`
69
+ })
70
+ );
71
+ return;
72
+ }
47
73
  const chunks = [];
48
74
  for await (const chunk of req) chunks.push(chunk);
49
75
  const body = Buffer.concat(chunks);
@@ -59,6 +85,8 @@ function createNodeListener(handler) {
59
85
  headers,
60
86
  body: method === "GET" || method === "HEAD" || body.length === 0 ? void 0 : body
61
87
  });
88
+ const peer = req.socket.remoteAddress;
89
+ if (peer) setRequestPeer(request, peer);
62
90
  const response = await handler(request);
63
91
  const outHeaders = {};
64
92
  response.headers.forEach((value, key) => {
@@ -120,7 +148,10 @@ async function startServe(options) {
120
148
  devTokens: devToken ? { [devToken]: { kind: "agent", id: "graft-serve", scopes } } : void 0
121
149
  });
122
150
  const approvalPolicy = process.env.GRAFT_APPROVAL_POLICY === "human" ? "human" : "none";
123
- const requireMcpActor = process.env.GRAFT_MCP_REQUIRE_AUTH === "1";
151
+ const host = options.host ?? process.env.HOST ?? "127.0.0.1";
152
+ const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
153
+ const anonymousOptIn = process.env.GRAFT_MCP_ALLOW_ANONYMOUS === "1";
154
+ const allowAnonymousMcp = loopback || anonymousOptIn;
124
155
  const functions = config.functions ?? {};
125
156
  const gitSha = localGitSha(options.cwd);
126
157
  const fnHandler = createFunctionsHandler({
@@ -138,11 +169,12 @@ async function startServe(options) {
138
169
  contentDir: config.contentDir,
139
170
  collections: config.collections,
140
171
  functions,
172
+ mdxTrust: config.mdxTrust,
141
173
  db: branch.db,
142
174
  branchId: writeBranch,
143
175
  scope: branch.scope,
144
176
  actor: resolveActor,
145
- requireActor: requireMcpActor
177
+ allowAnonymous: allowAnonymousMcp
146
178
  });
147
179
  const health = async () => {
148
180
  try {
@@ -167,7 +199,6 @@ async function startServe(options) {
167
199
  { status: 200, headers: { "content-type": "application/json" } }
168
200
  );
169
201
  };
170
- const host = options.host ?? process.env.HOST ?? "127.0.0.1";
171
202
  const requestedPort = options.port ?? Number(process.env.PORT ?? 3903);
172
203
  if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
173
204
  await branch.close();
@@ -178,31 +209,39 @@ async function startServe(options) {
178
209
  fix: "Pass --port <0-65535> (0 picks a free port) or set PORT."
179
210
  });
180
211
  }
181
- const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
182
- if (!loopback && !requireMcpActor && issuers.length === 0 && !devToken) {
212
+ if (!loopback && anonymousOptIn) {
213
+ console.warn(
214
+ "[graft serve] WARNING: GRAFT_MCP_ALLOW_ANONYMOUS=1 while bound beyond loopback \u2014 unauthenticated callers can write content, upload assets and decide approvals. Only do this behind an auth proxy you control."
215
+ );
216
+ } else if (!loopback && issuers.length === 0 && !devToken) {
183
217
  console.warn(
184
- "[graft serve] WARNING: binding beyond loopback with no identity configured \u2014 set GRAFT_MCP_REQUIRE_AUTH=1 and provide GRAFT_DEV_TOKEN or GRAFT_TRUSTED_ISSUERS before exposing this to a network."
218
+ "[graft serve] WARNING: binding beyond loopback with no identity configured \u2014 MCP will refuse every caller, because there is nothing to authenticate them against. Set GRAFT_DEV_TOKEN or GRAFT_TRUSTED_ISSUERS."
185
219
  );
186
220
  }
187
221
  let studioHandler;
188
222
  if (studioMod) {
189
- const authorize = !loopback && (devToken || issuers.length > 0) ? async (request) => {
223
+ const authenticate = !loopback && (devToken || issuers.length > 0) ? async (request) => {
190
224
  const actor = await resolveActor(request);
191
- return actor.kind !== "anonymous";
192
- } : !loopback ? () => false : void 0;
225
+ if (actor.kind === "anonymous" || !actor.id) return null;
226
+ return { kind: actor.kind, id: actor.id, scopes: actor.scopes ?? [] };
227
+ } : !loopback ? () => null : void 0;
193
228
  studioHandler = studioMod.createStudioHandler({
194
229
  db: branch.db,
195
230
  collections: config.collections,
196
231
  contentDir: config.contentDir,
232
+ mdxTrust: config.mdxTrust,
197
233
  defaultBranch: writeBranch,
198
- decidedBy: "studio-serve",
234
+ // Only reached on a loopback mount, where there is no caller identity to
235
+ // attribute to. Off loopback the authenticated principal is used instead.
236
+ decider: { kind: "agent", id: "studio-serve" },
199
237
  uiBasePath: "/studio",
200
- authorize
238
+ authenticate
201
239
  });
202
240
  }
203
241
  const server = createServer(
204
242
  createNodeListener(
205
- createServeRouter({ fn: fnHandler, mcp: mcpHandler, health, studio: studioHandler })
243
+ createServeRouter({ fn: fnHandler, mcp: mcpHandler, health, studio: studioHandler }),
244
+ { allowedHosts: allowedHostsFor(host) }
206
245
  )
207
246
  );
208
247
  await new Promise((resolve, reject) => {
@@ -250,6 +289,7 @@ async function serveCommand(options) {
250
289
 
251
290
  export {
252
291
  createServeRouter,
292
+ allowedHostsFor,
253
293
  createNodeListener,
254
294
  startServe,
255
295
  serveCommand
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadProjectEnv,
4
4
  requireDatabaseUrl
5
- } from "./chunk-BUPFXTUG.js";
5
+ } from "./chunk-2NH6KYTL.js";
6
6
 
7
7
  // src/commands/compilations.ts
8
8
  async function compilationsListCommand(options) {
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  assertNoStaticBranch,
4
4
  compileCommand
5
- } from "./chunk-KZ3FDWUK.js";
5
+ } from "./chunk-GDTOV3DU.js";
6
6
  import "./chunk-GPA66AP5.js";
7
- import "./chunk-BUPFXTUG.js";
7
+ import "./chunk-2NH6KYTL.js";
8
8
  export {
9
9
  assertNoStaticBranch,
10
10
  compileCommand
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  loadProjectEnv,
6
6
  requireDatabaseUrl
7
- } from "./chunk-BUPFXTUG.js";
7
+ } from "./chunk-2NH6KYTL.js";
8
8
 
9
9
  // src/commands/content.ts
10
10
  async function contentListCommand(options) {
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  loadProjectEnv,
6
6
  requireDatabaseUrl
7
- } from "./chunk-BUPFXTUG.js";
7
+ } from "./chunk-2NH6KYTL.js";
8
8
 
9
9
  // src/commands/db.ts
10
10
  import { GraftError } from "@usegraft/contracts";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNoStaticBranch
4
- } from "./chunk-KZ3FDWUK.js";
4
+ } from "./chunk-GDTOV3DU.js";
5
5
  import {
6
6
  formatCompileResult,
7
7
  printGraftError
@@ -12,7 +12,7 @@ import {
12
12
  loadConfig,
13
13
  loadProjectEnv,
14
14
  requireDatabaseUrl
15
- } from "./chunk-BUPFXTUG.js";
15
+ } from "./chunk-2NH6KYTL.js";
16
16
 
17
17
  // src/commands/dev.ts
18
18
  import { existsSync, watch } from "fs";
@@ -42,6 +42,7 @@ async function devCommand(options) {
42
42
  const branch = control !== void 0 && url !== void 0 ? await resolveBranchHandle(control.db, options.branchId ?? "main", { databaseUrl: url }) : void 0;
43
43
  const writeBranch = branch !== void 0 ? scopeWriteBranch(branch.scope) : "main";
44
44
  let timer;
45
+ let rewatchContent;
45
46
  let compiling = false;
46
47
  let dirty = false;
47
48
  let configDirty = false;
@@ -54,17 +55,21 @@ async function devCommand(options) {
54
55
  try {
55
56
  if (configDirty) {
56
57
  configDirty = false;
58
+ const previousContentDir = config.contentDir;
57
59
  config = await loadConfig(configPath);
58
60
  console.log(`reloaded ${basename(configPath)}`);
61
+ if (config.contentDir !== previousContentDir) rewatchContent?.(config.contentDir);
59
62
  }
60
63
  const result = branch !== void 0 ? await compile({
61
64
  db: branch.db,
62
65
  contentDir: config.contentDir,
63
66
  collections: config.collections,
67
+ mdxTrust: config.mdxTrust,
64
68
  branchId: writeBranch
65
69
  }) : await compileStatic({
66
70
  contentDir: config.contentDir,
67
71
  collections: config.collections,
72
+ mdxTrust: config.mdxTrust,
68
73
  indexPath: config.index.path
69
74
  });
70
75
  console.log(formatCompileResult(result));
@@ -85,12 +90,17 @@ async function devCommand(options) {
85
90
  timer = setTimeout(() => void runCompile(), DEBOUNCE_MS);
86
91
  };
87
92
  await runCompile();
93
+ let contentWatcher = watch(config.contentDir, { recursive: true }, () => schedule());
88
94
  const watchers = [
89
- watch(config.contentDir, { recursive: true }, () => schedule()),
90
95
  watch(config.projectDir, (_event, filename) => {
91
96
  if (filename && CONFIG_FILENAMES.includes(filename)) schedule(true);
92
97
  })
93
98
  ];
99
+ rewatchContent = (dir) => {
100
+ contentWatcher.close();
101
+ contentWatcher = watch(dir, { recursive: true }, () => schedule());
102
+ console.log(`now watching ${relative(options.cwd, dir) || "."}`);
103
+ };
94
104
  const watchedContent = relative(options.cwd, config.contentDir) || ".";
95
105
  const branchLabel = branch === void 0 ? "static index" : branch.scope.kind === "physical" ? `${branch.name} (neon)` : options.branchId ?? "main";
96
106
  console.log(
@@ -99,6 +109,7 @@ async function devCommand(options) {
99
109
  await new Promise((resolveStopped) => {
100
110
  const stop = () => {
101
111
  clearTimeout(timer);
112
+ contentWatcher.close();
102
113
  for (const watcher of watchers) watcher.close();
103
114
  resolveStopped();
104
115
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  loadProjectEnv,
4
4
  requireDatabaseUrl
5
- } from "./chunk-BUPFXTUG.js";
5
+ } from "./chunk-2NH6KYTL.js";
6
6
 
7
7
  // src/commands/harden.ts
8
8
  async function hardenCommand(options) {
package/dist/index.js CHANGED
@@ -156,7 +156,7 @@ async function run(argv, options = {}) {
156
156
  const args = parseArgs(rest);
157
157
  switch (command) {
158
158
  case "init": {
159
- const { initCommand } = await import("./init-3H2IP5US.js");
159
+ const { initCommand } = await import("./init-TRPSGUYW.js");
160
160
  const result = initCommand({
161
161
  targetDir: args.positionals[0] ?? cwd,
162
162
  driver: args.initDriver
@@ -187,12 +187,12 @@ async function run(argv, options = {}) {
187
187
  return 0;
188
188
  }
189
189
  case "compile": {
190
- const { compileCommand } = await import("./compile-AAUC4G7H.js");
190
+ const { compileCommand } = await import("./compile-TCBGZZAU.js");
191
191
  await compileCommand({ cwd, branchId: args.branchId, pruneUnknown: args.pruneUnknown });
192
192
  return 0;
193
193
  }
194
194
  case "dev": {
195
- const { devCommand } = await import("./dev-MX7TQOJ2.js");
195
+ const { devCommand } = await import("./dev-4YSMWIAS.js");
196
196
  await devCommand({ cwd, branchId: args.branchId });
197
197
  return 0;
198
198
  }
@@ -203,7 +203,7 @@ async function run(argv, options = {}) {
203
203
  subcommand ? `unknown db subcommand "${subcommand}" \u2014 the only one is \`graft db migrate\`` : "usage: graft db migrate [--dry-run]"
204
204
  );
205
205
  }
206
- const { dbMigrateCommand, formatMigrateResult } = await import("./db-VGDWG4QA.js");
206
+ const { dbMigrateCommand, formatMigrateResult } = await import("./db-WACSH3HN.js");
207
207
  const result = await dbMigrateCommand({ cwd, dryRun: args.dryRun });
208
208
  console.log(formatMigrateResult(result));
209
209
  return 0;
@@ -213,7 +213,7 @@ async function run(argv, options = {}) {
213
213
  if (subcommand !== "put" || !file) {
214
214
  throw new UsageError("usage: graft asset put <file> [key]");
215
215
  }
216
- const { assetPutCommand } = await import("./asset-FPEL463J.js");
216
+ const { assetPutCommand } = await import("./asset-DMNX5JBZ.js");
217
217
  const result = await assetPutCommand({ cwd, file, key });
218
218
  console.log(`Uploaded ${result.key} (${result.contentType}, ${result.bytes} bytes)`);
219
219
  console.log(
@@ -228,7 +228,7 @@ async function run(argv, options = {}) {
228
228
  return 0;
229
229
  }
230
230
  case "approvals": {
231
- const { approvalsListCommand, formatApproval } = await import("./approvals-Z3EOK5AM.js");
231
+ const { approvalsListCommand, formatApproval } = await import("./approvals-MN3IS3FO.js");
232
232
  const pending = await approvalsListCommand({ cwd });
233
233
  if (pending.length === 0) {
234
234
  console.log("No pending approvals.");
@@ -245,7 +245,7 @@ async function run(argv, options = {}) {
245
245
  case "deny": {
246
246
  const id = args.positionals[0];
247
247
  if (!id) throw new UsageError(`usage: graft ${command} <approval-id>`);
248
- const { decideCommand } = await import("./approvals-Z3EOK5AM.js");
248
+ const { decideCommand } = await import("./approvals-MN3IS3FO.js");
249
249
  const decision = command === "approve" ? "approved" : "denied";
250
250
  const row = await decideCommand({ cwd, id, decision });
251
251
  console.log(
@@ -259,7 +259,7 @@ async function run(argv, options = {}) {
259
259
  return 0;
260
260
  }
261
261
  case "migrate": {
262
- const { migrateCommand } = await import("./migrate-RRW4Z6RB.js");
262
+ const { migrateCommand } = await import("./migrate-QB44VL6R.js");
263
263
  await migrateCommand({ cwd, branchId: args.branchId, apply: args.apply });
264
264
  return 0;
265
265
  }
@@ -273,7 +273,7 @@ async function run(argv, options = {}) {
273
273
  if (subcommand && !name) {
274
274
  throw new UsageError(`usage: graft branch ${subcommand} <name>`);
275
275
  }
276
- const mod = await import("./branch-HRG46YIL.js");
276
+ const mod = await import("./branch-IDY42TAV.js");
277
277
  if (!subcommand) {
278
278
  const rows = await mod.branchListCommand({ cwd });
279
279
  if (rows.length === 0) {
@@ -316,12 +316,12 @@ async function run(argv, options = {}) {
316
316
  const branch = args.positionals[0];
317
317
  if (!branch)
318
318
  throw new UsageError("usage: graft merge <branch> [--into <target>] [--apply]");
319
- const { mergeCommand } = await import("./merge-CZUNMMB5.js");
319
+ const { mergeCommand } = await import("./merge-XMTFPLHO.js");
320
320
  await mergeCommand({ cwd, branch, into: args.into, apply: args.apply });
321
321
  return 0;
322
322
  }
323
323
  case "add": {
324
- const { addCommand } = await import("./add-UBAZMSP7.js");
324
+ const { addCommand } = await import("./add-ASXTMYCX.js");
325
325
  await addCommand({
326
326
  cwd,
327
327
  names: args.positionals,
@@ -331,12 +331,12 @@ async function run(argv, options = {}) {
331
331
  return 0;
332
332
  }
333
333
  case "mcp": {
334
- const { mcpCommand } = await import("./mcp-US3QFLOR.js");
334
+ const { mcpCommand } = await import("./mcp-LUJMTAQO.js");
335
335
  await mcpCommand({ cwd, branchId: args.branchId });
336
336
  return 0;
337
337
  }
338
338
  case "serve": {
339
- const { serveCommand } = await import("./serve-Q63UTTER.js");
339
+ const { serveCommand } = await import("./serve-ZGXLEAEB.js");
340
340
  await serveCommand({
341
341
  cwd,
342
342
  branchId: args.branchId,
@@ -347,7 +347,7 @@ async function run(argv, options = {}) {
347
347
  return 0;
348
348
  }
349
349
  case "studio": {
350
- const { studioCommand } = await import("./studio-TNPOB665.js");
350
+ const { studioCommand } = await import("./studio-HXVSO4WJ.js");
351
351
  await studioCommand({
352
352
  cwd,
353
353
  branchId: args.branchId,
@@ -357,7 +357,7 @@ async function run(argv, options = {}) {
357
357
  return 0;
358
358
  }
359
359
  case "content": {
360
- const { contentListCommand, formatContentLine } = await import("./content-DY2YI3LT.js");
360
+ const { contentListCommand, formatContentLine } = await import("./content-VLKNYIHL.js");
361
361
  const { branch, lines } = await contentListCommand({ cwd, branchId: args.branchId });
362
362
  if (lines.length === 0) {
363
363
  console.log(`No content on branch "${branch}" (run graft compile).`);
@@ -369,7 +369,7 @@ async function run(argv, options = {}) {
369
369
  return 0;
370
370
  }
371
371
  case "compilations": {
372
- const { compilationsListCommand, formatCompilation } = await import("./compilations-6P45MPAE.js");
372
+ const { compilationsListCommand, formatCompilation } = await import("./compilations-72DJ2VEP.js");
373
373
  const rows = await compilationsListCommand({ cwd, branchId: args.branchId });
374
374
  if (rows.length === 0) {
375
375
  console.log("No compilations recorded yet (run graft compile).");
@@ -381,7 +381,7 @@ async function run(argv, options = {}) {
381
381
  case "harden": {
382
382
  const role = args.positionals[0];
383
383
  if (!role) throw new UsageError("usage: graft harden <role>");
384
- const { hardenCommand } = await import("./harden-HQHONZPH.js");
384
+ const { hardenCommand } = await import("./harden-QASY2KMI.js");
385
385
  const result = await hardenCommand({ cwd, role });
386
386
  console.log(`Hardened runtime role "${result.role}" (${result.statements.length} grants):`);
387
387
  for (const statement of result.statements) console.log(` ${statement}`);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG_FILENAMES
4
- } from "./chunk-BUPFXTUG.js";
4
+ } from "./chunk-2NH6KYTL.js";
5
5
 
6
6
  // src/commands/init.ts
7
7
  import { existsSync, mkdirSync, writeFileSync } from "fs";
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNoStaticBranch
4
- } from "./chunk-KZ3FDWUK.js";
4
+ } from "./chunk-GDTOV3DU.js";
5
5
  import "./chunk-GPA66AP5.js";
6
6
  import {
7
7
  findConfig,
8
8
  loadConfig,
9
9
  loadProjectEnv,
10
10
  requireDatabaseUrl
11
- } from "./chunk-BUPFXTUG.js";
11
+ } from "./chunk-2NH6KYTL.js";
12
12
 
13
13
  // src/commands/mcp.ts
14
14
  import { createActorResolver } from "@usegraft/auth";
@@ -25,21 +25,25 @@ async function mcpCommand(options) {
25
25
  try {
26
26
  const devToken = process.env.GRAFT_DEV_TOKEN;
27
27
  const scopes = (process.env.GRAFT_DEV_SCOPES ?? "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
28
+ const cliActor = {
29
+ kind: "agent",
30
+ id: "graft-cli",
31
+ scopes: [.../* @__PURE__ */ new Set(["content:write", ...scopes])]
32
+ };
28
33
  const resolveActor = createActorResolver({
29
34
  issuers: [],
30
- devTokens: devToken ? {
31
- [devToken]: {
32
- kind: "agent",
33
- id: "graft-cli",
34
- scopes
35
- }
36
- } : void 0
35
+ devTokens: devToken ? { [devToken]: cliActor } : void 0
37
36
  });
37
+ const resolveStdioActor = async (request) => {
38
+ const resolved = await resolveActor(request);
39
+ return resolved.kind === "anonymous" ? cliActor : resolved;
40
+ };
38
41
  const server = createGraftMcp({
39
42
  name: "graft",
40
43
  contentDir: config.contentDir,
41
44
  collections: config.collections,
42
45
  functions: config.functions,
46
+ mdxTrust: config.mdxTrust,
43
47
  ...branch !== void 0 ? {
44
48
  db: branch.db,
45
49
  branchId: scopeWriteBranch(branch.scope),
@@ -47,7 +51,11 @@ async function mcpCommand(options) {
47
51
  // overlay branch searches its full parent chain, not just its own rows.
48
52
  scope: branch.scope
49
53
  } : { staticIndexPath: config.index.path },
50
- actor: resolveActor,
54
+ actor: resolveStdioActor,
55
+ connectionActor: cliActor,
56
+ // Local agents may upload a file from the project; remote mounts get no
57
+ // `path` argument at all.
58
+ localUploadRoot: config.projectDir,
51
59
  defaultAuthorization: devToken
52
60
  });
53
61
  await serveStdio(server);
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  discoverMigrations
4
- } from "./chunk-LTFTDG6Z.js";
4
+ } from "./chunk-6Z27SGVO.js";
5
5
  import {
6
6
  findConfig,
7
7
  loadConfig,
8
8
  loadProjectEnv,
9
9
  requireDatabaseUrl
10
- } from "./chunk-BUPFXTUG.js";
10
+ } from "./chunk-2NH6KYTL.js";
11
11
 
12
12
  // src/commands/merge.ts
13
13
  import { basename } from "path";
@@ -144,6 +144,7 @@ async function mergeCommand(options) {
144
144
  db: target.db,
145
145
  contentDir: config.contentDir,
146
146
  collections: config.collections,
147
+ mdxTrust: config.mdxTrust,
147
148
  branchId: targetWrite,
148
149
  gitSha
149
150
  });
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  discoverMigrations,
4
4
  migrateCommand
5
- } from "./chunk-LTFTDG6Z.js";
6
- import "./chunk-BUPFXTUG.js";
5
+ } from "./chunk-6Z27SGVO.js";
6
+ import "./chunk-2NH6KYTL.js";
7
7
  export {
8
8
  discoverMigrations,
9
9
  migrateCommand
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ allowedHostsFor,
3
4
  createNodeListener,
4
5
  createServeRouter,
5
6
  serveCommand,
6
7
  startServe
7
- } from "./chunk-TUWL7WFD.js";
8
- import "./chunk-BUPFXTUG.js";
8
+ } from "./chunk-U25NVB6W.js";
9
+ import "./chunk-2NH6KYTL.js";
9
10
  export {
11
+ allowedHostsFor,
10
12
  createNodeListener,
11
13
  createServeRouter,
12
14
  serveCommand,
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ allowedHostsFor,
3
4
  createNodeListener
4
- } from "./chunk-TUWL7WFD.js";
5
+ } from "./chunk-U25NVB6W.js";
5
6
  import {
6
7
  findConfig,
7
8
  loadConfig,
8
9
  loadProjectEnv,
9
10
  requireDatabaseUrl
10
- } from "./chunk-BUPFXTUG.js";
11
+ } from "./chunk-2NH6KYTL.js";
11
12
 
12
13
  // src/commands/studio.ts
13
14
  import { execFile } from "child_process";
@@ -17,11 +18,11 @@ import { GraftError } from "@usegraft/contracts";
17
18
  function isLoopback(host) {
18
19
  return host === "127.0.0.1" || host === "localhost" || host === "::1";
19
20
  }
20
- function operatorName() {
21
+ function operatorIdentity() {
21
22
  try {
22
- return userInfo().username;
23
+ return { kind: "human", id: userInfo().username };
23
24
  } catch {
24
- return process.env.USERNAME ?? process.env.USER ?? "operator";
25
+ return { kind: "human", id: process.env.USERNAME ?? process.env.USER ?? "operator" };
25
26
  }
26
27
  }
27
28
  function openBrowser(url) {
@@ -58,19 +59,24 @@ async function studioCommand(options) {
58
59
  "[graft studio] WARNING: binding beyond loopback with no GRAFT_DEV_TOKEN \u2014 set a bearer token before exposing Studio to a network."
59
60
  );
60
61
  }
61
- const authorize = !loopback && devToken ? (request) => {
62
+ const authenticate = !loopback && devToken ? (request) => {
62
63
  const header = request.headers.get("authorization") ?? "";
63
- return header === `Bearer ${devToken}`;
64
- } : !loopback ? () => false : void 0;
64
+ if (header !== `Bearer ${devToken}`) return null;
65
+ return {
66
+ ...operatorIdentity(),
67
+ scopes: ["studio:read", "studio:write", "approvals:decide"]
68
+ };
69
+ } : !loopback ? () => null : void 0;
65
70
  const handler = createStudioHandler({
66
71
  db: branch.db,
67
72
  collections: config.collections,
68
73
  contentDir: config.contentDir,
74
+ mdxTrust: config.mdxTrust,
69
75
  defaultBranch: writeBranch,
70
- decidedBy: operatorName,
71
- authorize
76
+ decider: operatorIdentity,
77
+ authenticate
72
78
  });
73
- const server = createServer(createNodeListener(handler));
79
+ const server = createServer(createNodeListener(handler, { allowedHosts: allowedHostsFor(host) }));
74
80
  await new Promise((resolve, reject) => {
75
81
  server.once("error", reject);
76
82
  server.listen(requestedPort, host, () => resolve());
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@usegraft/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
+ "description": "The graft CLI: scaffold, compile authored content, serve the runtime, and split your database credentials.",
5
+ "keywords": [
6
+ "agent",
7
+ "ai",
8
+ "cli",
9
+ "cms",
10
+ "content",
11
+ "graft",
12
+ "headless-cms",
13
+ "mcp",
14
+ "postgres",
15
+ "scaffolding",
16
+ "static-site",
17
+ "typescript"
18
+ ],
19
+ "homepage": "https://github.com/AndersonDesign1/graft#readme",
4
20
  "license": "MIT",
5
21
  "repository": {
6
22
  "type": "git",
@@ -18,20 +34,21 @@
18
34
  "access": "public"
19
35
  },
20
36
  "dependencies": {
21
- "@usegraft/assets": "0.1.1",
22
- "@usegraft/auth": "0.1.1",
23
- "@usegraft/compiler": "0.1.1",
24
- "@usegraft/content-migrations": "0.1.1",
25
- "@usegraft/contracts": "0.1.1",
26
- "@usegraft/core": "0.1.1",
27
- "@usegraft/db": "0.1.1",
28
- "@usegraft/mcp": "0.1.1",
29
- "@usegraft/registry": "0.1.1",
30
- "@usegraft/studio": "0.1.1",
37
+ "@usegraft/assets": "0.2.0",
38
+ "@usegraft/auth": "0.2.0",
39
+ "@usegraft/compiler": "0.2.0",
40
+ "@usegraft/content-migrations": "0.2.0",
41
+ "@usegraft/contracts": "0.2.0",
42
+ "@usegraft/core": "0.2.0",
43
+ "@usegraft/db": "0.2.0",
44
+ "@usegraft/mcp": "0.2.0",
45
+ "@usegraft/mdx-safety": "0.2.0",
46
+ "@usegraft/registry": "0.2.0",
47
+ "@usegraft/studio": "0.2.0",
31
48
  "jiti": "^2.4.0"
32
49
  },
33
50
  "devDependencies": {
34
- "@usegraft/sdk-core": "0.1.1"
51
+ "@usegraft/sdk-core": "0.2.0"
35
52
  },
36
53
  "engines": {
37
54
  "node": ">=22.16"
@@ -40,7 +57,6 @@
40
57
  "build": "tsup",
41
58
  "dev": "tsup --watch",
42
59
  "typecheck": "tsc --noEmit",
43
- "test": "vitest run --passWithNoTests",
44
- "lint": "oxlint ."
60
+ "test": "vitest run --passWithNoTests"
45
61
  }
46
62
  }