[object Object]

← back to Ventura Claw

Add 5 no-auth public-data connectors: MET, Cleveland, AIC, Wikipedia, Color API

4cd2509d847611b2e0f69a81a0c89afc90c519a2 · 2026-05-06 17:08:13 -0700 · SteveStudio2

All 5 are pre-connected — zero credentials required, just HTTPS calls.
Useful for DW content workflows (art reference, color tooling, encyclopedic
fact-checks). Live counts on health probe:
  - MET Museum:                 43,947 PD objects
  - Cleveland Museum of Art:    68,738 CC0 artworks
  - Art Institute of Chicago:  131,990 artworks (IIIF imagery)
  - Wikipedia:                 english REST + search APIs
  - Color API:                 identify + scheme actions

Each handler follows the archive.js pattern: meta + empty fields + always-
configured + health() probing a free endpoint + 2-3 read-only actions.

Registered in REAL map with WRITE_ACTIONS: Set([]) (read-only) and
READ_ACTIONS enumerated. Catalog entries added to connectors.json (62 total).

Files touched

Diff

commit 4cd2509d847611b2e0f69a81a0c89afc90c519a2
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 17:08:13 2026 -0700

    Add 5 no-auth public-data connectors: MET, Cleveland, AIC, Wikipedia, Color API
    
    All 5 are pre-connected — zero credentials required, just HTTPS calls.
    Useful for DW content workflows (art reference, color tooling, encyclopedic
    fact-checks). Live counts on health probe:
      - MET Museum:                 43,947 PD objects
      - Cleveland Museum of Art:    68,738 CC0 artworks
      - Art Institute of Chicago:  131,990 artworks (IIIF imagery)
      - Wikipedia:                 english REST + search APIs
      - Color API:                 identify + scheme actions
    
    Each handler follows the archive.js pattern: meta + empty fields + always-
    configured + health() probing a free endpoint + 2-3 read-only actions.
    
    Registered in REAL map with WRITE_ACTIONS: Set([]) (read-only) and
    READ_ACTIONS enumerated. Catalog entries added to connectors.json (62 total).
---
 server/connectors.json         | 62 +++++++++++++++++++++++++++++++++++++++++-
 server/connectors/aic.js       | 43 +++++++++++++++++++++++++++++
 server/connectors/cleveland.js | 46 +++++++++++++++++++++++++++++++
 server/connectors/colorapi.js  | 53 ++++++++++++++++++++++++++++++++++++
 server/connectors/index.js     | 19 ++++++++++++-
 server/connectors/met.js       | 44 ++++++++++++++++++++++++++++++
 server/connectors/wikipedia.js | 61 +++++++++++++++++++++++++++++++++++++++++
 7 files changed, 326 insertions(+), 2 deletions(-)

diff --git a/server/connectors.json b/server/connectors.json
index 005be68..cd8a8e2 100644
--- a/server/connectors.json
+++ b/server/connectors.json
@@ -691,5 +691,65 @@
       "textToSpeech"
     ],
     "sensitive": false
+  },
+  {
+    "id": "met",
+    "name": "MET Museum",
+    "category": "data",
+    "docs": "https://metmuseum.github.io/",
+    "auth": "none",
+    "triggers": 0,
+    "actions": 3,
+    "logo": null,
+    "tint": "#E4002B",
+    "sensitive": false
+  },
+  {
+    "id": "cleveland",
+    "name": "Cleveland Museum of Art",
+    "category": "data",
+    "docs": "https://openaccess-api.clevelandart.org/",
+    "auth": "none",
+    "triggers": 0,
+    "actions": 2,
+    "logo": null,
+    "tint": "#0A1F44",
+    "sensitive": false
+  },
+  {
+    "id": "aic",
+    "name": "Art Institute of Chicago",
+    "category": "data",
+    "docs": "https://api.artic.edu/docs/",
+    "auth": "none",
+    "triggers": 0,
+    "actions": 2,
+    "logo": null,
+    "tint": "#B22222",
+    "sensitive": false
+  },
+  {
+    "id": "wikipedia",
+    "name": "Wikipedia",
+    "category": "data",
+    "docs": "https://en.wikipedia.org/api/rest_v1/",
+    "auth": "none",
+    "triggers": 0,
+    "actions": 3,
+    "logo": "wikipedia",
+    "tint": "#000000",
+    "sensitive": false
+  },
+  {
+    "id": "colorapi",
+    "name": "Color API",
+    "category": "creative",
+    "docs": "https://www.thecolorapi.com/docs",
+    "auth": "none",
+    "triggers": 0,
+    "actions": 2,
+    "logo": null,
+    "tint": "#d4a04a",
+    "sensitive": false
   }
