@illusoryai/pi-framer 0.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/extensions/framer.ts +397 -0
- package/package.json +18 -0
- package/skills/framer-cms/SKILL.md +76 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "@sinclair/typebox";
|
|
3
|
+
|
|
4
|
+
const PROJECT_URL = process.env.FRAMER_PROJECT_URL;
|
|
5
|
+
const API_KEY = process.env.FRAMER_API_KEY;
|
|
6
|
+
|
|
7
|
+
export default function (pi: ExtensionAPI) {
|
|
8
|
+
if (!PROJECT_URL || !API_KEY) {
|
|
9
|
+
return; // Framer not configured — skip silently
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let framer: any = null;
|
|
13
|
+
|
|
14
|
+
async function getFramer(): Promise<any> {
|
|
15
|
+
if (framer) return framer;
|
|
16
|
+
const { connect } = await import("framer-api");
|
|
17
|
+
framer = await connect(PROJECT_URL!, API_KEY!);
|
|
18
|
+
return framer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Disconnect on session shutdown
|
|
22
|
+
pi.on("session_shutdown", async () => {
|
|
23
|
+
if (framer) {
|
|
24
|
+
try {
|
|
25
|
+
await framer.disconnect();
|
|
26
|
+
} catch {}
|
|
27
|
+
framer = null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// --- Helpers ---
|
|
32
|
+
|
|
33
|
+
async function getCollectionByName(f: any, name: string) {
|
|
34
|
+
const collections = await f.getCollections();
|
|
35
|
+
return (
|
|
36
|
+
collections.find(
|
|
37
|
+
(c: any) =>
|
|
38
|
+
c.name?.toLowerCase() === name.toLowerCase() ||
|
|
39
|
+
c.id === name
|
|
40
|
+
) || collections[0]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractFieldValue(entry: any, full = false): any {
|
|
45
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
46
|
+
if (entry.type === "image" && entry.value?.url) return entry.value.url;
|
|
47
|
+
if (entry.type === "formattedText" && entry.value) {
|
|
48
|
+
if (full) return entry.value; // Return full HTML
|
|
49
|
+
const text = entry.value.replace(/<[^>]+>/g, "");
|
|
50
|
+
return text.length > 200 ? text.substring(0, 200) + "..." : text;
|
|
51
|
+
}
|
|
52
|
+
if ("value" in entry) return entry.value;
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildFieldData(
|
|
57
|
+
fields: any[],
|
|
58
|
+
assignments: string[]
|
|
59
|
+
): Record<string, any> {
|
|
60
|
+
const data: Record<string, any> = {};
|
|
61
|
+
for (const assign of assignments) {
|
|
62
|
+
const eqIdx = assign.indexOf("=");
|
|
63
|
+
if (eqIdx === -1) continue;
|
|
64
|
+
const key = assign.substring(0, eqIdx).trim();
|
|
65
|
+
const val = assign.substring(eqIdx + 1).trim();
|
|
66
|
+
|
|
67
|
+
// Find field by name or ID
|
|
68
|
+
const field = fields.find(
|
|
69
|
+
(f: any) =>
|
|
70
|
+
f.name?.toLowerCase() === key.toLowerCase() || f.id === key
|
|
71
|
+
);
|
|
72
|
+
if (!field) continue;
|
|
73
|
+
|
|
74
|
+
const fieldId = field.id;
|
|
75
|
+
switch (field.type) {
|
|
76
|
+
case "boolean":
|
|
77
|
+
data[fieldId] = { type: "boolean", value: val === "true" };
|
|
78
|
+
break;
|
|
79
|
+
case "date":
|
|
80
|
+
data[fieldId] = { type: "date", value: new Date(val).toISOString() };
|
|
81
|
+
break;
|
|
82
|
+
case "number":
|
|
83
|
+
data[fieldId] = { type: "number", value: Number(val) };
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
data[fieldId] = {
|
|
87
|
+
type: "string",
|
|
88
|
+
value: val,
|
|
89
|
+
valueByLocale: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Command handlers ---
|
|
97
|
+
|
|
98
|
+
async function handleInfo() {
|
|
99
|
+
const f = await getFramer();
|
|
100
|
+
const info = await f.getProjectInfo();
|
|
101
|
+
return JSON.stringify(info, null, 2);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function handleCollections() {
|
|
105
|
+
const f = await getFramer();
|
|
106
|
+
const collections = await f.getCollections();
|
|
107
|
+
const result = collections.map((c: any) => ({
|
|
108
|
+
name: c.name,
|
|
109
|
+
id: c.id,
|
|
110
|
+
managedBy: c.managedBy,
|
|
111
|
+
slugField: c.slugFieldName,
|
|
112
|
+
}));
|
|
113
|
+
return JSON.stringify(result, null, 2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleSchema(collectionName: string) {
|
|
117
|
+
const f = await getFramer();
|
|
118
|
+
const col = await getCollectionByName(f, collectionName);
|
|
119
|
+
if (!col) return "Collection not found";
|
|
120
|
+
const fields = await col.getFields();
|
|
121
|
+
const result = fields.map((field: any) => ({
|
|
122
|
+
id: field.id,
|
|
123
|
+
name: field.name,
|
|
124
|
+
type: field.type,
|
|
125
|
+
}));
|
|
126
|
+
return `Collection: ${col.name} (${col.id})\nSlug field: ${col.slugFieldName}\n\n${JSON.stringify(result, null, 2)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleItems(collectionName: string) {
|
|
130
|
+
const f = await getFramer();
|
|
131
|
+
const col = await getCollectionByName(f, collectionName);
|
|
132
|
+
if (!col) return "Collection not found";
|
|
133
|
+
const fields = await col.getFields();
|
|
134
|
+
const items = await col.getItems();
|
|
135
|
+
|
|
136
|
+
const fieldMap: Record<string, string> = {};
|
|
137
|
+
for (const field of fields) fieldMap[field.id] = field.name;
|
|
138
|
+
|
|
139
|
+
// Find title field
|
|
140
|
+
const titleField = fields.find(
|
|
141
|
+
(field: any) =>
|
|
142
|
+
field.name?.toLowerCase() === "title" ||
|
|
143
|
+
field.name?.toLowerCase() === "name"
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const result = items.map((item: any) => {
|
|
147
|
+
const title = titleField
|
|
148
|
+
? extractFieldValue(item.fieldData?.[titleField.id])
|
|
149
|
+
: item.slug;
|
|
150
|
+
return {
|
|
151
|
+
id: item.id,
|
|
152
|
+
slug: item.slug,
|
|
153
|
+
draft: item.draft,
|
|
154
|
+
title,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
return `${items.length} items in ${col.name}:\n\n${JSON.stringify(result, null, 2)}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function handleItem(slugOrId: string) {
|
|
161
|
+
const f = await getFramer();
|
|
162
|
+
const collections = await f.getCollections();
|
|
163
|
+
|
|
164
|
+
for (const col of collections) {
|
|
165
|
+
const fields = await col.getFields();
|
|
166
|
+
const items = await col.getItems();
|
|
167
|
+
const fieldMap: Record<string, string> = {};
|
|
168
|
+
for (const field of fields) fieldMap[field.id] = field.name;
|
|
169
|
+
|
|
170
|
+
const item = items.find(
|
|
171
|
+
(i: any) => i.slug === slugOrId || i.id === slugOrId
|
|
172
|
+
);
|
|
173
|
+
if (!item) continue;
|
|
174
|
+
|
|
175
|
+
const data: Record<string, any> = {
|
|
176
|
+
id: item.id,
|
|
177
|
+
slug: item.slug,
|
|
178
|
+
draft: item.draft,
|
|
179
|
+
collection: col.name,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const [fieldId, value] of Object.entries(item.fieldData || {})) {
|
|
183
|
+
const fieldName = fieldMap[fieldId] || fieldId;
|
|
184
|
+
data[fieldName] = extractFieldValue(value, true);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return JSON.stringify(data, null, 2);
|
|
188
|
+
}
|
|
189
|
+
return `Item not found: ${slugOrId}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function handleUpdate(args: string) {
|
|
193
|
+
const parts = args.split(/\s+/);
|
|
194
|
+
const slugOrId = parts[0];
|
|
195
|
+
const assignments = parts.slice(1);
|
|
196
|
+
|
|
197
|
+
if (!slugOrId || assignments.length === 0) {
|
|
198
|
+
return "Usage: update <slug-or-id> field1=value1 field2=value2";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const f = await getFramer();
|
|
202
|
+
const collections = await f.getCollections();
|
|
203
|
+
|
|
204
|
+
for (const col of collections) {
|
|
205
|
+
const fields = await col.getFields();
|
|
206
|
+
const items = await col.getItems();
|
|
207
|
+
const item = items.find(
|
|
208
|
+
(i: any) => i.slug === slugOrId || i.id === slugOrId
|
|
209
|
+
);
|
|
210
|
+
if (!item) continue;
|
|
211
|
+
|
|
212
|
+
const fieldData = buildFieldData(fields, assignments);
|
|
213
|
+
if (Object.keys(fieldData).length === 0) {
|
|
214
|
+
return `No valid fields found. Available: ${fields.map((f: any) => f.name).join(", ")}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const updated = await item.setAttributes({ fieldData });
|
|
218
|
+
return `Updated ${slugOrId}:\n${JSON.stringify(fieldData, null, 2)}`;
|
|
219
|
+
}
|
|
220
|
+
return `Item not found: ${slugOrId}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function handleCreate(args: string) {
|
|
224
|
+
// Parse: create <collection> field1=value1 field2=value2
|
|
225
|
+
const parts = args.split(/\s+/);
|
|
226
|
+
const collectionName = parts[0];
|
|
227
|
+
const assignments = parts.slice(1);
|
|
228
|
+
|
|
229
|
+
if (!collectionName) {
|
|
230
|
+
return "Usage: create <collection> field1=value1 field2=value2";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const f = await getFramer();
|
|
234
|
+
const col = await getCollectionByName(f, collectionName);
|
|
235
|
+
if (!col) return "Collection not found";
|
|
236
|
+
|
|
237
|
+
const fields = await col.getFields();
|
|
238
|
+
const fieldData = buildFieldData(fields, assignments);
|
|
239
|
+
|
|
240
|
+
// Extract slug if provided
|
|
241
|
+
const slugAssign = assignments.find((a) =>
|
|
242
|
+
a.toLowerCase().startsWith("slug=")
|
|
243
|
+
);
|
|
244
|
+
const slug = slugAssign ? slugAssign.split("=")[1] : undefined;
|
|
245
|
+
|
|
246
|
+
const newItem: any = { fieldData, draft: false };
|
|
247
|
+
if (slug) newItem.slug = slug;
|
|
248
|
+
|
|
249
|
+
await col.addItems([newItem]);
|
|
250
|
+
return `Created item in ${col.name}${slug ? ` with slug: ${slug}` : ""}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function handlePublish() {
|
|
254
|
+
const f = await getFramer();
|
|
255
|
+
const result = await f.publish();
|
|
256
|
+
return JSON.stringify(result, null, 2);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function handleDeploy(deploymentId: string) {
|
|
260
|
+
if (!deploymentId) return "Usage: deploy <deployment-id>";
|
|
261
|
+
const f = await getFramer();
|
|
262
|
+
const result = await f.deploy(deploymentId);
|
|
263
|
+
return JSON.stringify(result, null, 2);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function handleChanges() {
|
|
267
|
+
const f = await getFramer();
|
|
268
|
+
const changes = await f.getChangedPaths();
|
|
269
|
+
const contributors = await f.getChangeContributors();
|
|
270
|
+
return JSON.stringify({ changes, contributors }, null, 2);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function handleCode(args?: string) {
|
|
274
|
+
const f = await getFramer();
|
|
275
|
+
if (!args) {
|
|
276
|
+
const code = await f.getCustomCode();
|
|
277
|
+
return JSON.stringify(code, null, 2);
|
|
278
|
+
}
|
|
279
|
+
// Set custom code: code <location> <html>
|
|
280
|
+
const [location, ...rest] = args.split(/\s+/);
|
|
281
|
+
const html = rest.join(" ");
|
|
282
|
+
const validLocations = ["headStart", "headEnd", "bodyStart", "bodyEnd"];
|
|
283
|
+
if (!validLocations.includes(location)) {
|
|
284
|
+
return `Invalid location. Use: ${validLocations.join(", ")}`;
|
|
285
|
+
}
|
|
286
|
+
await f.setCustomCode({ [location]: { html, disabled: false } });
|
|
287
|
+
return `Custom code set at ${location}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function handlePublishDeploy() {
|
|
291
|
+
const f = await getFramer();
|
|
292
|
+
const pubResult = await f.publish();
|
|
293
|
+
const deploymentId = pubResult.deployment?.id;
|
|
294
|
+
if (!deploymentId) return "Publish failed — no deployment ID returned";
|
|
295
|
+
const deployResult = await f.deploy(deploymentId);
|
|
296
|
+
return `Published and deployed!\n\nPublish: ${JSON.stringify(pubResult, null, 2)}\n\nDeploy: ${JSON.stringify(deployResult, null, 2)}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Register tool ---
|
|
300
|
+
|
|
301
|
+
pi.registerTool({
|
|
302
|
+
name: "framer",
|
|
303
|
+
label: "Framer CMS",
|
|
304
|
+
description:
|
|
305
|
+
"Manage Framer site via Server API. CMS operations, publishing, and custom code.\n\n" +
|
|
306
|
+
"Commands:\n" +
|
|
307
|
+
" info — Project info\n" +
|
|
308
|
+
" collections — List all CMS collections\n" +
|
|
309
|
+
" schema <collection> — Collection field schema\n" +
|
|
310
|
+
" items <collection> — List items (title, slug, draft)\n" +
|
|
311
|
+
" item <slug-or-id> — Full item details\n" +
|
|
312
|
+
" update <slug-or-id> field=val — Update item fields\n" +
|
|
313
|
+
" create <collection> field=val — Create new item\n" +
|
|
314
|
+
" publish — Create preview deployment\n" +
|
|
315
|
+
" deploy <deployment-id> — Promote to production\n" +
|
|
316
|
+
" ship — Publish + deploy in one step\n" +
|
|
317
|
+
" changes — Pending changes + contributors\n" +
|
|
318
|
+
" code — Get custom code\n" +
|
|
319
|
+
" code <location> <html> — Set custom code (headStart|headEnd|bodyStart|bodyEnd)",
|
|
320
|
+
parameters: Type.Object({
|
|
321
|
+
command: Type.String({
|
|
322
|
+
description:
|
|
323
|
+
'Framer command, e.g. \'items Blog\', \'update my-post-slug "Small Description"="New description"\'',
|
|
324
|
+
}),
|
|
325
|
+
}),
|
|
326
|
+
async execute(_toolCallId, params, _signal) {
|
|
327
|
+
try {
|
|
328
|
+
const cmd = params.command.trim();
|
|
329
|
+
const spaceIdx = cmd.indexOf(" ");
|
|
330
|
+
const action = spaceIdx === -1 ? cmd : cmd.substring(0, spaceIdx);
|
|
331
|
+
const args = spaceIdx === -1 ? "" : cmd.substring(spaceIdx + 1).trim();
|
|
332
|
+
|
|
333
|
+
let result: string;
|
|
334
|
+
|
|
335
|
+
switch (action.toLowerCase()) {
|
|
336
|
+
case "info":
|
|
337
|
+
result = await handleInfo();
|
|
338
|
+
break;
|
|
339
|
+
case "collections":
|
|
340
|
+
result = await handleCollections();
|
|
341
|
+
break;
|
|
342
|
+
case "schema":
|
|
343
|
+
result = await handleSchema(args || "Blog");
|
|
344
|
+
break;
|
|
345
|
+
case "items":
|
|
346
|
+
result = await handleItems(args || "Blog");
|
|
347
|
+
break;
|
|
348
|
+
case "item":
|
|
349
|
+
result = await handleItem(args);
|
|
350
|
+
break;
|
|
351
|
+
case "update":
|
|
352
|
+
result = await handleUpdate(args);
|
|
353
|
+
break;
|
|
354
|
+
case "create":
|
|
355
|
+
result = await handleCreate(args);
|
|
356
|
+
break;
|
|
357
|
+
case "publish":
|
|
358
|
+
result = await handlePublish();
|
|
359
|
+
break;
|
|
360
|
+
case "deploy":
|
|
361
|
+
result = await handleDeploy(args);
|
|
362
|
+
break;
|
|
363
|
+
case "ship":
|
|
364
|
+
result = await handlePublishDeploy();
|
|
365
|
+
break;
|
|
366
|
+
case "changes":
|
|
367
|
+
result = await handleChanges();
|
|
368
|
+
break;
|
|
369
|
+
case "code":
|
|
370
|
+
result = await handleCode(args || undefined);
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
result = `Unknown command: ${action}. Use: info, collections, schema, items, item, update, create, publish, deploy, ship, changes, code`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: result }],
|
|
378
|
+
details: { command: params.command },
|
|
379
|
+
};
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
// Reset connection on error
|
|
382
|
+
if (
|
|
383
|
+
err.message?.includes("closed") ||
|
|
384
|
+
err.message?.includes("disconnect")
|
|
385
|
+
) {
|
|
386
|
+
framer = null;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{ type: "text", text: `Error: ${err.message || String(err)}` },
|
|
391
|
+
],
|
|
392
|
+
isError: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@illusoryai/pi-framer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": ["pi-package"],
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": "extensions",
|
|
9
|
+
"skills": "skills"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"framer-api": "^0.2.4"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@mariozechner/pi-ai": "*",
|
|
16
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framer-cms
|
|
3
|
+
description: Manage Framer CMS content, publish deployments, and automate blog operations via the framer tool. Use when creating/editing blog posts, publishing site changes, or managing CMS collections.
|
|
4
|
+
allowed-tools: framer, read, bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Framer CMS Operations
|
|
8
|
+
|
|
9
|
+
**Use this when:** Creating or editing blog posts, publishing site changes, managing CMS collections, or automating content workflows.
|
|
10
|
+
|
|
11
|
+
## Quick Reference
|
|
12
|
+
|
|
13
|
+
| Command | Purpose |
|
|
14
|
+
|---------|---------|
|
|
15
|
+
| `framer info` | Project info |
|
|
16
|
+
| `framer collections` | List all CMS collections |
|
|
17
|
+
| `framer schema Blog` | Blog collection field schema |
|
|
18
|
+
| `framer items Blog` | List all blog posts (title, slug, draft) |
|
|
19
|
+
| `framer item <slug>` | Full item details |
|
|
20
|
+
| `framer update <slug> field=value` | Update item fields |
|
|
21
|
+
| `framer create Blog field=value` | Create new blog post |
|
|
22
|
+
| `framer ship` | Publish + deploy to production in one step |
|
|
23
|
+
| `framer changes` | Show pending changes |
|
|
24
|
+
| `framer code` | Get/set custom code injection |
|
|
25
|
+
|
|
26
|
+
## Blog Collection Schema
|
|
27
|
+
|
|
28
|
+
| Field | ID | Type |
|
|
29
|
+
|-------|----|------|
|
|
30
|
+
| Category | `V20PYwhSX` | string ("Guide" or "Insight") |
|
|
31
|
+
| Title | `RdldGed5b` | string |
|
|
32
|
+
| Small Description | `gZduoeUwm` | string (meta description) |
|
|
33
|
+
| Date | `oErJVDjg0` | date |
|
|
34
|
+
| Image | `wYf3IKXhH` | image |
|
|
35
|
+
| Featured | `BESrV0imp` | boolean |
|
|
36
|
+
| Author | `ifzqi5HZI` | string |
|
|
37
|
+
| Content | `JG8gJablh` | formattedText (HTML) |
|
|
38
|
+
|
|
39
|
+
## Common Workflows
|
|
40
|
+
|
|
41
|
+
### Edit a blog post's meta description
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
framer update ai-web-scraping-mobile-proxies "Small Description"="New meta description here"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Create a new blog post
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
framer create Blog Title="My New Post" "Small Description"="Post summary" Category=Guide Author="Josiah Richards" Date=2026-02-14 slug=my-new-post
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Publish and deploy changes
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
framer ship
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This runs `publish()` (creates preview) then `deploy(id)` (promotes to www.illusory.io).
|
|
60
|
+
|
|
61
|
+
### After publishing: regenerate llms.txt
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun run scripts/generate-llms.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add curated overrides in `workers/illusory-edge/llms-config.json` for correct titles/descriptions.
|
|
68
|
+
|
|
69
|
+
## Architecture Notes
|
|
70
|
+
|
|
71
|
+
- **WebSocket SDK** — `framer-api` connects via persistent WebSocket, not REST
|
|
72
|
+
- **Connection lifecycle** — extension connects on first use, disconnects on session shutdown
|
|
73
|
+
- **No transactions** — handle partial failures; CMS updates are immediate
|
|
74
|
+
- **Publishing** — CMS changes are NOT live until `ship` (or `publish` + `deploy`)
|
|
75
|
+
- **Custom domain** — deploys update `www.illusory.io` via Framer's CDN
|
|
76
|
+
- **Worker origin** — currently `smart-let-525421.framer.app`
|