@ompo-design/mcp-server 0.1.11 → 0.1.13
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/dist/apply-plan.d.ts +1 -1
- package/dist/apply-plan.js +218 -10
- package/dist/cli.js +2 -2
- package/dist/edit-billing.js +1 -1
- package/dist/iconify.d.ts +11 -0
- package/dist/iconify.js +42 -0
- package/dist/index.js +40 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +6 -2
- package/dist/types.d.ts +12 -3
- package/package.json +1 -1
package/dist/apply-plan.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type ApplyPlanFile = {
|
|
|
11
11
|
layoutIntent?: string;
|
|
12
12
|
};
|
|
13
13
|
export type DomStructurePlan = {
|
|
14
|
-
kind: 'dom.insert' | 'dom.move' | 'dom.delete' | 'dom.flexWrap';
|
|
14
|
+
kind: 'dom.insert' | 'dom.move' | 'dom.delete' | 'dom.flexWrap' | 'dom.iconReplace';
|
|
15
15
|
summary: string;
|
|
16
16
|
steps: string[];
|
|
17
17
|
payload: Record<string, unknown>;
|
package/dist/apply-plan.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
const OMPO_GLOSSARY = {
|
|
2
|
+
layoutNone: `Ompo layout "None" means the element is a normal block container (\`display: block\`), not a flex or grid layout parent. Do not add flex-direction, grid-template-columns, or gap unless a separate edit sets \`display: flex\` or \`display: grid\` on this selector.`,
|
|
3
|
+
layoutFlex: `Ompo layout "Flex" maps to \`display: flex\` (or inline-flex) on this selector. Apply flexDirection, justifyContent, alignItems, and gap together on the same container. Row/column toggles in Ompo map to flex-direction.`,
|
|
4
|
+
layoutGrid: `Ompo layout "Grid" maps to \`display: grid\` on this selector. Apply gridTemplateColumns (often \`repeat(n, 1fr)\`), optional gridTemplateRows / gridAutoRows, gridAutoFlow (row vs column), justifyContent, alignItems, and gap together on the grid parent. Prefer grid over flex for multi-column card/bento layouts.`,
|
|
2
5
|
gap: `Ompo "Gap" is the space between children inside a flex or grid container. Apply \`gap\` on the parent layout element (not on children). The parent needs \`display: flex\` or \`display: grid\`. Example: gap "16px" → CSS \`gap: 16px\` or Tailwind \`gap-4\`.`,
|
|
3
|
-
fill: `Ompo "Fill" means an element should expand to consume free space inside its parent. Fill is NOT a CSS property — read \`widthMode: fill\` / \`heightMode: fill\` together with \`ensureParentFlex\`, \`flexGrow\`, \`flexShrink\`, \`flexBasis\`, and \`alignSelf\` on the same selector. Parent must be \`display: flex
|
|
6
|
+
fill: `Ompo "Fill" means an element should expand to consume free space inside its parent. Fill is NOT a CSS property — read \`widthMode: fill\` / \`heightMode: fill\` together with \`ensureParentFlex\`, \`flexGrow\`, \`flexShrink\`, \`flexBasis\`, and \`alignSelf\` on the same selector. Parent must be \`display: flex\` (Fill does not apply inside grid parents). Main axis (direction of flex): use \`flex: 1 1 0\` (grow/shrink/basis). Cross axis: use \`align-self: stretch\` and remove fixed width/height on that axis. If \`ensureParentFlex\` is present, make the parent a flex container with that direction first.`,
|
|
4
7
|
fit: `Ompo "Fit" means size to content. Map \`widthMode: fit\` → \`width: fit-content\`, \`heightMode: fit\` → \`height: fit-content\` (or the project's equivalent).`,
|
|
5
|
-
imageFill: `Ompo image background fill:
|
|
8
|
+
imageFill: `Ompo image background fill: when \`backgroundImageSource\` is an https URL, download that asset into the project (e.g. public/images/) and set \`background-image\` to a project-relative url(). For legacy local paths, copy from the user's machine instead. Always use \`background-size: cover\`, \`background-position: center\`, and \`background-repeat: no-repeat\`. Do not commit file:// URLs to source.`,
|
|
6
9
|
domMove: `Ompo DOM move: reorder or reparent elements in source markup/components. Use movedElement anchors (tag, id, class, textSnippet) to find nodes. destinationChildren is the authoritative sibling order after the move — match this order in JSX/HTML or reorder mapped arrays. CSS alone cannot satisfy a dom.move.`,
|
|
7
10
|
insertText: `Ompo text insert: the user added a new text block (usually <p>). insertedElement carries insertKind "text", a stable data-ompo-insert-id selector, and textSnippet for the current copy. Apply as markup or a text component at parent/index; check separate text operations for edits after insert.`,
|
|
11
|
+
insertSection: `Ompo section insert: the user added a full-width page section (<div>, insertKind "section"). Default size is width 100% and height 300px with fill #AEC8F8. Append at the page root / bottom of main content unless parent/index says otherwise.`,
|
|
12
|
+
insertFrame: `Ompo frame insert: the user added a small div (insertKind "frame"). Default size is 150×80px with fill #AEC8F8. Use for nested containers, cards, or tiles inside a section.`,
|
|
8
13
|
insertIcon: `Ompo icon insert: the user added an Iconify icon (span.ompo-insert-icon) or editable SVG. iconId is the Iconify name (e.g. lucide:home) when known. Prefer the project's icon library or @iconify/react with that id instead of inlining raw SVG when possible. Size, fill color, and stroke changes appear as style operations on the same stable data-ompo-insert-id or data-ompo-icon-id selector.`,
|
|
9
|
-
|
|
14
|
+
replaceIcon: `Ompo icon replace: the user swapped an existing icon/SVG to a different Iconify icon. Update the project's icon component or @iconify/react usage to the new iconId — do not only patch inline SVG unless the project already inlines icons.`,
|
|
15
|
+
iconStroke: `Ompo icon/SVG stroke: iconStrokeWidth sets stroke-width on the inner SVG; iconStrokeColor sets stroke (often currentColor). Applies to inserted icons and native SVG elements selected in Ompo.`,
|
|
16
|
+
zIndex: `Ompo "Z Index" maps to CSS \`z-index\` on the selected element. Use the integer from the edit, or \`auto\` to clear an authored value. Higher values stack above lower values within the same stacking context. If stacking does not change in the browser, the element may also need a non-static \`position\` (e.g. relative) in source — Ompo may set inline z-index without changing position.`
|
|
10
17
|
};
|
|
11
18
|
function describeAnchor(anchor) {
|
|
12
19
|
const parts = [anchor.tagName];
|
|
@@ -28,6 +35,8 @@ function buildDomInsertPlan(operation) {
|
|
|
28
35
|
const inserted = operation.insertedElement;
|
|
29
36
|
const isIcon = operation.insertKind === 'icon';
|
|
30
37
|
const isText = operation.insertKind === 'text';
|
|
38
|
+
const isSection = operation.insertKind === 'section';
|
|
39
|
+
const isFrame = operation.insertKind === 'frame';
|
|
31
40
|
const steps = [];
|
|
32
41
|
if (operation.parent) {
|
|
33
42
|
steps.push(`Locate destination parent: ${describeAnchor(operation.parent)}.`);
|
|
@@ -44,11 +53,20 @@ function buildDomInsertPlan(operation) {
|
|
|
44
53
|
if (isText) {
|
|
45
54
|
steps.push(OMPO_GLOSSARY.insertText);
|
|
46
55
|
}
|
|
56
|
+
else if (isSection) {
|
|
57
|
+
steps.push(OMPO_GLOSSARY.insertSection);
|
|
58
|
+
}
|
|
59
|
+
else if (isFrame) {
|
|
60
|
+
steps.push(OMPO_GLOSSARY.insertFrame);
|
|
61
|
+
}
|
|
47
62
|
else if (isIcon) {
|
|
48
63
|
steps.push(OMPO_GLOSSARY.insertIcon);
|
|
49
64
|
if (operation.iconId) {
|
|
50
65
|
steps.push(`Iconify id: ${operation.iconId}`);
|
|
51
66
|
}
|
|
67
|
+
if (operation.iconSvg) {
|
|
68
|
+
steps.push('iconSvg is included in the plan payload for inline SVG or reference.');
|
|
69
|
+
}
|
|
52
70
|
}
|
|
53
71
|
steps.push(`Insert at child index ${operation.index}.`);
|
|
54
72
|
steps.push('Sanitize and adapt the HTML to the project’s component conventions before writing.');
|
|
@@ -56,7 +74,11 @@ function buildDomInsertPlan(operation) {
|
|
|
56
74
|
? `Insert Iconify icon${operation.iconId ? ` (${operation.iconId})` : ''}`
|
|
57
75
|
: isText
|
|
58
76
|
? 'Insert text block'
|
|
59
|
-
:
|
|
77
|
+
: isSection
|
|
78
|
+
? 'Insert page section'
|
|
79
|
+
: isFrame
|
|
80
|
+
? 'Insert frame div'
|
|
81
|
+
: 'Insert new element into the page';
|
|
60
82
|
return {
|
|
61
83
|
kind: 'dom.insert',
|
|
62
84
|
summary,
|
|
@@ -68,11 +90,41 @@ function buildDomInsertPlan(operation) {
|
|
|
68
90
|
selector: operation.selector,
|
|
69
91
|
insertKind: operation.insertKind,
|
|
70
92
|
iconId: operation.iconId,
|
|
93
|
+
iconSvg: operation.iconSvg,
|
|
71
94
|
insertedElement: operation.insertedElement,
|
|
72
95
|
parent: operation.parent
|
|
73
96
|
}
|
|
74
97
|
};
|
|
75
98
|
}
|
|
99
|
+
function buildIconReplacePlan(operation) {
|
|
100
|
+
const steps = [];
|
|
101
|
+
if (operation.element) {
|
|
102
|
+
steps.push(`Locate icon element: ${describeAnchor(operation.element)} (selector: "${operation.selector}").`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
steps.push(`Locate icon element "${operation.selector}" in source.`);
|
|
106
|
+
}
|
|
107
|
+
steps.push(OMPO_GLOSSARY.replaceIcon);
|
|
108
|
+
steps.push(`Set Iconify id to ${operation.iconId}.`);
|
|
109
|
+
if (operation.iconSvg) {
|
|
110
|
+
steps.push('Use iconSvg from the payload when the project inlines SVG markup.');
|
|
111
|
+
}
|
|
112
|
+
if (operation.previousIconId) {
|
|
113
|
+
steps.push(`Previous icon id: ${operation.previousIconId}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
kind: 'dom.iconReplace',
|
|
117
|
+
summary: `Replace icon with ${operation.iconId}`,
|
|
118
|
+
steps,
|
|
119
|
+
payload: {
|
|
120
|
+
selector: operation.selector,
|
|
121
|
+
iconId: operation.iconId,
|
|
122
|
+
iconSvg: operation.iconSvg,
|
|
123
|
+
previousIconId: operation.previousIconId,
|
|
124
|
+
element: operation.element
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
76
128
|
function formatChildOrder(children, movedSelector) {
|
|
77
129
|
return children.map((child, index) => {
|
|
78
130
|
const marker = child.selector === movedSelector ? ' ← moved element' : '';
|
|
@@ -162,10 +214,120 @@ function formatChangedValue(property, value) {
|
|
|
162
214
|
}
|
|
163
215
|
return '(empty / remove)';
|
|
164
216
|
}
|
|
217
|
+
function layoutModeFromChanged(changed) {
|
|
218
|
+
const display = typeof changed.display === 'string' ? changed.display : null;
|
|
219
|
+
if (display === 'flex' || display === 'inline-flex')
|
|
220
|
+
return 'flex';
|
|
221
|
+
if (display === 'grid' || display === 'inline-grid')
|
|
222
|
+
return 'grid';
|
|
223
|
+
if (display === 'block' || display === 'none')
|
|
224
|
+
return 'block';
|
|
225
|
+
if (changed.gridTemplateColumns !== undefined || changed.gridAutoFlow !== undefined) {
|
|
226
|
+
return 'grid';
|
|
227
|
+
}
|
|
228
|
+
if (changed.flexDirection !== undefined)
|
|
229
|
+
return 'flex';
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function imageFillSourceNotes(source) {
|
|
233
|
+
const trimmed = source.trim();
|
|
234
|
+
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
|
235
|
+
return `${OMPO_GLOSSARY.imageFill} Download ${trimmed}, save into the project (e.g. public/images/), and reference the copied asset in CSS.`;
|
|
236
|
+
}
|
|
237
|
+
return `${OMPO_GLOSSARY.imageFill} Read this file from the user's machine (${trimmed}), copy it into the repo, and reference the copied asset in CSS.`;
|
|
238
|
+
}
|
|
165
239
|
function styleSuggestionForProperty(property, value, changed) {
|
|
166
240
|
const parentFlex = changed.ensureParentFlex;
|
|
167
241
|
const parentFlexLabel = parentFlex === 'row' || parentFlex === 'column' ? parentFlex : 'row or column';
|
|
242
|
+
const layoutMode = layoutModeFromChanged(changed);
|
|
168
243
|
switch (property) {
|
|
244
|
+
case 'display': {
|
|
245
|
+
const displayValue = String(value);
|
|
246
|
+
if (displayValue === 'grid' || displayValue === 'inline-grid') {
|
|
247
|
+
return {
|
|
248
|
+
property,
|
|
249
|
+
value: displayValue,
|
|
250
|
+
strategy: 'css-rule',
|
|
251
|
+
notes: OMPO_GLOSSARY.layoutGrid
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (displayValue === 'flex' || displayValue === 'inline-flex') {
|
|
255
|
+
return {
|
|
256
|
+
property,
|
|
257
|
+
value: displayValue,
|
|
258
|
+
strategy: 'css-rule',
|
|
259
|
+
notes: OMPO_GLOSSARY.layoutFlex
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
property,
|
|
264
|
+
value: displayValue,
|
|
265
|
+
strategy: 'css-rule',
|
|
266
|
+
notes: OMPO_GLOSSARY.layoutNone
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
case 'gridTemplateColumns':
|
|
270
|
+
return {
|
|
271
|
+
property,
|
|
272
|
+
value: formatChangedValue(property, value),
|
|
273
|
+
strategy: 'css-rule',
|
|
274
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Column template for the grid parent — e.g. repeat(3, 1fr) for three equal columns. Tailwind: grid-cols-3 or arbitrary grid-template-columns.`
|
|
275
|
+
};
|
|
276
|
+
case 'gridTemplateRows':
|
|
277
|
+
return {
|
|
278
|
+
property,
|
|
279
|
+
value: formatChangedValue(property, value),
|
|
280
|
+
strategy: 'css-rule',
|
|
281
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Explicit row template. Use when Ompo authored fixed row tracks; otherwise grid-auto-rows may define implicit row sizing.`
|
|
282
|
+
};
|
|
283
|
+
case 'gridAutoRows':
|
|
284
|
+
return {
|
|
285
|
+
property,
|
|
286
|
+
value: formatChangedValue(property, value),
|
|
287
|
+
strategy: 'css-rule',
|
|
288
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Size for implicit grid rows (auto-generated tracks). Common: auto, minmax(180px, auto).`
|
|
289
|
+
};
|
|
290
|
+
case 'gridAutoColumns':
|
|
291
|
+
return {
|
|
292
|
+
property,
|
|
293
|
+
value: formatChangedValue(property, value),
|
|
294
|
+
strategy: 'css-rule',
|
|
295
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Size for implicit grid columns when grid-auto-flow creates new columns.`
|
|
296
|
+
};
|
|
297
|
+
case 'gridAutoFlow':
|
|
298
|
+
return {
|
|
299
|
+
property,
|
|
300
|
+
value: formatChangedValue(property, value),
|
|
301
|
+
strategy: 'css-rule',
|
|
302
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Ompo row/column direction toggle on grid maps to grid-auto-flow (row | column). Apply on the grid container.`
|
|
303
|
+
};
|
|
304
|
+
case 'gridColumn':
|
|
305
|
+
case 'gridRow':
|
|
306
|
+
return {
|
|
307
|
+
property,
|
|
308
|
+
value: formatChangedValue(property, value),
|
|
309
|
+
strategy: 'css-rule',
|
|
310
|
+
notes: `${OMPO_GLOSSARY.layoutGrid} Grid item placement on this child selector (span / line placement).`
|
|
311
|
+
};
|
|
312
|
+
case 'flexDirection':
|
|
313
|
+
return {
|
|
314
|
+
property,
|
|
315
|
+
value: formatChangedValue(property, value),
|
|
316
|
+
strategy: 'css-rule',
|
|
317
|
+
notes: layoutMode === 'grid'
|
|
318
|
+
? `${OMPO_GLOSSARY.layoutGrid} flex-direction does not apply when display is grid — use gridAutoFlow instead if present in the same edit.`
|
|
319
|
+
: `${OMPO_GLOSSARY.layoutFlex} Main axis direction for the flex container.`
|
|
320
|
+
};
|
|
321
|
+
case 'justifyContent':
|
|
322
|
+
case 'alignItems':
|
|
323
|
+
return {
|
|
324
|
+
property,
|
|
325
|
+
value: formatChangedValue(property, value),
|
|
326
|
+
strategy: 'css-rule',
|
|
327
|
+
notes: layoutMode === 'grid'
|
|
328
|
+
? `${OMPO_GLOSSARY.layoutGrid} Alignment on the grid container (${property}). Maps from Ompo’s alignment pinner / space-between toggle.`
|
|
329
|
+
: `${OMPO_GLOSSARY.layoutFlex} Alignment on the flex container (${property}). Maps from Ompo’s alignment pinner / space-between toggle.`
|
|
330
|
+
};
|
|
169
331
|
case 'gap':
|
|
170
332
|
return {
|
|
171
333
|
property,
|
|
@@ -255,7 +417,7 @@ function styleSuggestionForProperty(property, value, changed) {
|
|
|
255
417
|
property,
|
|
256
418
|
value: formatChangedValue(property, value),
|
|
257
419
|
strategy: 'component-markup',
|
|
258
|
-
notes:
|
|
420
|
+
notes: imageFillSourceNotes(String(value))
|
|
259
421
|
};
|
|
260
422
|
case 'backgroundImage':
|
|
261
423
|
if (changed.backgroundImageSource) {
|
|
@@ -263,7 +425,7 @@ function styleSuggestionForProperty(property, value, changed) {
|
|
|
263
425
|
property,
|
|
264
426
|
value: formatChangedValue(property, value),
|
|
265
427
|
strategy: 'css-rule',
|
|
266
|
-
notes:
|
|
428
|
+
notes: imageFillSourceNotes(String(changed.backgroundImageSource))
|
|
267
429
|
};
|
|
268
430
|
}
|
|
269
431
|
break;
|
|
@@ -287,6 +449,13 @@ function styleSuggestionForProperty(property, value, changed) {
|
|
|
287
449
|
strategy: 'inline-style',
|
|
288
450
|
notes: OMPO_GLOSSARY.iconStroke
|
|
289
451
|
};
|
|
452
|
+
case 'zIndex':
|
|
453
|
+
return {
|
|
454
|
+
property,
|
|
455
|
+
value: formatChangedValue(property, value),
|
|
456
|
+
strategy: 'css-rule',
|
|
457
|
+
notes: `${OMPO_GLOSSARY.zIndex} Apply as CSS \`z-index\` on this selector.`
|
|
458
|
+
};
|
|
290
459
|
}
|
|
291
460
|
return {
|
|
292
461
|
property,
|
|
@@ -301,6 +470,30 @@ function styleSuggestions(operation) {
|
|
|
301
470
|
}
|
|
302
471
|
function summarizeLayoutIntent(changed) {
|
|
303
472
|
const parts = [];
|
|
473
|
+
const layoutMode = layoutModeFromChanged(changed);
|
|
474
|
+
if (changed.display !== undefined) {
|
|
475
|
+
if (layoutMode === 'grid') {
|
|
476
|
+
parts.push('Layout: Grid container');
|
|
477
|
+
}
|
|
478
|
+
else if (layoutMode === 'flex') {
|
|
479
|
+
parts.push('Layout: Flex container');
|
|
480
|
+
}
|
|
481
|
+
else if (layoutMode === 'block') {
|
|
482
|
+
parts.push('Layout: None (block flow, not flex/grid)');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (changed.gridTemplateColumns !== undefined) {
|
|
486
|
+
parts.push(`Grid columns: ${changed.gridTemplateColumns}`);
|
|
487
|
+
}
|
|
488
|
+
if (changed.gridTemplateRows !== undefined) {
|
|
489
|
+
parts.push(`Grid rows: ${changed.gridTemplateRows}`);
|
|
490
|
+
}
|
|
491
|
+
if (changed.gridAutoFlow !== undefined) {
|
|
492
|
+
parts.push(`Grid auto-flow: ${changed.gridAutoFlow}`);
|
|
493
|
+
}
|
|
494
|
+
if (changed.flexDirection !== undefined && layoutMode !== 'grid') {
|
|
495
|
+
parts.push(`Flex direction: ${changed.flexDirection}`);
|
|
496
|
+
}
|
|
304
497
|
if (changed.gap !== undefined) {
|
|
305
498
|
parts.push(`Set container gap to ${changed.gap}`);
|
|
306
499
|
}
|
|
@@ -313,6 +506,9 @@ function summarizeLayoutIntent(changed) {
|
|
|
313
506
|
if (changed.ensureParentFlex) {
|
|
314
507
|
parts.push(`Parent needs display:flex with flex-direction:${changed.ensureParentFlex}`);
|
|
315
508
|
}
|
|
509
|
+
if (changed.zIndex !== undefined) {
|
|
510
|
+
parts.push(`Set z-index to ${changed.zIndex}`);
|
|
511
|
+
}
|
|
316
512
|
return parts.length > 0 ? parts.join('. ') : null;
|
|
317
513
|
}
|
|
318
514
|
function buildDomStructurePlan(operation) {
|
|
@@ -325,7 +521,8 @@ function buildDomStructurePlan(operation) {
|
|
|
325
521
|
`Locate the parent element matching "${operation.parentSelector}" in source.`,
|
|
326
522
|
`Insert a new <div> wrapper at child index ${operation.index} inside that parent.`,
|
|
327
523
|
`Move the matched child elements into the new wrapper. Use the child anchors (tag, id, class, textSnippet) to find the correct nodes in source.`,
|
|
328
|
-
`Apply only the wrapper styles listed in wrapperStyles to the new container.`,
|
|
524
|
+
`Apply only the wrapper styles listed in wrapperStyles to the new container (typically display:flex and flex-direction).`,
|
|
525
|
+
'For multi-column card layouts, prefer display:grid with grid-template-columns if wrapperStyles do not already specify layout.',
|
|
329
526
|
'Do not change unrelated siblings or parent styling.'
|
|
330
527
|
],
|
|
331
528
|
payload: {
|
|
@@ -341,6 +538,8 @@ function buildDomStructurePlan(operation) {
|
|
|
341
538
|
return buildDomInsertPlan(operation);
|
|
342
539
|
case 'dom.move':
|
|
343
540
|
return buildDomMovePlan(operation);
|
|
541
|
+
case 'dom.iconReplace':
|
|
542
|
+
return buildIconReplacePlan(operation);
|
|
344
543
|
case 'dom.delete':
|
|
345
544
|
return {
|
|
346
545
|
kind: 'dom.delete',
|
|
@@ -438,14 +637,19 @@ export function buildApplyPlan(bundle) {
|
|
|
438
637
|
instructions: [
|
|
439
638
|
'Only apply properties and operations listed in this plan.',
|
|
440
639
|
'Leave all untouched styling and markup unchanged.',
|
|
441
|
-
'Read ompoGlossary.fill and
|
|
442
|
-
'
|
|
640
|
+
'Read ompoGlossary.layoutNone, layoutFlex, layoutGrid, fill, and gap before applying layout changes.',
|
|
641
|
+
'Ompo layout types: None → display:block; Flex → display:flex + flexDirection; Grid → display:grid + gridTemplateColumns (+ gridAutoFlow for row/column).',
|
|
642
|
+
'When display is grid, apply gridTemplateColumns / gridAutoFlow / gap / justifyContent / alignItems on the grid parent — do not convert grid containers to flex.',
|
|
643
|
+
'When display is block (layout None), do not add flex or grid properties unless this edit explicitly sets display:flex or display:grid.',
|
|
644
|
+
'When widthMode or heightMode is "fill", apply the grouped flex properties together (flexGrow, flexBasis, alignSelf, removed width/height) — Fill requires a flex parent, not grid.',
|
|
443
645
|
'Gap belongs on the flex/grid parent. Fill belongs on the child inside a flex parent.',
|
|
444
646
|
'DOM moves are in domStructurePlans and domChanges — reorder or reparent elements in source markup/components, not just CSS.',
|
|
445
647
|
'Read ompoGlossary.domMove before applying dom.move operations.',
|
|
446
648
|
'For dom.move: destinationChildren is the structural snapshot of the final sibling order — match it in JSX/HTML or reorder mapped arrays.',
|
|
447
649
|
'For dom.flexWrap: create a new wrapper, move matched children, then apply wrapperStyles.',
|
|
448
650
|
'For dom.insert with insertKind text or icon: read ompoGlossary.insertText / insertIcon; use insertedElement anchors and iconId when applying.',
|
|
651
|
+
'For dom.iconReplace: read ompoGlossary.replaceIcon; update the project icon to the new iconId. Use iconSvg from domStructurePlans when inlining SVG.',
|
|
652
|
+
'For zIndex: read ompoGlossary.zIndex; map to CSS z-index on the matching selector.',
|
|
449
653
|
'Use child anchors (tag, id, class, textSnippet, insertKind, iconId) to find elements in source when selectors are unstable.',
|
|
450
654
|
'Prefer the smallest possible diff for each file.'
|
|
451
655
|
],
|
|
@@ -459,6 +663,7 @@ export function explainEdit(bundle) {
|
|
|
459
663
|
const moveCount = bundle.operations.filter((operation) => operation.kind === 'dom.move').length;
|
|
460
664
|
const textInsertCount = bundle.operations.filter((operation) => operation.kind === 'dom.insert' && operation.insertKind === 'text').length;
|
|
461
665
|
const iconInsertCount = bundle.operations.filter((operation) => operation.kind === 'dom.insert' && operation.insertKind === 'icon').length;
|
|
666
|
+
const iconReplaceCount = bundle.operations.filter((operation) => operation.kind === 'dom.iconReplace').length;
|
|
462
667
|
const domCount = bundle.operations.length - styleCount - textCount;
|
|
463
668
|
const scopeLabel = bundle.scope.mode === 'subtree' && bundle.scope.rootLabel
|
|
464
669
|
? `${bundle.scope.rootLabel} and its children`
|
|
@@ -479,7 +684,10 @@ export function explainEdit(bundle) {
|
|
|
479
684
|
lines.push(`${textInsertCount} text insert${textInsertCount === 1 ? '' : 's'} — apply via domStructurePlans (markup at parent/index)`);
|
|
480
685
|
}
|
|
481
686
|
if (iconInsertCount > 0) {
|
|
482
|
-
lines.push(`${iconInsertCount} icon insert${iconInsertCount === 1 ? '' : 's'} —
|
|
687
|
+
lines.push(`${iconInsertCount} icon insert${iconInsertCount === 1 ? '' : 's'} — apply iconId (and iconSvg when present) via domStructurePlans`);
|
|
688
|
+
}
|
|
689
|
+
if (iconReplaceCount > 0) {
|
|
690
|
+
lines.push(`${iconReplaceCount} icon replace${iconReplaceCount === 1 ? '' : 's'} — update iconId/iconSvg via domStructurePlans`);
|
|
483
691
|
}
|
|
484
692
|
lines.push(`Source preview: ${bundle.source.url}`);
|
|
485
693
|
return lines.join('\n');
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getOmpoEditsStorePath } from './edits-path.js';
|
|
|
7
7
|
import { readOmpoMcpSession } from './session.js';
|
|
8
8
|
import { readMcpTokenBalance } from './tokens.js';
|
|
9
9
|
const PACKAGE_NAME = '@ompo-design/mcp-server';
|
|
10
|
-
const PACKAGE_VERSION = '0.1.
|
|
10
|
+
const PACKAGE_VERSION = '0.1.12';
|
|
11
11
|
const SERVER_NAME = 'ompo';
|
|
12
12
|
function resolveExecutable(name) {
|
|
13
13
|
try {
|
|
@@ -187,7 +187,7 @@ async function runDoctor() {
|
|
|
187
187
|
console.log('Edits: none yet (click Send in Ompo first)');
|
|
188
188
|
}
|
|
189
189
|
console.log('');
|
|
190
|
-
console.log('If tools skip token usage, quit and reopen Claude Code to reload MCP v0.1.
|
|
190
|
+
console.log('If tools skip token usage, quit and reopen Claude Code to reload MCP v0.1.12+');
|
|
191
191
|
console.log('Global install: npx @ompo-design/mcp-server setup-global');
|
|
192
192
|
}
|
|
193
193
|
function printProjectNextSteps(projectRoot) {
|
package/dist/edit-billing.js
CHANGED
|
@@ -64,7 +64,7 @@ export async function consumeMcpTokenForEdit(toolName, editId) {
|
|
|
64
64
|
const balance = await readMcpTokenBalance();
|
|
65
65
|
return balance ?? 0;
|
|
66
66
|
}
|
|
67
|
-
const tokensRemaining = await consumeMcpToken(toolName);
|
|
67
|
+
const tokensRemaining = await consumeMcpToken(toolName, editId);
|
|
68
68
|
markEditBilled(editId);
|
|
69
69
|
return tokensRemaining;
|
|
70
70
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type IconifySearchResult = {
|
|
2
|
+
icons: string[];
|
|
3
|
+
total: number;
|
|
4
|
+
limit: number;
|
|
5
|
+
start: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function searchIconifyIcons(query: string, options?: {
|
|
8
|
+
limit?: number;
|
|
9
|
+
start?: number;
|
|
10
|
+
}): Promise<IconifySearchResult>;
|
|
11
|
+
export declare function fetchIconifySvg(iconId: string): Promise<string>;
|
package/dist/iconify.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const ICONIFY_API_BASE = 'https://api.iconify.design';
|
|
2
|
+
function splitIconifyId(iconId) {
|
|
3
|
+
const separator = iconId.indexOf(':');
|
|
4
|
+
if (separator <= 0)
|
|
5
|
+
return ['', ''];
|
|
6
|
+
return [iconId.slice(0, separator), iconId.slice(separator + 1)];
|
|
7
|
+
}
|
|
8
|
+
export async function searchIconifyIcons(query, options) {
|
|
9
|
+
const trimmed = query.trim();
|
|
10
|
+
const limit = Math.min(Math.max(options?.limit ?? 32, 1), 999);
|
|
11
|
+
const start = Math.max(options?.start ?? 0, 0);
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return { icons: [], total: 0, limit, start };
|
|
14
|
+
}
|
|
15
|
+
const url = new URL(`${ICONIFY_API_BASE}/search`);
|
|
16
|
+
url.searchParams.set('query', trimmed);
|
|
17
|
+
url.searchParams.set('limit', String(limit));
|
|
18
|
+
url.searchParams.set('start', String(start));
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Iconify search failed with ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
const payload = (await response.json());
|
|
24
|
+
return {
|
|
25
|
+
icons: payload.icons ?? [],
|
|
26
|
+
total: payload.total ?? payload.icons?.length ?? 0,
|
|
27
|
+
limit: payload.limit ?? limit,
|
|
28
|
+
start: payload.start ?? start
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function fetchIconifySvg(iconId) {
|
|
32
|
+
const [prefix, name] = splitIconifyId(iconId);
|
|
33
|
+
if (!prefix || !name) {
|
|
34
|
+
throw new Error(`Invalid Iconify icon id: ${iconId}`);
|
|
35
|
+
}
|
|
36
|
+
const url = `${ICONIFY_API_BASE}/${prefix}/${name}.svg`;
|
|
37
|
+
const response = await fetch(url);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Iconify SVG fetch failed with ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
return response.text();
|
|
42
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,11 @@ import { runCli } from './cli.js';
|
|
|
7
7
|
import { consumeMcpTokenForEdit, requireMcpSessionBalance } from './edit-billing.js';
|
|
8
8
|
import { editsStoreReady, listEdits, readEditBundle, recordEditApplied, recordEditPull } from './edit-store.js';
|
|
9
9
|
import { getOmpoEditsStorePath } from './edits-path.js';
|
|
10
|
+
import { fetchIconifySvg, searchIconifyIcons } from './iconify.js';
|
|
10
11
|
import { McpTokenError } from './tokens.js';
|
|
11
12
|
const server = new McpServer({
|
|
12
13
|
name: 'ompo-mcp-server',
|
|
13
|
-
version: '0.1.
|
|
14
|
+
version: '0.1.13'
|
|
14
15
|
});
|
|
15
16
|
function requireEditsStore() {
|
|
16
17
|
const storePath = getOmpoEditsStorePath();
|
|
@@ -129,6 +130,44 @@ server.tool('apply_edit', 'Build an apply plan for an Ompo edit. The agent shoul
|
|
|
129
130
|
return tokenErrorResult(error);
|
|
130
131
|
}
|
|
131
132
|
});
|
|
133
|
+
server.tool('search_icons', 'Search Iconify icons by keyword (same catalog Ompo uses in the editor)', {
|
|
134
|
+
query: z.string().describe('Search terms, e.g. "home", "menu", "arrow right"'),
|
|
135
|
+
limit: z.number().int().min(1).max(64).optional().describe('Max results (default 32)'),
|
|
136
|
+
start: z.number().int().min(0).optional().describe('Pagination offset')
|
|
137
|
+
}, async ({ query, limit, start }) => {
|
|
138
|
+
try {
|
|
139
|
+
const result = await searchIconifyIcons(query, { limit, start });
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: JSON.stringify(result, null, 2)
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return tokenErrorResult(error);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
server.tool('fetch_icon', 'Fetch Iconify SVG markup for an icon id (e.g. lucide:home)', {
|
|
154
|
+
iconId: z.string().describe('Iconify id in prefix:name format, e.g. lucide:home')
|
|
155
|
+
}, async ({ iconId }) => {
|
|
156
|
+
try {
|
|
157
|
+
const iconSvg = await fetchIconifySvg(iconId);
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: JSON.stringify({ iconId, iconSvg }, null, 2)
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return tokenErrorResult(error);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
132
171
|
async function main() {
|
|
133
172
|
if (runCli(process.argv.slice(2)))
|
|
134
173
|
return;
|
package/dist/tokens.d.ts
CHANGED
|
@@ -3,4 +3,4 @@ export declare class McpTokenError extends Error {
|
|
|
3
3
|
constructor(code: McpTokenError['code'], message: string);
|
|
4
4
|
}
|
|
5
5
|
export declare function readMcpTokenBalance(): Promise<number | null>;
|
|
6
|
-
export declare function consumeMcpToken(toolName: string): Promise<number>;
|
|
6
|
+
export declare function consumeMcpToken(toolName: string, editId?: string): Promise<number>;
|
package/dist/tokens.js
CHANGED
|
@@ -16,6 +16,9 @@ function mapRpcError(message) {
|
|
|
16
16
|
if (normalized.includes('INSUFFICIENT_TOKENS')) {
|
|
17
17
|
return new McpTokenError('INSUFFICIENT_TOKENS', 'You are out of MCP tokens. Add more tokens to your account before using Ompo MCP tools.');
|
|
18
18
|
}
|
|
19
|
+
if (normalized.includes('COULD NOT CHOOSE THE BEST CANDIDATE FUNCTION')) {
|
|
20
|
+
return new McpTokenError('TOKEN_SERVICE_UNAVAILABLE', 'MCP token service is misconfigured on the server. Update Ompo or contact support.');
|
|
21
|
+
}
|
|
19
22
|
return new McpTokenError('TOKEN_SERVICE_UNAVAILABLE', 'Could not verify MCP token balance. Check your connection and try again.');
|
|
20
23
|
}
|
|
21
24
|
async function createAuthedSupabase(session) {
|
|
@@ -70,14 +73,15 @@ export async function readMcpTokenBalance() {
|
|
|
70
73
|
throw mapRpcError(error.message);
|
|
71
74
|
return data?.tokens_available ?? null;
|
|
72
75
|
}
|
|
73
|
-
export async function consumeMcpToken(toolName) {
|
|
76
|
+
export async function consumeMcpToken(toolName, editId) {
|
|
74
77
|
const session = readOmpoMcpSession();
|
|
75
78
|
if (!session) {
|
|
76
79
|
throw new McpTokenError('NOT_SIGNED_IN', 'Sign in to Ompo and keep the app open so your MCP session stays active.');
|
|
77
80
|
}
|
|
78
81
|
const supabase = await createAuthedSupabase(session);
|
|
79
82
|
const { data, error } = await supabase.rpc('consume_mcp_token', {
|
|
80
|
-
p_tool_name: toolName
|
|
83
|
+
p_tool_name: toolName,
|
|
84
|
+
p_edit_id: editId ?? null
|
|
81
85
|
});
|
|
82
86
|
if (error) {
|
|
83
87
|
throw mapRpcError(error.message);
|
package/dist/types.d.ts
CHANGED
|
@@ -15,8 +15,9 @@ export type DomInsertOperation = {
|
|
|
15
15
|
index: number;
|
|
16
16
|
html: string;
|
|
17
17
|
selector?: string;
|
|
18
|
-
insertKind?: 'text' | 'icon';
|
|
18
|
+
insertKind?: 'text' | 'icon' | 'frame' | 'section';
|
|
19
19
|
iconId?: string;
|
|
20
|
+
iconSvg?: string;
|
|
20
21
|
insertedElement?: ElementAnchor;
|
|
21
22
|
parent?: ElementAnchor;
|
|
22
23
|
};
|
|
@@ -38,13 +39,21 @@ export type DomDeleteOperation = {
|
|
|
38
39
|
kind: 'dom.delete';
|
|
39
40
|
selector: string;
|
|
40
41
|
};
|
|
42
|
+
export type DomIconReplaceOperation = {
|
|
43
|
+
kind: 'dom.iconReplace';
|
|
44
|
+
selector: string;
|
|
45
|
+
iconId: string;
|
|
46
|
+
iconSvg?: string;
|
|
47
|
+
previousIconId?: string;
|
|
48
|
+
element?: ElementAnchor;
|
|
49
|
+
};
|
|
41
50
|
export type ElementAnchor = {
|
|
42
51
|
selector: string;
|
|
43
52
|
tagName: string;
|
|
44
53
|
id?: string;
|
|
45
54
|
className?: string;
|
|
46
55
|
textSnippet?: string;
|
|
47
|
-
insertKind?: 'text' | 'icon';
|
|
56
|
+
insertKind?: 'text' | 'icon' | 'frame' | 'section';
|
|
48
57
|
iconId?: string;
|
|
49
58
|
};
|
|
50
59
|
export type DomFlexWrapOperation = {
|
|
@@ -62,7 +71,7 @@ export type TextOperation = {
|
|
|
62
71
|
textContent?: string;
|
|
63
72
|
innerHTML?: string;
|
|
64
73
|
};
|
|
65
|
-
export type OmpoOperation = StyleOperation | DomInsertOperation | DomMoveOperation | DomDeleteOperation | DomFlexWrapOperation | TextOperation;
|
|
74
|
+
export type OmpoOperation = StyleOperation | DomInsertOperation | DomMoveOperation | DomDeleteOperation | DomIconReplaceOperation | DomFlexWrapOperation | TextOperation;
|
|
66
75
|
export type OmpoEditBundle = {
|
|
67
76
|
schemaVersion: number;
|
|
68
77
|
id: string;
|