@usegraft/cli 0.1.0 → 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
@@ -4,7 +4,8 @@ import {
4
4
  } from "./chunk-GPA66AP5.js";
5
5
 
6
6
  // src/cli.ts
7
- var VERSION = "0.0.0";
7
+ import { createRequire } from "module";
8
+ var VERSION = createRequire(import.meta.url)("../package.json").version;
8
9
  var PLANNED = [];
9
10
  function printHelp() {
10
11
  const lines = [
@@ -155,7 +156,7 @@ async function run(argv, options = {}) {
155
156
  const args = parseArgs(rest);
156
157
  switch (command) {
157
158
  case "init": {
158
- const { initCommand } = await import("./init-3H2IP5US.js");
159
+ const { initCommand } = await import("./init-TRPSGUYW.js");
159
160
  const result = initCommand({
160
161
  targetDir: args.positionals[0] ?? cwd,
161
162
  driver: args.initDriver
@@ -186,12 +187,12 @@ async function run(argv, options = {}) {
186
187
  return 0;
187
188
  }
188
189
  case "compile": {
189
- const { compileCommand } = await import("./compile-AAUC4G7H.js");
190
+ const { compileCommand } = await import("./compile-TCBGZZAU.js");
190
191
  await compileCommand({ cwd, branchId: args.branchId, pruneUnknown: args.pruneUnknown });
191
192
  return 0;
192
193
  }
193
194
  case "dev": {
194
- const { devCommand } = await import("./dev-MX7TQOJ2.js");
195
+ const { devCommand } = await import("./dev-4YSMWIAS.js");
195
196
  await devCommand({ cwd, branchId: args.branchId });
196
197
  return 0;
197
198
  }
@@ -202,7 +203,7 @@ async function run(argv, options = {}) {
202
203
  subcommand ? `unknown db subcommand "${subcommand}" \u2014 the only one is \`graft db migrate\`` : "usage: graft db migrate [--dry-run]"
203
204
  );
204
205
  }
205
- const { dbMigrateCommand, formatMigrateResult } = await import("./db-VGDWG4QA.js");
206
+ const { dbMigrateCommand, formatMigrateResult } = await import("./db-WACSH3HN.js");
206
207
  const result = await dbMigrateCommand({ cwd, dryRun: args.dryRun });
207
208
  console.log(formatMigrateResult(result));
208
209
  return 0;
@@ -212,7 +213,7 @@ async function run(argv, options = {}) {
212
213
  if (subcommand !== "put" || !file) {
213
214
  throw new UsageError("usage: graft asset put <file> [key]");
214
215
  }
215
- const { assetPutCommand } = await import("./asset-FPEL463J.js");
216
+ const { assetPutCommand } = await import("./asset-DMNX5JBZ.js");
216
217
  const result = await assetPutCommand({ cwd, file, key });
217
218
  console.log(`Uploaded ${result.key} (${result.contentType}, ${result.bytes} bytes)`);
218
219
  console.log(
@@ -227,7 +228,7 @@ async function run(argv, options = {}) {
227
228
  return 0;
228
229
  }
229
230
  case "approvals": {
230
- const { approvalsListCommand, formatApproval } = await import("./approvals-Z3EOK5AM.js");
231
+ const { approvalsListCommand, formatApproval } = await import("./approvals-MN3IS3FO.js");
231
232
  const pending = await approvalsListCommand({ cwd });
232
233
  if (pending.length === 0) {
233
234
  console.log("No pending approvals.");
@@ -244,7 +245,7 @@ async function run(argv, options = {}) {
244
245
  case "deny": {
245
246
  const id = args.positionals[0];
246
247
  if (!id) throw new UsageError(`usage: graft ${command} <approval-id>`);
247
- const { decideCommand } = await import("./approvals-Z3EOK5AM.js");
248
+ const { decideCommand } = await import("./approvals-MN3IS3FO.js");
248
249
  const decision = command === "approve" ? "approved" : "denied";
249
250
  const row = await decideCommand({ cwd, id, decision });
250
251
  console.log(
@@ -258,7 +259,7 @@ async function run(argv, options = {}) {
258
259
  return 0;
259
260
  }
260
261
  case "migrate": {
261
- const { migrateCommand } = await import("./migrate-RRW4Z6RB.js");
262
+ const { migrateCommand } = await import("./migrate-QB44VL6R.js");
262
263
  await migrateCommand({ cwd, branchId: args.branchId, apply: args.apply });
263
264
  return 0;
264
265
  }
@@ -272,7 +273,7 @@ async function run(argv, options = {}) {
272
273
  if (subcommand && !name) {
273
274
  throw new UsageError(`usage: graft branch ${subcommand} <name>`);
274
275
  }
275
- const mod = await import("./branch-HRG46YIL.js");
276
+ const mod = await import("./branch-IDY42TAV.js");
276
277
  if (!subcommand) {
277
278
  const rows = await mod.branchListCommand({ cwd });
278
279
  if (rows.length === 0) {
@@ -315,12 +316,12 @@ async function run(argv, options = {}) {
315
316
  const branch = args.positionals[0];
316
317
  if (!branch)
317
318
  throw new UsageError("usage: graft merge <branch> [--into <target>] [--apply]");
318
- const { mergeCommand } = await import("./merge-CZUNMMB5.js");
319
+ const { mergeCommand } = await import("./merge-XMTFPLHO.js");
319
320
  await mergeCommand({ cwd, branch, into: args.into, apply: args.apply });
320
321
  return 0;
321
322
  }
322
323
  case "add": {
323
- const { addCommand } = await import("./add-UBAZMSP7.js");
324
+ const { addCommand } = await import("./add-ASXTMYCX.js");
324
325
  await addCommand({
325
326
  cwd,
326
327
  names: args.positionals,
@@ -330,12 +331,12 @@ async function run(argv, options = {}) {
330
331
  return 0;
331
332
  }
332
333
  case "mcp": {
333
- const { mcpCommand } = await import("./mcp-US3QFLOR.js");
334
+ const { mcpCommand } = await import("./mcp-LUJMTAQO.js");
334
335
  await mcpCommand({ cwd, branchId: args.branchId });
335
336
  return 0;
336
337
  }
337
338
  case "serve": {
338
- const { serveCommand } = await import("./serve-Q63UTTER.js");
339
+ const { serveCommand } = await import("./serve-ZGXLEAEB.js");
339
340
  await serveCommand({
340
341
  cwd,
341
342
  branchId: args.branchId,
@@ -346,7 +347,7 @@ async function run(argv, options = {}) {
346
347
  return 0;
347
348
  }
348
349
  case "studio": {
349
- const { studioCommand } = await import("./studio-TNPOB665.js");
350
+ const { studioCommand } = await import("./studio-HXVSO4WJ.js");
350
351
  await studioCommand({
351
352
  cwd,
352
353
  branchId: args.branchId,
@@ -356,7 +357,7 @@ async function run(argv, options = {}) {
356
357
  return 0;
357
358
  }
358
359
  case "content": {
359
- const { contentListCommand, formatContentLine } = await import("./content-DY2YI3LT.js");
360
+ const { contentListCommand, formatContentLine } = await import("./content-VLKNYIHL.js");
360
361
  const { branch, lines } = await contentListCommand({ cwd, branchId: args.branchId });
361
362
  if (lines.length === 0) {
362
363
  console.log(`No content on branch "${branch}" (run graft compile).`);
@@ -368,7 +369,7 @@ async function run(argv, options = {}) {
368
369
  return 0;
369
370
  }
370
371
  case "compilations": {
371
- const { compilationsListCommand, formatCompilation } = await import("./compilations-6P45MPAE.js");
372
+ const { compilationsListCommand, formatCompilation } = await import("./compilations-72DJ2VEP.js");
372
373
  const rows = await compilationsListCommand({ cwd, branchId: args.branchId });
373
374
  if (rows.length === 0) {
374
375
  console.log("No compilations recorded yet (run graft compile).");
@@ -380,7 +381,7 @@ async function run(argv, options = {}) {
380
381
  case "harden": {
381
382
  const role = args.positionals[0];
382
383
  if (!role) throw new UsageError("usage: graft harden <role>");
383
- const { hardenCommand } = await import("./harden-HQHONZPH.js");
384
+ const { hardenCommand } = await import("./harden-QASY2KMI.js");
384
385
  const result = await hardenCommand({ cwd, role });
385
386
  console.log(`Hardened runtime role "${result.role}" (${result.statements.length} grants):`);
386
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.0",
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
- "jiti": "^2.4.0",
22
- "@usegraft/content-migrations": "0.1.0",
23
- "@usegraft/compiler": "0.1.0",
24
- "@usegraft/auth": "0.1.0",
25
- "@usegraft/assets": "0.1.0",
26
- "@usegraft/core": "0.1.0",
27
- "@usegraft/contracts": "0.1.0",
28
- "@usegraft/db": "0.1.0",
29
- "@usegraft/mcp": "0.1.0",
30
- "@usegraft/registry": "0.1.0",
31
- "@usegraft/studio": "0.1.0"
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",
48
+ "jiti": "^2.4.0"
32
49
  },
33
50
  "devDependencies": {
34
- "@usegraft/sdk-core": "0.1.0"
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
  }