logomarks 0.0.0-stage → 1.0.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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/bin/logomarks.js +4 -0
- package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
- package/data/fonts/Cagliostro-Regular.ttf +0 -0
- package/data/fonts/Fraunces-SemiBold.ttf +0 -0
- package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
- package/data/fonts/Inter-SemiBold.ttf +0 -0
- package/data/fonts/Limelight-Regular.ttf +0 -0
- package/data/fonts/OFL.txt +103 -0
- package/data/fonts/Shrikhand-Regular.ttf +0 -0
- package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
- package/data/index.bin +0 -0
- package/docs/api.md +31 -0
- package/docs/commands.md +412 -0
- package/docs/editing.md +61 -0
- package/docs/mcp.md +45 -0
- package/package.json +94 -4
- package/src/brand/brand.d.ts +23 -0
- package/src/brand/brand.js +29 -0
- package/src/brand/fonts.d.ts +61 -0
- package/src/brand/fonts.js +93 -0
- package/src/brand/mark.d.ts +53 -0
- package/src/brand/mark.js +151 -0
- package/src/checks/finding.d.ts +56 -0
- package/src/checks/finding.js +57 -0
- package/src/checks/fit.d.ts +5 -0
- package/src/checks/fit.js +46 -0
- package/src/checks/index.d.ts +33 -0
- package/src/checks/index.js +57 -0
- package/src/checks/specks.d.ts +5 -0
- package/src/checks/specks.js +38 -0
- package/src/checks/widths.d.ts +7 -0
- package/src/checks/widths.js +48 -0
- package/src/cli/args.d.ts +20 -0
- package/src/cli/args.js +35 -0
- package/src/cli/commands/check.d.ts +10 -0
- package/src/cli/commands/check.js +66 -0
- package/src/cli/commands/combine.d.ts +10 -0
- package/src/cli/commands/combine.js +54 -0
- package/src/cli/commands/cover.d.ts +10 -0
- package/src/cli/commands/cover.js +53 -0
- package/src/cli/commands/edit.d.ts +10 -0
- package/src/cli/commands/edit.js +70 -0
- package/src/cli/commands/find.d.ts +10 -0
- package/src/cli/commands/find.js +61 -0
- package/src/cli/commands/get.d.ts +10 -0
- package/src/cli/commands/get.js +57 -0
- package/src/cli/commands/glitch.d.ts +10 -0
- package/src/cli/commands/glitch.js +57 -0
- package/src/cli/commands/index.d.ts +15 -0
- package/src/cli/commands/index.js +24 -0
- package/src/cli/commands/kit.d.ts +10 -0
- package/src/cli/commands/kit.js +66 -0
- package/src/cli/commands/mcp.d.ts +9 -0
- package/src/cli/commands/mcp.js +36 -0
- package/src/cli/commands/mockup.d.ts +10 -0
- package/src/cli/commands/mockup.js +61 -0
- package/src/cli/commands/parts.d.ts +10 -0
- package/src/cli/commands/parts.js +44 -0
- package/src/cli/commands/sheet.d.ts +10 -0
- package/src/cli/commands/sheet.js +66 -0
- package/src/cli/commands/similar.d.ts +10 -0
- package/src/cli/commands/similar.js +111 -0
- package/src/cli/commands/social.d.ts +10 -0
- package/src/cli/commands/social.js +65 -0
- package/src/cli/commands/specks.d.ts +20 -0
- package/src/cli/commands/specks.js +103 -0
- package/src/cli/edited.d.ts +6 -0
- package/src/cli/edited.js +32 -0
- package/src/cli/files.d.ts +4 -0
- package/src/cli/files.js +27 -0
- package/src/cli/help.d.ts +20 -0
- package/src/cli/help.js +55 -0
- package/src/cli/index.d.ts +10 -0
- package/src/cli/index.js +104 -0
- package/src/cli/options.d.ts +30 -0
- package/src/cli/options.js +155 -0
- package/src/cli/report.d.ts +4 -0
- package/src/cli/report.js +28 -0
- package/src/cli/style.d.ts +4 -0
- package/src/cli/style.js +34 -0
- package/src/cover/index.d.ts +49 -0
- package/src/cover/index.js +63 -0
- package/src/edit/combine.d.ts +31 -0
- package/src/edit/combine.js +103 -0
- package/src/edit/doc.d.ts +34 -0
- package/src/edit/doc.js +96 -0
- package/src/edit/glitch.d.ts +48 -0
- package/src/edit/glitch.js +43 -0
- package/src/edit/index.d.ts +8 -0
- package/src/edit/index.js +11 -0
- package/src/edit/ops.d.ts +51 -0
- package/src/edit/ops.js +90 -0
- package/src/edit/preview.d.ts +8 -0
- package/src/edit/preview.js +51 -0
- package/src/find/index.d.ts +54 -0
- package/src/find/index.js +88 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +28 -0
- package/src/kit/index.d.ts +26 -0
- package/src/kit/index.js +31 -0
- package/src/mcp/content.d.ts +30 -0
- package/src/mcp/content.js +56 -0
- package/src/mcp/edited.d.ts +5 -0
- package/src/mcp/edited.js +25 -0
- package/src/mcp/index.d.ts +41 -0
- package/src/mcp/index.js +72 -0
- package/src/mcp/inputs.d.ts +46 -0
- package/src/mcp/inputs.js +122 -0
- package/src/mcp/preview.d.ts +4 -0
- package/src/mcp/preview.js +24 -0
- package/src/mcp/tools/check.d.ts +15 -0
- package/src/mcp/tools/check.js +47 -0
- package/src/mcp/tools/combine.d.ts +15 -0
- package/src/mcp/tools/combine.js +42 -0
- package/src/mcp/tools/cover.d.ts +20 -0
- package/src/mcp/tools/cover.js +46 -0
- package/src/mcp/tools/edit.d.ts +21 -0
- package/src/mcp/tools/edit.js +73 -0
- package/src/mcp/tools/find.d.ts +14 -0
- package/src/mcp/tools/find.js +36 -0
- package/src/mcp/tools/get.d.ts +15 -0
- package/src/mcp/tools/get.js +44 -0
- package/src/mcp/tools/glitch.d.ts +12 -0
- package/src/mcp/tools/glitch.js +39 -0
- package/src/mcp/tools/index.d.ts +4 -0
- package/src/mcp/tools/index.js +23 -0
- package/src/mcp/tools/kit.d.ts +28 -0
- package/src/mcp/tools/kit.js +51 -0
- package/src/mcp/tools/mockup.d.ts +22 -0
- package/src/mcp/tools/mockup.js +51 -0
- package/src/mcp/tools/sheet.d.ts +15 -0
- package/src/mcp/tools/sheet.js +43 -0
- package/src/mcp/tools/similar.d.ts +13 -0
- package/src/mcp/tools/similar.js +42 -0
- package/src/mcp/tools/social.d.ts +5 -0
- package/src/mcp/tools/social.js +68 -0
- package/src/mockup/index.d.ts +52 -0
- package/src/mockup/index.js +152 -0
- package/src/raster/render.d.ts +30 -0
- package/src/raster/render.js +100 -0
- package/src/rules.d.ts +73 -0
- package/src/rules.js +139 -0
- package/src/sheet/card.d.ts +20 -0
- package/src/sheet/card.js +190 -0
- package/src/sheet/draw.d.ts +26 -0
- package/src/sheet/draw.js +94 -0
- package/src/sheet/grid.d.ts +2 -0
- package/src/sheet/grid.js +32 -0
- package/src/sheet/index.d.ts +31 -0
- package/src/sheet/index.js +47 -0
- package/src/sheet/theme.d.ts +8 -0
- package/src/sheet/theme.js +45 -0
- package/src/similar/folder.d.ts +20 -0
- package/src/similar/folder.js +66 -0
- package/src/similar/nearest.d.ts +32 -0
- package/src/similar/nearest.js +60 -0
- package/src/similar/signature.d.ts +12 -0
- package/src/similar/signature.js +42 -0
- package/src/similar/store.d.ts +67 -0
- package/src/similar/store.js +88 -0
- package/src/util/color.d.ts +2 -0
- package/src/util/color.js +23 -0
- package/src/util/files.d.ts +8 -0
- package/src/util/files.js +60 -0
- package/src/util/round.d.ts +2 -0
- package/src/util/round.js +14 -0
- package/src/util/version.d.ts +2 -0
- package/src/util/version.js +10 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/edit/preview
|
|
3
|
+
*
|
|
4
|
+
* Edited marks to look at: the result on white, its two tones drawn as
|
|
5
|
+
* their colours; and its parts numbered, so an agent can name them in an
|
|
6
|
+
* edit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { render } from '../raster/render.js';
|
|
10
|
+
import { mark, text, panel } from '../sheet/draw.js';
|
|
11
|
+
|
|
12
|
+
/** The preview's side in pixels. */
|
|
13
|
+
export const PREVIEW_SIZE = 512;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A colour variable drawn as its fallback (`var(--tone-2, #FF5C3A)` → `#FF5C3A`), which resvg can't resolve.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} svg The mark.
|
|
19
|
+
* @returns {string} It, with plain fills.
|
|
20
|
+
*/
|
|
21
|
+
const plainFills = (svg) => svg.replace(/var\(--[\w-]+,\s*(#[0-9a-fA-F]{3,8})\s*\)/g, '$1');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A PNG of an SVG drawn at the preview's size.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} body The markup, on the 512 canvas.
|
|
27
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
28
|
+
*/
|
|
29
|
+
const png = async (body) =>
|
|
30
|
+
/** @type {Uint8Array} */ ((await render(`<svg xmlns="http://www.w3.org/2000/svg" width="${PREVIEW_SIZE}" height="${PREVIEW_SIZE}" viewBox="0 0 512 512"><rect width="512" height="512" fill="#FFFFFF"/>${body}</svg>`, { text: true, png: true })).png);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The mark on white, in its own colours (`currentColor` as near-black).
|
|
34
|
+
*
|
|
35
|
+
* @param {string} svg The mark.
|
|
36
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
37
|
+
*/
|
|
38
|
+
export const markPreview = (svg) => png(mark(plainFills(svg), 0, 0, 512, '#111111'));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The mark in grey with each part's box and index, as `listParts` numbers them.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} svg The mark.
|
|
44
|
+
* @param {readonly import('./doc.js').PartInfo[]} parts Its parts.
|
|
45
|
+
* @returns {Promise<Uint8Array>} The PNG.
|
|
46
|
+
*/
|
|
47
|
+
export function partsPreview(svg, parts) {
|
|
48
|
+
const labels = parts.map(({ index, box: [x0, y0, x1, y1], centre: [cx, cy] }) =>
|
|
49
|
+
`<rect x="${x0}" y="${y0}" width="${x1 - x0}" height="${y1 - y0}" fill="none" stroke="#FF5C3A" stroke-width="1.5" stroke-dasharray="4 3"/>${panel(cx - 15, cy - 13, 30, 26, '#111111', { r: 6 })}${text(cx, cy + 6, String(index), { size: 16, weight: 600, color: '#FFFFFF', anchor: 'middle' })}`);
|
|
50
|
+
return png(mark(svg, 0, 0, 512, '#C9C9C9') + labels.join(''));
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MarkIndex } from '../similar/store.js';
|
|
2
|
+
|
|
3
|
+
/** Exact filters for a search, and how many to return. */
|
|
4
|
+
export interface FindOptions {
|
|
5
|
+
/** One style (`letter`, `badge`, `iso`, `line`, `animal` …; see `facets`). */
|
|
6
|
+
style?: string;
|
|
7
|
+
/** Tags a mark must all have (`round`, `symmetric`, `solid` …). */
|
|
8
|
+
tags?: string[];
|
|
9
|
+
/** An industry it suits (`coffee`, `fintech`, `law-firm` …). */
|
|
10
|
+
industry?: string;
|
|
11
|
+
/** The letter or number it shows (`K`, `7`). */
|
|
12
|
+
letter?: string;
|
|
13
|
+
/** How many to return (20). */
|
|
14
|
+
limit?: number;
|
|
15
|
+
/** The live set (the bundled index). */
|
|
16
|
+
index?: MarkIndex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A mark found: what it is and its page. */
|
|
20
|
+
export interface Found {
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
style: string;
|
|
24
|
+
tags: string[];
|
|
25
|
+
mood: string[];
|
|
26
|
+
letter?: string;
|
|
27
|
+
industries: string[];
|
|
28
|
+
description: string;
|
|
29
|
+
/** Its page on logomarks.ai. */
|
|
30
|
+
url: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A search's result. */
|
|
34
|
+
export interface FindResult {
|
|
35
|
+
/** How many marks matched (more than `marks` when the limit cut it). */
|
|
36
|
+
total: number;
|
|
37
|
+
/** The best matches, best first. */
|
|
38
|
+
marks: Found[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Each filter's values, with how many marks have each. */
|
|
42
|
+
export interface Facets {
|
|
43
|
+
styles: Record<string, number>;
|
|
44
|
+
tags: Record<string, number>;
|
|
45
|
+
moods: Record<string, number>;
|
|
46
|
+
industries: Record<string, number>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How many marks a search returns when no limit is given: 20. */
|
|
50
|
+
export const DEFAULT_LIMIT: number;
|
|
51
|
+
/** Searches the live set as the board does (every word found, synonyms too, best first) and by exact filters. */
|
|
52
|
+
export function findMarks(query?: string, o?: FindOptions): FindResult;
|
|
53
|
+
/** The values the filters take, with how many marks have each, most first. */
|
|
54
|
+
export function facets(index?: MarkIndex): Facets;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/find
|
|
3
|
+
*
|
|
4
|
+
* Searches the bundled live set the way the board on logomarks.ai does (its
|
|
5
|
+
* packed @arraypress/search index): every word must be found, typed at the
|
|
6
|
+
* start of a word or as a synonym (UK and US trades: "solicitor" finds the
|
|
7
|
+
* lawyers' marks), best first (a word of the name or a search word, then an
|
|
8
|
+
* industry, then elsewhere). Filler words (`a`, `for`, `logo` …) are left out.
|
|
9
|
+
* Exact filters (style, tags, industry, letter) narrow it; the vocabulary to
|
|
10
|
+
* filter by comes from the set itself (`facets`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { unpack, facets as count } from '@arraypress/search';
|
|
14
|
+
import { loadIndex } from '../similar/store.js';
|
|
15
|
+
import { MARK_URL } from '../brand/mark.js';
|
|
16
|
+
|
|
17
|
+
/** Words a brief is full of that say nothing about a mark. */
|
|
18
|
+
const STOP = ['a', 'an', 'and', 'the', 'for', 'of', 'with', 'in', 'on', 'to', 'my', 'our', 'logo', 'logos', 'mark', 'marks', 'icon', 'brand', 'company', 'something', 'like'];
|
|
19
|
+
|
|
20
|
+
/** How many marks a search returns when no limit is given. */
|
|
21
|
+
export const DEFAULT_LIMIT = 20;
|
|
22
|
+
|
|
23
|
+
/** Each index's search, built once. */
|
|
24
|
+
const searches = new WeakMap();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An index's search (the board's, with filler words left out).
|
|
28
|
+
*
|
|
29
|
+
* @param {import('../similar/store.js').MarkIndex} index The live set.
|
|
30
|
+
* @returns {import('@arraypress/search').Search<Record<string, unknown>>} Its search.
|
|
31
|
+
* @throws {Error} When the index has no search (only the bundled one does).
|
|
32
|
+
*/
|
|
33
|
+
function searchOf(index) {
|
|
34
|
+
let s = searches.get(index);
|
|
35
|
+
if (!s) {
|
|
36
|
+
if (!index.search) throw new Error('this index has no search: findMarks needs the bundled index (npm run index)');
|
|
37
|
+
searches.set(index, (s = unpack(index.search, { stopWords: STOP })));
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether a mark passes the exact filters.
|
|
44
|
+
*
|
|
45
|
+
* @param {import('../similar/store.js').IndexedMark} m The mark.
|
|
46
|
+
* @param {import('./index.js').FindOptions} o The filters.
|
|
47
|
+
* @returns {boolean} Whether it passes.
|
|
48
|
+
*/
|
|
49
|
+
function passes(m, { style, tags = [], industry, letter }) {
|
|
50
|
+
if (style && m.style !== style.toLowerCase()) return false;
|
|
51
|
+
if (tags.some((t) => !m.tags.includes(t.toLowerCase()))) return false;
|
|
52
|
+
if (industry && !m.industries.includes(industry.toLowerCase())) return false;
|
|
53
|
+
return !letter || m.letter?.toLowerCase() === letter.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A mark as a search result: what it is, without its drawing.
|
|
58
|
+
*
|
|
59
|
+
* @param {import('../similar/store.js').IndexedMark} m The mark.
|
|
60
|
+
* @returns {import('./index.js').Found} The result.
|
|
61
|
+
*/
|
|
62
|
+
const found = ({ slug, name, style, tags, mood, letter, industries, description }) =>
|
|
63
|
+
({ slug, name, style, tags, mood, ...(letter && { letter }), industries, description, url: MARK_URL + slug });
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Searches the live set as the board does: a mark matches when every query
|
|
67
|
+
* word (or synonym phrase) is found in it; the best come first, ties in the
|
|
68
|
+
* board's order. With no words, the filtered marks come back in that order.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [query] Free words (`fox`, `calm letter k`, `solicitor`).
|
|
71
|
+
* @param {import('./index.js').FindOptions} [o] Filters and the limit.
|
|
72
|
+
* @returns {import('./index.js').FindResult} The total that matched and the first `limit`.
|
|
73
|
+
*/
|
|
74
|
+
export function findMarks(query = '', { index = loadIndex(), limit = DEFAULT_LIMIT, ...filters } = {}) {
|
|
75
|
+
const r = searchOf(index).search(query, { filter: (_, i) => passes(index.marks[i], filters), limit });
|
|
76
|
+
return { total: r.total, marks: r.hits.map((h) => found(index.marks[h.index])) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The values the filters take, with how many marks have each, most first.
|
|
81
|
+
*
|
|
82
|
+
* @param {import('../similar/store.js').MarkIndex} [index] The live set.
|
|
83
|
+
* @returns {import('./index.js').Facets} Styles, tags, moods and industries.
|
|
84
|
+
*/
|
|
85
|
+
export function facets(index = loadIndex()) {
|
|
86
|
+
const { style, tags, mood, industries } = count(index.marks, ['style', 'tags', 'mood', 'industries']);
|
|
87
|
+
return { styles: style, tags, moods: mood, industries };
|
|
88
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { check, fix } from './checks/index.js';
|
|
2
|
+
export type { CheckOptions, Report, Finding, FindingId, Severity } from './checks/index.js';
|
|
3
|
+
export { FINDINGS } from './checks/finding.js';
|
|
4
|
+
export type { FindingKind } from './checks/finding.js';
|
|
5
|
+
export { similar } from './similar/nearest.js';
|
|
6
|
+
export type { Match, Verdict, SimilarOptions } from './similar/nearest.js';
|
|
7
|
+
export { loadIndex } from './similar/store.js';
|
|
8
|
+
export type { MarkIndex, IndexedMark, IndexedLetter, MarkAbout } from './similar/store.js';
|
|
9
|
+
export { findMarks, facets } from './find/index.js';
|
|
10
|
+
export type { FindOptions, FindResult, Found, Facets } from './find/index.js';
|
|
11
|
+
export { sheet } from './sheet/index.js';
|
|
12
|
+
export type { Sheet, SheetMark, SheetOptions } from './sheet/index.js';
|
|
13
|
+
export { fitMark } from '@logomarks/core/fit';
|
|
14
|
+
export type { Fitted } from '@logomarks/core/fit';
|
|
15
|
+
export * as rules from './rules.js';
|
|
16
|
+
export { loadMark, findMark, paint, markPng, MarkNotFound } from './brand/mark.js';
|
|
17
|
+
export type { Mark, TileOptions } from './brand/mark.js';
|
|
18
|
+
export { parseColor } from './util/color.js';
|
|
19
|
+
export { FONTS, brandFont } from './brand/fonts.js';
|
|
20
|
+
export type { Font, FontId, BrandFont } from './brand/fonts.js';
|
|
21
|
+
export { brandInput } from './brand/brand.js';
|
|
22
|
+
export type { BrandOptions } from './brand/brand.js';
|
|
23
|
+
export { kit, social } from './kit/index.js';
|
|
24
|
+
export type { KitOptions, SocialOptions, PackOptions } from './kit/index.js';
|
|
25
|
+
export { glitchMark, editMark, markParts, combineMarks, markPreview, partsPreview, PRESETS, OPS, COMBINE_OPS } from './edit/index.js';
|
|
26
|
+
export type { GlitchPreset, GlitchOptions, Glitched, EditOp, OpName, EditResult, CombineOp, CombineOptions, Placement, Edited, PartInfo } from './edit/index.js';
|
|
27
|
+
export { mockup, OBJECTS } from './mockup/index.js';
|
|
28
|
+
export type { Mockup, MockupOptions } from './mockup/index.js';
|
|
29
|
+
export { cover, RECIPES } from './cover/index.js';
|
|
30
|
+
export type { Cover, CoverOptions } from './cover/index.js';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks
|
|
3
|
+
*
|
|
4
|
+
* Check logomarks the way the Logomarks set is checked: the house rules
|
|
5
|
+
* (`check`, `fix`), the nearest live marks (`similar`), and review cards to
|
|
6
|
+
* look at (`sheet`); searching the live set (`findMarks`). Then build a brand on one: the mark itself (`loadMark`,
|
|
7
|
+
* `paint`, `markPng`), edits (`glitchMark`, `editMark`, `combineMarks`), the
|
|
8
|
+
* brand kit and social pack (`kit`, `social`, from `@logomarks/brand-kit`),
|
|
9
|
+
* mockups (`mockup`) and covers (`cover`). The same functions run the `logomarks` command and its MCP
|
|
10
|
+
* server (`logomarks/mcp`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { check, fix } from './checks/index.js';
|
|
14
|
+
export { FINDINGS } from './checks/finding.js';
|
|
15
|
+
export { similar } from './similar/nearest.js';
|
|
16
|
+
export { loadIndex } from './similar/store.js';
|
|
17
|
+
export { findMarks, facets } from './find/index.js';
|
|
18
|
+
export { sheet } from './sheet/index.js';
|
|
19
|
+
export { fitMark } from '@logomarks/core/fit';
|
|
20
|
+
export * as rules from './rules.js';
|
|
21
|
+
export { loadMark, findMark, paint, markPng, MarkNotFound } from './brand/mark.js';
|
|
22
|
+
export { parseColor } from './util/color.js';
|
|
23
|
+
export { FONTS, brandFont } from './brand/fonts.js';
|
|
24
|
+
export { brandInput } from './brand/brand.js';
|
|
25
|
+
export { kit, social } from './kit/index.js';
|
|
26
|
+
export { glitchMark, editMark, markParts, combineMarks, markPreview, partsPreview, PRESETS, OPS, COMBINE_OPS } from './edit/index.js';
|
|
27
|
+
export { mockup, OBJECTS } from './mockup/index.js';
|
|
28
|
+
export { cover, RECIPES } from './cover/index.js';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BrandKit, SocialPack, StyleId } from '@logomarks/brand-kit';
|
|
2
|
+
import type { BrandOptions } from '../brand/brand.js';
|
|
3
|
+
|
|
4
|
+
/** What goes in a social pack. */
|
|
5
|
+
export interface PackOptions {
|
|
6
|
+
/** Image ids (`x-header`) or platform names (`instagram`). Every image by default. */
|
|
7
|
+
platforms?: string[];
|
|
8
|
+
/** `minimal`, `bold`, `pattern`, `gradient`. The first three by default. */
|
|
9
|
+
styles?: StyleId[];
|
|
10
|
+
/** Animated SVGs for avatars, stories and banners too. Default false. */
|
|
11
|
+
animated?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** What a kit is made from. */
|
|
15
|
+
export interface KitOptions extends BrandOptions, PackOptions {
|
|
16
|
+
/** Include the social pack (`social/`). Default true. */
|
|
17
|
+
social?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What a social pack is made from. */
|
|
21
|
+
export interface SocialOptions extends BrandOptions, PackOptions {}
|
|
22
|
+
|
|
23
|
+
/** The brand kit, every file in memory. */
|
|
24
|
+
export function kit(o: KitOptions): Promise<BrandKit>;
|
|
25
|
+
/** The social pack, every file in memory. */
|
|
26
|
+
export function social(o: SocialOptions): Promise<SocialPack>;
|
package/src/kit/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/kit
|
|
3
|
+
*
|
|
4
|
+
* The brand kit and the social pack, from `@logomarks/brand-kit`: the CLI's
|
|
5
|
+
* marks and bundled faces handed in, PNGs drawn by resvg (WebAssembly), so
|
|
6
|
+
* neither needs sharp. Every file comes back in memory, by path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { brandKit, socialPack } from '@logomarks/brand-kit';
|
|
10
|
+
import { brandInput } from '../brand/brand.js';
|
|
11
|
+
import { rasterize } from '../raster/render.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A brand kit: the mark, favicons, lockups, colours, the brand sheet, `brand.json`, a README and the social pack.
|
|
15
|
+
*
|
|
16
|
+
* @param {import('./index.js').KitOptions} o The brand, and what goes in the social pack.
|
|
17
|
+
* @returns {Promise<import('@logomarks/brand-kit').BrandKit>} The files and `brand.json`'s contents.
|
|
18
|
+
*/
|
|
19
|
+
export function kit({ styles, platforms, animated = false, social, ...brand }) {
|
|
20
|
+
return brandKit({ ...brandInput(brand), styles, platforms, animated, social, rasterize });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A social pack: every platform image in each style, the email signature, the Open Graph card and a README.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('./index.js').SocialOptions} o The brand, the images and the styles.
|
|
27
|
+
* @returns {Promise<import('@logomarks/brand-kit').SocialPack>} The files and the brand.
|
|
28
|
+
*/
|
|
29
|
+
export function social({ styles, platforms, animated = false, ...brand }) {
|
|
30
|
+
return socialPack(brandInput(brand), { styles, platforms, animated, rasterize });
|
|
31
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Text content. */
|
|
2
|
+
export interface TextContent {
|
|
3
|
+
type: 'text';
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Image content: base64 bytes and their type. */
|
|
8
|
+
export interface ImageContent {
|
|
9
|
+
type: 'image';
|
|
10
|
+
data: string;
|
|
11
|
+
mimeType: 'image/png';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A tool's result: its content, and whether it failed. */
|
|
15
|
+
export interface ToolResult {
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
content: (TextContent | ImageContent)[];
|
|
18
|
+
isError?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A value as text content: pretty JSON, or a string as it is. */
|
|
22
|
+
export function text(value: unknown): TextContent;
|
|
23
|
+
/** A PNG as image content. */
|
|
24
|
+
export function image(png: Uint8Array): ImageContent;
|
|
25
|
+
/** A tool's result from its content. */
|
|
26
|
+
export function result(...content: (TextContent | ImageContent)[]): ToolResult;
|
|
27
|
+
/** What went wrong, as the caller should read it. */
|
|
28
|
+
export function explain(e: unknown): string;
|
|
29
|
+
/** A tool error: the message, flagged `isError`. */
|
|
30
|
+
export function failure(e: unknown): ToolResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/mcp/content
|
|
3
|
+
*
|
|
4
|
+
* What a tool hands back: JSON as text, PNGs as images, and errors the
|
|
5
|
+
* caller can act on.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MarkNotFound } from '../brand/mark.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A value as MCP text content: pretty JSON, or a string as it is.
|
|
12
|
+
*
|
|
13
|
+
* @param {unknown} value The value.
|
|
14
|
+
* @returns {import('./content.js').TextContent} The content.
|
|
15
|
+
*/
|
|
16
|
+
export const text = (value) => ({ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A PNG as MCP image content.
|
|
20
|
+
*
|
|
21
|
+
* @param {Uint8Array} png The PNG's bytes.
|
|
22
|
+
* @returns {import('./content.js').ImageContent} The content.
|
|
23
|
+
*/
|
|
24
|
+
export const image = (png) => ({ type: 'image', data: Buffer.from(png).toString('base64'), mimeType: 'image/png' });
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A tool's result.
|
|
28
|
+
*
|
|
29
|
+
* @param {...(import('./content.js').TextContent | import('./content.js').ImageContent)} content What it returns, in order.
|
|
30
|
+
* @returns {import('./content.js').ToolResult} The result.
|
|
31
|
+
*/
|
|
32
|
+
export const result = (...content) => ({ content });
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What went wrong, as the caller should read it: a missing mark with the
|
|
36
|
+
* closest slugs and where to look, a missing file by its path, anything
|
|
37
|
+
* else by its message.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} e The error.
|
|
40
|
+
* @returns {string} The message.
|
|
41
|
+
*/
|
|
42
|
+
export function explain(e) {
|
|
43
|
+
if (e instanceof MarkNotFound) return `${e.message} Search the set with find_marks, or pass the path to an .svg file.`;
|
|
44
|
+
const err = /** @type {NodeJS.ErrnoException} */ (e);
|
|
45
|
+
if (err?.code === 'ENOENT') return `no such file: ${err.path}`;
|
|
46
|
+
if (err?.code === 'EACCES') return `can't write ${err.path}: permission denied`;
|
|
47
|
+
return err instanceof Error ? err.message : String(e);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A tool error: the message as text, flagged so the caller knows the call failed.
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} e The error.
|
|
54
|
+
* @returns {import('./content.js').ToolResult} The result.
|
|
55
|
+
*/
|
|
56
|
+
export const failure = (e) => ({ content: [text(explain(e))], isError: true });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Edited } from '../edit/doc.js';
|
|
2
|
+
import type { ToolResult } from './content.js';
|
|
3
|
+
|
|
4
|
+
/** An edited mark as a tool result: `{ ok, …, file?, svg, checks }` and its image; written to `out` when given. */
|
|
5
|
+
export function edited(r: Edited & Record<string, unknown>, o: { out?: string; cwd: string }): Promise<ToolResult>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/mcp/edited
|
|
3
|
+
*
|
|
4
|
+
* What the editing tools hand back: the result's SVG (or, when it was
|
|
5
|
+
* written to a file, its path) and its checks as JSON, and the mark as an
|
|
6
|
+
* image to look at.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { markPreview } from '../edit/preview.js';
|
|
10
|
+
import { writeFile } from '../util/files.js';
|
|
11
|
+
import { resolvePath } from './inputs.js';
|
|
12
|
+
import { result, text, image } from './content.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* An edited mark as a tool result.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../edit/doc.js').Edited & Record<string, unknown>} r The result, and anything more to report.
|
|
18
|
+
* @param {{ out?: string, cwd: string }} o Where to write it, if anywhere.
|
|
19
|
+
* @returns {Promise<import('./content.js').ToolResult>} The result.
|
|
20
|
+
*/
|
|
21
|
+
export async function edited({ svg, checks, ...more }, { out, cwd }) {
|
|
22
|
+
const file = out && resolvePath(cwd, out);
|
|
23
|
+
if (file) writeFile(file, svg);
|
|
24
|
+
return result(text({ ok: checks.ok, ...more, ...(file ? { file } : { svg }), checks }), image(await markPreview(svg)));
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ZodRawShape } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import type { ToolResult } from './content.js';
|
|
5
|
+
|
|
6
|
+
/** What a tool's run is given besides its arguments. */
|
|
7
|
+
export interface ToolContext {
|
|
8
|
+
/** Where relative paths resolve. */
|
|
9
|
+
cwd: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** One tool: what the agent sees, and what runs. */
|
|
13
|
+
export interface Tool<Input = Record<string, unknown>> {
|
|
14
|
+
/** The tool's name (snake_case). */
|
|
15
|
+
name: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Written for the agent: what it does, what it returns, when to use it. */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Its arguments, as zod (the server publishes them as JSON Schema). */
|
|
20
|
+
inputSchema: ZodRawShape;
|
|
21
|
+
annotations: ToolAnnotations;
|
|
22
|
+
/** Runs it on validated arguments; throws on failure (the server returns a tool error). */
|
|
23
|
+
run(args: Input, ctx: ToolContext): Promise<ToolResult>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** How to set the server up. */
|
|
27
|
+
export interface ServerOptions {
|
|
28
|
+
/** Where relative paths resolve (the process's working folder). */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every tool, in the order an agent uses them. */
|
|
33
|
+
export const TOOLS: Tool<never>[];
|
|
34
|
+
/** What the server tells the agent when it connects. */
|
|
35
|
+
export const INSTRUCTIONS: string;
|
|
36
|
+
/** An MCP server with every tool registered, not yet connected. */
|
|
37
|
+
export function createServer(o?: ServerOptions): McpServer;
|
|
38
|
+
/** Loads the renderer, the typefaces and the live set. */
|
|
39
|
+
export function warm(): Promise<void>;
|
|
40
|
+
/** Runs the server over stdio until the client hangs up. */
|
|
41
|
+
export function serve(o?: ServerOptions): Promise<McpServer>;
|
package/src/mcp/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/mcp
|
|
3
|
+
*
|
|
4
|
+
* The MCP server: the library's functions as typed tools for an AI agent
|
|
5
|
+
* (Claude Code, Codex, Cursor …), over stdio. It runs on the user's machine;
|
|
6
|
+
* nothing leaves it. The renderer, the typefaces and the live set load once
|
|
7
|
+
* and stay warm across calls. `logomarks mcp` starts it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
|
+
import { TOOLS } from './tools/index.js';
|
|
13
|
+
import { failure } from './content.js';
|
|
14
|
+
import { loadIndex } from '../similar/store.js';
|
|
15
|
+
import { warmRenderer } from '../raster/render.js';
|
|
16
|
+
import { VERSION } from '../util/version.js';
|
|
17
|
+
|
|
18
|
+
export { TOOLS };
|
|
19
|
+
|
|
20
|
+
/** What the server tells the agent when it connects. */
|
|
21
|
+
export const INSTRUCTIONS = `Logomarks: the free Logomarks set of geometric logomarks (CC0, bundled, searched offline), and the checks the set is held to, run locally.
|
|
22
|
+
- Pick a mark: find_marks, then get_mark.
|
|
23
|
+
- Draw your own: write the SVG to the house rules (512 canvas, longest side 448, centred; filled paths in currentColor; one gap of 22; nothing under 10 units), then check_mark, similar_marks and review_sheet. Fix every error, look at the sheet, and repeat until check_mark is ok.
|
|
24
|
+
- Make it your own: list_parts, then edit_mark (delete, move, round, offset, mirror, two-tone …), glitch_mark, combine_marks. Each result is fitted and checked.
|
|
25
|
+
- Build on a mark: build_brand_kit and build_social_pack (folders of files), render_mockup, render_cover.
|
|
26
|
+
Tools that make images return a preview to look at; pass out to write the file. Relative paths resolve against the server's working folder. The full brand kit, photoreal mockups and custom logos are at https://logomarks.ai/pricing.`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* An MCP server with every tool registered. Errors come back as tool errors
|
|
30
|
+
* (`isError`) with a message the agent can act on.
|
|
31
|
+
*
|
|
32
|
+
* @param {import('./index.js').ServerOptions} [o] Where relative paths resolve.
|
|
33
|
+
* @returns {McpServer} The server, not yet connected.
|
|
34
|
+
*/
|
|
35
|
+
export function createServer({ cwd = process.cwd() } = {}) {
|
|
36
|
+
const server = new McpServer({ name: 'logomarks', title: 'Logomarks', version: VERSION, websiteUrl: 'https://logomarks.ai' }, { instructions: INSTRUCTIONS });
|
|
37
|
+
for (const t of TOOLS) {
|
|
38
|
+
const run = /** @type {(args: object, ctx: import('./index.js').ToolContext) => Promise<import('./content.js').ToolResult>} */ (t.run);
|
|
39
|
+
server.registerTool(t.name, { title: t.title, description: t.description, inputSchema: t.inputSchema, annotations: t.annotations }, async (args) => {
|
|
40
|
+
try {
|
|
41
|
+
return await run(args, { cwd });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return failure(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return server;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Loads the renderer, the typefaces and the live set in the background, so
|
|
52
|
+
* the first call is as quick as the rest.
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<void>} Resolves when all three are ready.
|
|
55
|
+
*/
|
|
56
|
+
export async function warm() {
|
|
57
|
+
loadIndex();
|
|
58
|
+
await warmRenderer();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Runs the server on stdin and stdout until the client hangs up.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('./index.js').ServerOptions} [o] Where relative paths resolve.
|
|
65
|
+
* @returns {Promise<McpServer>} The connected server.
|
|
66
|
+
*/
|
|
67
|
+
export async function serve(o) {
|
|
68
|
+
const server = createServer(o);
|
|
69
|
+
await server.connect(new StdioServerTransport());
|
|
70
|
+
setImmediate(() => warm().catch((e) => console.error(`logomarks: ${e.message}`)));
|
|
71
|
+
return server;
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { Mark } from '../brand/mark.js';
|
|
3
|
+
|
|
4
|
+
/** A mark to check: its name, its file (absolute; none for SVG text) and its SVG. */
|
|
5
|
+
export interface SvgInput {
|
|
6
|
+
name: string;
|
|
7
|
+
file?: string;
|
|
8
|
+
svg: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** The schemas the tools share. */
|
|
12
|
+
export const schema: {
|
|
13
|
+
mark: z.ZodString;
|
|
14
|
+
color: z.ZodOptional<z.ZodString>;
|
|
15
|
+
font: z.ZodOptional<z.ZodEnum<Record<string, string>>>;
|
|
16
|
+
name: z.ZodOptional<z.ZodString>;
|
|
17
|
+
svg: z.ZodOptional<z.ZodString>;
|
|
18
|
+
svgName: z.ZodOptional<z.ZodString>;
|
|
19
|
+
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
|
+
ignore: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
21
|
+
strict: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
seed: z.ZodOptional<z.ZodString>;
|
|
23
|
+
source: z.ZodOptional<z.ZodString>;
|
|
24
|
+
sourceSvg: z.ZodOptional<z.ZodString>;
|
|
25
|
+
out: z.ZodOptional<z.ZodString>;
|
|
26
|
+
tagline: z.ZodOptional<z.ZodString>;
|
|
27
|
+
url: z.ZodOptional<z.ZodString>;
|
|
28
|
+
handle: z.ZodOptional<z.ZodString>;
|
|
29
|
+
platforms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
styles: z.ZodOptional<z.ZodArray<z.ZodEnum<{ minimal: 'minimal'; bold: 'bold'; pattern: 'pattern'; gradient: 'gradient' }>>>;
|
|
31
|
+
animated: z.ZodOptional<z.ZodBoolean>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A path resolved against the working folder. */
|
|
35
|
+
export function resolvePath(cwd: string, p: string): string;
|
|
36
|
+
/** A mark by slug, name or file (relative to the working folder); throws `MarkNotFound`. */
|
|
37
|
+
export function readMark(ref: string, cwd: string): Mark;
|
|
38
|
+
/** A colour input as `#RRGGBB` (undefined when not given); throws when it isn't one. */
|
|
39
|
+
export function readColor(value: string | undefined): string | undefined;
|
|
40
|
+
/** The marks a check names: SVG text, or files and folders; throws when neither, both, or missing. */
|
|
41
|
+
export function readSvgs(input: { svg?: string; svgName?: string; paths?: string[] }, cwd: string): SvgInput[];
|
|
42
|
+
|
|
43
|
+
/** A mark as SVG from a live slug or name, an .svg path, or SVG text (fitted). */
|
|
44
|
+
export function readAnyMark(ref: string, cwd: string): { name: string; svg: string };
|
|
45
|
+
/** The mark an edit starts from: `mark` (a slug, name or file) or `svg` (text, fitted); throws when neither or both are given. */
|
|
46
|
+
export function readSource(input: { mark?: string; svg?: string }, cwd: string): { name: string; svg: string };
|