@se-studio/contentful-cms 3.0.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/commands/asset.d.ts.map +1 -1
- package/dist/commands/asset.js +40 -7
- package/dist/commands/asset.js.map +1 -1
- package/dist/commands/host-scaffold.d.ts.map +1 -1
- package/dist/commands/host-scaffold.js +2 -0
- package/dist/commands/host-scaffold.js.map +1 -1
- package/dist/generated/hosted-guide.d.ts +1 -1
- package/dist/generated/hosted-guide.d.ts.map +1 -1
- package/dist/generated/hosted-guide.js +1 -1
- package/dist/generated/hosted-guide.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/hosted/index.d.ts +3 -0
- package/dist/hosted/index.d.ts.map +1 -1
- package/dist/hosted/index.js +3 -0
- package/dist/hosted/index.js.map +1 -1
- package/dist/hosted/next/redis.d.ts.map +1 -1
- package/dist/hosted/next/redis.js +4 -0
- package/dist/hosted/next/redis.js.map +1 -1
- package/dist/hosted/next/routes/mcp.d.ts.map +1 -1
- package/dist/hosted/next/routes/mcp.js +2 -0
- package/dist/hosted/next/routes/mcp.js.map +1 -1
- package/dist/hosted/next/routes/staged-upload.d.ts +2 -0
- package/dist/hosted/next/routes/staged-upload.d.ts.map +1 -0
- package/dist/hosted/next/routes/staged-upload.js +83 -0
- package/dist/hosted/next/routes/staged-upload.js.map +1 -0
- package/dist/mcp/http-handler.d.ts.map +1 -1
- package/dist/mcp/http-handler.js +19 -19
- package/dist/mcp/http-handler.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/cms-edit.d.ts +1 -1
- package/dist/mcp/tools/cms-edit.d.ts.map +1 -1
- package/dist/mcp/tools/cms-edit.js +3 -1
- package/dist/mcp/tools/cms-edit.js.map +1 -1
- package/dist/mcp/tools/staged-upload.d.ts +4 -0
- package/dist/mcp/tools/staged-upload.d.ts.map +1 -0
- package/dist/mcp/tools/staged-upload.js +77 -0
- package/dist/mcp/tools/staged-upload.js.map +1 -0
- package/dist/runtime/context.d.ts +3 -0
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/context.js.map +1 -1
- package/dist/session/session-store.d.ts +2 -0
- package/dist/session/session-store.d.ts.map +1 -1
- package/dist/session/session-store.js.map +1 -1
- package/dist/staged-upload/hmac.d.ts +6 -0
- package/dist/staged-upload/hmac.d.ts.map +1 -0
- package/dist/staged-upload/hmac.js +43 -0
- package/dist/staged-upload/hmac.js.map +1 -0
- package/dist/staged-upload/limits.d.ts +6 -0
- package/dist/staged-upload/limits.d.ts.map +1 -0
- package/dist/staged-upload/limits.js +9 -0
- package/dist/staged-upload/limits.js.map +1 -0
- package/dist/staged-upload/mime.d.ts +2 -0
- package/dist/staged-upload/mime.d.ts.map +1 -0
- package/dist/staged-upload/mime.js +10 -0
- package/dist/staged-upload/mime.js.map +1 -0
- package/dist/staged-upload/request.d.ts +11 -0
- package/dist/staged-upload/request.d.ts.map +1 -0
- package/dist/staged-upload/request.js +40 -0
- package/dist/staged-upload/request.js.map +1 -0
- package/dist/staged-upload/store.d.ts +19 -0
- package/dist/staged-upload/store.d.ts.map +1 -0
- package/dist/staged-upload/store.js +124 -0
- package/dist/staged-upload/store.js.map +1 -0
- package/dist/staged-upload/types.d.ts +27 -0
- package/dist/staged-upload/types.d.ts.map +1 -0
- package/dist/staged-upload/types.js +2 -0
- package/dist/staged-upload/types.js.map +1 -0
- package/docs/editor-tasks/media-reuse-and-upload.md +16 -9
- package/docs/mcp/hosted-guide.md +18 -5
- package/package.json +1 -1
- package/skills/core/SKILL.md +4 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RedisClient } from '../session/session-store.js';
|
|
2
|
+
import type { StagedUploadConsumeResult, StagedUploadRecord } from './types.js';
|
|
3
|
+
export declare class StagedUploadStore {
|
|
4
|
+
private readonly redis;
|
|
5
|
+
private readonly keyPrefix;
|
|
6
|
+
constructor(redis: RedisClient, keyPrefix?: string);
|
|
7
|
+
private fullKey;
|
|
8
|
+
assertRateLimit(userId: string): Promise<void>;
|
|
9
|
+
createPending(params: {
|
|
10
|
+
userId: string;
|
|
11
|
+
fileName: string;
|
|
12
|
+
mimeType: string;
|
|
13
|
+
byteLength?: number;
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
storeFile(userId: string, uploadId: string, buffer: Buffer, mimeType: string): Promise<StagedUploadRecord>;
|
|
16
|
+
consume(userId: string, uploadId: string): Promise<StagedUploadConsumeResult>;
|
|
17
|
+
getRecord(userId: string, uploadId: string): Promise<StagedUploadRecord | null>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/staged-upload/store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAO/D,OAAO,KAAK,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA0BhF,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,WAAW,EAClB,SAAS,SAAa;IAGzC,OAAO,CAAC,OAAO;IAIT,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAc9C,aAAa,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqCb,SAAS,CACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC;IAgCxB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAuB7E,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;CAGtF"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { assertUploadBufferSize } from '../contentful/upload-limits.js';
|
|
3
|
+
import { hostedStagedMaxBytes, STAGED_UPLOAD_RATE_LIMIT_PER_HOUR, STAGED_UPLOAD_TTL_SECONDS, } from './limits.js';
|
|
4
|
+
import { isAllowedStagedMimeType } from './mime.js';
|
|
5
|
+
const KEY_PREFIX = 'cms-edit:staged:';
|
|
6
|
+
const RATE_PREFIX = 'cms-edit:staged-rate:';
|
|
7
|
+
function rateKey(userId, hourBucket) {
|
|
8
|
+
return `${RATE_PREFIX}${userId}:${String(hourBucket)}`;
|
|
9
|
+
}
|
|
10
|
+
function parseRecord(raw) {
|
|
11
|
+
if (!raw) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (typeof raw === 'string') {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw === 'object') {
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
export class StagedUploadStore {
|
|
28
|
+
redis;
|
|
29
|
+
keyPrefix;
|
|
30
|
+
constructor(redis, keyPrefix = KEY_PREFIX) {
|
|
31
|
+
this.redis = redis;
|
|
32
|
+
this.keyPrefix = keyPrefix;
|
|
33
|
+
}
|
|
34
|
+
fullKey(userId, uploadId) {
|
|
35
|
+
return `${this.keyPrefix}${userId}:${uploadId}`;
|
|
36
|
+
}
|
|
37
|
+
async assertRateLimit(userId) {
|
|
38
|
+
const hourBucket = Math.floor(Date.now() / (60 * 60 * 1000));
|
|
39
|
+
const key = rateKey(userId, hourBucket);
|
|
40
|
+
const count = await this.redis.incr(key);
|
|
41
|
+
if (count === 1) {
|
|
42
|
+
await this.redis.expire(key, 60 * 60);
|
|
43
|
+
}
|
|
44
|
+
if (count > STAGED_UPLOAD_RATE_LIMIT_PER_HOUR) {
|
|
45
|
+
throw new Error(`Staged upload rate limit exceeded (${String(STAGED_UPLOAD_RATE_LIMIT_PER_HOUR)} per hour).`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async createPending(params) {
|
|
49
|
+
if (!isAllowedStagedMimeType(params.mimeType)) {
|
|
50
|
+
throw new Error(`MIME type not allowed for staged upload: ${params.mimeType}`);
|
|
51
|
+
}
|
|
52
|
+
if (params.byteLength !== undefined && params.byteLength > hostedStagedMaxBytes()) {
|
|
53
|
+
throw new Error(`Declared byteLength exceeds hosted staged upload limit (${String(hostedStagedMaxBytes())} bytes).`);
|
|
54
|
+
}
|
|
55
|
+
await this.assertRateLimit(params.userId);
|
|
56
|
+
const uploadId = crypto.randomUUID().replace(/-/g, '');
|
|
57
|
+
const record = {
|
|
58
|
+
uploadId,
|
|
59
|
+
userId: params.userId,
|
|
60
|
+
fileName: params.fileName,
|
|
61
|
+
mimeType: params.mimeType,
|
|
62
|
+
byteLength: params.byteLength,
|
|
63
|
+
status: 'pending',
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
};
|
|
66
|
+
const set = await this.redis.set(this.fullKey(params.userId, uploadId), JSON.stringify(record), {
|
|
67
|
+
ex: STAGED_UPLOAD_TTL_SECONDS,
|
|
68
|
+
nx: true,
|
|
69
|
+
});
|
|
70
|
+
if (set !== true) {
|
|
71
|
+
throw new Error('Failed to reserve staged upload slot.');
|
|
72
|
+
}
|
|
73
|
+
return uploadId;
|
|
74
|
+
}
|
|
75
|
+
async storeFile(userId, uploadId, buffer, mimeType) {
|
|
76
|
+
assertUploadBufferSize(buffer);
|
|
77
|
+
if (buffer.length > hostedStagedMaxBytes()) {
|
|
78
|
+
throw new Error(`File exceeds hosted staged upload limit (${String(hostedStagedMaxBytes())} bytes).`);
|
|
79
|
+
}
|
|
80
|
+
if (!isAllowedStagedMimeType(mimeType)) {
|
|
81
|
+
throw new Error(`MIME type not allowed for staged upload: ${mimeType}`);
|
|
82
|
+
}
|
|
83
|
+
const key = this.fullKey(userId, uploadId);
|
|
84
|
+
const existing = parseRecord(await this.redis.get(key));
|
|
85
|
+
if (!existing || existing.userId !== userId || existing.uploadId !== uploadId) {
|
|
86
|
+
throw new Error('Staged upload slot not found or expired.');
|
|
87
|
+
}
|
|
88
|
+
if (existing.status === 'ready') {
|
|
89
|
+
throw new Error('Staged upload already received.');
|
|
90
|
+
}
|
|
91
|
+
const record = {
|
|
92
|
+
...existing,
|
|
93
|
+
status: 'ready',
|
|
94
|
+
mimeType,
|
|
95
|
+
byteLength: buffer.length,
|
|
96
|
+
dataBase64: buffer.toString('base64'),
|
|
97
|
+
};
|
|
98
|
+
await this.redis.set(key, JSON.stringify(record), { ex: STAGED_UPLOAD_TTL_SECONDS });
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
async consume(userId, uploadId) {
|
|
102
|
+
const key = this.fullKey(userId, uploadId);
|
|
103
|
+
const existing = parseRecord(await this.redis.get(key));
|
|
104
|
+
if (!existing || existing.userId !== userId || existing.uploadId !== uploadId) {
|
|
105
|
+
throw new Error('Staged upload not found or expired.');
|
|
106
|
+
}
|
|
107
|
+
if (existing.status !== 'ready' || !existing.dataBase64) {
|
|
108
|
+
throw new Error('Staged upload has no file data yet. POST the file to uploadUrl first.');
|
|
109
|
+
}
|
|
110
|
+
await this.redis.del(key);
|
|
111
|
+
const buffer = Buffer.from(existing.dataBase64, 'base64');
|
|
112
|
+
assertUploadBufferSize(buffer);
|
|
113
|
+
return {
|
|
114
|
+
buffer,
|
|
115
|
+
fileName: existing.fileName,
|
|
116
|
+
mimeType: existing.mimeType,
|
|
117
|
+
byteLength: buffer.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async getRecord(userId, uploadId) {
|
|
121
|
+
return parseRecord(await this.redis.get(this.fullKey(userId, uploadId)));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/staged-upload/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type StagedUploadStatus = 'pending' | 'ready';
|
|
2
|
+
export interface StagedUploadRecord {
|
|
3
|
+
uploadId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
fileName: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
byteLength?: number;
|
|
8
|
+
status: StagedUploadStatus;
|
|
9
|
+
dataBase64?: string;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface StagedUploadRequestResult {
|
|
13
|
+
uploadId: string;
|
|
14
|
+
uploadUrl: string;
|
|
15
|
+
expiresIn: number;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
curlCommand: string;
|
|
19
|
+
consumeArgs: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface StagedUploadConsumeResult {
|
|
22
|
+
buffer: Buffer;
|
|
23
|
+
fileName: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
byteLength: number;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/staged-upload/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/staged-upload/types.ts"],"names":[],"mappings":""}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
## Prerequisites
|
|
15
15
|
|
|
16
16
|
- **Always run `index sync` first** if the catalog may be stale
|
|
17
|
-
- On **hosted MCP**, the server has **no filesystem** — use `--base64
|
|
17
|
+
- On **hosted MCP**, the server has **no filesystem** — use **staged upload**, `--base64`, or `--url` (never a local file path in `args`)
|
|
18
18
|
|
|
19
19
|
## Reuse-first workflow (default)
|
|
20
20
|
|
|
@@ -29,21 +29,28 @@
|
|
|
29
29
|
|
|
30
30
|
### Hosted MCP (default)
|
|
31
31
|
|
|
32
|
-
**Prefer
|
|
32
|
+
**Prefer staged upload** for files larger than ~500 KB or when base64 in `cms_edit` args fails (agent harness message limits):
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
1. `cms_edit_request_staged_upload` with `fileName`, `mimeType`, optional `byteLength`, optional `title`
|
|
35
|
+
2. Run the returned `curlCommand` in a shell (POST file to `uploadUrl`)
|
|
36
|
+
3. `cms_edit` with the returned `consumeArgs` (e.g. `["asset","upload","--staged","<uploadId>",…]`)
|
|
37
37
|
|
|
38
38
|
Rules:
|
|
39
39
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
40
|
+
- Staged upload keeps binary **out of MCP args** — only a short `uploadId` is passed to Contentful
|
|
41
|
+
- Hosted limit: **4 MB** per file; TTL **15 minutes**; single-use
|
|
42
42
|
- **PNG** when the source has transparency (portraits on brand-colour cards); JPEG flattens alpha
|
|
43
|
-
- **Stress-tested:** ~2 MB PNG (~2.8 MB base64) uploads successfully on hosted `pedestal-cms-edit` (Jun 2026)
|
|
44
43
|
- Upload alone does **not** attach the asset — follow with **link + save** (below)
|
|
45
44
|
|
|
46
|
-
**
|
|
45
|
+
**Small files:** `--base64` with actual encoded bytes in `args` when the harness accepts the payload:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
cms_edit ["asset", "upload", "--base64", "<encoded-bytes>", "--mime", "image/png", "--file-name", "icon.png", "--title", "…"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pass **raw base64 text** — not a filesystem path, not `file:///…`.
|
|
52
|
+
|
|
53
|
+
**Last resort `--url`** only when staged upload and base64 are impractical:
|
|
47
54
|
|
|
48
55
|
```
|
|
49
56
|
cms_edit ["asset", "upload", "--url", "https://<public-https-url>/file.png", "--mime", "image/png", "--file-name", "headshot-name-1200.png", "--title", "…"]
|
package/docs/mcp/hosted-guide.md
CHANGED
|
@@ -20,6 +20,7 @@ All `save` operations create **drafts only**. A human must review and publish in
|
|
|
20
20
|
| Tool | Purpose |
|
|
21
21
|
|------|---------|
|
|
22
22
|
| `cms_edit` | Run any cms-edit command via an `args` array |
|
|
23
|
+
| `cms_edit_request_staged_upload` | Get a presigned upload URL for large files (then `curl` + `asset upload --staged`) |
|
|
23
24
|
| `cms_edit_version` | Check the installed cms-edit version (no session required) |
|
|
24
25
|
| `cms_edit_help` | Read this guide by topic (`overview`, `workflow`, `commands`, `troubleshooting`, `hosted`) |
|
|
25
26
|
|
|
@@ -136,19 +137,31 @@ cms-edit saves **drafts only**. A colleague must **publish in Contentful** for c
|
|
|
136
137
|
|
|
137
138
|
Hosted connectors have **no server filesystem**. Use one source per upload:
|
|
138
139
|
|
|
139
|
-
| Source | Example |
|
|
140
|
-
|
|
141
|
-
|
|
|
142
|
-
| **`--
|
|
140
|
+
| Source | When | Example |
|
|
141
|
+
|--------|------|---------|
|
|
142
|
+
| **Staged upload** (preferred for files >~500 KB) | Agent harness cannot pass large base64 in `cms_edit` args | See **Staged upload** below |
|
|
143
|
+
| **`--base64`** | Small files when inline base64 works | `["asset", "upload", "--base64", "<bytes>", "--mime", "image/png", "--file-name", "photo.png"]` |
|
|
144
|
+
| **`--url`** (last resort) | Public HTTPS URL fetchable by the host | `["asset", "upload", "--url", "https://…/photo.png", "--mime", "image/png", "--file-name", "photo.png"]` |
|
|
143
145
|
|
|
144
146
|
**Required with `--base64`:** `--mime`, `--file-name`. Pass **encoded bytes** in `args` — never a local path (`./file.png`, `file:///tmp/…`) or placeholder string.
|
|
145
147
|
|
|
146
148
|
**After upload:** note the returned asset `id`, attach to the entry (`asset set @root media <id>` for person portraits), then `diff` → `save`. Upload alone does not update entries.
|
|
147
149
|
|
|
148
|
-
**PNG** when transparency matters.
|
|
150
|
+
**PNG** when transparency matters.
|
|
149
151
|
|
|
150
152
|
Full workflow: `cms-edit://customer/task-media-reuse-and-upload`.
|
|
151
153
|
|
|
154
|
+
### Staged upload (large files)
|
|
155
|
+
|
|
156
|
+
Keeps bulk binary **out of MCP tool args**. The agent uploads via `curl`, then passes only a short `uploadId`.
|
|
157
|
+
|
|
158
|
+
1. `cms_edit_request_staged_upload` with `fileName`, `mimeType`, optional `byteLength`, optional `title`
|
|
159
|
+
2. Run the returned `curlCommand` in a shell (POST multipart to `uploadUrl`)
|
|
160
|
+
3. `cms_edit` with the returned `consumeArgs` (includes `asset upload --staged <uploadId>`)
|
|
161
|
+
4. Link and save as usual
|
|
162
|
+
|
|
163
|
+
Hosted limit: **4 MB** per staged file (Vercel body limit). TTL: **15 minutes**, single-use.
|
|
164
|
+
|
|
152
165
|
## Featured images (articles)
|
|
153
166
|
|
|
154
167
|
Read `cms-edit://customer/defaults` when present. Otherwise:
|
package/package.json
CHANGED
package/skills/core/SKILL.md
CHANGED
|
@@ -369,11 +369,14 @@ cms-edit asset search "hero background"
|
|
|
369
369
|
cms-edit asset search --filename istockphoto-123.jpg
|
|
370
370
|
cms-edit asset search --filename-match 'istockphoto-.*'
|
|
371
371
|
|
|
372
|
-
# Upload from local file, URL, or
|
|
372
|
+
# Upload from local file, URL, base64, or staged ref (hosted MCP)
|
|
373
373
|
cms-edit asset upload ./poster.jpg --title "Poster"
|
|
374
374
|
cms-edit asset upload --url https://example.com/image.jpg --if-exists-by-filename
|
|
375
375
|
cms-edit asset upload --base64 "$B64" --mime image/png --file-name poster.png
|
|
376
376
|
|
|
377
|
+
# Hosted MCP — large files: cms_edit_request_staged_upload → curl uploadUrl → consumeArgs
|
|
378
|
+
# cms_edit ["asset", "upload", "--staged", "<uploadId>", "--mime", "image/png", "--file-name", "photo.png"]
|
|
379
|
+
|
|
377
380
|
# Upload and create a Media wrapper in one step
|
|
378
381
|
cms-edit asset upload ./figure.png --with-media --media-name "Figure 1" --media-position Middle
|
|
379
382
|
|