@yojahny/wp-design-library 1.1.1 → 1.3.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/README.md +26 -5
- package/bin/library.mjs +1 -0
- package/entries/aker/entry.md +107 -0
- package/entries/amrit-palace/entry.md +105 -0
- package/entries/apple-airpods-5/entry.md +110 -0
- package/entries/apple-airpods-pro/entry.md +106 -0
- package/entries/apple-watch-se-3/entry.md +107 -0
- package/entries/apple-watch-series-12/entry.md +108 -0
- package/entries/arsenijs-fabrica/entry.md +100 -0
- package/entries/augen-pro/entry.md +100 -0
- package/entries/auros/entry.md +110 -0
- package/entries/awe-network/entry.md +101 -0
- package/entries/ayocin/entry.md +101 -0
- package/entries/bmw/entry.md +101 -0
- package/entries/busuu/entry.md +100 -0
- package/entries/caldera/entry.md +106 -0
- package/entries/calendly/entry.md +106 -0
- package/entries/custo/entry.md +100 -0
- package/entries/dayos-ai/entry.md +109 -0
- package/entries/dimension/entry.md +106 -0
- package/entries/dovetail/entry.md +108 -0
- package/entries/ease-health/entry.md +105 -0
- package/entries/factory-ai/entry.md +106 -0
- package/entries/flighty/entry.md +105 -0
- package/entries/frame-io/entry.md +108 -0
- package/entries/hungry-tiger/entry.md +106 -0
- package/entries/hyer-aviation/entry.md +108 -0
- package/entries/impilo/entry.md +108 -0
- package/entries/monad/entry.md +108 -0
- package/entries/n8n/entry.md +108 -0
- package/entries/palais/entry.md +100 -0
- package/entries/partiful/entry.md +104 -0
- package/entries/phantom/entry.md +105 -0
- package/entries/podia/entry.md +102 -0
- package/entries/posthog/entry.md +101 -0
- package/entries/redbrick-coffee/entry.md +98 -0
- package/entries/samara/entry.md +107 -0
- package/entries/sauce-labs/entry.md +110 -0
- package/entries/seline-analytics/entry.md +106 -0
- package/entries/sequel/entry.md +104 -0
- package/entries/slash/entry.md +110 -0
- package/entries/supabase/entry.md +107 -0
- package/entries/t1-energy/entry.md +101 -0
- package/entries/vana/entry.md +100 -0
- package/entries/veed/entry.md +98 -0
- package/entries/webflow/entry.md +108 -0
- package/entries/zkpass/entry.md +103 -0
- package/package.json +2 -1
- package/src/cli/upload.mjs +57 -0
- package/src/ingest/save.mjs +28 -6
- package/src/ingest/upload.mjs +110 -0
- package/src/mcp/http.mjs +21 -0
- package/src/mcp/prompts.mjs +71 -10
- package/src/mcp/tools.mjs +104 -8
package/src/mcp/prompts.mjs
CHANGED
|
@@ -23,24 +23,31 @@ session did not make.
|
|
|
23
23
|
|
|
24
24
|
## Order of operations
|
|
25
25
|
|
|
26
|
-
1. **Arguments.** \`
|
|
27
|
-
|
|
28
|
-
returns \`refused\` starting with \`ffmpeg\`, relay the
|
|
26
|
+
1. **Arguments.** \`input\` may be a live HTTP(S) page or an animated webp/gif/video
|
|
27
|
+
(mp4/mov/webm/mkv). A local file uses an absolute path. Video needs ffmpeg on the
|
|
28
|
+
server's PATH. If \`add\` returns \`refused\` starting with \`ffmpeg\`, relay the
|
|
29
|
+
message and stop. \`source\` is
|
|
29
30
|
one of ${list(v.source)}. Derive \`slug\` from the file name when absent (lowercase,
|
|
30
31
|
dashes, no extension) and confirm it with the user; derive \`title\` from the slug and
|
|
31
32
|
confirm. \`url\` must be a full URL with scheme. \`license\` defaults by kind: own →
|
|
32
33
|
"own work"; open → ask for the licence name; paid and public-site → "description
|
|
33
34
|
only, no redistribution of the source".
|
|
34
|
-
2. **
|
|
35
|
+
2. **Stage when hosted.** A hosted server cannot read a path on the caller's machine.
|
|
36
|
+
Upload it first with \`LIBRARY_TOKEN\` configured locally: \`library upload
|
|
37
|
+
<absolute-path> --server <server-origin> --source <kind> --license "<licence>"
|
|
38
|
+
[--url <source-url>] [--title <title>]\`. Use returned \`file\` name as the \`add\`
|
|
39
|
+
input; hosted MCP resolves a bare name inside its inbox. Never print or pass the token as an argument.
|
|
40
|
+
Live page URLs need no upload.
|
|
41
|
+
3. **Ingest.** Call the \`add\` tool with those values. If it returns \`refused\` because
|
|
35
42
|
the slug exists, ask before calling again with \`force: true\`; a forced re-add resets
|
|
36
43
|
the draft. Over a hosted (HTTP) server \`add\` can only read files already in the server's
|
|
37
44
|
inbox; if the refusal mentions \`inbox\`, say so and stop.
|
|
38
|
-
|
|
45
|
+
4. **Read.** Open the returned \`strip\`. Read \`media.duration_ms\` from the draft at
|
|
39
46
|
\`<dir>/entry.md\`; if it is above 10000, call \`add\` again with \`force: true\` and
|
|
40
47
|
\`frames: 36\` before tagging. The server caps at 36 frames; beyond about 18 seconds a
|
|
41
48
|
section can still be missed, so build a denser sheet yourself from the source file
|
|
42
49
|
before writing the motion note.
|
|
43
|
-
|
|
50
|
+
5. **Tag.** Fill every field in the returned \`blanks\` list, per the rubric below. Write
|
|
44
51
|
the motion note last, after the roster. Two ways, same result: call \`set_fields\` with
|
|
45
52
|
those field names — this needs **no filesystem** and is the only way that works against
|
|
46
53
|
a hosted server — or, in a local checkout, edit \`<dir>/entry.md\` directly.
|
|
@@ -48,11 +55,11 @@ session did not make.
|
|
|
48
55
|
anything, so a refusal never leaves a half-filled draft. It writes judgement fields
|
|
49
56
|
only; provenance is refused. \`get_vocab\` returns the terms if you did not read them
|
|
50
57
|
above.
|
|
51
|
-
|
|
58
|
+
6. **Approve.** Show the draft to the user and **show the draft and wait for a yes** before
|
|
52
59
|
anything else. Apply their edits.
|
|
53
|
-
|
|
60
|
+
7. **Save.** Call \`save_entry\`. On \`refused\`, show the field names, fix, and call again;
|
|
54
61
|
**never remove a field to make it pass**.
|
|
55
|
-
|
|
62
|
+
8. **Offer the git step.** The library checkout is the directory that contains the
|
|
56
63
|
\`bin/library.mjs\` this server runs; if that is not a git checkout (the \`npx\` case), ask
|
|
57
64
|
the user for the path to their clone. If the \`dir\` returned by \`add\` is not inside the
|
|
58
65
|
checkout, first \`cp -r <dir> <checkout>/entries/\`. Then, only with \`git -C <checkout>\`
|
|
@@ -102,6 +109,46 @@ and \`public-site\` are inspiration only and \`save_entry\` refuses \`tier: port
|
|
|
102
109
|
- Never run git in the current project.`;
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
export function addBatchPrompt(ctx, a) {
|
|
113
|
+
const v = ctx.vocab.facets;
|
|
114
|
+
const given = Object.entries(a).filter(([, value]) => value).map(([key, value]) => `- ${key}: ${value}`).join('\n');
|
|
115
|
+
return `# Add a batch of design references to wp-design-library
|
|
116
|
+
|
|
117
|
+
Arguments given:
|
|
118
|
+
${given || '- (none)'}
|
|
119
|
+
|
|
120
|
+
Use the hosted workflow end to end; do not edit the server filesystem.
|
|
121
|
+
|
|
122
|
+
1. **Stage files.** If recordings are on this machine, run \`library upload\` once per file
|
|
123
|
+
with \`--server\`, \`--source\`, \`--license\`, and optional \`--url\`/\`--title\`.
|
|
124
|
+
Authentication comes from \`LIBRARY_TOKEN\`; never print it or put it in an argument.
|
|
125
|
+
Files already in the server inbox need no upload.
|
|
126
|
+
2. **Draft.** Call \`add_batch\` once, passing the supplied source/licence as defaults for
|
|
127
|
+
files without sidecars. It resumes interrupted work, skips an existing slug,
|
|
128
|
+
and reports a refusal per bad file while continuing the rest. Report refusals, then keep
|
|
129
|
+
processing every drafted slug.
|
|
130
|
+
3. **Inspect.** Call \`get_vocab\` once. For every drafted slug call \`get_draft\`; inspect
|
|
131
|
+
every returned strip image. Long recordings may need another capture before the motion
|
|
132
|
+
note can honestly cover every section.
|
|
133
|
+
4. **Tag in chunks.** Build \`set_fields_batch.entries\` records and submit chunks of at
|
|
134
|
+
most 25 so results remain reviewable. Fill every blank. Roles must come from
|
|
135
|
+
${list(v.role)}; feel from ${list(v.feel)}; motion devices from ${list(v.motion)}.
|
|
136
|
+
Each body has \`## What it does\`, \`## Section roster\`, and \`## Why it works\`.
|
|
137
|
+
Motion notes begin with the sampled-frame limitation and describe sections in order.
|
|
138
|
+
A refused item does not undo the others; fix it and resubmit only that slug.
|
|
139
|
+
5. **Review.** Show a compact table containing slug, roles, feel, palette, motion devices,
|
|
140
|
+
and any refusal. Wait for the user's approval of the batch and apply requested changes.
|
|
141
|
+
6. **Save once.** Call \`save_batch\` with all approved slugs. It validates each entry,
|
|
142
|
+
isolates failures, and rebuilds the index once. Fix and resubmit refused slugs without
|
|
143
|
+
repeating successful ones.
|
|
144
|
+
7. **Ship.** Run \`library check --remote\`. For a hosted volume, use
|
|
145
|
+
\`library export --saved-only\` to bring entries into a checkout, then create one content
|
|
146
|
+
branch and PR for the batch.
|
|
147
|
+
|
|
148
|
+
Never infer a visual field without inspecting its strip, never write provenance through
|
|
149
|
+
\`set_fields_batch\`, and never drop a field merely to pass validation.`;
|
|
150
|
+
}
|
|
151
|
+
|
|
105
152
|
// brief-from-library turns a client brief into a reference set: one `recommend` call,
|
|
106
153
|
// then `get_entry` on what survives, then a `## References` block the session fills in
|
|
107
154
|
// with the slugs it actually used. The per-role `search` fan-out is kept as a fallback
|
|
@@ -157,7 +204,8 @@ export function registerPrompts(server, ctx) {
|
|
|
157
204
|
{
|
|
158
205
|
description: 'Add a design reference to the library: ingest a recording, tag it in this session per the rubric, approve the draft, save it, offer the PR.',
|
|
159
206
|
argsSchema: {
|
|
160
|
-
|
|
207
|
+
input: z.string().optional().describe('live page URL, inbox filename, or absolute local media path'),
|
|
208
|
+
path: z.string().optional().describe('legacy alias for an absolute local media path'),
|
|
161
209
|
source: z.string().describe('own | open | paid | public-site'),
|
|
162
210
|
slug: z.string().optional().describe('lowercase, digits and dashes'),
|
|
163
211
|
title: z.string().optional(),
|
|
@@ -167,4 +215,17 @@ export function registerPrompts(server, ctx) {
|
|
|
167
215
|
},
|
|
168
216
|
async (a) => ({ messages: [{ role: 'user', content: { type: 'text', text: addEntryPrompt(ctx, a) } }] }),
|
|
169
217
|
);
|
|
218
|
+
|
|
219
|
+
server.registerPrompt(
|
|
220
|
+
'add-batch',
|
|
221
|
+
{
|
|
222
|
+
description: 'Ingest, inspect, tag, review, and save every recording in the hosted inbox with batch tools and isolated failures.',
|
|
223
|
+
argsSchema: {
|
|
224
|
+
source: z.string().optional().describe('default own | open | paid | public-site when sidecars omit it'),
|
|
225
|
+
license: z.string().optional().describe('default licence text when sidecars omit it'),
|
|
226
|
+
server: z.string().optional().describe('hosted library origin used by library upload'),
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
async (a) => ({ messages: [{ role: 'user', content: { type: 'text', text: addBatchPrompt(ctx, a) } }] }),
|
|
230
|
+
);
|
|
170
231
|
}
|
package/src/mcp/tools.mjs
CHANGED
|
@@ -43,7 +43,7 @@ function resolveFilters(vocab, filters = {}) {
|
|
|
43
43
|
// (the operator running the CLI already has that file access), so it stays
|
|
44
44
|
// unrestricted there. Shared by `add` (input) and `similar` (image).
|
|
45
45
|
function confineToInbox(ctx, inputPath) {
|
|
46
|
-
if (ctx.transport !== 'http') return null;
|
|
46
|
+
if (ctx.transport !== 'http') return { path: inputPath, refused: null };
|
|
47
47
|
// Realpath the inbox root itself (when it exists) so a symlinked <data>/inbox still
|
|
48
48
|
// confines correctly: inputPath's own realpath resolves through that symlink too, and
|
|
49
49
|
// comparing against the unresolved join would then never match.
|
|
@@ -53,9 +53,12 @@ function confineToInbox(ctx, inputPath) {
|
|
|
53
53
|
// existsSync is false for a dangling symlink too, not just a missing path: refuse
|
|
54
54
|
// both explicitly instead of falling through to path.resolve, which would let a
|
|
55
55
|
// dangling symlink under inbox pass confinement on its unresolved path alone.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const candidate = path.isAbsolute(inputPath) ? inputPath : path.join(inboxRoot, inputPath);
|
|
57
|
+
if (!fs.existsSync(candidate)) return { path: candidate, refused: 'input does not exist' };
|
|
58
|
+
const real = fs.realpathSync(candidate);
|
|
59
|
+
return real.startsWith(inbox)
|
|
60
|
+
? { path: real, refused: null }
|
|
61
|
+
: { path: real, refused: 'over http, the path must be a file under <data>/inbox' };
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
export function makeTools(ctx) {
|
|
@@ -129,6 +132,51 @@ export function makeTools(ctx) {
|
|
|
129
132
|
return { ...out, body: first.trim(), detail: 'summary' };
|
|
130
133
|
}),
|
|
131
134
|
},
|
|
135
|
+
{
|
|
136
|
+
name: 'get_draft',
|
|
137
|
+
description: 'Return a saved or unsaved draft directly from the corpus volume, including blank judgement fields and every strip as paths, public URLs, and MCP image content. Use after add_batch because drafts are not searchable until saved.',
|
|
138
|
+
schema: z.object({ slug: z.string().regex(/^[a-z0-9-]+$/) }),
|
|
139
|
+
handler: async ({ slug }) => {
|
|
140
|
+
const dir = path.join(ctx.paths.entries, slug);
|
|
141
|
+
if (!fs.existsSync(path.join(dir, 'entry.md'))) return { refused: `no draft with slug ${slug}` };
|
|
142
|
+
const entry = parseEntry(dir);
|
|
143
|
+
const captures = entry.fm?.captured?.captures ?? [];
|
|
144
|
+
let config = null;
|
|
145
|
+
try { config = readConfig(); } catch { config = null; }
|
|
146
|
+
const strips = [];
|
|
147
|
+
const stripUrls = [];
|
|
148
|
+
for (const capture of captures) {
|
|
149
|
+
const artifact = (capture.artifacts ?? []).find((a) => a?.key?.endsWith('/strip.webp'));
|
|
150
|
+
if (!artifact) { strips.push(null); stripUrls.push(null); continue; }
|
|
151
|
+
let local = null;
|
|
152
|
+
try { local = await artifactFile(ctx.paths, artifact); } catch { local = null; }
|
|
153
|
+
strips.push(local);
|
|
154
|
+
stripUrls.push(config ? objectUrl(config, artifact.key) : null);
|
|
155
|
+
}
|
|
156
|
+
const { REQUIRED_BLANKS } = await import('../entry.mjs');
|
|
157
|
+
const view = { ...entry.fm, body: entry.body };
|
|
158
|
+
const valueAt = (dotted) => dotted.split('.').reduce((o, key) => (o == null ? undefined : o[key]), view);
|
|
159
|
+
const blanks = REQUIRED_BLANKS.filter((field) => {
|
|
160
|
+
const value = valueAt(field);
|
|
161
|
+
return value == null || value === '' || (Array.isArray(value) && value.length === 0);
|
|
162
|
+
});
|
|
163
|
+
return { ...entry, blanks, strip: strips[0] ?? null, strips, strip_url: stripUrls[0] ?? null, strip_urls: stripUrls };
|
|
164
|
+
},
|
|
165
|
+
content: (out) => {
|
|
166
|
+
if (out.refused) return [];
|
|
167
|
+
return (out.strips ?? []).flatMap((strip, index) => {
|
|
168
|
+
if (!strip) return [];
|
|
169
|
+
try {
|
|
170
|
+
return [
|
|
171
|
+
{ type: 'text', text: `Capture ${index + 1} strip for ${out.fm.slug}` },
|
|
172
|
+
{ type: 'image', mimeType: 'image/webp', data: fs.readFileSync(strip).toString('base64') },
|
|
173
|
+
];
|
|
174
|
+
} catch {
|
|
175
|
+
return [{ type: 'text', text: `Capture ${index + 1} strip for ${out.fm.slug} became unavailable` }];
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
},
|
|
132
180
|
{
|
|
133
181
|
name: 'get_motion',
|
|
134
182
|
description: 'Inspect one motion clip at original playback speed. Returns video URL when published, provenance, observed motion notes, and timestamped frames as native images by default. Use clip to select from available clips; timestamps are relative to the clip. A video URL alone does not provide video understanding.',
|
|
@@ -165,8 +213,9 @@ export function makeTools(ctx) {
|
|
|
165
213
|
handler: withDb(async (db, a) => {
|
|
166
214
|
if (!!a.slug === !!a.image) return { refused: 'similar needs exactly one of slug or image' };
|
|
167
215
|
if (a.image) {
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
216
|
+
const confined = confineToInbox(ctx, a.image);
|
|
217
|
+
if (confined.refused) return { refused: confined.refused };
|
|
218
|
+
a = { ...a, image: confined.path };
|
|
170
219
|
}
|
|
171
220
|
return similar(db, { ...a, embedder: ctx.embedder, embedderReason: ctx.embedderReason });
|
|
172
221
|
}),
|
|
@@ -241,8 +290,9 @@ export function makeTools(ctx) {
|
|
|
241
290
|
const refusal = await refuseUnsafeUrl(a.input, { transport: ctx.transport });
|
|
242
291
|
if (refusal) return { refused: refusal };
|
|
243
292
|
} else {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
293
|
+
const confined = confineToInbox(ctx, a.input);
|
|
294
|
+
if (confined.refused) return { refused: confined.refused };
|
|
295
|
+
a = { ...a, input: confined.path };
|
|
246
296
|
}
|
|
247
297
|
return (await import('../cli/add.mjs')).addEntry(a);
|
|
248
298
|
},
|
|
@@ -282,6 +332,24 @@ export function makeTools(ctx) {
|
|
|
282
332
|
return saveEntry({ dir: path.join(ctx.paths.entries, slug), vocab: ctx.vocab, indexPath: ctx.paths.index, entriesDir: ctx.paths.entries, models: ctx.paths.models, p: ctx.paths });
|
|
283
333
|
},
|
|
284
334
|
},
|
|
335
|
+
{
|
|
336
|
+
name: 'save_batch',
|
|
337
|
+
description: 'Validate and save many reviewed drafts, isolating invalid entries and rebuilding the index once for every valid slug. Returns one result per slug plus saved/refused counts.',
|
|
338
|
+
schema: z.object({
|
|
339
|
+
slugs: z.array(z.string().regex(/^[a-z0-9-]+$/)).min(1).max(2000),
|
|
340
|
+
}),
|
|
341
|
+
handler: async ({ slugs }) => {
|
|
342
|
+
const { saveEntries } = await import('../ingest/save.mjs');
|
|
343
|
+
return saveEntries({
|
|
344
|
+
dirs: slugs.map((slug) => path.join(ctx.paths.entries, slug)),
|
|
345
|
+
vocab: ctx.vocab,
|
|
346
|
+
indexPath: ctx.paths.index,
|
|
347
|
+
entriesDir: ctx.paths.entries,
|
|
348
|
+
models: ctx.paths.models,
|
|
349
|
+
p: ctx.paths,
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
},
|
|
285
353
|
{
|
|
286
354
|
name: 'set_fields',
|
|
287
355
|
description: "Fill a draft's judgement fields: roles, feel, motion.devices, motion.notes, palette, type, body, tier, ported_from — never provenance. Vocabulary terms are resolved through aliases and an unknown one is refused by name with nothing written, so a refusal never leaves a half-filled draft. Returns the fields still blank. This is the step between add and save_entry, and it is the only one that does not need a filesystem.",
|
|
@@ -294,6 +362,34 @@ export function makeTools(ctx) {
|
|
|
294
362
|
return setFields({ dir: path.join(ctx.paths.entries, slug), fields, vocab: ctx.vocab });
|
|
295
363
|
},
|
|
296
364
|
},
|
|
365
|
+
{
|
|
366
|
+
name: 'set_fields_batch',
|
|
367
|
+
description: 'Fill judgement fields for many drafts in one call. Each entry is isolated: an unknown term, provenance field, or missing slug refuses only that item and never partially writes it. Use chunks small enough to inspect, then save_batch after review.',
|
|
368
|
+
schema: z.object({
|
|
369
|
+
entries: z.array(z.object({
|
|
370
|
+
slug: z.string().regex(/^[a-z0-9-]+$/),
|
|
371
|
+
fields: z.record(z.any()),
|
|
372
|
+
})).min(1).max(200),
|
|
373
|
+
}),
|
|
374
|
+
handler: async ({ entries }) => {
|
|
375
|
+
const { setFields } = await import('../ingest/fields.mjs');
|
|
376
|
+
const seen = new Set();
|
|
377
|
+
const results = entries.map(({ slug, fields }) => {
|
|
378
|
+
if (seen.has(slug)) return { ok: false, slug, written: [], refused: ['duplicate slug in batch'] };
|
|
379
|
+
seen.add(slug);
|
|
380
|
+
try {
|
|
381
|
+
return setFields({ dir: path.join(ctx.paths.entries, slug), fields, vocab: ctx.vocab });
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return { ok: false, slug, written: [], refused: [err.message] };
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
return {
|
|
387
|
+
results,
|
|
388
|
+
updated: results.filter((r) => r.ok).length,
|
|
389
|
+
refused: results.filter((r) => !r.ok).length,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
},
|
|
297
393
|
];
|
|
298
394
|
}
|
|
299
395
|
|