Aller au contenu
CADCAMFUN
Docs

Documentation

Plugins

Manifestes d'extension, API d'hôte de travail, outils d'assistance et contrat d'installation du plug-in de navigateur distinct.

Traduit automatiquement de l'anglais. Voir l'original en anglais

Choisissez la surface d'extension

@cadcamfun/plugin-sdk définit le contrat du plugin de travail avec un plugin.json version du manifeste et de l'API 1. Son hôte expose des services contrôlés par les capacités et exécute des fonctions de plugin dans les travailleurs Bun. Le chargeur de l'assistant de production accepte actuellement les plugins qui demandent uniquement ai:tool.

Le /api/plugins La famille HTTP utilise un contrat de plug-in de navigateur différent : packages ZIP avec manifest.json, a main entrée, autorisations du navigateur, configuration et démarrage JavaScript. L'installation d'un package via cette API n'en fait pas un plugin SDK de travail. Utilisez le manifeste et le cycle de vie correspondant à l'intégration prévue.

Manifeste du SDK de travail

plugin.json pour une extension d'assistant
{
  "id": "com.example.shop-helper",
  "name": "Shop helper",
  "version": "1.0.0",
  "apiVersion": "1",
  "entry": "index.js",
  "capabilities": ["ai:tool"],
  "contributes": {
    "aiTools": [{
      "id": "lookup",
      "description": "Look up the shop's documented inspection checklist.",
      "kind": "read",
      "parameters": { "type": "object", "properties": {} },
      "entry": "lookup"
    }]
  }
}

Les identifiants utilisent une forme reverse-DNS, la version est sémantique major.minor.patch avec une avant-première facultative, et apiVersion doit être "1". Les chemins d'entrée sont relatifs au répertoire du plugin. Les contributions peuvent déclarer des commandes, des panneaux, des post-processeurs, des opérations et des outils d'assistance ; déclarer une contribution ne connecte pas cette fonctionnalité à chaque hôte.

Le schéma permet en option signature et dev champs. L'hôte de travail actuel enregistre ces valeurs mais n'effectue pas de vérification de signature cryptographique lors de l'installation. Un champ de signature à lui seul ne constitue pas une preuve d'un éditeur vérifié.

Entrée du module et fonctions IA

definePlugin est un assistant d'identité typé pour un module avec un manifeste et activate(ctx). L'activation peut renvoyer une fonction de nettoyage et le module peut exposer deactivate. Le SDK décrit ces hooks de cycle de vie ; leur disponibilité dépend du bootstrap concret du travailleur, comme détaillé ci-dessous.

Les fonctions d'assistant sont exportées via un aiTools enregistrer; la contribution entry nomme la fonction dans cet enregistrement. Une contribution à un outil comprend sa description, le schéma des paramètres JSON et read ou write gentil. Un outil d'écriture est acheminé via une demande d'approbation humaine dans l'assistant intégré ; un pont extérieur ne reçoit que les outils de plug-in de lecture autorisés.

Regroupez l’entrée du travailleur en tant que module autonome. L'hôte de travail lit la source d'entrée et l'importe via un module blob. Traitez les importations relatives de packages ou de fichiers comme un problème d'intégration distinct plutôt que de supposer un chargeur de système de fichiers de style Node.

API contrôlées par les capacités

Le SDK reconnaît drawing:read, drawing:write, toolpath:read, storage, ui:panel, et ai:tool. Dessiner écrire implique dessiner lire. L'hôte général dérive les capacités efficaces du manifeste installé et les vérifie sur RPC ; l'application d'intégration décide quels manifestes et surfaces elle souhaite charger.

Le contexte du SDK peut exposer les lectures de dessins et les correctifs Add/Update/RemoveEntity, les lectures de parcours d'outils/programmes, le stockage de valeurs-clés de chaînes privées et l'enregistrement déclaratif de commandes/panneaux. Les services manquants sont absents du contexte. Utilisation des clés de stockage plugin:<id>: espaces de noms et rejeter les clés absolues ou de parcours parent.

Le chargeur de l'assistant de production prend uniquement en charge ai:tool. Un manifeste demandant des fonctionnalités de dessin, de parcours d'outil, de stockage ou d'interface utilisateur est ignoré avec un avertissement. Il n’accorde pas silencieusement ces surfaces ni ne charge leurs capacités plus larges dans l’hôte assistant.

