@usegraft/cli 0.1.1 → 1.0.0-beta.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 +64 -0
- package/dist/{add-UBAZMSP7.js → add-TCQY3YAN.js} +1 -1
- package/dist/approvals-4PAANLA4.js +14 -0
- package/dist/{asset-FPEL463J.js → asset-JWER6AOG.js} +1 -1
- package/dist/{branch-HRG46YIL.js → branch-U7FFDTFV.js} +1 -1
- package/dist/{chunk-BUPFXTUG.js → chunk-2LG6TWYY.js} +28 -5
- package/dist/{chunk-KZ3FDWUK.js → chunk-2N3HXYNN.js} +3 -1
- package/dist/{chunk-TUWL7WFD.js → chunk-CONTLKSZ.js} +99 -17
- package/dist/{chunk-LTFTDG6Z.js → chunk-DW6WTDRX.js} +2 -1
- package/dist/{approvals-Z3EOK5AM.js → chunk-NBJ4N4O6.js} +7 -7
- package/dist/{compilations-6P45MPAE.js → compilations-CH6AIOFG.js} +1 -1
- package/dist/{compile-AAUC4G7H.js → compile-AWPSEA7U.js} +2 -2
- package/dist/{content-DY2YI3LT.js → content-ZULMTJMD.js} +1 -1
- package/dist/{db-VGDWG4QA.js → db-5YMFKHGO.js} +1 -1
- package/dist/{dev-MX7TQOJ2.js → dev-WHTIRWMR.js} +14 -3
- package/dist/{harden-HQHONZPH.js → harden-QONL2MAO.js} +1 -1
- package/dist/index.js +27 -18
- package/dist/{init-3H2IP5US.js → init-FV7Q6VXM.js} +1 -1
- package/dist/{mcp-US3QFLOR.js → mcp-77QFFJXK.js} +24 -11
- package/dist/{merge-CZUNMMB5.js → merge-ZWYZJVFE.js} +3 -2
- package/dist/{migrate-RRW4Z6RB.js → migrate-6ZYG2H5C.js} +2 -2
- package/dist/{serve-Q63UTTER.js → serve-5ADUQG66.js} +4 -2
- package/dist/{studio-TNPOB665.js → studio-7VAYFEWS.js} +17 -11
- package/package.json +31 -14
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
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 @usegraft/cli init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Once installed, the command is `graft`. Run it without installing under the
|
|
16
|
+
full package name: the bare name `graft` on npm belongs to an unrelated
|
|
17
|
+
package, so `npx graft` fetches the wrong thing.
|
|
18
|
+
|
|
19
|
+
## Quickstart, with no database
|
|
20
|
+
|
|
21
|
+
A content project needs no services at all. `graft compile` writes the index to a SQLite artifact your app reads embedded.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @usegraft/cli init # scaffolds graft.config.ts, content/, llms.txt
|
|
25
|
+
npx @usegraft/cli compile # → .graft/index.db
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Move to Postgres by setting `DATABASE_URL` and changing one line in `graft.config.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export const index = "postgres";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Does |
|
|
37
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
|
|
38
|
+
| `graft init` | Scaffold a project |
|
|
39
|
+
| `graft compile` | Validate authored MDX and project it into the index |
|
|
40
|
+
| `graft dev` | Recompile on save |
|
|
41
|
+
| `graft serve` | Run the headless runtime: typed functions, MCP, authored-content reads, `/healthz` |
|
|
42
|
+
| `graft studio` | Open the optional editing UI |
|
|
43
|
+
| `graft mcp` | Serve MCP on stdio, for `.mcp.json` |
|
|
44
|
+
| `graft add <item>` | Copy a registry primitive into your repo, shadcn-style |
|
|
45
|
+
| `graft approve <id>` | Decide a pending approval. Operator credential only |
|
|
46
|
+
| `graft harden <role>` | Grant a Postgres role exactly what a deployment needs, and nothing that can decide an approval |
|
|
47
|
+
| `graft branch` / `graft merge` | Copy-on-write content branches |
|
|
48
|
+
| `graft migrate` | Run content codemods when the schema changes |
|
|
49
|
+
|
|
50
|
+
Run `graft <command> --help` for flags.
|
|
51
|
+
|
|
52
|
+
## Two credentials
|
|
53
|
+
|
|
54
|
+
`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.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
graft harden graft_runtime
|
|
58
|
+
# deploy with the runtime role's URL as DATABASE_URL
|
|
59
|
+
# keep the operator URL for compile, migrate, merge and approve
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/cli/CHANGELOG.md) · [Security policy](https://github.com/AndersonDesign1/graft/blob/main/SECURITY.md)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
approvalsListCommand,
|
|
4
|
+
decideCommand,
|
|
5
|
+
formatApproval,
|
|
6
|
+
operatorName
|
|
7
|
+
} from "./chunk-NBJ4N4O6.js";
|
|
8
|
+
import "./chunk-2LG6TWYY.js";
|
|
9
|
+
export {
|
|
10
|
+
approvalsListCommand,
|
|
11
|
+
decideCommand,
|
|
12
|
+
formatApproval,
|
|
13
|
+
operatorName
|
|
14
|
+
};
|
|
@@ -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,8 @@ 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);
|
|
101
|
+
const approvalPolicy = parseApprovalPolicy(mod.approvalPolicy, configPath);
|
|
100
102
|
const contentDirSetting = typeof mod.contentDir === "string" ? mod.contentDir : "content";
|
|
101
103
|
const migrationsDirSetting = typeof mod.migrationsDir === "string" ? mod.migrationsDir : "migrations";
|
|
102
104
|
return {
|
|
@@ -106,9 +108,31 @@ async function loadConfig(configPath) {
|
|
|
106
108
|
migrationsDir: resolve(projectDir, migrationsDirSetting),
|
|
107
109
|
index,
|
|
108
110
|
collections,
|
|
109
|
-
functions
|
|
111
|
+
functions,
|
|
112
|
+
mdxTrust,
|
|
113
|
+
approvalPolicy
|
|
110
114
|
};
|
|
111
115
|
}
|
|
116
|
+
function parseMdxTrust(value, configPath) {
|
|
117
|
+
if (value === void 0) return "restricted";
|
|
118
|
+
if (value === "restricted" || value === "full") return value;
|
|
119
|
+
throw new GraftError({
|
|
120
|
+
code: "CONFIG_INVALID",
|
|
121
|
+
message: `${configPath} exports \`mdxTrust\` as ${JSON.stringify(value)}, which is not "restricted" or "full".`,
|
|
122
|
+
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.',
|
|
123
|
+
details: { mdxTrust: value }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function parseApprovalPolicy(value, configPath) {
|
|
127
|
+
if (value === void 0) return "none";
|
|
128
|
+
if (value === "none" || value === "human" || value === "unattended") return value;
|
|
129
|
+
throw new GraftError({
|
|
130
|
+
code: "CONFIG_INVALID",
|
|
131
|
+
message: `${configPath} exports \`approvalPolicy\` as ${JSON.stringify(value)}, which is not "none", "human" or "unattended".`,
|
|
132
|
+
fix: 'Export `approvalPolicy = "none"` (the default: only `destructive` functions are gated), `"human"` (every mutation is gated), or `"unattended"` (nothing is gated, for a caller with no human behind it). Omit the export to keep the default.',
|
|
133
|
+
details: { approvalPolicy: value }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
112
136
|
function assertStaticSupports(index, collections, functions, configPath) {
|
|
113
137
|
if (index.driver !== "static") return;
|
|
114
138
|
const dbCollections = Object.values(collections).filter((collection) => collection.authority === "db-authoritative").map((collection) => collection.name);
|
|
@@ -125,18 +149,17 @@ function assertStaticSupports(index, collections, functions, configPath) {
|
|
|
125
149
|
details: { dbCollections, functions: functionNames }
|
|
126
150
|
});
|
|
127
151
|
}
|
|
128
|
-
var STATIC_INDEX_DEFAULT = ".graft/index.db";
|
|
129
152
|
function parseIndexConfig(value, projectDir, configPath) {
|
|
130
153
|
if (value === void 0 || value === "postgres") return { driver: "postgres" };
|
|
131
154
|
if (value === "static") {
|
|
132
|
-
return { driver: "static", path: resolve(projectDir,
|
|
155
|
+
return { driver: "static", path: resolve(projectDir, STATIC_INDEX_DEFAULT_PATH) };
|
|
133
156
|
}
|
|
134
157
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
135
158
|
const driver = value.driver;
|
|
136
159
|
const path = value.path;
|
|
137
160
|
if (driver === "postgres") return { driver: "postgres" };
|
|
138
161
|
if (driver === "static" && (path === void 0 || typeof path === "string")) {
|
|
139
|
-
return { driver: "static", path: resolve(projectDir, path ??
|
|
162
|
+
return { driver: "static", path: resolve(projectDir, path ?? STATIC_INDEX_DEFAULT_PATH) };
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
throw new GraftError({
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadConfig,
|
|
8
8
|
loadProjectEnv,
|
|
9
9
|
requireDatabaseUrl
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2LG6TWYY.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,19 +4,23 @@ import {
|
|
|
4
4
|
loadConfig,
|
|
5
5
|
loadProjectEnv,
|
|
6
6
|
requireDatabaseUrl
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2LG6TWYY.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);
|
|
17
18
|
if (pathname === "/healthz") return routes.health(request);
|
|
18
19
|
if (pathname === "/api/mcp") return routes.mcp(request);
|
|
19
20
|
if (pathname === "/api/fn" || pathname.startsWith("/api/fn/")) return routes.fn(request);
|
|
21
|
+
if (pathname === "/api/content/v1" || pathname.startsWith("/api/content/v1/")) {
|
|
22
|
+
return routes.content(request);
|
|
23
|
+
}
|
|
20
24
|
if (routes.studio && (pathname === "/studio" || pathname.startsWith("/studio/") || pathname.startsWith("/api/studio/"))) {
|
|
21
25
|
return routes.studio(request);
|
|
22
26
|
}
|
|
@@ -24,7 +28,7 @@ function createServeRouter(routes) {
|
|
|
24
28
|
const error = new GraftError({
|
|
25
29
|
code: "ROUTE_NOT_FOUND",
|
|
26
30
|
message: `Nothing is mounted at ${pathname}.`,
|
|
27
|
-
fix: `Use POST /api/fn/<name> (typed functions), POST /api/mcp (MCP Streamable HTTP), or GET /healthz${studioHint}.`,
|
|
31
|
+
fix: `Use POST /api/fn/<name> (typed functions), POST /api/mcp (MCP Streamable HTTP), GET /api/content/v1/documents (authored content), GET /api/content/v1/search, or GET /healthz${studioHint}.`,
|
|
28
32
|
details: { pathname }
|
|
29
33
|
});
|
|
30
34
|
return new Response(JSON.stringify(error.toJSON()), {
|
|
@@ -41,9 +45,34 @@ var HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
|
41
45
|
"upgrade",
|
|
42
46
|
"proxy-connection"
|
|
43
47
|
]);
|
|
44
|
-
function
|
|
48
|
+
function allowedHostsFor(host) {
|
|
49
|
+
if (host === "0.0.0.0" || host === "::" || host === "") return void 0;
|
|
50
|
+
if (host === "127.0.0.1" || host === "localhost" || host === "::1") {
|
|
51
|
+
return ["127.0.0.1", "localhost", "[::1]"];
|
|
52
|
+
}
|
|
53
|
+
return [host];
|
|
54
|
+
}
|
|
55
|
+
function hostname(host) {
|
|
56
|
+
const trimmed = host.trim().toLowerCase();
|
|
57
|
+
if (trimmed.startsWith("[")) return trimmed.slice(0, trimmed.indexOf("]") + 1);
|
|
58
|
+
const colon = trimmed.lastIndexOf(":");
|
|
59
|
+
return colon === -1 ? trimmed : trimmed.slice(0, colon);
|
|
60
|
+
}
|
|
61
|
+
function createNodeListener(handler, options = {}) {
|
|
62
|
+
const allowed = options.allowedHosts?.map(hostname);
|
|
45
63
|
return async (req, res) => {
|
|
46
64
|
try {
|
|
65
|
+
if (allowed && !allowed.includes(hostname(req.headers.host ?? ""))) {
|
|
66
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
67
|
+
res.end(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
error: "INPUT_VALIDATION_FAILED",
|
|
70
|
+
message: `This server does not answer to Host "${req.headers.host ?? ""}".`,
|
|
71
|
+
fix: `Reach it at one of: ${allowed.join(", ")}. A mismatched Host is either a misconfigured proxy or a DNS-rebinding attempt.`
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
47
76
|
const chunks = [];
|
|
48
77
|
for await (const chunk of req) chunks.push(chunk);
|
|
49
78
|
const body = Buffer.concat(chunks);
|
|
@@ -59,6 +88,8 @@ function createNodeListener(handler) {
|
|
|
59
88
|
headers,
|
|
60
89
|
body: method === "GET" || method === "HEAD" || body.length === 0 ? void 0 : body
|
|
61
90
|
});
|
|
91
|
+
const peer = req.socket.remoteAddress;
|
|
92
|
+
if (peer) setRequestPeer(request, peer);
|
|
62
93
|
const response = await handler(request);
|
|
63
94
|
const outHeaders = {};
|
|
64
95
|
response.headers.forEach((value, key) => {
|
|
@@ -100,11 +131,13 @@ async function startServe(options) {
|
|
|
100
131
|
const [
|
|
101
132
|
{ createFunctionsHandler },
|
|
102
133
|
{ createGraftMcpHandler },
|
|
103
|
-
{
|
|
134
|
+
{ createContentApiHandler },
|
|
135
|
+
{ createDb, createDbIndexReader, resolveBranchHandle, scopeWriteBranch, sql },
|
|
104
136
|
studioMod
|
|
105
137
|
] = await Promise.all([
|
|
106
138
|
import("@usegraft/core"),
|
|
107
139
|
import("@usegraft/mcp"),
|
|
140
|
+
import("@usegraft/content-api"),
|
|
108
141
|
import("@usegraft/db"),
|
|
109
142
|
enableStudio ? import("@usegraft/studio") : Promise.resolve(null)
|
|
110
143
|
]);
|
|
@@ -119,8 +152,16 @@ async function startServe(options) {
|
|
|
119
152
|
issuers,
|
|
120
153
|
devTokens: devToken ? { [devToken]: { kind: "agent", id: "graft-serve", scopes } } : void 0
|
|
121
154
|
});
|
|
122
|
-
const approvalPolicy =
|
|
123
|
-
|
|
155
|
+
const approvalPolicy = config.approvalPolicy;
|
|
156
|
+
if (process.env.GRAFT_APPROVAL_POLICY) {
|
|
157
|
+
console.warn(
|
|
158
|
+
`[graft serve] GRAFT_APPROVAL_POLICY is set and is now ignored. Approval policy moved to graft.config.ts so that turning off the gate on destructive functions appears in a diff. Export \`approvalPolicy = "${process.env.GRAFT_APPROVAL_POLICY}"\` there instead. Serving with "${approvalPolicy}".`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
162
|
+
const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
163
|
+
const anonymousOptIn = process.env.GRAFT_MCP_ALLOW_ANONYMOUS === "1";
|
|
164
|
+
const allowAnonymousMcp = loopback || anonymousOptIn;
|
|
124
165
|
const functions = config.functions ?? {};
|
|
125
166
|
const gitSha = localGitSha(options.cwd);
|
|
126
167
|
const fnHandler = createFunctionsHandler({
|
|
@@ -138,11 +179,31 @@ async function startServe(options) {
|
|
|
138
179
|
contentDir: config.contentDir,
|
|
139
180
|
collections: config.collections,
|
|
140
181
|
functions,
|
|
182
|
+
mdxTrust: config.mdxTrust,
|
|
141
183
|
db: branch.db,
|
|
142
184
|
branchId: writeBranch,
|
|
143
185
|
scope: branch.scope,
|
|
144
186
|
actor: resolveActor,
|
|
145
|
-
|
|
187
|
+
allowAnonymous: allowAnonymousMcp,
|
|
188
|
+
// The same backstop the functions handler gets. Without it a function with
|
|
189
|
+
// no per-function rateLimit was capped over POST /api/fn and uncapped over
|
|
190
|
+
// run_function, so the transport decided the limit — and tools/functions.ts
|
|
191
|
+
// claims the two surfaces apply rate limits identically.
|
|
192
|
+
rateLimit: { limit: 60, windowSeconds: 60 }
|
|
193
|
+
});
|
|
194
|
+
const allowedOriginsRaw = process.env.GRAFT_CONTENT_ALLOWED_ORIGINS?.trim();
|
|
195
|
+
const allowedOrigins = allowedOriginsRaw === void 0 || allowedOriginsRaw === "" ? void 0 : allowedOriginsRaw === "*" ? "*" : allowedOriginsRaw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
196
|
+
const contentHandler = createContentApiHandler({
|
|
197
|
+
collections: Object.keys(config.collections),
|
|
198
|
+
branch: writeBranch,
|
|
199
|
+
index: createDbIndexReader(branch.db),
|
|
200
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins },
|
|
201
|
+
// The same backstop the functions handler gets, for the same reason. These
|
|
202
|
+
// routes authenticate nobody and run database listings and full-text
|
|
203
|
+
// searches, so without a limit here one anonymous caller can keep the index
|
|
204
|
+
// busy indefinitely — and `graft serve` is the mount most likely to face
|
|
205
|
+
// the open internet.
|
|
206
|
+
rateLimit: { limit: 60, windowSeconds: 60 }
|
|
146
207
|
});
|
|
147
208
|
const health = async () => {
|
|
148
209
|
try {
|
|
@@ -167,7 +228,6 @@ async function startServe(options) {
|
|
|
167
228
|
{ status: 200, headers: { "content-type": "application/json" } }
|
|
168
229
|
);
|
|
169
230
|
};
|
|
170
|
-
const host = options.host ?? process.env.HOST ?? "127.0.0.1";
|
|
171
231
|
const requestedPort = options.port ?? Number(process.env.PORT ?? 3903);
|
|
172
232
|
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
|
|
173
233
|
await branch.close();
|
|
@@ -178,31 +238,50 @@ async function startServe(options) {
|
|
|
178
238
|
fix: "Pass --port <0-65535> (0 picks a free port) or set PORT."
|
|
179
239
|
});
|
|
180
240
|
}
|
|
181
|
-
|
|
182
|
-
|
|
241
|
+
if (!loopback && anonymousOptIn) {
|
|
242
|
+
console.warn(
|
|
243
|
+
"[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."
|
|
244
|
+
);
|
|
245
|
+
} else if (!loopback && issuers.length === 0 && !devToken) {
|
|
183
246
|
console.warn(
|
|
184
|
-
"[graft serve] WARNING: binding beyond loopback with no identity configured \u2014
|
|
247
|
+
"[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."
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (approvalPolicy === "unattended") {
|
|
251
|
+
console.warn(
|
|
252
|
+
'[graft serve] WARNING: approvalPolicy = "unattended" in graft.config.ts \u2014 destructive functions run without human approval. Deletes are not recoverable from git: deleteRecord removes rows outright and the asset store keeps no history. Audit rows are still written. This applies to POST /api/fn only; run_function over /api/mcp stays gated, because the policy is not part of the MCP surface.'
|
|
185
253
|
);
|
|
186
254
|
}
|
|
187
255
|
let studioHandler;
|
|
188
256
|
if (studioMod) {
|
|
189
|
-
const
|
|
257
|
+
const authenticate = !loopback && (devToken || issuers.length > 0) ? async (request) => {
|
|
190
258
|
const actor = await resolveActor(request);
|
|
191
|
-
|
|
192
|
-
|
|
259
|
+
if (actor.kind === "anonymous" || !actor.id) return null;
|
|
260
|
+
return { kind: actor.kind, id: actor.id, scopes: actor.scopes ?? [] };
|
|
261
|
+
} : !loopback ? () => null : void 0;
|
|
193
262
|
studioHandler = studioMod.createStudioHandler({
|
|
194
263
|
db: branch.db,
|
|
195
264
|
collections: config.collections,
|
|
196
265
|
contentDir: config.contentDir,
|
|
266
|
+
mdxTrust: config.mdxTrust,
|
|
197
267
|
defaultBranch: writeBranch,
|
|
198
|
-
|
|
268
|
+
// Only reached on a loopback mount, where there is no caller identity to
|
|
269
|
+
// attribute to. Off loopback the authenticated principal is used instead.
|
|
270
|
+
decider: { kind: "agent", id: "studio-serve" },
|
|
199
271
|
uiBasePath: "/studio",
|
|
200
|
-
|
|
272
|
+
authenticate
|
|
201
273
|
});
|
|
202
274
|
}
|
|
203
275
|
const server = createServer(
|
|
204
276
|
createNodeListener(
|
|
205
|
-
createServeRouter({
|
|
277
|
+
createServeRouter({
|
|
278
|
+
fn: fnHandler,
|
|
279
|
+
mcp: mcpHandler,
|
|
280
|
+
content: contentHandler,
|
|
281
|
+
health,
|
|
282
|
+
studio: studioHandler
|
|
283
|
+
}),
|
|
284
|
+
{ allowedHosts: allowedHostsFor(host) }
|
|
206
285
|
)
|
|
207
286
|
);
|
|
208
287
|
await new Promise((resolve, reject) => {
|
|
@@ -233,6 +312,8 @@ async function serveCommand(options) {
|
|
|
233
312
|
`graft serve \u2014 branch "${running.branch}"`,
|
|
234
313
|
` functions POST ${base}/api/fn/<name>`,
|
|
235
314
|
` mcp POST ${base}/api/mcp`,
|
|
315
|
+
` documents GET ${base}/api/content/v1/documents`,
|
|
316
|
+
` search GET ${base}/api/content/v1/search`,
|
|
236
317
|
` health GET ${base}/healthz`,
|
|
237
318
|
...studioOn ? [
|
|
238
319
|
` studio GET ${base}/studio`,
|
|
@@ -250,6 +331,7 @@ async function serveCommand(options) {
|
|
|
250
331
|
|
|
251
332
|
export {
|
|
252
333
|
createServeRouter,
|
|
334
|
+
allowedHostsFor,
|
|
253
335
|
createNodeListener,
|
|
254
336
|
startServe,
|
|
255
337
|
serveCommand
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
loadConfig,
|
|
5
5
|
loadProjectEnv,
|
|
6
6
|
requireDatabaseUrl
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-2LG6TWYY.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
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
loadProjectEnv,
|
|
4
4
|
requireDatabaseUrl
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2LG6TWYY.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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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",
|
|
@@ -61,7 +59,9 @@ function formatApproval(row) {
|
|
|
61
59
|
` input: ${JSON.stringify(row.input)}`
|
|
62
60
|
].join("\n");
|
|
63
61
|
}
|
|
62
|
+
|
|
64
63
|
export {
|
|
64
|
+
operatorName,
|
|
65
65
|
approvalsListCommand,
|
|
66
66
|
decideCommand,
|
|
67
67
|
formatApproval
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
assertNoStaticBranch,
|
|
4
4
|
compileCommand
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-2N3HXYNN.js";
|
|
6
6
|
import "./chunk-GPA66AP5.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-2LG6TWYY.js";
|
|
8
8
|
export {
|
|
9
9
|
assertNoStaticBranch,
|
|
10
10
|
compileCommand
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
assertNoStaticBranch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2N3HXYNN.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-
|
|
15
|
+
} from "./chunk-2LG6TWYY.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
|
};
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,11 @@ function printHelp() {
|
|
|
50
50
|
" --port <n> Port for `graft serve` / `graft studio` (serve default 3903; studio 4983)",
|
|
51
51
|
" --host <h> Host for `graft serve` / `graft studio` (default: 127.0.0.1, or HOST)",
|
|
52
52
|
" --studio Mount opt-in Studio on `graft serve` at /studio (or GRAFT_STUDIO=1)",
|
|
53
|
+
" --elicit-approvals",
|
|
54
|
+
" `graft mcp` asks THIS terminal's operator to decide a destructive call,",
|
|
55
|
+
" instead of failing with an id for `graft approve`. Local only: the human",
|
|
56
|
+
" must be present and the client must support MCP elicitation. Decisions",
|
|
57
|
+
" are recorded as the OS user, exactly as `graft approve` records them.",
|
|
53
58
|
" --from <name> Parent to fork from (branch create; default: main)",
|
|
54
59
|
" --into <name> Merge target (merge; default: main)",
|
|
55
60
|
" --backend <kind> Branch backend: overlay (default) or neon (branch create)",
|
|
@@ -80,6 +85,7 @@ function parseArgs(rest) {
|
|
|
80
85
|
let overwrite = false;
|
|
81
86
|
let pruneUnknown = false;
|
|
82
87
|
let studio = false;
|
|
88
|
+
let elicitApprovals = false;
|
|
83
89
|
let initDriver;
|
|
84
90
|
const value = (flag, raw) => {
|
|
85
91
|
if (!raw || raw.startsWith("-")) {
|
|
@@ -115,6 +121,8 @@ function parseArgs(rest) {
|
|
|
115
121
|
pruneUnknown = true;
|
|
116
122
|
} else if (arg === "--studio") {
|
|
117
123
|
studio = true;
|
|
124
|
+
} else if (arg === "--elicit-approvals") {
|
|
125
|
+
elicitApprovals = true;
|
|
118
126
|
} else if (arg === "--postgres") {
|
|
119
127
|
initDriver = "postgres";
|
|
120
128
|
} else if (arg === "--static") {
|
|
@@ -138,6 +146,7 @@ function parseArgs(rest) {
|
|
|
138
146
|
overwrite,
|
|
139
147
|
pruneUnknown,
|
|
140
148
|
studio,
|
|
149
|
+
elicitApprovals,
|
|
141
150
|
initDriver
|
|
142
151
|
};
|
|
143
152
|
}
|
|
@@ -156,7 +165,7 @@ async function run(argv, options = {}) {
|
|
|
156
165
|
const args = parseArgs(rest);
|
|
157
166
|
switch (command) {
|
|
158
167
|
case "init": {
|
|
159
|
-
const { initCommand } = await import("./init-
|
|
168
|
+
const { initCommand } = await import("./init-FV7Q6VXM.js");
|
|
160
169
|
const result = initCommand({
|
|
161
170
|
targetDir: args.positionals[0] ?? cwd,
|
|
162
171
|
driver: args.initDriver
|
|
@@ -187,12 +196,12 @@ async function run(argv, options = {}) {
|
|
|
187
196
|
return 0;
|
|
188
197
|
}
|
|
189
198
|
case "compile": {
|
|
190
|
-
const { compileCommand } = await import("./compile-
|
|
199
|
+
const { compileCommand } = await import("./compile-AWPSEA7U.js");
|
|
191
200
|
await compileCommand({ cwd, branchId: args.branchId, pruneUnknown: args.pruneUnknown });
|
|
192
201
|
return 0;
|
|
193
202
|
}
|
|
194
203
|
case "dev": {
|
|
195
|
-
const { devCommand } = await import("./dev-
|
|
204
|
+
const { devCommand } = await import("./dev-WHTIRWMR.js");
|
|
196
205
|
await devCommand({ cwd, branchId: args.branchId });
|
|
197
206
|
return 0;
|
|
198
207
|
}
|
|
@@ -203,7 +212,7 @@ async function run(argv, options = {}) {
|
|
|
203
212
|
subcommand ? `unknown db subcommand "${subcommand}" \u2014 the only one is \`graft db migrate\`` : "usage: graft db migrate [--dry-run]"
|
|
204
213
|
);
|
|
205
214
|
}
|
|
206
|
-
const { dbMigrateCommand, formatMigrateResult } = await import("./db-
|
|
215
|
+
const { dbMigrateCommand, formatMigrateResult } = await import("./db-5YMFKHGO.js");
|
|
207
216
|
const result = await dbMigrateCommand({ cwd, dryRun: args.dryRun });
|
|
208
217
|
console.log(formatMigrateResult(result));
|
|
209
218
|
return 0;
|
|
@@ -213,7 +222,7 @@ async function run(argv, options = {}) {
|
|
|
213
222
|
if (subcommand !== "put" || !file) {
|
|
214
223
|
throw new UsageError("usage: graft asset put <file> [key]");
|
|
215
224
|
}
|
|
216
|
-
const { assetPutCommand } = await import("./asset-
|
|
225
|
+
const { assetPutCommand } = await import("./asset-JWER6AOG.js");
|
|
217
226
|
const result = await assetPutCommand({ cwd, file, key });
|
|
218
227
|
console.log(`Uploaded ${result.key} (${result.contentType}, ${result.bytes} bytes)`);
|
|
219
228
|
console.log(
|
|
@@ -228,7 +237,7 @@ async function run(argv, options = {}) {
|
|
|
228
237
|
return 0;
|
|
229
238
|
}
|
|
230
239
|
case "approvals": {
|
|
231
|
-
const { approvalsListCommand, formatApproval } = await import("./approvals-
|
|
240
|
+
const { approvalsListCommand, formatApproval } = await import("./approvals-4PAANLA4.js");
|
|
232
241
|
const pending = await approvalsListCommand({ cwd });
|
|
233
242
|
if (pending.length === 0) {
|
|
234
243
|
console.log("No pending approvals.");
|
|
@@ -245,7 +254,7 @@ async function run(argv, options = {}) {
|
|
|
245
254
|
case "deny": {
|
|
246
255
|
const id = args.positionals[0];
|
|
247
256
|
if (!id) throw new UsageError(`usage: graft ${command} <approval-id>`);
|
|
248
|
-
const { decideCommand } = await import("./approvals-
|
|
257
|
+
const { decideCommand } = await import("./approvals-4PAANLA4.js");
|
|
249
258
|
const decision = command === "approve" ? "approved" : "denied";
|
|
250
259
|
const row = await decideCommand({ cwd, id, decision });
|
|
251
260
|
console.log(
|
|
@@ -259,7 +268,7 @@ async function run(argv, options = {}) {
|
|
|
259
268
|
return 0;
|
|
260
269
|
}
|
|
261
270
|
case "migrate": {
|
|
262
|
-
const { migrateCommand } = await import("./migrate-
|
|
271
|
+
const { migrateCommand } = await import("./migrate-6ZYG2H5C.js");
|
|
263
272
|
await migrateCommand({ cwd, branchId: args.branchId, apply: args.apply });
|
|
264
273
|
return 0;
|
|
265
274
|
}
|
|
@@ -273,7 +282,7 @@ async function run(argv, options = {}) {
|
|
|
273
282
|
if (subcommand && !name) {
|
|
274
283
|
throw new UsageError(`usage: graft branch ${subcommand} <name>`);
|
|
275
284
|
}
|
|
276
|
-
const mod = await import("./branch-
|
|
285
|
+
const mod = await import("./branch-U7FFDTFV.js");
|
|
277
286
|
if (!subcommand) {
|
|
278
287
|
const rows = await mod.branchListCommand({ cwd });
|
|
279
288
|
if (rows.length === 0) {
|
|
@@ -316,12 +325,12 @@ async function run(argv, options = {}) {
|
|
|
316
325
|
const branch = args.positionals[0];
|
|
317
326
|
if (!branch)
|
|
318
327
|
throw new UsageError("usage: graft merge <branch> [--into <target>] [--apply]");
|
|
319
|
-
const { mergeCommand } = await import("./merge-
|
|
328
|
+
const { mergeCommand } = await import("./merge-ZWYZJVFE.js");
|
|
320
329
|
await mergeCommand({ cwd, branch, into: args.into, apply: args.apply });
|
|
321
330
|
return 0;
|
|
322
331
|
}
|
|
323
332
|
case "add": {
|
|
324
|
-
const { addCommand } = await import("./add-
|
|
333
|
+
const { addCommand } = await import("./add-TCQY3YAN.js");
|
|
325
334
|
await addCommand({
|
|
326
335
|
cwd,
|
|
327
336
|
names: args.positionals,
|
|
@@ -331,12 +340,12 @@ async function run(argv, options = {}) {
|
|
|
331
340
|
return 0;
|
|
332
341
|
}
|
|
333
342
|
case "mcp": {
|
|
334
|
-
const { mcpCommand } = await import("./mcp-
|
|
335
|
-
await mcpCommand({ cwd, branchId: args.branchId });
|
|
343
|
+
const { mcpCommand } = await import("./mcp-77QFFJXK.js");
|
|
344
|
+
await mcpCommand({ cwd, branchId: args.branchId, elicitApprovals: args.elicitApprovals });
|
|
336
345
|
return 0;
|
|
337
346
|
}
|
|
338
347
|
case "serve": {
|
|
339
|
-
const { serveCommand } = await import("./serve-
|
|
348
|
+
const { serveCommand } = await import("./serve-5ADUQG66.js");
|
|
340
349
|
await serveCommand({
|
|
341
350
|
cwd,
|
|
342
351
|
branchId: args.branchId,
|
|
@@ -347,7 +356,7 @@ async function run(argv, options = {}) {
|
|
|
347
356
|
return 0;
|
|
348
357
|
}
|
|
349
358
|
case "studio": {
|
|
350
|
-
const { studioCommand } = await import("./studio-
|
|
359
|
+
const { studioCommand } = await import("./studio-7VAYFEWS.js");
|
|
351
360
|
await studioCommand({
|
|
352
361
|
cwd,
|
|
353
362
|
branchId: args.branchId,
|
|
@@ -357,7 +366,7 @@ async function run(argv, options = {}) {
|
|
|
357
366
|
return 0;
|
|
358
367
|
}
|
|
359
368
|
case "content": {
|
|
360
|
-
const { contentListCommand, formatContentLine } = await import("./content-
|
|
369
|
+
const { contentListCommand, formatContentLine } = await import("./content-ZULMTJMD.js");
|
|
361
370
|
const { branch, lines } = await contentListCommand({ cwd, branchId: args.branchId });
|
|
362
371
|
if (lines.length === 0) {
|
|
363
372
|
console.log(`No content on branch "${branch}" (run graft compile).`);
|
|
@@ -369,7 +378,7 @@ async function run(argv, options = {}) {
|
|
|
369
378
|
return 0;
|
|
370
379
|
}
|
|
371
380
|
case "compilations": {
|
|
372
|
-
const { compilationsListCommand, formatCompilation } = await import("./compilations-
|
|
381
|
+
const { compilationsListCommand, formatCompilation } = await import("./compilations-CH6AIOFG.js");
|
|
373
382
|
const rows = await compilationsListCommand({ cwd, branchId: args.branchId });
|
|
374
383
|
if (rows.length === 0) {
|
|
375
384
|
console.log("No compilations recorded yet (run graft compile).");
|
|
@@ -381,7 +390,7 @@ async function run(argv, options = {}) {
|
|
|
381
390
|
case "harden": {
|
|
382
391
|
const role = args.positionals[0];
|
|
383
392
|
if (!role) throw new UsageError("usage: graft harden <role>");
|
|
384
|
-
const { hardenCommand } = await import("./harden-
|
|
393
|
+
const { hardenCommand } = await import("./harden-QONL2MAO.js");
|
|
385
394
|
const result = await hardenCommand({ cwd, role });
|
|
386
395
|
console.log(`Hardened runtime role "${result.role}" (${result.statements.length} grants):`);
|
|
387
396
|
for (const statement of result.statements) console.log(` ${statement}`);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
assertNoStaticBranch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2N3HXYNN.js";
|
|
5
5
|
import "./chunk-GPA66AP5.js";
|
|
6
|
+
import {
|
|
7
|
+
operatorName
|
|
8
|
+
} from "./chunk-NBJ4N4O6.js";
|
|
6
9
|
import {
|
|
7
10
|
findConfig,
|
|
8
11
|
loadConfig,
|
|
9
12
|
loadProjectEnv,
|
|
10
13
|
requireDatabaseUrl
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2LG6TWYY.js";
|
|
12
15
|
|
|
13
16
|
// src/commands/mcp.ts
|
|
14
17
|
import { createActorResolver } from "@usegraft/auth";
|
|
@@ -25,21 +28,26 @@ async function mcpCommand(options) {
|
|
|
25
28
|
try {
|
|
26
29
|
const devToken = process.env.GRAFT_DEV_TOKEN;
|
|
27
30
|
const scopes = (process.env.GRAFT_DEV_SCOPES ?? "").split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
31
|
+
const cliActor = {
|
|
32
|
+
kind: "agent",
|
|
33
|
+
id: "graft-cli",
|
|
34
|
+
scopes: [.../* @__PURE__ */ new Set(["content:write", ...scopes])]
|
|
35
|
+
};
|
|
28
36
|
const resolveActor = createActorResolver({
|
|
29
37
|
issuers: [],
|
|
30
|
-
devTokens: devToken ? {
|
|
31
|
-
[devToken]: {
|
|
32
|
-
kind: "agent",
|
|
33
|
-
id: "graft-cli",
|
|
34
|
-
scopes
|
|
35
|
-
}
|
|
36
|
-
} : void 0
|
|
38
|
+
devTokens: devToken ? { [devToken]: cliActor } : void 0
|
|
37
39
|
});
|
|
40
|
+
const resolveStdioActor = async (request) => {
|
|
41
|
+
const resolved = await resolveActor(request);
|
|
42
|
+
return resolved.kind === "anonymous" ? cliActor : resolved;
|
|
43
|
+
};
|
|
44
|
+
const approvalElicitation = options.elicitApprovals ? { decider: { kind: "human", id: operatorName() } } : void 0;
|
|
38
45
|
const server = createGraftMcp({
|
|
39
46
|
name: "graft",
|
|
40
47
|
contentDir: config.contentDir,
|
|
41
48
|
collections: config.collections,
|
|
42
49
|
functions: config.functions,
|
|
50
|
+
mdxTrust: config.mdxTrust,
|
|
43
51
|
...branch !== void 0 ? {
|
|
44
52
|
db: branch.db,
|
|
45
53
|
branchId: scopeWriteBranch(branch.scope),
|
|
@@ -47,8 +55,13 @@ async function mcpCommand(options) {
|
|
|
47
55
|
// overlay branch searches its full parent chain, not just its own rows.
|
|
48
56
|
scope: branch.scope
|
|
49
57
|
} : { staticIndexPath: config.index.path },
|
|
50
|
-
actor:
|
|
51
|
-
|
|
58
|
+
actor: resolveStdioActor,
|
|
59
|
+
connectionActor: cliActor,
|
|
60
|
+
// Local agents may upload a file from the project; remote mounts get no
|
|
61
|
+
// `path` argument at all.
|
|
62
|
+
localUploadRoot: config.projectDir,
|
|
63
|
+
defaultAuthorization: devToken,
|
|
64
|
+
approvalElicitation
|
|
52
65
|
});
|
|
53
66
|
await serveStdio(server);
|
|
54
67
|
} finally {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
discoverMigrations
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DW6WTDRX.js";
|
|
5
5
|
import {
|
|
6
6
|
findConfig,
|
|
7
7
|
loadConfig,
|
|
8
8
|
loadProjectEnv,
|
|
9
9
|
requireDatabaseUrl
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-2LG6TWYY.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
|
});
|
|
@@ -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-
|
|
8
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-CONTLKSZ.js";
|
|
9
|
+
import "./chunk-2LG6TWYY.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-
|
|
5
|
+
} from "./chunk-CONTLKSZ.js";
|
|
5
6
|
import {
|
|
6
7
|
findConfig,
|
|
7
8
|
loadConfig,
|
|
8
9
|
loadProjectEnv,
|
|
9
10
|
requireDatabaseUrl
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-2LG6TWYY.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
|
|
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
|
|
62
|
+
const authenticate = !loopback && devToken ? (request) => {
|
|
62
63
|
const header = request.headers.get("authorization") ?? "";
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.0-beta.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,22 @@
|
|
|
18
34
|
"access": "public"
|
|
19
35
|
},
|
|
20
36
|
"dependencies": {
|
|
21
|
-
"@usegraft/assets": "0.
|
|
22
|
-
"@usegraft/auth": "0.
|
|
23
|
-
"@usegraft/compiler": "0.
|
|
24
|
-
"@usegraft/content-
|
|
25
|
-
"@usegraft/
|
|
26
|
-
"@usegraft/
|
|
27
|
-
"@usegraft/
|
|
28
|
-
"@usegraft/
|
|
29
|
-
"@usegraft/
|
|
30
|
-
"@usegraft/
|
|
37
|
+
"@usegraft/assets": "1.0.0-beta.0",
|
|
38
|
+
"@usegraft/auth": "1.0.0-beta.0",
|
|
39
|
+
"@usegraft/compiler": "1.0.0-beta.0",
|
|
40
|
+
"@usegraft/content-api": "1.0.0-beta.0",
|
|
41
|
+
"@usegraft/content-migrations": "1.0.0-beta.0",
|
|
42
|
+
"@usegraft/contracts": "1.0.0-beta.0",
|
|
43
|
+
"@usegraft/core": "1.0.0-beta.0",
|
|
44
|
+
"@usegraft/db": "1.0.0-beta.0",
|
|
45
|
+
"@usegraft/mcp": "1.0.0-beta.0",
|
|
46
|
+
"@usegraft/mdx-safety": "1.0.0-beta.0",
|
|
47
|
+
"@usegraft/registry": "1.0.0-beta.0",
|
|
48
|
+
"@usegraft/studio": "1.0.0-beta.0",
|
|
31
49
|
"jiti": "^2.4.0"
|
|
32
50
|
},
|
|
33
51
|
"devDependencies": {
|
|
34
|
-
"@usegraft/sdk-core": "0.
|
|
52
|
+
"@usegraft/sdk-core": "1.0.0-beta.0"
|
|
35
53
|
},
|
|
36
54
|
"engines": {
|
|
37
55
|
"node": ">=22.16"
|
|
@@ -40,7 +58,6 @@
|
|
|
40
58
|
"build": "tsup",
|
|
41
59
|
"dev": "tsup --watch",
|
|
42
60
|
"typecheck": "tsc --noEmit",
|
|
43
|
-
"test": "vitest run --passWithNoTests"
|
|
44
|
-
"lint": "oxlint ."
|
|
61
|
+
"test": "vitest run --passWithNoTests"
|
|
45
62
|
}
|
|
46
63
|
}
|