@subbly/plugin-lint 0.0.4 → 0.0.7

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # @subbly/plugin-lint
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - A config field may carry `description` beside `label`: a string of 1-300 characters after trimming, shown under the input in the install dialog. Before, the linter rejected it as an unknown key (rule `manifest/namespace-unknown-key`), though the builder accepts it. A blank or over-long description fails rule `field/description`.
8
+
9
+ ## 0.0.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 937b7e6: A connector marker may carry `liveTools` beside `auth` and `tools`: the read tools that a live page may call. It is a non-empty array of unique tool names, 1-100 characters each, as `tools` is. When `tools` is there too, each live tool must also be in `tools` (rule `connector/live-tools`). A marker with none of `auth`, `tools` and `liveTools` is rejected, as the builder refresh rejects it.
14
+
15
+ ## 0.0.5
16
+
17
+ ### Patch Changes
18
+
19
+ - 1efe820: The plugin slug `custom` is an error, as the builder refresh reserves it for user-created connectors and credentials. No other rule changes.
20
+
3
21
  ## 0.0.4
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subbly/plugin-lint",
3
- "version": "0.0.4",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Subbly marketplace release gate, offline. Lints marketplace.json, plugin manifests, mcp.json, skills, agents and automations.",
package/src/report.mjs CHANGED
@@ -41,6 +41,7 @@ export const RULES = {
41
41
  'field/key-format': 'field keys must match /^[A-Z][A-Z0-9_]*$/',
42
42
  'field/key-duplicate': 'a duplicate field key silently overrides the first',
43
43
  'field/label': 'label is required, 1-100 characters',
44
+ 'field/description': 'description is optional, 1-300 characters after trimming',
44
45
  'field/type': 'type must be "text" or "select"',
45
46
  'field/options': 'options belong to select fields and select fields require them',
46
47
  'field/public-secret': 'public and secret are mutually exclusive',
@@ -64,9 +65,10 @@ export const RULES = {
64
65
  'setup/missing-install-skill': 'setup: true requires a skills/install skill',
65
66
 
66
67
  // connectors and mcp.json
67
- 'connector/marker': 'a connector marker holds auth, tools or both, and nothing else',
68
+ 'connector/marker': 'a connector marker holds one or more of auth, tools and liveTools, and nothing else',
68
69
  'connector/auth': 'auth, when present, is exactly "oauth"',
69
- 'connector/tools': 'tools is a non-empty array of unique tool names, 1-100 characters each',
70
+ 'connector/tools': 'tools and liveTools are each a non-empty array of unique tool names, 1-100 characters each',
71
+ 'connector/live-tools': 'each live tool is also in tools, when tools is present',
70
72
  'connector/name-length': 'connector names are 1-100 characters',
71
73
  'connector/undefined-server': 'a declared connector needs a matching mcp.json server',
72
74
  'connector/no-mcp': 'declaring a connector requires an mcp.json',
package/src/rules.mjs CHANGED
@@ -35,7 +35,7 @@ const CONTENT_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i
35
35
 
36
36
  const MANIFEST_KEYS = ['$schema', 'name', 'version', 'description', 'author', 'homepage', 'repository', 'license', 'keywords', 'extensions']
37
37
  const NAMESPACE_KEYS = ['displayName', 'image', 'default', 'fields', 'automations', 'setup', 'connectors']
38
- const FIELD_KEYS = ['key', 'label', 'type', 'required', 'secret', 'public', 'pattern', 'options']
38
+ const FIELD_KEYS = ['key', 'label', 'description', 'type', 'required', 'secret', 'public', 'pattern', 'options']
39
39
  const AUTOMATION_KEYS = ['name', 'schedule', 'model']
40
40
  const AUTOMATION_MODELS = ['normal', 'intelligent-high']
41
41
  const SERVER_KEYS = ['type', 'url', 'headers']
@@ -51,6 +51,8 @@ export const DEFAULT_ENV_KEYS = [
51
51
  'NEXT_PUBLIC_TERMS_CONDITIONS_URL',
52
52
  ]
53
53
 
54
+ const RESERVED_SLUG = 'custom'
55
+
54
56
  const MISPLACED = ['agents', 'automations', 'scripts', 'instructions.md']
55
57
 
56
58
  // The builder-maintained symlink to the installed version, the only stable
@@ -224,8 +226,13 @@ function lintMarketplaceManifest(root, report) {
224
226
 
225
227
  let slug = null
226
228
  if (check.string('marketplace/entry-slug', `${base}.slug`, entry.slug, { min: 1, max: 64, required: true })) {
227
- if (SLUG_PATTERN.test(entry.slug)) slug = entry.slug
228
- else report.error('marketplace/entry-slug', file, `${base}.slug: ${SLUG_MESSAGE}`, at(`${base}.slug`))
229
+ if (entry.slug === RESERVED_SLUG) {
230
+ report.error('marketplace/entry-slug', file, `${base}.slug: "${RESERVED_SLUG}" is reserved for user-created connectors and credentials`, at(`${base}.slug`))
231
+ } else if (SLUG_PATTERN.test(entry.slug)) {
232
+ slug = entry.slug
233
+ } else {
234
+ report.error('marketplace/entry-slug', file, `${base}.slug: ${SLUG_MESSAGE}`, at(`${base}.slug`))
235
+ }
229
236
  }
230
237
 
231
238
  if (slug) {
@@ -451,6 +458,9 @@ function lintFields(report, file, at, check, ns, ext, slug) {
451
458
  }
452
459
 
453
460
  check.string('field/label', `${p}.label`, field.label, { min: 1, max: 100, required: true, label: `${label}.label` })
461
+ if (field.description !== undefined) {
462
+ check.string('field/description', `${p}.description`, field.description, { min: 1, max: 300, trim: true, label: `${label}.description` })
463
+ }
454
464
 
455
465
  if (field.type !== 'text' && field.type !== 'select') {
456
466
  report.error('field/type', file, `${label}.type: expected "text" or "select", received ${JSON.stringify(field.type)}`, at(`${p}.type`))
@@ -580,9 +590,9 @@ function lintConnectors(report, file, at, check, ns, ext) {
580
590
  report.error('connector/marker', file, `${label}: expected object, received ${typeOf(marker)}`, at(p))
581
591
  continue
582
592
  }
583
- check.strictKeys('connector/marker', p, marker, ['auth', 'tools'], label)
584
- if (marker.auth === undefined && marker.tools === undefined) {
585
- report.error('connector/marker', file, `${label}: declare auth, tools or both`, at(p))
593
+ check.strictKeys('connector/marker', p, marker, ['auth', 'tools', 'liveTools'], label)
594
+ if (marker.auth === undefined && marker.tools === undefined && marker.liveTools === undefined) {
595
+ report.error('connector/marker', file, `${label}: declare auth, tools or liveTools`, at(p))
586
596
  }
587
597
  if (marker.auth !== undefined && marker.auth !== 'oauth') {
588
598
  report.error('connector/auth', file, `${label}.auth: expected "oauth", received ${JSON.stringify(marker.auth)}`, at(`${p}.auth`))
@@ -590,6 +600,10 @@ function lintConnectors(report, file, at, check, ns, ext) {
590
600
  if (marker.tools !== undefined) {
591
601
  lintToolAllowlist(report, file, at, check, `${p}.tools`, `${label}.tools`, marker.tools)
592
602
  }
603
+ if (marker.liveTools !== undefined) {
604
+ lintToolAllowlist(report, file, at, check, `${p}.liveTools`, `${label}.liveTools`, marker.liveTools)
605
+ lintLiveToolsInTools(report, file, at, p, label, marker)
606
+ }
593
607
  names.push(name)
594
608
  }
595
609
  return names
@@ -617,6 +631,15 @@ function lintToolAllowlist(report, file, at, check, p, label, tools) {
617
631
  }
618
632
  }
619
633
 
634
+ function lintLiveToolsInTools(report, file, at, p, label, marker) {
635
+ if (!Array.isArray(marker.tools) || !Array.isArray(marker.liveTools)) return
636
+ for (const [index, tool] of marker.liveTools.entries()) {
637
+ if (typeof tool === 'string' && !marker.tools.includes(tool)) {
638
+ report.error('connector/live-tools', file, `${label}.liveTools[${index}]: live tool "${tool}" must also be in tools`, at(`${p}.liveTools.${index}`))
639
+ }
640
+ }
641
+ }
642
+
620
643
  // --- mcp.json --------------------------------------------------------------
621
644
 
622
645
  function lintMcp(root, entry, manifest, report) {