Chargement et cycle de vie des travailleurs

Pour activer une extension d'assistant, placez son plugin.json et entrée groupée sous <dataDir>/plugins/<id>/ et indiquez l'identifiant dans <dataDir>/plugins/enabled.json, un tableau JSON. L'identifiant du dossier doit correspondre au manifeste. Le chargement a lieu pendant la création du service de plug-in d'assistant ; un plugin défaillant enregistre un avertissement sans arrêter l'application.

L'hôte général prend en charge les états installé, activé, désactivé et défectueux, avec les opérations d'installation, d'activation, de désactivation, de désinstallation, d'appel de commande, d'outil AI et de configuration. Enable crée un travailleur et termine sa poignée de main d'amorçage ; désactiver le démolit. La désinstallation supprime l'état du registre et l'espace de noms de stockage via les dépendances injectées de l'hôte.

Les Workers démarrent avec un environnement vide et le bootstrap supprime les API globales sélectionnées. Il s'agit d'une restriction au niveau de l'application, et non d'un bac à sable du système d'exploitation : les globaux Bun restent accessibles. Activez uniquement le code d’extension approprié pour s’exécuter à l’intérieur des limites du processus d’application.

Limites d'exécution

L'hôte définit un paramètre de mémoire de travail de 256 Mo, une limite de débit de 2 000 appels par minute et au maximum 64 appels en attente par plug-in. Le comportement du délai d'expiration de la commande est fourni par l'hôte d'intégration ; le chargeur assistant utilise 10 secondes. Les appels aux outils d’IA ont un délai distinct de 30 secondes. Les délais d'attente, les outils absents, les pannes de travail, les échecs de fonctionnalités et l'épuisement de la file d'attente renvoient des échecs d'hôte typés ou des résultats d'outils lisibles.

Le suivi des pannes fait échouer un plugin après trois plantages en dix minutes. Ces contrôles hôtes liaient le RPC coopératif et le comportement du cycle de vie ; le paramètre de mémoire transmis à Bun ne remplace pas un exécutable isolé séparément.

