openpencil/oxlint.json
Chris Gliddon 2cb32cfb00
feat(mcp): add list_available_fonts tool for font discovery (#210)
* feat(mcp): add list_available_fonts tool for font discovery

The existing list_fonts tool reports only fonts currently used in the
current page (it walks every TEXT node and groups by fontFamily).
Agents authoring new text have no way to discover what fonts the host
can render — they have to guess and hope set_font succeeds.

This adds list_available_fonts, which returns the set of font
families the host advertises. On desktop this is the system fonts
font-kit enumerates plus any bundled fonts the host registers; in
non-desktop runtimes that don't expose enumeration, it returns an
empty list with an explanatory note rather than throwing.

The optional `family` filter mirrors list_fonts for a substring
search, so agents can do `list_available_fonts({family: "mono"})` to
narrow before set_font.

Wired through:
- packages/core/src/figma-api/index.ts: adds optional
  listAvailableFontFamilies hook on FigmaAPI (same pattern as
  exportImage). Optional so non-desktop hosts aren't forced to
  implement it.
- packages/core/src/tools/read.ts: defines the tool.
- packages/core/src/tools/registry.ts: registers it in CORE_TOOLS so
  the MCP server advertises it.
- src/automation/figma-factory.ts: provides the desktop
  implementation by delegating to engine/fonts.ts#listFamilies which
  already wraps the Tauri list_system_fonts sidecar.

* refactor(mcp): reshape to listAvailableFontsAsync matching Figma API

Per review feedback on #210: FigmaAPI mirrors the Figma Plugin API, and
Figma already exposes figma.listAvailableFontsAsync(): Promise<Font[]>
where Font = { fontName: { family, style } }. Replacing the custom
listAvailableFontFamilies hook with the Figma-shaped method means eval'd
plugin code and any future plugin-compat layer get font enumeration for
free. Follows the same pattern as loadFontAsync on FigmaAPI.

- packages/core/src/figma-api/proxy.ts: export FigmaFont type
  ({ fontName: FigmaFontName }) alongside FigmaFontName.
- packages/core/src/figma-api/index.ts: drop optional
  listAvailableFontFamilies; add listAvailableFontsAsync() as a real
  method (non-optional) with a safe [] default, mirroring loadFontAsync.
- packages/core/src/tools/read.ts: list_available_fonts execute() now
  consumes Font[] and dedupes families via a Set. Drops the
  host-capability fallback note — the new method always returns an
  array, so a non-desktop host just yields {count: 0, fonts: []}.
- src/engine/fonts.ts: add listFonts() returning TauriFontFamily[]
  ({family, styles[]}) so the factory can emit one Font per (family,
  style) pair.
- src/automation/figma-factory.ts: override listAvailableFontsAsync,
  flattening {family, styles[]}[] to Font[].
- oxlint.json: add listFonts to the engine/fonts allowlist.

Tool public shape unchanged (still returns {count, fonts: string[]})
so agents calling list_available_fonts see no change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:56:33 +03:00

214 lines
5.8 KiB
JSON

{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"typescript",
"import",
"unicorn",
"vue"
],
"jsPlugins": [
"./lint/plugin.js"
],
"env": {
"browser": true,
"es2024": true
},
"rules": {
"no-unused-vars": "warn",
"no-console": ["warn", { "allow": ["warn", "error", "debug", "time", "timeEnd"] }],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"prefer-const": "error",
"radix": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "@/types",
"message": "Import types directly from @open-pencil/core."
}
],
"patterns": [
{
"group": [
"@/engine/*"
],
"message": "Import from @open-pencil/core unless the module adds platform-specific logic.",
"allowImportNames": [
"loadFont",
"listFamilies",
"listFonts",
"preloadFonts"
]
},
{
"group": [
"../../packages/*",
"../../../packages/*"
],
"message": "Use the workspace package name (e.g. @open-pencil/core) instead of relative paths to packages/."
}
]
}
],
"no-empty-function": "error",
"max-lines": [
"warn",
{
"max": 600
}
],
"complexity": [
"error",
{
"max": 20
}
],
"typescript/no-explicit-any": "warn",
"typescript/no-non-null-assertion": "warn",
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
"typescript/consistent-type-imports": "error",
"typescript/consistent-return": "error",
"typescript/no-unnecessary-condition": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-parameters": "error",
"typescript/no-unnecessary-boolean-literal-compare": "error",
"typescript/no-unnecessary-template-expression": "error",
"typescript/no-unnecessary-type-arguments": "error",
"typescript/prefer-optional-chain": "error",
"typescript/prefer-for-of": "error",
"import/no-cycle": "error",
"import/no-self-import": "error",
"import/no-duplicates": "error",
"import/no-mutable-exports": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-nested-ternary": "error",
"unicorn/no-useless-fallback-in-spread": "error",
"unicorn/no-length-as-slice-end": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/prefer-string-starts-ends-with": "error",
"unicorn/consistent-existence-index-check": "error",
"vue/no-arrow-functions-in-watch": "error",
"vue/no-deprecated-destroyed-lifecycle": "error",
"vue/no-export-in-script-setup": "error",
"vue/no-lifecycle-after-await": "error",
"vue/no-import-compiler-macros": "error",
"vue/prefer-import-from-vue": "error",
"vue/valid-define-emits": "error",
"vue/valid-define-props": "error",
"vue/no-required-prop-with-default": "error",
"vue/no-multiple-slot-args": "error",
"vue/define-emits-declaration": "error",
"vue/define-props-destructuring": "error",
"vue/require-typed-ref": "error",
"open-pencil/no-inline-named-types": [
"error",
{
"Vector": "x:number,y:number",
"Color": "a:number,b:number,g:number,r:number",
"GUID": "localID:number,sessionID:number",
"Rect": "height:number,width:number,x:number,y:number",
"Matrix": "m00:number,m01:number,m02:number,m10:number,m11:number,m12:number"
}
],
"open-pencil/no-structuredclone-scene-arrays": "error",
"open-pencil/no-math-random": "error",
"open-pencil/no-hand-rolled-color": "error",
"open-pencil/no-raw-console-format": "off",
"open-pencil/no-silent-catch": "error",
"open-pencil/no-typeof-window-check": "error"
},
"overrides": [
{
"files": [
"**/*.test.ts",
"**/*.test.tsx"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/no-non-null-assertion": "off",
"open-pencil/no-silent-catch": "off"
}
},
{
"files": [
"**/tools/ai-adapter.ts"
],
"rules": {
"typescript/no-explicit-any": "off"
}
},
{
"files": [
"**/figma-api.ts"
],
"rules": {
"no-console": "off"
}
},
{
"files": [
"**/render/render.ts"
],
"rules": {
"typescript/no-implied-eval": "off"
}
},
{
"files": [
"**/kiwi/kiwi-schema/**"
],
"rules": {
"typescript/no-explicit-any": "off",
"typescript/no-non-null-assertion": "off",
"typescript/no-implied-eval": "off",
"typescript/prefer-for-of": "off",
"typescript/prefer-optional-chain": "off",
"typescript/consistent-type-imports": "off",
"typescript/no-unnecessary-condition": "off",
"import/no-mutable-exports": "off",
"complexity": "off",
"prefer-const": "off",
"eqeqeq": "off",
"radix": "off"
}
},
{
"files": [
"src/stores/editor.ts",
"src/composables/use-canvas-input.ts",
"src/demo.ts"
],
"rules": {
"max-lines": "off"
}
},
{
"files": [
"packages/cli/src/**",
"packages/mcp/src/**"
],
"rules": {
"no-console": "off"
}
},
{
"files": [
"packages/cli/src/commands/**"
],
"rules": {
"open-pencil/no-raw-console-format": "error"
}
}
],
"ignorePatterns": [
"node_modules",
"dist",
"desktop",
"*.config.*"
]
}