@se-studio/contentful-cms 3.0.2 → 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.
Files changed (75) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/commands/asset.d.ts.map +1 -1
  3. package/dist/commands/asset.js +40 -7
  4. package/dist/commands/asset.js.map +1 -1
  5. package/dist/commands/host-scaffold.d.ts.map +1 -1
  6. package/dist/commands/host-scaffold.js +2 -0
  7. package/dist/commands/host-scaffold.js.map +1 -1
  8. package/dist/generated/hosted-guide.d.ts +1 -1
  9. package/dist/generated/hosted-guide.d.ts.map +1 -1
  10. package/dist/generated/hosted-guide.js +1 -1
  11. package/dist/generated/hosted-guide.js.map +1 -1
  12. package/dist/generated/version.d.ts +1 -1
  13. package/dist/generated/version.js +1 -1
  14. package/dist/hosted/index.d.ts +3 -0
  15. package/dist/hosted/index.d.ts.map +1 -1
  16. package/dist/hosted/index.js +3 -0
  17. package/dist/hosted/index.js.map +1 -1
  18. package/dist/hosted/next/redis.d.ts.map +1 -1
  19. package/dist/hosted/next/redis.js +4 -0
  20. package/dist/hosted/next/redis.js.map +1 -1
  21. package/dist/hosted/next/routes/mcp.d.ts.map +1 -1
  22. package/dist/hosted/next/routes/mcp.js +2 -0
  23. package/dist/hosted/next/routes/mcp.js.map +1 -1
  24. package/dist/hosted/next/routes/staged-upload.d.ts +2 -0
  25. package/dist/hosted/next/routes/staged-upload.d.ts.map +1 -0
  26. package/dist/hosted/next/routes/staged-upload.js +83 -0
  27. package/dist/hosted/next/routes/staged-upload.js.map +1 -0
  28. package/dist/mcp/http-handler.d.ts.map +1 -1
  29. package/dist/mcp/http-handler.js +19 -19
  30. package/dist/mcp/http-handler.js.map +1 -1
  31. package/dist/mcp/server.d.ts.map +1 -1
  32. package/dist/mcp/server.js +2 -0
  33. package/dist/mcp/server.js.map +1 -1
  34. package/dist/mcp/tools/cms-edit.d.ts +1 -1
  35. package/dist/mcp/tools/cms-edit.d.ts.map +1 -1
  36. package/dist/mcp/tools/cms-edit.js +3 -1
  37. package/dist/mcp/tools/cms-edit.js.map +1 -1
  38. package/dist/mcp/tools/staged-upload.d.ts +4 -0
  39. package/dist/mcp/tools/staged-upload.d.ts.map +1 -0
  40. package/dist/mcp/tools/staged-upload.js +77 -0
  41. package/dist/mcp/tools/staged-upload.js.map +1 -0
  42. package/dist/runtime/context.d.ts +3 -0
  43. package/dist/runtime/context.d.ts.map +1 -1
  44. package/dist/runtime/context.js.map +1 -1
  45. package/dist/session/session-store.d.ts +2 -0
  46. package/dist/session/session-store.d.ts.map +1 -1
  47. package/dist/session/session-store.js.map +1 -1
  48. package/dist/staged-upload/hmac.d.ts +6 -0
  49. package/dist/staged-upload/hmac.d.ts.map +1 -0
  50. package/dist/staged-upload/hmac.js +43 -0
  51. package/dist/staged-upload/hmac.js.map +1 -0
  52. package/dist/staged-upload/limits.d.ts +6 -0
  53. package/dist/staged-upload/limits.d.ts.map +1 -0
  54. package/dist/staged-upload/limits.js +9 -0
  55. package/dist/staged-upload/limits.js.map +1 -0
  56. package/dist/staged-upload/mime.d.ts +2 -0
  57. package/dist/staged-upload/mime.d.ts.map +1 -0
  58. package/dist/staged-upload/mime.js +10 -0
  59. package/dist/staged-upload/mime.js.map +1 -0
  60. package/dist/staged-upload/request.d.ts +11 -0
  61. package/dist/staged-upload/request.d.ts.map +1 -0
  62. package/dist/staged-upload/request.js +40 -0
  63. package/dist/staged-upload/request.js.map +1 -0
  64. package/dist/staged-upload/store.d.ts +19 -0
  65. package/dist/staged-upload/store.d.ts.map +1 -0
  66. package/dist/staged-upload/store.js +124 -0
  67. package/dist/staged-upload/store.js.map +1 -0
  68. package/dist/staged-upload/types.d.ts +27 -0
  69. package/dist/staged-upload/types.d.ts.map +1 -0
  70. package/dist/staged-upload/types.js +2 -0
  71. package/dist/staged-upload/types.js.map +1 -0
  72. package/docs/editor-tasks/media-reuse-and-upload.md +58 -5
  73. package/docs/mcp/hosted-guide.md +33 -3
  74. package/package.json +1 -1
  75. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/staged-upload/types.ts"],"names":[],"mappings":""}
@@ -5,6 +5,7 @@
5
5
  - upload image
6
6
  - find existing asset
7
7
  - add photo to page or article
8
+ - update person headshot / portrait
8
9
 
9
10
  ## Requires capabilities
10
11
 
@@ -13,6 +14,7 @@
13
14
  ## Prerequisites
14
15
 
15
16
  - **Always run `index sync` first** if the catalog may be stale