Les limites sont définies dans apps/sidecar/src/plugins/host.ts: CALL_TIMEOUT_MS (5,000 ms par appel ordinaire), AI_TOOL_TIMEOUT_MS (30,000 ms par appel à l'outil Assistant), MEMORY_LIMIT_MB (256), CALL_RATE_LIMIT (2,000 appels par plugin et par minute), MAX_PENDING_CALLS (64), et CRASH_LIMIT (3 plante en moins de 10 minute CRASH_WINDOW_MS, après quoi le plugin est automatiquement désactivé).

Installation ZIP et API du plugin de navigateur

POST /api/plugins/install accepte une archive en plusieurs parties. Le programme d'installation lit manifest.json à partir de la racine ZIP ou d'un seul répertoire de niveau supérieur, valide le manifeste du plug-in de navigateur et stocke ses fichiers sous <dataDir>/storage/plugins/<id>/. Le navigateur manifeste son utilisation main, engines.cadcam, et une liste d'autorisations de navigateur, plutôt qu'un SDK entry, apiVersion, et les capacités.

GET /api/plugins répertorie ces entrées installées ; Les points de terminaison de détail, d'activation/désactivation, de désinstallation, de mise à jour et de configuration fonctionnent sur leur registre. /api/plugins/bootstrap.js?id=… sert JavaScript ; /api/plugins/serve?id=…&file=… sert les fichiers de plugin. Ce sont des opérations texte/binaires avec leurs types de contenu déclarés.

Le navigateur de registre local lit les métadonnées du package sous <dataDir>/plugins-registry. Il s'agit d'un catalogue manifeste local. L'installation des archives, la liste du registre et le chargement de l'extension Worker Assistant sont des opérations distinctes.

Assistants SDK

effectiveCapabilities, hasCapability, et missingCapabilities inspection des capacités de soutien. requireCapability génère une erreur de capacité lorsqu’une surface de contexte demandée est absente. storageNamespace et storageKey créer des clés avec espace de noms ; parseManifest valide JSON ; manifestKey formate une paire ID/version.

Points d’entrée de mise en œuvre

packages/plugin-sdk/src possède les API de manifeste public, de capacité, de contexte, de plug-in et d'hôte de test. apps/sidecar/src/plugins/host.ts possède le cycle de vie des travailleurs et le RPC ; services/ai-plugins.ts possède le chargement du fichier de consentement et l'enregistrement de l'assistant. L'installation du navigateur est implémentée dans http/plugins.ts et repos/plugin-registry-repo.ts.

Examen des extensions

Gardez les schémas de contribution précis, renvoyez de petits résultats structurés et déclarez honnêtement le comportement de lecture/écriture. Test sur l'hôte d'intégration réel : la disponibilité du SDK n'établit pas l'accès en production aux dessins, au stockage, à l'interface utilisateur ou aux registres de publication. Le plugin nikcli autonome est un client d'action HTTP et a un objectif différent du code chargé dans un travailleur.

Complétez l’exemple de module Assistant

Le plugin.json exemple au-dessus des noms aiTools.lookup. C'est le correspondant autonome index.js. Le code du module de niveau supérieur s'exécute lors de l'importation ; la fonction de recherche déclarée s'exécute lorsqu'elle est appelée.

index.js
export const aiTools = {
  lookup: async () => ({
    ok: true,
    checklist: [
      "Compare the drawing revision with the job traveller.",
      "Record the measuring instrument and its calibration status.",
      "Record each inspected dimension and the measured value."
    ]
  })
}

Il s’agit d’exemples d’instructions d’atelier et non d’une norme d’inspection intégrée. Remplacez-les par le contenu que possède réellement votre extension. Pour admettre ce module à la prochaine initialisation du service, le fichier activé est :

<dataDir>/plugins/enabled.json
["com.example.shop-helper"]

Le catalogue en cours d'exécution expose un identifiant dérivé des ID de plug-in et d'outil. Découvrez-le à travers GET /api/ai/actions; ne présumez pas que le registre ZIP du navigateur est la liste activée de l'assistant. Le rechargement de l'application/du service est le moyen fiable de relire le fichier de consentement de production ; aucun observateur de système de fichiers n'est configuré par ce chargeur.

Source: apps/sidecar/src/services/ai-plugins.ts, packages/ai/src/tools/plugin-tool.ts, et apps/sidecar/test/ai-plugin-tools.test.ts.

Schémas d'arguments et limites du cycle de vie

L'ID local d'un outil doit commencer par une lettre minuscule et contenir des lettres minuscules, des chiffres ou des traits de soulignement, avec un maximum de 40 caractères. Les descriptions contiennent 10–2,000 caractères. La contribution doit nommer une fonction d'entrée non vide et un schéma de paramètres JSON. L'adaptateur d'application valide les arguments avant d'appeler le travailleur ; un refus peut renvoyer un structuré ok: false résultat sans terminer le plugin.

Le SDK public décrit activate, le nettoyage et deactivate points d'entrée. Les gestionnaires RPC d'activation/désactivation du bootstrap du travailleur Bun actuel accusent réception des messages de cycle de vie mais n'invoquent pas ces fonctions de module exportées. Les extensions Assistant doivent donc mettre leur comportement appelable dans aiTools et évitez de dépendre des rappels d’activation du SDK pour la configuration ou le nettoyage.

Le bootstrap général du travailleur contient également des gestionnaires de dessin/chemin d'outil/interface utilisateur d'espace réservé. Ils ne constituent pas un dessin de production ou une intégration d’interface utilisateur. La restriction de l'assistant chargeur à ai:tool empêche ces surfaces non prises en charge d'être annoncées comme accès aux applications fonctionnelles.

Source: packages/plugin-sdk/src/manifest.ts, plugin.ts, apps/sidecar/src/plugins/host.ts (PLUGIN_BOOTSTRAP), et services/ai-plugins.ts.

Diagnostiquer une extension manquante ou défaillante

  • Aucun outil au catalogue : vérifiez le JSON activé valide, la correspondance exacte de l'ID dossier/manifeste, le décodage du manifeste et le ai:tool capacité. Lisez l'avertissement de démarrage pour un plugin ignoré.
  • Le manifeste demande des surfaces non prises en charge : supprimez les fonctionnalités non prises en charge uniquement si le code n’en a vraiment pas besoin. Un plugin assistant ne peut pas acquérir un accès en écriture de dessin simplement en le déclarant.
  • Aucun outil similaire : faire contributes.aiTools[].entry correspondre à une fonction exportée sous aiTools. L'ID de contribution et le nom de l'entrée servent à des fins différentes.
  • Échec de la validation des arguments : inspectez le schéma JSON et les champs obligatoires. Les valeurs erronées sont refusées avant l’exécution, comme le démontrent les tests d’intégration des applications et des travailleurs réels.
  • Délai d'attente ou état défectueux : enquêter sur les blocages de travail, les promesses non résolues ou les plantages ; des crashs répétés endommagent le plugin. Réactivez-le uniquement après avoir corrigé la cause.
  • Le programme d'installation ZIP rejette le manifeste : utilisez le plugin du navigateur manifest.json contrat pour ce point final. Renommer un travailleur plugin.json ne traduit pas ses champs.

Source: apps/sidecar/test/plugins-ai-tools.test.ts, ai-plugin-tools.test.ts, plugins-http.test.ts, et src/plugins/host.ts.

Référence du manifeste

Chaque plugin est livré avec un plugin.json. parseManifest(json) l'analyse et le valide en une seule étape, transformant une erreur de syntaxe JSON en le même canal d'erreur qu'une erreur de schéma, de sorte qu'un écran d'installation n'a besoin de gérer qu'un seul type d'échec.

ChampRègle
idIdentifiant DNS inversé : 3–128 caractères, lettres minuscules, chiffres et traits d'union dans au moins deux étiquettes séparées par des points, par exemple com.example.gear-generator.
nameNom d’affichage non vide.
versionmajor.minor.patch, éventuellement suivi d'un suffixe de pré-version tel que -beta.1.
apiVersionÇa doit être la chaîne "1"; c'est la seule version API du plugin.
entryChemin du module ES par rapport au dossier du plugin.
capabilitiesTableau de chaînes de capacités (voir ci-dessous). Tout ce qui se trouve en dehors de la liste fermée est une erreur manifeste.
contributesFacultatif commands, panels, postProcessors, operations, et aiTools.
signatureSignature Ed25519 sur le hachage de l'archive ; requis pour les installations packagées.
devtrue marque un plugin de développement local : les vérifications de signature sont ignorées et l'interface utilisateur affiche un badge.
ContributionChamps
commands[]id, title, facultatif keybinding tel que Mod+Shift+G.
panels[]id, title, entry: une fonction exportée qui renvoie l'arbre déclaratif du panneau. Il n'y a jamais d'accès au DOM.
postProcessors[]id, title, entry: un exporté (program, options) => string fonction.
operations[]id, title, entry: un enregistrement exporté avec un paramètre schema et un pur generate(params, geometry, tool, stock) qui renvoie un parcours d'outil.
aiTools[]id correspondance ^[a-z][a-z0-9_]{0,39}$, description (10–2,000 caractères ; le modèle le lit), kind read ou write, parameters (Schéma JSON des arguments), et entry.

Aides : manifestKey(m) retours id@version; requestedCapabilities(m) renvoie la liste des capacités triées pour l'affichage ; isDevPlugin(m) et isSigned(m) répondez aux questions sur la politique d'installation.

Capacités et contexte du plugin

Les capacités sont la seule chose par laquelle un plugin peut agir. Le manifeste déclare ce dont le plugin a besoin, l'utilisateur accorde un sous-ensemble et l'hôte crée un contexte qui contient exactement les surfaces accordées. Chaque surface est facultative dans le type, donc une fonctionnalité manquante est visible à la fois au moment de la compilation et au moment de l'exécution au lieu de devenir une non-opération silencieuse.

CapacitéSurface contextuellePermetImplique
drawing:readctx.drawing.read()Lisez le dessin ouvert (ou undefined quand aucun n'est ouvert).—
drawing:writectx.drawing.patch(patch)Appliquer AddEntity, UpdateEntity, ou RemoveEntity des correctifs ; renvoie la nouvelle révision.drawing:read
toolpath:readctx.toolpath.list(), .program(id)Lire les parcours d'outils et les programmes G-code.—
storagectx.storageChaînes clé-valeur avec espace de noms : get, set, delete, keys.—
ui:panelctx.uiregisterCommand et registerPanel (déclaratif uniquement).—
ai:toolaucun (contributions manifestes)Offrez des outils à l'assistant et, en lecture seule, à un nikcli autonome.—
effectiveCapabilities(granted) résout les implications, donc un plugin qui déclare drawing:write n'a pas besoin de déclarer drawing:read. Chaque contexte a également pluginId, le résolu capabilities, et un log avec debug, info, warn, et error; l'hôte préfixe les lignes de journal avec l'ID du plugin. Utiliser requireCapability(ctx, capability, surface) pour obtenir une surface ou échouer avec un CapabilityError qui nomme la subvention manquante.

Tester un plugin avec l'hôte de test du SDK

createTestHost construit un hôte en mémoire avec les mêmes règles de capacité que le sidecar. Seules les capacités accordées apparaissent dans le contexte et vous pouvez fournir un dessin, des parcours d'outils et des programmes. L'exemple ci-dessous est complet et fonctionne avec bun dans l'espace de travail.
frame-plugin.test.ts
import { cad } from "@cadcamfun/core"
import { createTestHost, definePlugin, requireCapability, testManifest } from "@cadcamfun/plugin-sdk"

const manifest = testManifest({
  id: "com.example.frame",
  capabilities: ["drawing:write", "storage", "ui:panel"],
})

const plugin = definePlugin({
  manifest,
  activate(ctx) {
    const drawing = requireCapability(ctx, "drawing:write", "drawing")
    ctx.ui?.registerCommand({ id: "frame.add", title: "Add frame line" })
    ctx.storage?.set("runs", "1")
    drawing.patch({
      _tag: "AddEntity",
      entity: { ...cad.baseEntity("frame-1"), _tag: "Line",
                start: { x: 0, y: 0 }, end: { x: 100, y: 0 } },
    })                                   // returns revision 1
    return () => ctx.log.info("teardown") // runs on deactivation
  },
})

const host = createTestHost({ manifest, drawing: cad.emptyDrawing("d1", "Demo") })
const stop = await host.activate(plugin)
host.drawing()?.entities.length     // 1
host.context.storage?.get("runs")   // "1"
host.ui.commands                    // [{ id: "frame.add", title: "Add frame line" }]
await stop()

// Without the grant, requireCapability throws:
const denied = createTestHost({ manifest, granted: ["storage"] })
await denied.activate(plugin)
// CapabilityError: Plugin com.example.frame may not use "drawing:write":
// it was not granted at install time
  • testManifest(overrides) commence à partir d'un manifeste minimal valide (com.example.test-plugin, version 1.0.0, API 1, entrée index.js, aucune capacité).
  • granted par défaut, tout ce que le manifeste demande. Passez une liste plus petite pour tester les chemins refusés.
  • host.storage est un instantané pris lors de la création de l'hôte. Lire les valeurs en direct à travers host.context.storage.
  • host.on(commandId, handler) et host.invoke(commandId, args) exercer des commandes enregistrées.
  • runPostProcessor exécute une publication contribuée sur un programme, et keyOf(pluginId, key) affiche la clé de stockage avec espace de noms afin que les tests puissent affirmer l'isolement.

Comment l'application charge les plugins d'assistant

  1. Au démarrage, le sidecar lit <data dir>/plugins/enabled.json, un tableau JSON d'ID de plugin. Rien n'est chargé à moins qu'il n'y soit répertorié.
  2. Pour chaque identifiant, il lit <data dir>/plugins/<id>/plugin.json et l'analyse. Le nom du dossier doit être égal au manifeste id.
  3. Seulement ai:tool est câblé dans cet hôte. Un manifeste qui demande un accès au dessin, au parcours d'outil, au stockage ou à l'interface utilisateur est ignoré avec un avertissement plutôt que chargé avec des surfaces qui ne feraient rien en silence.
  4. Le plugin est installé sur un hôte de travail et activé. Un plugin qui ne parvient pas à charger les journaux Plugin <id> was not loaded: ... et n'empêche jamais le démarrage de l'application.
  5. Chaque outil contribué est proposé à l'assistant sous forme plugin_<slug of plugin id>_<tool id>, au maximum 80 caractères, où le slug met l'ID en minuscule et remplace les autres caractères par des traits de soulignement. com.example.bolt-circle avec outil bolt_circle devient plugin_com_example_bolt_circle_bolt_circle.
  6. Les arguments sont validés par rapport au schéma JSON de l'outil avant que le travailleur ne les voie. Si le schéma dépasse 20,000 caractères, n'est pas un schéma d'objet ou ne peut pas être converti, l'hôte accepte n'importe quel objet et le plugin doit valider.
  7. Un appel qui lance, expire ou trouve que le plugin a disparu revient { ok: false, error } (texte d'erreur jusqu'à 500 caractères) à l'assistant au lieu de planter le tour. UN write l'outil est confirmé auprès de la personne avant chaque appel, quoi qu'en disent ses arguments.

Pas un bac à sable de système d'exploitation

Le travailleur n'a pas de processus, de récupération ou de gestionnaire de fichier propre, mais les globaux Bun restent accessibles à l'intérieur. Activez uniquement les plugins dont vous faites confiance au code et réduisez les outils d’écriture au minimum.

Règles d'identifiant et de version, testées

ValeurValide?Règle
com.example.gear-generatorouiDNS inversé : segments minuscules de lettres, chiffres et -, au moins deux, 3–128 caractères
a.bouiDeux segments d'une lettre suffisent
gearnonNécessite au moins un point
Com.Example.xnonLes majuscules ne sont pas autorisées
com.example_x.ynonLe trait de soulignement n'est pas autorisé
VersionValide?
1.0.0oui
1.2.3-beta.1oui
1.0non : trois chiffres requis
v1.0.0non : pas de préfixe
apiVersion doit être la chaîne "1", le seul plugin API majeur qui existe. Les identifiants des outils d'IA doivent correspondre ^[a-z][a-z0-9_]{0,39}$ et sont présentés à l'assistant comme plugin_<plugin>_<id>; les descriptions doivent comporter 10–2,000 caractères car le modèle les lit.

Types de cotisations

CléChampsBut
commandsid, title, facultatif keybinding (e.g. Mod+Shift+G)Commandes dans la palette
panelsid, title, entryUn panneau rendu à partir d'un arbre déclaratif renvoyé par la fonction nommée ; pas d'accès au DOM
postProcessorsid, title, entryA (program, options) => string message enregistré au moment de l'exécution
operationsid, title, entryUn exporté { schema, generate } enregistrer
aiToolsid, description, kind lire|écrire, parameters (schéma JSON), entryOutils offerts à l'assistant; un outil d'écriture est confirmé avant chaque appel

Autres champs du manifeste : signature (Ed25519 sur le hachage d'archive, requis pour les installations packagées) et dev (plugin de développement local : ignore les vérifications de signature et affiche un badge).

Résolution de capacité, testée

Implication, subventions manquantes, clés de stockage
import { effectiveCapabilities, missingCapabilities, storageKey, requireCapability } from "@cadcamfun/plugin-sdk"

effectiveCapabilities(["drawing:write", "storage"])
// ["drawing:read", "drawing:write", "storage"]   (write implies read; sorted)

missingCapabilities(["drawing:read", "toolpath:read", "ai:tool"], ["drawing:write"])
// ["toolpath:read", "ai:tool"]

storageKey("com.example.gear", "presets/m2")   // "plugin:com.example.gear:presets/m2"
storageKey("com.example.gear", "../x")
// RangeError: storage key "../x" may not escape the plugin namespace   (same for "/abs")

requireCapability(ctxWithoutDrawing, "drawing:read", "drawing")
// CapabilityError (_tag "CapabilityDenied"):
// Plugin com.example.gear may not use "drawing:read": it was not granted at install time
Surface sur le contextePrésent lorsqu'il est accordéMéthodes
drawingdrawing:read ou drawing:writeread() → dessin ou undefined; patch(p) → nouvelle révision, avec AddEntity, UpdateEntity, RemoveEntity correctifs
toolpathtoolpath:readlist(), program(toolpathId)
storagestorageget, set, delete, keys; valeurs de chaîne
uiui:panelregisterCommand, registerPanel
logtoujoursdebug, info, warn, error

En 3D

Packages de plugins dans l'espace de travail Faites glisser vers l'orbite · faites défiler pour zoomer · faites glisser vers la droite pour effectuer un panoramique
All 13 workspace packages from their package.json files; each line is a workspace:* dependency (17 in total). Rows are dependency depth: packages with no workspace dependencies at the front, the sidecar, which depends on 8 of them, at the back. Orange boxes are apps. Highlighted: plugin-sdk (depends on core) and the nikcli plugin, both loaded by the sidecar.