-]
+]
\ No newline at end of file
diff --git a/server/connectors/aic.js b/server/connectors/aic.js
new file mode 100644
index 0000000..aa30d7e
--- /dev/null
+++ b/server/connectors/aic.js
@@ -0,0 +1,43 @@
+// Art Institute of Chicago — Public API. No key.
+// https://api.artic.edu/docs/
+const BASE = "https://api.artic.edu/api/v1";
+async function call(path) {
+  const r = await fetch(`${BASE}${path}`, {
+    headers: { "User-Agent": "VenturaClaw/1.0", Accept: "application/json", "AIC-User-Agent": "VenturaClaw (steve@venturaclaw.com)" },
+    signal: AbortSignal.timeout(15_000),
+  });
+  if (!r.ok) throw new Error(`aic ${r.status}`);
+  return r.json();
+}
+module.exports = {
+  meta: { id: "aic", name: "Art Institute of Chicago", category: "data", docsUrl: "https://api.artic.edu/docs/", auth: "none", realImpl: true },
+  fields: [],
+  configured() { return true; },
+  async health() {
+    try {
+      const d = await call("/artworks?limit=1");
+      return { ok: true, label: `${(d.pagination?.total ?? 0).toLocaleString()} artworks · public-domain images via IIIF` };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+  actions: {
+    async "search"({ q, limit }) {
+      if (!q) throw new Error("q required");
+      const qs = new URLSearchParams({ q, limit: String(Math.min(limit || 20, 100)), fields: "id,title,artist_display,date_display,image_id,is_public_domain,thumbnail" });
+      const d = await call(`/artworks/search?${qs}`);
+      return {
+        total: d.pagination?.total || 0,
+        artworks: (d.data || []).map(a => ({
+          id: a.id, title: a.title, artist: a.artist_display, date: a.date_display,
+          is_public_domain: a.is_public_domain,
+          thumbnail: a.thumbnail?.lqip,
+          image: a.image_id ? `https://www.artic.edu/iiif/2/${a.image_id}/full/843,/0/default.jpg` : null,
+        })),
+      };
+    },
+    async "artwork"({ id }) {
+      if (!id) throw new Error("id required");
+      const d = await call(`/artworks/${id}`);
+      return d.data;
+    },
+  },
+};
diff --git a/server/connectors/cleveland.js b/server/connectors/cleveland.js
new file mode 100644
index 0000000..2ab8e5d
--- /dev/null
+++ b/server/connectors/cleveland.js
@@ -0,0 +1,46 @@
+// Cleveland Museum of Art — Open Access. No API key.
+// https://openaccess-api.clevelandart.org/
+const BASE = "https://openaccess-api.clevelandart.org/api";
+async function call(path) {
+  const r = await fetch(`${BASE}${path}`, {
+    headers: { "User-Agent": "VenturaClaw/1.0", Accept: "application/json" },
+    signal: AbortSignal.timeout(15_000),
+  });
+  if (!r.ok) throw new Error(`cleveland ${r.status}`);
+  return r.json();
+}
+module.exports = {
+  meta: { id: "cleveland", name: "Cleveland Museum of Art", category: "data", docsUrl: "https://openaccess-api.clevelandart.org/", auth: "none", realImpl: true },
+  fields: [],
+  configured() { return true; },
+  async health() {
+    try {
+      const d = await call("/artworks?limit=1");
+      return { ok: true, label: `${(d.info?.total ?? 0).toLocaleString()} artworks · CC0 + open access` };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+  actions: {
+    async "search"({ q, hasImage, cc0only, limit }) {
+      const qs = new URLSearchParams();
+      if (q) qs.set("q", q);
+      if (hasImage) qs.set("has_image", "1");
+      if (cc0only) qs.set("cc0", "1");
+      qs.set("limit", String(Math.min(limit || 25, 100)));
+      const d = await call(`/artworks?${qs}`);
+      return {
+        total: d.info?.total || 0,
+        artworks: (d.data || []).map(a => ({
+          id: a.id, accession_number: a.accession_number,
+          title: a.title, creators: (a.creators || []).map(c => c.description).join(" · "),
+          date: a.creation_date, medium: a.technique, image: a.images?.web?.url || a.images?.print?.url,
+          url: a.url, license: a.share_license_status,
+        })),
+      };
+    },
+    async "artwork"({ id }) {
+      if (!id) throw new Error("id required");
+      const d = await call(`/artworks/${id}`);
+      return d.data;
+    },
+  },
+};
diff --git a/server/connectors/colorapi.js b/server/connectors/colorapi.js
new file mode 100644
index 0000000..77473d6
--- /dev/null
+++ b/server/connectors/colorapi.js
@@ -0,0 +1,53 @@
+// The Color API — no key. https://www.thecolorapi.com/docs
+const BASE = "https://www.thecolorapi.com";
+async function call(path) {
+  const r = await fetch(`${BASE}${path}`, {
+    headers: { Accept: "application/json", "User-Agent": "VenturaClaw/1.0" },
+    signal: AbortSignal.timeout(12_000),
+  });
+  if (!r.ok) throw new Error(`colorapi ${r.status}`);
+  return r.json();
+}
+function normHex(h) {
+  if (!h) return null;
+  let s = String(h).trim().replace(/^#/, "");
+  if (s.length === 3) s = s.split("").map(c => c + c).join("");
+  if (!/^[0-9a-fA-F]{6}$/.test(s)) return null;
+  return s.toLowerCase();
+}
+module.exports = {
+  meta: { id: "colorapi", name: "Color API", category: "creative", docsUrl: "https://www.thecolorapi.com/docs", auth: "none", realImpl: true },
+  fields: [],
+  configured() { return true; },
+  async health() {
+    try {
+      const d = await call("/id?hex=d4a04a&format=json");
+      return { ok: true, label: `live · sample: ${d.name?.value || "named"}` };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+  actions: {
+    async "identify"({ hex, rgb }) {
+      const h = normHex(hex);
+      if (!h && !rgb) throw new Error("hex or rgb required");
+      const qs = h ? `hex=${h}` : `rgb=${encodeURIComponent(rgb)}`;
+      const d = await call(`/id?${qs}&format=json`);
+      return {
+        hex: d.hex?.value, name: d.name?.value, rgb: d.rgb?.value,
+        hsl: d.hsl?.value, cmyk: d.cmyk?.value,
+        contrast_text: d.contrast?.value,
+        nearest_named: d.name?.exact_match_name === false ? d.name?.closest_named_hex : null,
+      };
+    },
+    async "scheme"({ hex, mode, count }) {
+      const h = normHex(hex);
+      if (!h) throw new Error("hex required");
+      const qs = new URLSearchParams({ hex: h, mode: mode || "analogic", count: String(Math.min(count || 5, 10)), format: "json" });
+      const d = await call(`/scheme?${qs}`);
+      return {
+        seed: d.seed?.hex?.value, mode: d.mode,
+        colors: (d.colors || []).map(c => ({ hex: c.hex?.value, name: c.name?.value, rgb: c.rgb?.value })),
+        scheme_image: d.image?.bare,
+      };
+    },
+  },
+};
diff --git a/server/connectors/index.js b/server/connectors/index.js
index 2a36560..6c699e8 100644
--- a/server/connectors/index.js
+++ b/server/connectors/index.js
@@ -15,8 +15,14 @@ const purelymail = require("./purelymail");
 const archive    = require("./archive");
 const elevenlabs = require("./elevenlabs");
 const shopify    = require("./shopify");
+// Public no-auth APIs (no key required) — pre-connected for everyone.
+const met        = require("./met");
+const cleveland  = require("./cleveland");
+const aic        = require("./aic");
+const wikipedia  = require("./wikipedia");
+const colorapi   = require("./colorapi");
 
-const REAL = { slack, stripe, cloudflare, mailchimp, hubspot, notion, airtable, twilio, figma, canva, etsy, gmail, purelymail, archive, elevenlabs, shopify };
+const REAL = { slack, stripe, cloudflare, mailchimp, hubspot, notion, airtable, twilio, figma, canva, etsy, gmail, purelymail, archive, elevenlabs, shopify, met, cleveland, aic, wikipedia, colorapi };
 
 // Phase 8 — central sensitivity registry. Source of truth for "this action mutates state."
 // Anything in WRITE_ACTIONS[id] requires the approval gate. Anything NOT listed is treated as read-only.
@@ -38,6 +44,12 @@ const WRITE_ACTIONS = {
   twilio:     new Set(["sms.send"]),
   elevenlabs: new Set(["textToSpeech"]),
   shopify:    new Set(["order.fulfill", "order.refund", "product.update"]),
+  // Public no-auth APIs are read-only — no writes.
+  met:        new Set([]),
+  cleveland:  new Set([]),
+  aic:        new Set([]),
+  wikipedia:  new Set([]),
+  colorapi:   new Set([]),
 };
 
 // Read actions enumerated per connector — explicit allowlist for fail-safe behavior.
@@ -58,6 +70,11 @@ const READ_ACTIONS = {
   twilio:     new Set(["messages.list", "phone_numbers.list"]),
   elevenlabs: new Set(["voicesList", "user", "modelsList"]),
   shopify:    new Set(["shop.info", "products.list", "products.count", "orders.list", "orders.recent", "collections.list"]),
+  met:        new Set(["search", "object", "departments"]),
+  cleveland:  new Set(["search", "artwork"]),
+  aic:        new Set(["search", "artwork"]),
+  wikipedia:  new Set(["summary", "search", "onthisday"]),
+  colorapi:   new Set(["identify", "scheme"]),
 };
 
 function isWrite(id, action) {
diff --git a/server/connectors/met.js b/server/connectors/met.js
new file mode 100644
index 0000000..c15d942
--- /dev/null
+++ b/server/connectors/met.js
@@ -0,0 +1,44 @@
+// Metropolitan Museum of Art — open access collection.
+// No API key. https://metmuseum.github.io/
+const BASE = "https://collectionapi.metmuseum.org/public/collection/v1";
+async function call(path) {
+  const r = await fetch(`${BASE}${path}`, {
+    headers: { "User-Agent": "VenturaClaw/1.0 (steve@venturaclaw.com)", Accept: "application/json" },
+    signal: AbortSignal.timeout(15_000),
+  });
+  if (!r.ok) throw new Error(`met ${r.status}`);
+  return r.json();
+}
+module.exports = {
+  meta: { id: "met", name: "MET Museum", category: "data", docsUrl: "https://metmuseum.github.io/", auth: "none", realImpl: true },
+  fields: [],
+  configured() { return true; },
+  async health() {
+    try {
+      const d = await call("/objects?metadataDate=2026-01-01");
+      return { ok: true, label: `${(d.total ?? 0).toLocaleString()} objects · public domain catalog` };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+  actions: {
+    async "search"({ q, hasImages, isPublicDomain, departmentId }) {
+      if (!q) throw new Error("q required");
+      const qs = new URLSearchParams({ q });
+      if (hasImages) qs.set("hasImages", "true");
+      if (isPublicDomain) qs.set("isPublicDomain", "true");
+      if (departmentId) qs.set("departmentId", String(departmentId));
+      const d = await call(`/search?${qs}`);
+      return { total: d.total, objectIDs: (d.objectIDs || []).slice(0, 100) };
+    },
+    async "object"({ objectID }) {
+      if (!objectID) throw new Error("objectID required");
+      const d = await call(`/objects/${objectID}`);
+      return {
+        objectID: d.objectID, title: d.title, artist: d.artistDisplayName, date: d.objectDate,
+        medium: d.medium, classification: d.classification, department: d.department,
+        isPublicDomain: d.isPublicDomain, primaryImage: d.primaryImage, primaryImageSmall: d.primaryImageSmall,
+        objectURL: d.objectURL, creditLine: d.creditLine, dimensions: d.dimensions,
+      };
+    },
+    async "departments"() { return await call("/departments"); },
+  },
+};
diff --git a/server/connectors/wikipedia.js b/server/connectors/wikipedia.js
new file mode 100644
index 0000000..7bbe050
--- /dev/null
+++ b/server/connectors/wikipedia.js
@@ -0,0 +1,61 @@
+// Wikipedia / Wikimedia REST API. No key.
+// https://en.wikipedia.org/api/rest_v1/
+const BASE = "https://en.wikipedia.org/api/rest_v1";
+async function call(path) {
+  const r = await fetch(`${BASE}${path}`, {
+    headers: { "User-Agent": "VenturaClaw/1.0 (steve@venturaclaw.com)", Accept: "application/json" },
+    signal: AbortSignal.timeout(15_000),
+  });
+  if (!r.ok) throw new Error(`wikipedia ${r.status}`);
+  return r.json();
+}
+module.exports = {
+  meta: { id: "wikipedia", name: "Wikipedia", category: "data", docsUrl: "https://en.wikipedia.org/api/rest_v1/", auth: "none", realImpl: true },
+  fields: [],
+  configured() { return true; },
+  async health() {
+    try {
+      const d = await call("/page/summary/Designer_Wallcoverings");
+      return { ok: true, label: "live · " + (d.title || "english wikipedia") };
+    } catch (e) {
+      // Fallback to a guaranteed page
+      try {
+        await call("/page/summary/Earth");
+        return { ok: true, label: "live · english wikipedia" };
+      } catch (e2) { return { ok: false, reason: e2.message }; }
+    }
+  },
+  actions: {
+    async "summary"({ title }) {
+      if (!title) throw new Error("title required");
+      const d = await call(`/page/summary/${encodeURIComponent(title.replace(/\s+/g, "_"))}`);
+      return {
+        title: d.title, description: d.description, extract: d.extract,
+        thumbnail: d.thumbnail?.source, url: d.content_urls?.desktop?.page,
+        wikibase_item: d.wikibase_item, type: d.type, lang: d.lang,
+      };
+    },
+    async "search"({ q, limit }) {
+      if (!q) throw new Error("q required");
+      const qs = new URLSearchParams({ q, limit: String(Math.min(limit || 10, 50)) });
+      const r = await fetch(`https://en.wikipedia.org/w/rest.php/v1/search/page?${qs}`, {
+        headers: { "User-Agent": "VenturaClaw/1.0", Accept: "application/json" },
+        signal: AbortSignal.timeout(15_000),
+      });
+      if (!r.ok) throw new Error(`wikipedia search ${r.status}`);
+      const d = await r.json();
+      return {
+        pages: (d.pages || []).map(p => ({
+          id: p.id, title: p.title, excerpt: p.excerpt?.replace(/<[^>]+>/g, ""), thumbnail: p.thumbnail?.url,
+          url: `https://en.wikipedia.org/wiki/${encodeURIComponent(p.key)}`,
+        })),
+      };
+    },
+    async "onthisday"({ type, month, day }) {
+      const t = type || "events";
+      const m = String(month || (new Date().getMonth() + 1)).padStart(2, "0");
+      const d = String(day || new Date().getDate()).padStart(2, "0");
+      return await call(`/feed/onthisday/${t}/${m}/${d}`);
+    },
+  },
+};

← a6a5067 ENV_KEY_TO_CONNECTOR: align field names with what the handle  ·  back to Ventura Claw  ·  Add 5 no-auth SMB / civic public-data connectors a5630c3 →