17
+ - On **hosted MCP**, the server has **no filesystem** — use **staged upload**, `--base64`, or `--url` (never a local file path in `args`)
16
18
 
17
19
  ## Reuse-first workflow (default)
18
20
 
@@ -25,27 +27,78 @@
25
27
 
26
28
  ## Upload only when no match
27
29
 
30
+ ### Hosted MCP (default)
31
+
32
+ **Prefer staged upload** for files larger than ~500 KB or when base64 in `cms_edit` args fails (agent harness message limits):
33
+
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
+
38
+ Rules:
39
+
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
+ - **PNG** when the source has transparency (portraits on brand-colour cards); JPEG flattens alpha
43
+ - Upload alone does **not** attach the asset — follow with **link + save** (below)
44
+
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:
54
+
28
55
  ```
29
- cms_edit ["asset", "upload", "./file.jpg", "--title", "…", "--if-exists-by-filename"]
56
+ cms_edit ["asset", "upload", "--url", "https://<public-https-url>/file.png", "--mime", "image/png", "--file-name", "headshot-name-1200.png", "--title", "…"]
30
57
  ```
31
58
 
32
- Sources: `--url <https>` or `--base64` with `--mime` and `--file-name` (hosted MCP has no filesystem).
59
+ The URL must be fetchable by the hosted MCP server (public HTTPS). Do not use local-only URLs or tunnel hacks unless the user explicitly approves.
33
60
 
34
- Optional Media wrapper:
61
+ Optional Media wrapper (RTF embeds, not person `media`):
35
62
 
36
63
  ```
37
- cms_edit ["asset", "upload", "./figure.png", "--with-media", "--media-name", "Figure 1"]
64
+ cms_edit ["asset", "upload", "--base64", "<encoded>", "--mime", "image/png", "--file-name", "figure.png", "--with-media", "--media-name", "Figure 1"]
38
65
  ```
39
66
 
67
+ Use `--if-exists-by-filename` to avoid duplicate assets when re-running.
68
+
69
+ ### Local CLI (only when user approves)
70
+
71
+ Local `cms-edit` may accept `--file` in some versions; hosted agents must not use local CLI without permission (see repo `Agents.md`).
72
+
73
+ ## Link uploaded asset to an entry
74
+
75
+ Upload returns an **asset ID**. Attach it, then save:
76
+
77
+ **Person portrait (`media` is an Asset link):**
78
+
79
+ ```
80
+ cms_edit ["open", "--id", "<person-entry-id>"]
81
+ cms_edit ["asset", "set", "@root", "media", "<new-asset-id>"]
82
+ cms_edit ["diff"]
83
+ cms_edit ["save"]
84
+ ```
85
+
86
+ **Component visual / article featured image:** same pattern — `asset set <ref> <field> <asset-id>` (or `set … --asset`).
87
+
88
+ Always `diff` → `save`. Verify with `preview urls` on the public path.
89
+
40
90
  ## Confirmation gates
41
91
 
42
92
  1. Report search results before uploading
43
93
  2. Confirm upload filename/title with user if ambiguous
94
+ 3. After upload, confirm `asset info <id>` shows expected width/height/size before linking
44
95
 
45
96
  ## Out of scope
46
97
 
47
98
  - Publish assets (draft upload; human publishes in Contentful UI if needed)
99
+ - Deleting orphan assets from failed uploads (human cleanup in Contentful)
48
100
 
49
101
  ## Related resources
50
102
 
51
- - `cms-edit://customer/defaults` (featured image rules)
103
+ - `cms-edit://customer/defaults` (featured image rules)
104
+ - `cms-edit://customer/people` (person `media` field, when present)
@@ -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
 
@@ -125,13 +126,42 @@ Always run `diff` before `save`. To abandon changes: `["discard"]`.
125
126
 
126
127
  | Task | Where |
127
128
  |------|-------|
128
- | Edit copy, add sections, create pages/articles | **Claude + cms-edit** (drafts) |
129
+ | Edit copy, add sections, create pages/articles | **Agent + cms-edit** (drafts) |
130
+ | Upload new images and files | **Agent + cms-edit** (`asset upload`) — search/reuse first |
129
131
  | Publish approved drafts | **Contentful UI** |
130
- | Upload new images and files | **Contentful UI** asset library |
131
- | Edit copy directly in Contentful | **Avoid** — use Claude instead |
132
+ | Edit copy directly in Contentful | **Avoid** — use cms-edit instead |
132
133
 
133
134
  cms-edit saves **drafts only**. A colleague must **publish in Contentful** for changes to go live.
134
135
 
136
+ ## Asset upload (hosted MCP)
137
+
138
+ Hosted connectors have **no server filesystem**. Use one source per upload:
139
+
140
+ | Source | When | Example |
141
+ |--------|------|---------|
142
+ | **Staged upload** (preferred for files &gt;~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"]` |
145
+
146
+ **Required with `--base64`:** `--mime`, `--file-name`. Pass **encoded bytes** in `args` — never a local path (`./file.png`, `file:///tmp/…`) or placeholder string.
147
+
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.
149
+
150
+ **PNG** when transparency matters.
151
+
152
+ Full workflow: `cms-edit://customer/task-media-reuse-and-upload`.
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
+
135
165
  ## Featured images (articles)
136
166
 
137
167
  Read `cms-edit://customer/defaults` when present. Otherwise:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@se-studio/contentful-cms",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "CLI tool for AI agents to read and edit Contentful draft content without publish permissions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 base64
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