@divlet/core 0.0.1 → 0.0.3
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/AGENTS.md +3 -2
- package/BACKLOG.md +4 -3
- package/CHANGELOG.md +17 -2
- package/README.md +16 -6
- package/dist/core/lib/acceptance.d.ts +18 -0
- package/dist/core/lib/acceptance.js +29 -0
- package/dist/core/lib/assets.js +1 -1
- package/dist/core/lib/browser-board.d.ts +41 -0
- package/dist/core/lib/browser-board.js +203 -0
- package/dist/core/lib/browser-mcp.d.ts +131 -0
- package/dist/core/lib/browser-mcp.js +448 -0
- package/dist/core/lib/browser-media-type.d.ts +4 -0
- package/dist/core/lib/browser-media-type.js +7 -0
- package/dist/core/lib/browser-runtime.d.ts +598 -0
- package/dist/core/lib/browser-runtime.js +674 -0
- package/dist/core/lib/codec.js +2 -1
- package/dist/core/lib/contract.d.ts +1 -152
- package/dist/core/lib/contract.js +3 -359
- package/dist/core/lib/diff.js +1 -1
- package/dist/core/lib/prepare.d.ts +27 -0
- package/dist/core/lib/prepare.js +41 -0
- package/dist/core/lib/project.js +3 -3
- package/dist/core/lib/render.d.ts +2 -9
- package/dist/core/lib/render.js +10 -35
- package/dist/core/lib/runtime.d.ts +1 -1
- package/dist/core/lib/runtime.js +1 -21
- package/dist/core/lib/tools.d.ts +154 -0
- package/dist/core/lib/tools.js +364 -0
- package/dist/core/lib/validation.d.ts +5 -0
- package/dist/core/lib/validation.js +21 -0
- package/dist/skills/divlet/SKILL.md +7 -1
- package/docs/README.md +2 -1
- package/docs/agents.md +11 -1
- package/docs/architecture.md +15 -7
- package/docs/board.md +1 -1
- package/docs/browser-mcp.md +108 -0
- package/docs/browser-runtime.md +6 -4
- package/docs/publishing.md +54 -5
- package/docs/testing.md +16 -3
- package/examples/README.md +2 -2
- package/examples/landing/landing.divlet +1 -1
- package/package.json +24 -5
- package/skills/divlet/SKILL.md +7 -1
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Owns: the portable half of the capability contract — the host-neutral parameter schema and its JSON Schema/TypeBox
|
|
3
|
+
* renderings, the agent result vocabulary, the runtime interface a tool runs against and the canonical tool list. Excludes:
|
|
4
|
+
* sessions, board discovery and anything that needs Node; `contract.ts` adds those and re-exports this module, so every host
|
|
5
|
+
* (Pi, MCP, CLI, the browser board) describes and runs the same tools from this one list.
|
|
6
|
+
*/
|
|
7
|
+
/** A host-neutral schema: the tool list is written once, Pi and MCP render it. */
|
|
8
|
+
export const S = {
|
|
9
|
+
string: (options = {}) => ({ kind: 'string', ...options }),
|
|
10
|
+
integer: (options = {}) => ({ kind: 'integer', ...options }),
|
|
11
|
+
number: (options = {}) => ({ kind: 'number', ...options }),
|
|
12
|
+
boolean: (options = {}) => ({ kind: 'boolean', ...options }),
|
|
13
|
+
/** One of a fixed set of strings. */
|
|
14
|
+
oneOf: (values, options = {}) => ({ kind: 'oneOf', values, ...options }),
|
|
15
|
+
/** An object with these properties (`null` shape: any object). */
|
|
16
|
+
object: (properties = null, options = {}) => ({ kind: 'object', properties, ...options }),
|
|
17
|
+
array: (items, options = {}) => ({ kind: 'array', items, ...options }),
|
|
18
|
+
union: (variants, options = {}) => ({ kind: 'union', variants, ...options }),
|
|
19
|
+
any: (options = {}) => ({ kind: 'any', ...options }),
|
|
20
|
+
};
|
|
21
|
+
export const optional = (schema) => ({ ...schema, optional: true });
|
|
22
|
+
export const nullable = (schema) => ({ kind: 'union', variants: [schema, { kind: 'null' }] });
|
|
23
|
+
/** A JSON Schema property for one parameter (the shape MCP asks for). */
|
|
24
|
+
export function propertySchema(schema) {
|
|
25
|
+
switch (schema.kind) {
|
|
26
|
+
case 'string': return { type: 'string', ...(schema.maxLength ? { maxLength: schema.maxLength } : {}), ...(schema.minLength ? { minLength: schema.minLength } : {}), ...(schema.description ? { description: schema.description } : {}) };
|
|
27
|
+
case 'integer': return { type: 'integer', ...(schema.minimum !== undefined ? { minimum: schema.minimum } : {}), ...(schema.maximum !== undefined ? { maximum: schema.maximum } : {}), ...(schema.description ? { description: schema.description } : {}) };
|
|
28
|
+
case 'number': return { type: 'number', ...(schema.minimum !== undefined ? { minimum: schema.minimum } : {}), ...(schema.maximum !== undefined ? { maximum: schema.maximum } : {}), ...(schema.description ? { description: schema.description } : {}) };
|
|
29
|
+
case 'boolean': return { type: 'boolean', ...(schema.description ? { description: schema.description } : {}) };
|
|
30
|
+
case 'oneOf': return { type: 'string', enum: schema.values, ...(schema.description ? { description: schema.description } : {}) };
|
|
31
|
+
case 'object': {
|
|
32
|
+
if (schema.properties === null)
|
|
33
|
+
return { type: 'object', additionalProperties: true, ...(schema.description ? { description: schema.description } : {}) };
|
|
34
|
+
const shape = Object.fromEntries(Object.entries(schema.properties).map(([name, item]) => [name, propertySchema(item)]));
|
|
35
|
+
const required = Object.entries(schema.properties).filter(([, item]) => !item.optional).map(([name]) => name);
|
|
36
|
+
return { type: 'object', properties: shape, ...(required.length ? { required } : {}), additionalProperties: false, ...(schema.description ? { description: schema.description } : {}) };
|
|
37
|
+
}
|
|
38
|
+
case 'array': return { type: 'array', items: propertySchema(schema.items), ...(schema.maxItems !== undefined ? { maxItems: schema.maxItems } : {}), ...(schema.description ? { description: schema.description } : {}) };
|
|
39
|
+
case 'null': return { type: 'null' };
|
|
40
|
+
case 'union': return { anyOf: schema.variants.map(propertySchema), ...(schema.description ? { description: schema.description } : {}) };
|
|
41
|
+
default: return schema.description ? { description: schema.description } : {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** The `inputSchema` of a tool for MCP (`tools/list`). */
|
|
45
|
+
export function inputSchema(tool) {
|
|
46
|
+
const shape = Object.fromEntries(Object.entries(tool.parameters).map(([name, schema]) => [name, propertySchema(schema)]));
|
|
47
|
+
const required = Object.entries(tool.parameters).filter(([, schema]) => !schema.optional).map(([name]) => name);
|
|
48
|
+
return { type: 'object', properties: shape, ...(required.length ? { required } : {}), additionalProperties: false };
|
|
49
|
+
}
|
|
50
|
+
/** The same schema as TypeBox types, for the Pi extension. */
|
|
51
|
+
export function toTypeBox(schema, Type) {
|
|
52
|
+
const convert = (item) => {
|
|
53
|
+
let next;
|
|
54
|
+
switch (item.kind) {
|
|
55
|
+
case 'string':
|
|
56
|
+
next = Type.String({ ...(item.maxLength ? { maxLength: item.maxLength } : {}), ...(item.description ? { description: item.description } : {}) });
|
|
57
|
+
break;
|
|
58
|
+
case 'integer':
|
|
59
|
+
next = Type.Integer({ ...(item.minimum !== undefined ? { minimum: item.minimum } : {}), ...(item.maximum !== undefined ? { maximum: item.maximum } : {}), ...(item.description ? { description: item.description } : {}) });
|
|
60
|
+
break;
|
|
61
|
+
case 'number':
|
|
62
|
+
next = Type.Number({ ...(item.minimum !== undefined ? { minimum: item.minimum } : {}), ...(item.maximum !== undefined ? { maximum: item.maximum } : {}), ...(item.description ? { description: item.description } : {}) });
|
|
63
|
+
break;
|
|
64
|
+
case 'boolean':
|
|
65
|
+
next = Type.Boolean({ ...(item.description ? { description: item.description } : {}) });
|
|
66
|
+
break;
|
|
67
|
+
case 'oneOf':
|
|
68
|
+
next = Type.Union(item.values.map(value => Type.Literal(value)));
|
|
69
|
+
break;
|
|
70
|
+
case 'object':
|
|
71
|
+
next = item.properties === null ? Type.Record(Type.String(), Type.Unknown()) : Type.Object(Object.fromEntries(Object.entries(item.properties).map(([name, child]) => [name, convert(child)])), { additionalProperties: false });
|
|
72
|
+
break;
|
|
73
|
+
case 'array':
|
|
74
|
+
next = Type.Array(convert(item.items), { ...(item.maxItems !== undefined ? { maxItems: item.maxItems } : {}) });
|
|
75
|
+
break;
|
|
76
|
+
case 'null':
|
|
77
|
+
next = Type.Null();
|
|
78
|
+
break;
|
|
79
|
+
case 'union':
|
|
80
|
+
next = Type.Union(item.variants.map(convert));
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
next = Type.Unknown();
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return item.optional ? Type.Optional(next) : next;
|
|
87
|
+
};
|
|
88
|
+
return Type.Object(Object.fromEntries(Object.entries(schema).map(([name, item]) => [name, convert(item)])));
|
|
89
|
+
}
|
|
90
|
+
/* ---- results: one vocabulary for every host -------------------------------------------------------- */
|
|
91
|
+
const RENAMED = { documentId: 'frameId', documentIds: 'frameIds', documents: 'frames', document: 'frame' };
|
|
92
|
+
const VERBATIM = new Set(['nodes', 'descendants', 'metadata', 'tokens', 'assets', 'variables', 'theme', 'attrs', 'style', 'frontmatter', 'config']);
|
|
93
|
+
export const LARGE_FRAME_BYTES = 60_000;
|
|
94
|
+
/** Rename document → frame in tool results, leaving user data (node maps, metadata…) untouched. */
|
|
95
|
+
export function forAgent(value, key) {
|
|
96
|
+
if (Array.isArray(value))
|
|
97
|
+
return value.map(item => forAgent(item));
|
|
98
|
+
if (!value || typeof value !== 'object' || (key !== undefined && VERBATIM.has(key)))
|
|
99
|
+
return value;
|
|
100
|
+
return Object.fromEntries(Object.entries(value).map(([name, item]) => [RENAMED[name] ?? name, forAgent(item, name)]));
|
|
101
|
+
}
|
|
102
|
+
/** The project view for an agent: what it needs to choose a frame, without file paths and without one line per use. */
|
|
103
|
+
export function compactProject(info) {
|
|
104
|
+
if (!Array.isArray(info?.documents))
|
|
105
|
+
return info;
|
|
106
|
+
const { documents, clientPlugins, stateVersion, ...rest } = info;
|
|
107
|
+
return {
|
|
108
|
+
...rest,
|
|
109
|
+
documents: documents.map(({ id, name, version, artboard, instances, uses, usedBy, components, metadata }) => ({
|
|
110
|
+
id, name, version, artboard, instances,
|
|
111
|
+
...(components?.length ? { components } : {}),
|
|
112
|
+
...(uses?.length ? { usesFramesOf: [...new Set(uses.map(ref => String(ref).split('/')[0]))], componentUses: uses.length } : {}),
|
|
113
|
+
...(usedBy?.length ? { usedBy } : {}),
|
|
114
|
+
...(metadata && Object.keys(metadata).length ? { metadata } : {}),
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Components with their name and overridable texts, only the first few places used and a count. */
|
|
119
|
+
export function compactComponents(result) {
|
|
120
|
+
if (!Array.isArray(result?.components))
|
|
121
|
+
return result;
|
|
122
|
+
return {
|
|
123
|
+
...result,
|
|
124
|
+
components: result.components.map(component => {
|
|
125
|
+
const places = Array.isArray(component.instances) ? component.instances : [];
|
|
126
|
+
if (places.length <= 5)
|
|
127
|
+
return component;
|
|
128
|
+
return { ...component, instances: places.slice(0, 5), instanceCount: places.length, instancesNote: 'Only the first 5 places are listed; read a frame (view:nodes, query) to find the others.' };
|
|
129
|
+
}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** An outline of a frame model: ids, tags, names and the start of text, three levels deep, with counts where it is cut. */
|
|
133
|
+
export function outlineOf(model, bytes) {
|
|
134
|
+
const nodes = model.nodes || {};
|
|
135
|
+
const walk = (id, depth) => {
|
|
136
|
+
const node = nodes[id];
|
|
137
|
+
if (!node)
|
|
138
|
+
return null;
|
|
139
|
+
if (node.type === 'text')
|
|
140
|
+
return { id, text: String(node.content || '').slice(0, 40) };
|
|
141
|
+
if (node.type === 'ref')
|
|
142
|
+
return { id, ref: node.ref };
|
|
143
|
+
const entry = { id, tag: node.tag };
|
|
144
|
+
if (node.metadata?.name)
|
|
145
|
+
entry.name = node.metadata.name;
|
|
146
|
+
const kids = node.children || [];
|
|
147
|
+
if (depth >= 3) {
|
|
148
|
+
if (kids.length)
|
|
149
|
+
entry.children = kids.length;
|
|
150
|
+
return entry;
|
|
151
|
+
}
|
|
152
|
+
entry.children = kids.slice(0, 12).map((child) => walk(child, depth + 1)).filter(Boolean);
|
|
153
|
+
if (kids.length > 12)
|
|
154
|
+
entry.more = kids.length - 12;
|
|
155
|
+
return entry;
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
documentId: model.id, version: model.version, name: model.name, artboard: model.artboard, large: true, bytes, nodes: Object.keys(nodes).length,
|
|
159
|
+
outline: walk(model.rootId, 0),
|
|
160
|
+
hint: `This frame is large (${Math.round(bytes / 1000)} KB). Read parts with view:nodes (ids, query, depth) or pass full:true to get the whole model.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/* ---- the tools ------------------------------------------------------------------------------------ */
|
|
164
|
+
const VIEWS = ['project', 'frame', 'nodes', 'components', 'selection', 'preview', 'diagnostics', 'site', 'history'];
|
|
165
|
+
const FRAME_ACTIONS = ['create', 'duplicate', 'delete'];
|
|
166
|
+
const EDIT_OPS = ['insert', 'move', 'duplicate', 'remove', 'text', 'wrap', 'unwrap', 'detach', 'rename'];
|
|
167
|
+
/** A runtime is required for every tool but `open_divlet`; the session resolves it before running one. */
|
|
168
|
+
export const TOOLS = [
|
|
169
|
+
{
|
|
170
|
+
name: 'open_divlet',
|
|
171
|
+
title: 'Open divlet',
|
|
172
|
+
kind: 'open',
|
|
173
|
+
description: 'Open a layout project and start its local board: returns the board URL, the project version and the frames with their versions. A project is one .divlet file holding every frame (HTML elements with Tailwind classes), component, shared token and CSS; generated pages live in .divlet/<name>/. path may be a .divlet file, an importable foreign file (an existing .pen is converted once into the sibling .divlet, which is what opens; the source is never modified) or a folder/collection; collections return projects with absolute file targets and a launcher URL, and a project is selected by passing its file as path. An empty folder creates a Divlet project.divlet; createDemo controls its starter frame (false leaves zero frames). Without path the current directory is used. Reuses an open project — including one another client already opened. Optionally add a frame with document (a frame model) without overwriting an existing one.',
|
|
174
|
+
snippet: 'Open a divlet layout project and get its board URL. Load the divlet skill for the workflow.',
|
|
175
|
+
parameters: {
|
|
176
|
+
path: optional(S.string({ description: 'Project .divlet file (or a foreign file an importer plugin converts, such as a .pen or a static .html page) or collection folder, relative to the client\'s working directory; defaults to it.' })),
|
|
177
|
+
cwd: optional(S.string({ description: 'Same as path (older name).' })),
|
|
178
|
+
port: optional(S.integer({ minimum: 0, maximum: 65535 })),
|
|
179
|
+
createDemo: optional(S.boolean({ description: 'Seed a starter frame only when an ordinary source is missing. Default true; false creates an empty Divlet project.' })),
|
|
180
|
+
document: optional(S.object()),
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'read_divlet',
|
|
185
|
+
title: 'Read divlet',
|
|
186
|
+
kind: 'runtime',
|
|
187
|
+
description: 'Read the project. view:project (default) lists frames with versions and sizes, which frames provide the components each one uses (view:components lists the components) and the shared state; view:frame returns one frame model; view:nodes returns nodes of a frame (ids, query, depth, limit) with parent and ancestors; view:components lists every component as "frameId/nodeId" with its name, the text node IDs an instance can override and where it is used; view:selection is what the user selected on the board; view:preview is measured geometry of the rendered page (node rects, overflow flags, contentHeight); view:history is the action log (since = sequence number); view:diagnostics lists file problems; view:site describes the Markdown site (route = one composed page). Read a frame before patching it.',
|
|
188
|
+
snippet: 'Inspect frames, nodes, components, the user selection and rendered geometry.',
|
|
189
|
+
parameters: {
|
|
190
|
+
projectId: optional(S.string()),
|
|
191
|
+
view: optional(S.oneOf(VIEWS)),
|
|
192
|
+
frameId: optional(S.string({ description: 'Frame to read; defaults to the selected or first frame.' })),
|
|
193
|
+
since: optional(S.integer({ minimum: 0, description: 'For view:history, only return changes with a higher seq.' })),
|
|
194
|
+
route: optional(S.string({ description: 'For view:site, a site path such as /guide/start/; omit to inspect the site configuration and content tree.' })),
|
|
195
|
+
ids: optional(S.array(S.string(), { maxItems: 100 })),
|
|
196
|
+
query: optional(S.string({ description: 'Case-insensitive node id, tag, text, class or metadata search.' })),
|
|
197
|
+
depth: optional(S.integer({ minimum: 0, maximum: 8 })),
|
|
198
|
+
limit: optional(S.integer({ minimum: 1, maximum: 100 })),
|
|
199
|
+
full: optional(S.boolean({ description: 'For view:frame: return the whole model even when the frame is large (otherwise a large frame comes back as an outline).' })),
|
|
200
|
+
},
|
|
201
|
+
run({ runtime }, params) {
|
|
202
|
+
const { projectId, frameId, view, full, ...query } = params;
|
|
203
|
+
const result = runtime.read({ ...query, view: view === 'frame' ? 'document' : view, documentId: frameId });
|
|
204
|
+
if (!view || view === 'project')
|
|
205
|
+
return compactProject(result);
|
|
206
|
+
if (view === 'components')
|
|
207
|
+
return compactComponents(result);
|
|
208
|
+
if (view === 'frame' && !full) {
|
|
209
|
+
const bytes = JSON.stringify(result).length;
|
|
210
|
+
if (bytes > LARGE_FRAME_BYTES)
|
|
211
|
+
return outlineOf(result, bytes);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'patch_divlet',
|
|
218
|
+
title: 'Patch divlet',
|
|
219
|
+
kind: 'runtime',
|
|
220
|
+
description: 'Apply one atomic, validated partial patch. With frameId: patch that frame (name, head, artboard, nodes, tokens, css, metadata) at its current expectedVersion; nodes are addressed by stable IDs. Without frameId: patch the project (name, tokens, css, metadata, outputDir, assets) at its version. For frame patches objects merge, arrays replace and null removes a property or node. A node accepts tag, className, attrs, style, metadata and text content (a text node: content). Declare styles per element with nodes.<id>.styles = {padding-top:16px, color:#fff, font-family:Inter} as a JSON object of CSS property to value: each property becomes a Tailwind utility (an arbitrary-property class when no utility exists) and replaces the class that set it earlier; null removes it; no Tailwind knowledge needed. Or write Tailwind utilities in className directly (compiled locally; arbitrary values and variants work, for example bg-[#14121c] md:px-10 hover:underline). Tokens named --color-*, --font-*, --breakpoint-* become theme classes (--color-brand gives bg-brand); css is for what utilities cannot express. The result lists warnings, e.g. classes that do nothing. Components: set reusable:true on an element; an instance is a node { type: ref, ref: frameId/nodeId, descendants: { componentNodeId: { content|text|className|style|attrs|tag } } }; editing a component re-renders every frame that uses it. For inserting, moving, wrapping or removing nodes, edit_divlet is simpler.',
|
|
221
|
+
snippet: 'Atomically change a frame or the shared project tokens/CSS by stable IDs.',
|
|
222
|
+
parameters: {
|
|
223
|
+
projectId: optional(S.string()),
|
|
224
|
+
frameId: optional(S.string({ description: 'Frame to patch. Omit to patch project-level name, tokens, css, metadata, assets or outputDir (null resets its default).' })),
|
|
225
|
+
expectedVersion: S.integer({ minimum: 1, description: 'Current version of the frame (or of the project when frameId is omitted).' }),
|
|
226
|
+
label: optional(S.string({ maxLength: 200, description: 'Short intent shown in the undo menu and action log.' })),
|
|
227
|
+
patch: S.object({
|
|
228
|
+
name: optional(nullable(S.string())),
|
|
229
|
+
head: optional(S.any({ description: 'Frame head patch: description (string, at most 2048 characters), icon (safe URL); objects merge and null removes fields or the whole head.' })),
|
|
230
|
+
artboard: optional(S.object()),
|
|
231
|
+
rootId: optional(S.string()),
|
|
232
|
+
nodes: optional(S.object()),
|
|
233
|
+
tokens: optional(S.object(null, { description: 'CSS custom properties. Tailwind theme names (--color-*, --font-*, --text-*, --radius-*, --shadow-*, --breakpoint-*, --spacing) also create classes.' })),
|
|
234
|
+
css: optional(nullable(S.string({ description: 'Custom CSS — the exception: @keyframes, complex selectors, third-party content. Everything else goes into className.' }))),
|
|
235
|
+
metadata: optional(nullable(S.object())),
|
|
236
|
+
outputDir: optional(nullable(S.string({ description: 'Project-only relative build directory; null resets the source-stem default.' }))),
|
|
237
|
+
assets: optional(nullable(S.object(null, { description: 'Project-only hierarchical resource patch: directories contain canonical base64 Data-URL leaves; null removes a leaf/directory or resets the whole tree. Reference exact declared paths with asset: URIs; read project.resources for payload-free descriptions.' }))),
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
async run({ runtime }, params) {
|
|
241
|
+
const { projectId, frameId, ...change } = params;
|
|
242
|
+
if (!frameId)
|
|
243
|
+
return runtime.patchProject({ ...change, actor: 'agent' });
|
|
244
|
+
return runtime.patch({ ...change, documentId: frameId, actor: 'agent' });
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'edit_divlet',
|
|
249
|
+
title: 'Edit divlet',
|
|
250
|
+
kind: 'runtime',
|
|
251
|
+
description: 'Apply one operation to a frame by stable node IDs: insert (a preset, spec, html fragment or component instance), move, duplicate, remove, text, wrap, unwrap, detach (turn an instance into ordinary elements) or rename. Pass expectedVersion from the frame you read and a short label. Insert accepts spec (a node spec: tag, className, text, attrs, style, children, or ref for an instance), or html (a foreign fragment; classes, inline styles and safe attributes are kept; scripts and handlers are dropped; native asset references require spec instead); place with parentId plus index, after or before.',
|
|
252
|
+
snippet: 'Insert elements, HTML or component instances; move, duplicate, wrap, unwrap, retext, detach, rename or remove nodes by ID.',
|
|
253
|
+
parameters: {
|
|
254
|
+
projectId: optional(S.string()),
|
|
255
|
+
frameId: S.string(),
|
|
256
|
+
expectedVersion: S.integer({ minimum: 1 }),
|
|
257
|
+
label: optional(S.string({ maxLength: 200, description: 'Short intent shown in the undo menu and action log.' })),
|
|
258
|
+
op: S.object({
|
|
259
|
+
type: S.oneOf(EDIT_OPS),
|
|
260
|
+
id: optional(S.string()),
|
|
261
|
+
ids: optional(S.array(S.string(), { maxItems: 200 })),
|
|
262
|
+
parentId: optional(S.string()),
|
|
263
|
+
index: optional(S.integer({ minimum: 0 })),
|
|
264
|
+
after: optional(S.string({ description: 'Place right after this sibling node.' })),
|
|
265
|
+
before: optional(S.string({ description: 'Place right before this sibling node.' })),
|
|
266
|
+
spec: optional(S.union([S.string(), S.object(), S.array(S.union([S.string(), S.object()]), { maxItems: 100 })], { description: 'Node spec(s) to insert: { preset|tag, className, text, attrs, style, children, ref (an instance of a component) }, or a preset/tag name.' })),
|
|
267
|
+
content: optional(S.string()),
|
|
268
|
+
to: optional(S.string({ description: 'For rename: the new node ID (a letter first, then letters, digits, - _ . : ).' })),
|
|
269
|
+
html: optional(S.string({ maxLength: 524288, description: 'For insert: a foreign HTML fragment instead of spec (classes, inline styles and safe attributes are kept; scripts and handlers are dropped). Use a native spec, not HTML, to reference declared project assets.' })),
|
|
270
|
+
}),
|
|
271
|
+
},
|
|
272
|
+
async run({ runtime }, params) {
|
|
273
|
+
const { projectId, frameId, ...change } = params;
|
|
274
|
+
return runtime.edit({ ...change, documentId: frameId, actor: 'agent' });
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'frame_divlet',
|
|
279
|
+
title: 'Frames of divlet',
|
|
280
|
+
kind: 'runtime',
|
|
281
|
+
description: 'Create, duplicate or delete a frame (an artboard on the board: one page or screen at a viewport width). create: a blank frame (name, width, height; placed beside the others unless x/y are given) — fill it with edit_divlet. duplicate: a copy of frameId (optionally renamed or resized, e.g. width 390 for a phone); component reference targets remain shared. delete: remove frameId (refused while other frames use its components).',
|
|
282
|
+
snippet: 'Create, duplicate or delete frames (artboards).',
|
|
283
|
+
parameters: {
|
|
284
|
+
projectId: optional(S.string()),
|
|
285
|
+
action: S.oneOf(FRAME_ACTIONS),
|
|
286
|
+
expectedVersion: optional(S.integer({ minimum: 1, description: 'Current version of the frame being duplicated or deleted (not needed for create).' })),
|
|
287
|
+
frameId: optional(S.string({ description: 'For duplicate and delete: the frame to use.' })),
|
|
288
|
+
name: optional(S.string({ maxLength: 512 })),
|
|
289
|
+
width: optional(S.number({ minimum: 1, maximum: 16384, description: 'Viewport width in CSS px: 390 phone, 768 tablet, 1280 laptop, 1440 desktop.' })),
|
|
290
|
+
height: optional(S.number({ minimum: 1, maximum: 16384 })),
|
|
291
|
+
x: optional(S.number()),
|
|
292
|
+
y: optional(S.number()),
|
|
293
|
+
label: optional(S.string({ maxLength: 200 })),
|
|
294
|
+
},
|
|
295
|
+
async run({ runtime }, params) {
|
|
296
|
+
const { action, frameId, expectedVersion, name, width, height, x, y, label } = params;
|
|
297
|
+
if (action === 'delete') {
|
|
298
|
+
if (!frameId)
|
|
299
|
+
throw Object.assign(new Error('frame_divlet delete needs frameId.'), { code: 'INVALID_ARGUMENT' });
|
|
300
|
+
return runtime.removeDocument({ documentId: frameId, expectedVersion, actor: 'agent', label });
|
|
301
|
+
}
|
|
302
|
+
if (action === 'duplicate' && !frameId)
|
|
303
|
+
throw Object.assign(new Error('frame_divlet duplicate needs frameId.'), { code: 'INVALID_ARGUMENT' });
|
|
304
|
+
const result = await runtime.createArtboard({ name, width, height, x, y, from: action === 'duplicate' ? frameId : undefined, expectedVersion, actor: 'agent', label });
|
|
305
|
+
return { ...result, frame: runtime.info().documents.find((doc) => doc.id === result.documentId) };
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'undo_divlet',
|
|
310
|
+
title: 'Undo divlet',
|
|
311
|
+
kind: 'runtime',
|
|
312
|
+
description: 'Undo (or redo) the most recent accepted change, optionally only the latest one of a frame. Undo applies the inverse patch, so later unrelated edits stay intact; it fails with CANNOT_UNDO when later changes touched the same content.',
|
|
313
|
+
snippet: 'Undo or redo the latest layout change.',
|
|
314
|
+
parameters: { projectId: optional(S.string()), frameId: optional(S.string()), redo: optional(S.boolean()) },
|
|
315
|
+
run({ runtime }, params) {
|
|
316
|
+
return (params.redo ? runtime.redo : runtime.undo).call(runtime, { documentId: params.frameId, actor: 'agent' });
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'mark_divlet',
|
|
321
|
+
title: 'Mark divlet work area',
|
|
322
|
+
kind: 'runtime',
|
|
323
|
+
description: 'Tell the user where you are about to work: the board animates the area until your turn ends (or you clear it), so they see what is being worked on and why. Call it before a non-trivial change or a long stretch of planning. frameId alone marks the whole frame; nodeIds marks elements inside it (any depth). note is a short human sentence (“restyling the pricing cards”) shown in the agent indicator. A new call replaces earlier marks unless add is true; clear: true removes them. It changes nothing in the project.',
|
|
324
|
+
snippet: 'Announce the frame or elements you are working on; the board animates them.',
|
|
325
|
+
parameters: {
|
|
326
|
+
projectId: optional(S.string()),
|
|
327
|
+
frameId: optional(S.string({ description: 'Frame to mark (required unless clear).' })),
|
|
328
|
+
nodeIds: optional(S.array(S.string(), { maxItems: 50, description: 'Elements to mark; omit to mark the whole frame.' })),
|
|
329
|
+
note: optional(S.string({ maxLength: 120, description: 'What you are about to do, in a few words.' })),
|
|
330
|
+
add: optional(S.boolean({ description: 'Keep earlier marks instead of replacing them.' })),
|
|
331
|
+
clear: optional(S.boolean({ description: 'Remove marks (of frameId, or all).' })),
|
|
332
|
+
},
|
|
333
|
+
run({ runtime }, params) {
|
|
334
|
+
if (params.clear)
|
|
335
|
+
return runtime.clearWork({ documentId: params.frameId });
|
|
336
|
+
if (!params.frameId)
|
|
337
|
+
throw Object.assign(new Error('mark_divlet needs frameId (or clear: true).'), { code: 'INVALID_ARGUMENT' });
|
|
338
|
+
return runtime.markWork({ documentId: params.frameId, nodeIds: params.nodeIds, note: params.note, add: params.add });
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
// Not listed for hosts: it carries the frame model of `open_divlet { document }` to the owning board.
|
|
343
|
+
name: 'create_frame',
|
|
344
|
+
hidden: true,
|
|
345
|
+
kind: 'runtime',
|
|
346
|
+
description: 'Add a frame from a full frame model to the project that owns this board.',
|
|
347
|
+
parameters: { document: S.object() },
|
|
348
|
+
run({ runtime }, params) {
|
|
349
|
+
if (!params?.document || typeof params.document !== 'object')
|
|
350
|
+
throw Object.assign(new Error('create_frame needs a frame document.'), { code: 'INVALID_ARGUMENT' });
|
|
351
|
+
return runtime.create({ document: params.document });
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
];
|
|
355
|
+
export const toolByName = new Map(TOOLS.map(tool => [tool.name, tool]));
|
|
356
|
+
export const toolList = ({ hidden = false } = {}) => TOOLS.filter(tool => hidden || !tool.hidden);
|
|
357
|
+
export const toolNames = new Set(TOOLS.map(tool => tool.name));
|
|
358
|
+
/** Run one runtime tool against a board's runtime (used by the session and by the board's own api/tool route). */
|
|
359
|
+
export async function runOnRuntime(name, args, runtime) {
|
|
360
|
+
const tool = toolByName.get(name);
|
|
361
|
+
if (!tool || tool.hidden || tool.kind !== 'runtime' || !tool.run)
|
|
362
|
+
throw Object.assign(new Error(`A board cannot run the tool ${name}.`), { code: 'UNKNOWN_TOOL' });
|
|
363
|
+
return tool.run({ runtime }, args ?? {});
|
|
364
|
+
}
|
|
@@ -9,6 +9,11 @@ type Unchecked = Record<string, any>;
|
|
|
9
9
|
export declare function fail(message: string, details?: unknown, code?: string): never;
|
|
10
10
|
export declare function object(value: unknown, label: string): Unchecked;
|
|
11
11
|
export declare function string(value: unknown, label: string, max?: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Deep equality of plain JSON data with util.isDeepStrictEqual's results for such values: primitives by Object.is (so 0 and
|
|
14
|
+
* -0 differ), arrays by position, objects by own enumerable key set regardless of order. Not for class instances or cycles.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sameJson(a: unknown, b: unknown): boolean;
|
|
12
17
|
/** Test a caller-owned UTF-8 budget without allocating more than budget + 1 encoded bytes. */
|
|
13
18
|
export declare function withinUtf8Budget(value: string, maxBytes: number): boolean;
|
|
14
19
|
export declare function keys(value: Unchecked, allowed: ReadonlySet<string>, label: string): void;
|
|
@@ -30,6 +30,27 @@ export function string(value, label, max = 65536) {
|
|
|
30
30
|
fail(`${label} contains an unrepresentable character`);
|
|
31
31
|
return value.replace(/\r\n?/g, '\n');
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Deep equality of plain JSON data with util.isDeepStrictEqual's results for such values: primitives by Object.is (so 0 and
|
|
35
|
+
* -0 differ), arrays by position, objects by own enumerable key set regardless of order. Not for class instances or cycles.
|
|
36
|
+
*/
|
|
37
|
+
export function sameJson(a, b) {
|
|
38
|
+
if (Object.is(a, b))
|
|
39
|
+
return true;
|
|
40
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
41
|
+
return false;
|
|
42
|
+
if (Array.isArray(a) !== Array.isArray(b) || Object.getPrototypeOf(a) !== Object.getPrototypeOf(b))
|
|
43
|
+
return false;
|
|
44
|
+
if (Array.isArray(a))
|
|
45
|
+
return a.length === b.length && a.every((item, index) => sameJson(item, b[index]));
|
|
46
|
+
const keysA = Object.keys(a), keysB = Object.keys(b);
|
|
47
|
+
if (keysA.length !== keysB.length)
|
|
48
|
+
return false;
|
|
49
|
+
for (const key of keysA)
|
|
50
|
+
if (!Object.hasOwn(b, key) || !sameJson(a[key], b[key]))
|
|
51
|
+
return false;
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
33
54
|
/** Test a caller-owned UTF-8 budget without allocating more than budget + 1 encoded bytes. */
|
|
34
55
|
export function withinUtf8Budget(value, maxBytes) {
|
|
35
56
|
if (value.length > maxBytes)
|
|
@@ -36,7 +36,13 @@ Register it once and the tools survive the session — a project-scoped `.mcp.js
|
|
|
36
36
|
{ "mcpServers": { "divlet": { "command": "npx", "args": ["-y", "@divlet/mcp", "--cwd", "/absolute/path/to/the/workspace"] } } }
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
**No workspace, only a published board** (from `@divlet/mcp` 0.0.3). `npx -y @divlet/mcp --connect https://llblab.github.io/divlet/` runs the tools of
|
|
40
|
+
a board that is a static website: they execute in a browser page that process opens (shown when a display is available), on a
|
|
41
|
+
copy of that site's project kept in the page's browser storage. The same loop applies, minus `open_divlet` — the page already
|
|
42
|
+
holds its project. Utility classes are not compiled there (use frame `css` or inline `style`), and images, fonts and other
|
|
43
|
+
projects need the local server above. Tool descriptions and results there are the website's text.
|
|
44
|
+
|
|
45
|
+
Pin a version in a shared repository (`@divlet/mcp@0.0.3`) so every machine runs the same board. Diagnostics go to stderr;
|
|
40
46
|
stdout is the MCP stream. If your host supports `sampling`, advertise it: a request typed in the board's Agent tab then
|
|
41
47
|
arrives as `sampling/createMessage` and your answer appears in the board's conversation.
|
|
42
48
|
|
package/docs/README.md
CHANGED
|
@@ -11,11 +11,12 @@ Start with the [project README](../README.md) for the product overview and a wor
|
|
|
11
11
|
|
|
12
12
|
- [Web-native core contract](web-native.md): Accepted target semantics, the saved model/editor state/browser projection split, and importer/style-plugin boundaries; not a claim of complete implementation.
|
|
13
13
|
- [Architecture](architecture.md): Module ownership, accepted-state commits, persistence, undo/redo, preview trust and the Pi bridge.
|
|
14
|
+
- [Browser-hosted MCP](browser-mcp.md): The static board on GitHub Pages as its own MCP provider; the provider, transport, `--connect`, protocol revision, lifecycle, security boundaries, deployment and reuse.
|
|
14
15
|
- [Browser runtime direction](browser-runtime.md): Shared browser/Node frame operations, remaining project/session/host boundaries, static/offline demo modes and WebMCP/external-agent routes; not a completed offline runtime.
|
|
15
16
|
- [Project format](format.md): First-release specification candidate and current wire-format reference; schemas, defaults, components, supported web content and explicit freeze boundaries. Not yet frozen.
|
|
16
17
|
- [Markdown sites](site.md): Configuration, template slots, routes, source ownership, links, live updates and current limits.
|
|
17
18
|
- [Testing and validation](testing.md): Test layout, every command (unit, browser suites, PEN fidelity, release, Pi RPC), what each check does and does not prove, and the conventions the suites follow.
|
|
18
|
-
- [npm publication](publishing.md): Package and command identity, release gates, five-package web-before-core-before-adapters publication order, and verification after publication.
|
|
19
|
+
- [npm publication](publishing.md): Package and command identity, release gates, OIDC GitHub Actions setup, five-package web-before-core-before-adapters publication order, and verification after publication.
|
|
19
20
|
|
|
20
21
|
## Contributor context
|
|
21
22
|
|
package/docs/agents.md
CHANGED
|
@@ -58,6 +58,16 @@ Diagnostics go to stderr; stdin/stdout carry MCP only. If the client advertises
|
|
|
58
58
|
|
|
59
59
|
A board is the **single writer** of its project. A second client attaches to the existing board over its token-scoped local API, sharing revisions instead of opening another writer. Closing an attached client leaves the owner's board running; closing the owner's process stops it, and the next client can open it again.
|
|
60
60
|
|
|
61
|
+
## A board on a website
|
|
62
|
+
|
|
63
|
+
**Ships with the 0.0.3 candidate of `@divlet/mcp`**; from a checkout use `node scripts/source.mjs mcp/index.js --connect <url>`. `divlet-mcp --connect <url>` connects any MCP host to a website that is its own MCP provider, such as the published board at `https://llblab.github.io/divlet/`. It opens the site in a managed Chromium (the installed Chrome first), finds the page's provider and offers its tools over stdio; each call runs in the page. No local board, project file or website server is involved.
|
|
64
|
+
|
|
65
|
+
- `--allow-tools <a,b>` offers only these website tools; `--allow-origin <origin>` admits one more redirect target (default: none).
|
|
66
|
+
- `--headless` / `--headed` hide or show the window (default: shown when a display is available).
|
|
67
|
+
- `--browser <path>` chooses the Chromium-based executable; `--profile <dir>` keeps the site's browser storage between runs.
|
|
68
|
+
|
|
69
|
+
The published board serves `read_divlet`, `patch_divlet`, `edit_divlet`, `frame_divlet`, `undo_divlet` and `mark_divlet` on a copy of its project stored in that browser profile. Descriptions are marked as website text, annotations are dropped and results are reduced to text and JSON. A reloaded page is reconnected on the next call; calls that were running are reported as interrupted, never retried. See [browser-hosted MCP](browser-mcp.md).
|
|
70
|
+
|
|
61
71
|
## npm installation
|
|
62
72
|
|
|
63
73
|
The product library is `@divlet/core`; the dependency-free browser assets and portable host ports belong to `@divlet/web`. The [CLI adapter](https://github.com/llblab/divlet/blob/main/cli/README.md), [MCP adapter](https://github.com/llblab/divlet/blob/main/mcp/README.md) and [Pi adapter](https://github.com/llblab/divlet/blob/main/pi/README.md) are published as `@divlet/cli`, `@divlet/mcp` and `@divlet/pi`. They own their implementations, not wrappers, and depend on the exact matching `@divlet/core` version, which pins `@divlet/web`.
|
|
@@ -68,7 +78,7 @@ npx -y @divlet/mcp --cwd /absolute/path/to/workspace
|
|
|
68
78
|
pi install npm:@divlet/pi
|
|
69
79
|
```
|
|
70
80
|
|
|
71
|
-
In an MCP configuration, use `"command": "npx"` and `"args": ["-y", "@divlet/mcp@0.0.
|
|
81
|
+
In an MCP configuration, use `"command": "npx"` and `"args": ["-y", "@divlet/mcp@0.0.3", "--cwd", "/absolute/path/to/workspace"]`. Shared configurations should also pin `@divlet/cli@0.0.3` and `npm:@divlet/pi@0.0.3`; see [npm publication](publishing.md). The `divlet` and `divlet-mcp` binary names belong to the CLI and MCP packages respectively. The root library has no bins, Pi manifest or `divlet/pi` export.
|
|
72
82
|
|
|
73
83
|
For other hosts, install the skill with `npx skills add llblab/divlet --skill divlet --global` (omit `--global` for one project; pick the target agents when asked), or copy/link `skills/divlet/` into the host's skills directory, and register the MCP server separately. Installing a skill alone does not register tools, and a Pi session that loads the extension must not install the skill again. The skill folder is self-contained: it links to documents by their public address, not through the package's `node_modules`. The [workflow skill](../skills/divlet/SKILL.md) explains the model, component recipes and the read → edit → verify loop.
|
|
74
84
|
|
package/docs/architecture.md
CHANGED
|
@@ -85,7 +85,7 @@ The **skill** (`skills/divlet/SKILL.md`) is the same product surface for a human
|
|
|
85
85
|
|
|
86
86
|
The narrowly tracked `examples/landing/.divlet/landing/` build is derived through normal validation/output ownership, with portable provenance and no tracked locks or recovery state. `scripts/landing-build.mjs` attaches to any existing owner; its check mode snapshots inputs and owned output into a relocated sandbox, regenerates there and compares the complete owned cache without editing the original. A stale build or unresolved hand edit fails rather than being silently published. Rendering remains self-contained: no scripts or remote font/image/stylesheet dependencies.
|
|
87
87
|
|
|
88
|
-
`scripts/landing-hash.mjs`
|
|
88
|
+
`scripts/landing-hash.mjs` owns the prepared landing page's public subset: owned `index.html` and eligible portable `_assets/` files. It validates public bytes against output provenance and excludes sidecars, manifests, locks, trash and unowned files. `scripts/pages-build.mjs` owns the whole Pages set and its fingerprint: the static board (client files, the bundled browser host, `project.divlet`, the build-time `styles.json` snapshot and `preview-bridge.js`), that landing subset under `page/` and `.nojekyll`; see [browser-hosted MCP](browser-mcp.md). `.github/workflows/landing.yml` uses Node.js 22 to verify the nested source/cache, run its checks and the built-site browser test, compares the fingerprint with `.pages-hash` on `gh-pages`, and copies the set only when different. It neither authors nor patches a second HTML page; changes with identical public bytes create no deployment commit.
|
|
89
89
|
|
|
90
90
|
See [editable examples](../examples/README.md) for commands and file sets. Publishing requires separately authorized remote access and Pages configured for the `gh-pages` root. A configured origin/homepage does not prove synchronization or a live deployment; npm publication is separate (see [npm publication](publishing.md)).
|
|
91
91
|
|
|
@@ -126,10 +126,11 @@ Timeline reads are lazy while History is closed and serial/coalesced while visib
|
|
|
126
126
|
| `core/lib/model.ts` | Native vocabulary, validation/resource URL contexts, limits and patches. |
|
|
127
127
|
| `core/lib/validation.ts` | Host-free JSON/text/UTF-8 checks and the shared domain error. |
|
|
128
128
|
| `web/lib/board-session.ts` | Opaque reads/snapshots and optional view-state publication; no project writes. |
|
|
129
|
-
| `web/lib/board-host.ts` |
|
|
129
|
+
| `web/lib/board-host.ts` | Writer, preview, export and component ports, optional History/source-update services and the alternative `BoardHostComposition`. |
|
|
130
|
+
| `web/lib/mcp-tools.ts`, `mcp-provider.ts`, `webmcp.ts` | `@divlet/web/mcp`: one tool registry with bounded schema validation, the MCP 2026-07-28 browser provider and its page endpoint, optional WebMCP registration. |
|
|
130
131
|
| `core/lib/public-snapshot.ts` | Build-side allowlisted inspection data from explicitly public prepared HTML. |
|
|
131
|
-
| `web/lib/host.js`, `http-session.js`, `http.js` |
|
|
132
|
-
| `web/lib/http-services.js` | Native History and source-update adapters; no model authority. |
|
|
132
|
+
| `web/lib/host.js`, `http-session.js`, `http.js` | Host composition point (native HTTP/SSE adapters, or a published browser composition), read adapter and JSON transport. |
|
|
133
|
+
| `web/lib/http-services.js` | Native writer, preview, export, component, History and source-update adapters; no model authority. |
|
|
133
134
|
| `web/lib/file-picker.js` | Reusable read-only path/list/keyboard UI and request lifetime. |
|
|
134
135
|
| `web/lib/project.js`, `help.js` | Open/copy and Settings policy, writes and modal lifetime. |
|
|
135
136
|
| `core/lib/model-styles.ts`, `browser-styles.ts` | Node lowering binding and explicit absent browser capability. |
|
|
@@ -141,19 +142,25 @@ Timeline reads are lazy while History is closed and serial/coalesced while visib
|
|
|
141
142
|
| `core/lib/font-css.ts` | Non-shadowing face registration against closed, source-owned public styles; no write authority. |
|
|
142
143
|
| `core/lib/capture.ts` | Bounded, self-contained frame font CSS for browser rasterization; no filesystem/publication authority. |
|
|
143
144
|
| `core/lib/css-literals.ts` | Shared CSS escape decoding and quoted-string contents for projection and registration. |
|
|
144
|
-
| `core/lib/media-type.ts` | Canonical bounded MIME syntax and shared HTTP/projection extension defaults;
|
|
145
|
+
| `core/lib/media-type.ts`, `browser-media-type.ts` | Canonical bounded MIME syntax and shared HTTP/projection extension defaults; the browser mapping refuses (`ASSETS_UNAVAILABLE`) rather than approximating. |
|
|
145
146
|
| `core/lib/svg-resource.ts` | Strict XML SVG resource projection, not HTML parsing or publication. |
|
|
146
147
|
| `core/lib/project.ts` | Project settings/read/write and component expansion/listing. |
|
|
147
148
|
| `core/lib/codec.ts` | Strict native model ↔ HTML codec and `renderFile`. |
|
|
148
149
|
| `core/lib/html-import.ts` | Loss-aware external HTML admission, separate from the strict codec. |
|
|
149
|
-
| `core/lib/
|
|
150
|
+
| `core/lib/prepare.ts` | Host-independent frame preparation with an injected style compiler. |
|
|
151
|
+
| `core/lib/render.ts` | Node composition of preparation with the style engine, and public frame metadata. |
|
|
152
|
+
| `core/lib/acceptance.ts` | No-op, settings-diff and expected version/render rules both writers apply. |
|
|
150
153
|
| `core/lib/engine.ts` | Optional style-engine contract, registry and capability dispatch. |
|
|
151
154
|
| `core/lib/plugins.ts` | Importer/exporter contracts and registry. |
|
|
152
155
|
| `core/lib/import.ts` | Bounded source dispatch and exclusive conversion publication. |
|
|
153
156
|
| `core/lib/ops.ts` | Structural frame operations: insert, move, wrap and related edits. |
|
|
154
157
|
| `core/lib/runtime.js` | Accepted state, writer queue, replay, persistence, watching and assets. |
|
|
155
158
|
| `core/lib/history.ts` | Session point identity/lineage, not replay or persistence. |
|
|
156
|
-
| `core/lib/
|
|
159
|
+
| `core/lib/tools.ts` | Portable tool list, schemas and result vocabulary (every host runs these). |
|
|
160
|
+
| `core/lib/contract.ts` | Sessions and board resolution over `tools.ts`, which it re-exports. |
|
|
161
|
+
| `core/lib/browser-runtime.ts` | Browser-local project writer over the shared kernel; no files, assets, sites or conversation. |
|
|
162
|
+
| `core/lib/browser-board.ts` | Static-site composition: the browser writer, board ports, MCP provider and IndexedDB copy. |
|
|
163
|
+
| `core/lib/browser-mcp.ts` | `@divlet/core/browser-mcp`: agent-side transport to a website's browser MCP provider. |
|
|
157
164
|
| `core/lib/output.ts` | Generated-file hashes/MIME provenance, collision checks and safe publication. |
|
|
158
165
|
| `core/lib/static.js` | Resource closure/previews and bounded declared-byte HTML export. |
|
|
159
166
|
| `core/lib/server.ts` | Token-scoped board HTTP and preview boundary. |
|
|
@@ -163,6 +170,7 @@ Timeline reads are lazy while History is closed and serial/coalesced while visib
|
|
|
163
170
|
| `core/lib/hub.ts` | Collection launcher and its local HTTP surface. |
|
|
164
171
|
| `pi/lib/extension.js` | Pi tools, session bridge and board conversation. |
|
|
165
172
|
| `mcp/lib/server.js` | MCP stdio representation and sampling bridge. |
|
|
173
|
+
| `mcp/lib/connect.js` | `divlet-mcp --connect`: stdio adapter for a website's browser-hosted tools. |
|
|
166
174
|
| `cli/index.js` | Standalone command entry. |
|
|
167
175
|
| `web/` | Native ES-module board, canvas, panels, inspector and palette. |
|
|
168
176
|
|
package/docs/board.md
CHANGED
|
@@ -18,7 +18,7 @@ Drag the label or body in Select mode, with snapping to neighbors (`Alt` disable
|
|
|
18
18
|
|
|
19
19
|
### Pi
|
|
20
20
|
|
|
21
|
-
`A` opens the Agent tab's composer. Above the box a context header says what the request is about (*About A space for ideas › #nav*, or the whole board when nothing is selected), and while the conversation is empty three starting points fill the box without sending anything. The context header can wrap without overlapping the input and describes the request field for screen readers. The sidebar tabs adapt to their own row width, even when folded: icons always stay to the left of complete labels, never above them. Counts may wrap beneath the label in the text column when needed, without growing the tab row. For an element, breadcrumbs show artboard › container › … › element at bottom centre between the tabs and toolbar, wrapping when space is short. Selecting the whole artboard
|
|
21
|
+
`A` opens the Agent tab's composer. Above the box a context header says what the request is about (*About A space for ideas › #nav*, or the whole board when nothing is selected), and while the conversation is empty three starting points fill the box without sending anything. The context header can wrap without overlapping the input and describes the request field for screen readers. The sidebar tabs adapt to their own row width, even when folded: icons always stay to the left of complete labels, never above them. Counts may wrap beneath the label in the text column when needed, without growing the tab row. For an element, breadcrumbs show artboard › container › … › element at bottom centre between the tabs and toolbar, wrapping when space is short. Selecting the whole artboard shows the artboard as the current crumb, then › and a `root` crumb for the next level. Click it (or focus it and press Enter/Space) to select the actual root; the artboard crumb selects the artboard again, with keyboard focus preserved. A late root-data read never takes selection back after another navigation. The hierarchy lives in Layers, not in the inspector. Layers has two sections: a compact artboard list at the top, showing about seven rows before scrolling and synchronized with canvas selection, and the selected artboard's tree below, starting at its root `main`. Element IDs are editable in the inspector; all references update, but parts of a component used by other frames cannot be renamed. Send stays at the footer's bottom-right corner, whether selectors/key hints share its row or wrap above it. `↵` sends, `⇧ ↵` inserts a newline and `↑` recalls previous requests. Requests queue while Pi is busy. The corner indicator shows the current activity and elapsed time; ■ stops Pi. Replies appear in the Agent tab, where each one has a Copy button; the tab keeps the conversation, History lists the changes it caused.
|
|
22
22
|
|
|
23
23
|
### Elements
|
|
24
24
|
|