@agent-native/core 0.141.6 → 0.144.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/agent-native.eject.json +9 -0
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +3 -14
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +11 -6
- package/corpus/templates/analytics/.agents/skills/prometheus/SKILL.md +5 -5
- package/corpus/templates/analytics/.agents/skills/provider-api/SKILL.md +19 -0
- package/corpus/templates/analytics/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/analytics/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/analytics/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/analytics/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/analytics/AGENTS.md +37 -40
- package/corpus/templates/analytics/actions/compose-dashboard.ts +1 -1
- package/corpus/templates/analytics/actions/ensure-native-v2-dashboards.ts +1 -1
- package/corpus/templates/analytics/actions/list-dashboard-usage-stats.ts +17 -22
- package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +135 -0
- package/corpus/templates/analytics/actions/navigate.ts +5 -6
- package/corpus/templates/analytics/actions/provider-api-register.ts +50 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +46 -13
- package/corpus/templates/analytics/actions/test-custom-api-connection.ts +106 -0
- package/corpus/templates/analytics/actions/update-dashboard.ts +0 -1
- package/corpus/templates/analytics/actions/view-screen.ts +0 -20
- package/corpus/templates/analytics/app/components/CustomApiCard.tsx +430 -0
- package/corpus/templates/analytics/app/components/layout/Header.tsx +0 -1
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +5 -5
- package/corpus/templates/analytics/app/i18n-data.ts +682 -90
- package/corpus/templates/analytics/app/lib/native-mcp-exclusions.ts +6 -0
- package/corpus/templates/analytics/app/lib/sql-query.ts +33 -6
- package/corpus/templates/analytics/app/pages/Agents.tsx +0 -3
- package/corpus/templates/analytics/app/pages/DataSources.tsx +4 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +22 -43
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +23 -20
- package/corpus/templates/analytics/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/analytics/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/analytics/app/routes/team.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-settings-no-longer-includes-an-unnecessary-about-section.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-load-reliably-during-background-data-sw.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-analytics-dashboards-remain-responsive-while-historical-data.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-analytics-query-results-now-render-as-a-table-with-csv-downl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-connect-public-https-custom-apis-and-hand-tested-endpoints-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-durable-agent-worker-memory.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-live-dashboard-panels-use-the-shared-query-timeout.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-on-demand-billing-is-available-as-a-native-dashboard-with-ta.md +6 -0
- package/corpus/templates/analytics/changelog/2026-08-06-visible-dashboard-names-are-unique.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +1 -1
- package/corpus/templates/analytics/netlify.toml +7 -0
- package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +4 -0
- package/corpus/templates/analytics/scripts/first-party-analytics-bigquery.sql +104 -0
- package/corpus/templates/analytics/server/db/index.ts +27 -0
- package/corpus/templates/analytics/server/db/schema.ts +10 -0
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -2
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +227 -60
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +1147 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-health.ts +34 -15
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +57 -4
- package/corpus/templates/analytics/server/lib/native-v2-dashboards.ts +227 -1
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +1 -3
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +7 -4
- package/corpus/templates/analytics/server/plugins/analytics-rollup-backfill-jobs.ts +1 -0
- package/corpus/templates/analytics/server/plugins/db.ts +52 -18
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +36 -8
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +16 -1
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +16 -1
- package/corpus/templates/assets/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/assets/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/assets/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/assets/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/assets/AGENTS.md +1 -0
- package/corpus/templates/assets/actions/_helpers.ts +12 -0
- package/corpus/templates/assets/actions/generate-image.ts +9 -3
- package/corpus/templates/assets/actions/import-style-from-url.ts +115 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +3 -1
- package/corpus/templates/assets/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/assets/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/assets/app/routes/settings.tsx +5 -0
- package/corpus/templates/assets/app/routes/team.tsx +1 -1
- package/corpus/templates/assets/changelog/2026-08-05-brand-kits-can-import-a-website-s-hydrated-visual-language-a.md +6 -0
- package/corpus/templates/assets/changelog/2026-08-05-generated-images-now-hand-off-embeddable-previews-to-other-w.md +6 -0
- package/corpus/templates/assets/server/lib/generation.ts +58 -0
- package/corpus/templates/assets/shared/api.ts +12 -0
- package/corpus/templates/brain/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/brain/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/brain/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/brain/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/brain/actions/list-connection-providers.ts +24 -3
- package/corpus/templates/brain/app/lib/dispatch-links.ts +44 -0
- package/corpus/templates/brain/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/brain/app/routes/sources.tsx +15 -7
- package/corpus/templates/brain/app/routes/team.tsx +1 -1
- package/corpus/templates/brain/app/routes.ts +1 -1
- package/corpus/templates/brain/changelog/2026-08-06-brain-provider-setup-links-open-dispatch-correctly-in-standa.md +6 -0
- package/corpus/templates/brain/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +26 -0
- package/corpus/templates/calendar/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/calendar/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/calendar/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/calendar/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/calendar/AGENTS.md +11 -32
- package/corpus/templates/calendar/actions/update-event.ts +107 -1
- package/corpus/templates/calendar/app/components/calendar/EventCalendarSelect.tsx +83 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +57 -0
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +59 -73
- package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +3 -1
- package/corpus/templates/calendar/app/hooks/use-events.ts +4 -1
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/calendar/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/calendar/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
- package/corpus/templates/calendar/changelog/2026-08-06-existing-events-can-move-between-connected-google-account-ca.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +101 -0
- package/corpus/templates/calendar/server/plugins/agent-chat.ts +7 -1
- package/corpus/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/chat/app/root.tsx +1 -1
- package/corpus/templates/chat/app/routes/agent.tsx +7 -7
- package/corpus/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/chat/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/chat/app/routes/team.tsx +1 -1
- package/corpus/templates/clips/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/clips/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/clips/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/clips/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +9 -24
- package/corpus/templates/clips/app/components/editor/timeline-geometry.ts +15 -0
- package/corpus/templates/clips/app/components/editor/waveform.tsx +4 -17
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +14 -3
- package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
- package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/clips/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +28 -38
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +21 -27
- package/corpus/templates/clips/changelog/2026-08-06-clips-recovers-from-stalled-recording-setup-without-requirin.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-06-clips-starts-faster-after-a-deploy-or-idle-period-startup-no.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-06-deleting-a-recording-returns-cleanly-to-the-library-and-fit-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-06-hide-clip-insights-from-viewers-without-analytics-access.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +48 -20
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +275 -74
- package/corpus/templates/clips/desktop/src/lib/recording-start-guard.ts +84 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +14 -4
- package/corpus/templates/clips/server/plugins/db.ts +214 -26
- package/corpus/templates/content/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/content/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/content/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/content/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/content/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/content/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/content/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/content/e2e/global-setup.ts +1 -1
- package/corpus/templates/crm/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/crm/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/crm/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/crm/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/crm/changelog/2026-08-06-crm-starts-faster-after-a-deploy-or-idle-period-one-time-dat.md +6 -0
- package/corpus/templates/crm/server/plugins/db.ts +11 -8
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +36 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +6 -1
- package/corpus/templates/design/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/design/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/design/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/design/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/design/actions/analyze-brand-assets.ts +7 -7
- package/corpus/templates/design/actions/import-from-url.ts +6 -5
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +27 -18
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +2 -2
- package/corpus/templates/design/app/pages/Index.tsx +6 -2
- package/corpus/templates/design/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/design/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/design/app/routes/team.tsx +1 -1
- package/corpus/templates/design/changelog/2026-08-05-deleting-a-design-from-the-home-page-no-longer-leaves-the-ap.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-renaming-a-layer-now-focuses-its-name-field-automatically.md +6 -0
- package/corpus/templates/design/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-de.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +1 -1
- package/corpus/templates/design/server/plugins/agent-chat.ts +2 -0
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/dispatch/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/dispatch/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/dispatch/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/dispatch/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/dispatch/app/routes/team.tsx +1 -1
- package/corpus/templates/dispatch/changelog/2026-08-06-ready-workspace-apps-stay-one-click-away-in-a-compact-dispat.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-06-shared-vault-values-and-management-are-now-restricted-to-wor.md +6 -0
- package/corpus/templates/factory/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/factory/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/factory/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/factory/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/factory/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/factory/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/factory/app/routes/team.tsx +1 -1
- package/corpus/templates/forms/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/forms/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/forms/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/forms/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/forms/app/routes/_app.extensions._index.tsx +1 -1
- package/corpus/templates/forms/app/routes/_app.settings.$.tsx +1 -0
- package/corpus/templates/forms/app/routes/_app.team.tsx +1 -1
- package/corpus/templates/forms/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +4 -0
- package/corpus/templates/forms/shared/navigation.ts +1 -1
- package/corpus/templates/macros/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/macros/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/macros/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/macros/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/macros/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/macros/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/macros/app/routes/team.tsx +1 -1
- package/corpus/templates/mail/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/mail/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/mail/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/mail/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/mail/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/mail/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/mail/changelog/2026-08-06-delegated-requests-continue-in-the-background.md +6 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
- package/corpus/templates/plan/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/plan/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/plan/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/plan/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +1 -1
- package/corpus/templates/plan/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/plan/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/plan/app/routes/team.tsx +1 -1
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +48 -5
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +40 -2
- package/corpus/templates/slides/.agents/skills/image-generation-via-a2a/SKILL.md +6 -0
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +15 -2
- package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +29 -24
- package/corpus/templates/slides/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/slides/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/slides/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/slides/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/slides/AGENTS.md +7 -9
- package/corpus/templates/slides/_gitignore +1 -0
- package/corpus/templates/slides/actions/analyze-brand-assets.ts +7 -7
- package/corpus/templates/slides/actions/export-pptx.ts +402 -36
- package/corpus/templates/slides/actions/import-file.ts +46 -85
- package/corpus/templates/slides/actions/import-from-url.ts +6 -6
- package/corpus/templates/slides/actions/import-google-slides-reference.ts +271 -12
- package/corpus/templates/slides/actions/import-pptx.ts +85 -60
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +10 -13
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +5 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +25 -12
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +112 -57
- package/corpus/templates/slides/app/components/editor/GoogleDriveConnectionCta.tsx +202 -0
- package/corpus/templates/slides/app/components/editor/GoogleSlidesReferenceImport.tsx +26 -14
- package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +176 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +182 -141
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +18 -0
- package/corpus/templates/slides/app/components/editor/slide-text-targets.ts +85 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -0
- package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +13 -3
- package/corpus/templates/slides/app/components/presentation/PresentationView.tsx +27 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +71 -20
- package/corpus/templates/slides/app/global.css +6 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +1 -0
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +10 -6
- package/corpus/templates/slides/app/lib/deck-preview-frame.ts +15 -0
- package/corpus/templates/slides/app/lib/deck-share-links.ts +9 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +89 -0
- package/corpus/templates/slides/app/lib/slide-animation-elements.ts +3 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +17 -0
- package/corpus/templates/slides/app/pages/Index.tsx +107 -6
- package/corpus/templates/slides/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/slides/app/routes/settings.$.tsx +1 -0
- package/corpus/templates/slides/app/routes/team.tsx +1 -1
- package/corpus/templates/slides/changelog/2026-08-05-failed-deck-creation-and-reference-uploads-now-recover-clean.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-slides-recover-completed-generation-results-after-a-sync-hic.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-05-website-url-imports-now-capture-hydrated-browser-computed-st.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-clicking-the-gray-canvas-around-a-slide-clears-its-selection.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-deck-thumbnails-keep-a-consistent-listing-frame-across-aspec.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-deep-linked-decks-open-directly-instead-of-being-interrupted.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-google-slides-exports-now-show-clear-import-steps-when-direc.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-import-pdf-google-slides-and-powerpoint-decks-directly-from-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-imported-powerpoint-exports-keep-text-and-images-editable-an.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-presentation-prompts-continue-adding-rendered-slides-until-t.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-public-decks-now-share-read-only-presentation-links-by-defau.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-slides-offers-a-direct-connect-google-button-when-a-drive-co.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-slides-preserve-uploaded-pdf-layouts-accept-larger-reference.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-06-styled-words-stay-part-of-their-text-box-instead-of-becoming.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +7 -20
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +236 -13
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +67 -0
- package/corpus/templates/slides/server/handlers/import/pptx-parser.ts +2 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +7 -3
- package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -3
- package/corpus/templates/slides/server/lib/emails.ts +7 -2
- package/corpus/templates/slides/server/lib/google-docs-error.ts +50 -0
- package/corpus/templates/slides/server/lib/google-docs-oauth.ts +1 -0
- package/corpus/templates/slides/server/lib/import-asset-storage.ts +127 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +4 -3
- package/corpus/templates/slides/server/routes/api/import-assets/[token].get.ts +70 -0
- package/corpus/templates/slides/shared/upload-types.ts +3 -0
- package/corpus/templates/tasks/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/corpus/templates/tasks/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/corpus/templates/tasks/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/app/components/layout/Header.tsx +3 -1
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +1 -1
- package/corpus/templates/tasks/app/routes/settings.$.tsx +1 -0
- package/dist/agent/engine/ai-sdk-engine.js +49 -3
- package/dist/agent/production-agent.d.ts +18 -0
- package/dist/agent/production-agent.js +80 -1
- package/dist/agent/types.d.ts +7 -0
- package/dist/cli/agent-plugin.d.ts +112 -0
- package/dist/cli/agent-plugin.js +1081 -0
- package/dist/cli/app-skill.js +39 -1
- package/dist/cli/code-agent-executor.d.ts +3 -0
- package/dist/cli/code-agent-executor.js +51 -13
- package/dist/cli/index.js +13 -0
- package/dist/cli/templates-meta.js +1 -1
- package/dist/client/AgentPanel.d.ts +3 -1
- package/dist/client/AgentPanel.js +9 -2
- package/dist/client/AssistantChat.d.ts +8 -0
- package/dist/client/AssistantChat.js +23 -8
- package/dist/client/NewWorkspaceAppFlow.js +1 -0
- package/dist/client/agent-chat-adapter.d.ts +1 -1
- package/dist/client/agent-chat-adapter.js +153 -7
- package/dist/client/agent-page/AgentWorkspaceContent.d.ts +1 -1
- package/dist/client/agent-page/AgentWorkspaceContent.js +13 -13
- package/dist/client/assistant-ui-recovery.d.ts +2 -1
- package/dist/client/assistant-ui-recovery.js +7 -3
- package/dist/client/chat/run-recovery.js +5 -1
- package/dist/client/embed-auth.js +6 -2
- package/dist/client/error-format.js +12 -0
- package/dist/client/integrations/IntegrationsPanel.js +40 -6
- package/dist/client/onboarding/FirstRunOnboarding.js +11 -4
- package/dist/client/org/OrgSwitcher.js +2 -2
- package/dist/client/org/TeamPage.js +92 -72
- package/dist/client/require-session.d.ts +1 -1
- package/dist/client/require-session.js +4 -4
- package/dist/client/resources/McpConnectionSuggestion.d.ts +1 -0
- package/dist/client/resources/McpConnectionSuggestion.js +27 -10
- package/dist/client/resources/McpIntegrationDialog.js +77 -45
- package/dist/client/resources/mcp-integration-catalog.d.ts +13 -1
- package/dist/client/resources/mcp-integration-catalog.js +255 -2
- package/dist/client/resources/mcp-integration-logos.js +18 -2
- package/dist/client/resources/use-mcp-servers.d.ts +5 -0
- package/dist/client/resources/use-mcp-servers.js +36 -0
- package/dist/client/settings/AccountSettingsCard.js +42 -36
- package/dist/client/settings/AutomationsSection.js +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -1
- package/dist/client/settings/DemoModeSection.js +4 -1
- package/dist/client/settings/ExtensionsSettingsContent.js +1 -1
- package/dist/client/settings/SchedulingTimezoneField.d.ts +3 -1
- package/dist/client/settings/SchedulingTimezoneField.js +9 -5
- package/dist/client/settings/SettingsPanel.d.ts +4 -1
- package/dist/client/settings/SettingsPanel.js +21 -21
- package/dist/client/settings/SettingsTabsPage.d.ts +2 -2
- package/dist/client/settings/SettingsTabsPage.js +98 -45
- package/dist/client/settings/VoiceTranscriptionSection.js +6 -1
- package/dist/client/settings/useSettingsPanelController.js +47 -8
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +9 -0
- package/dist/db/client.js +51 -16
- package/dist/db/create-get-db.js +2 -2
- package/dist/db/ddl-guard.d.ts +11 -10
- package/dist/db/ddl-guard.js +40 -12
- package/dist/deploy/build.d.ts +2 -0
- package/dist/deploy/build.js +97 -4
- package/dist/deploy/workspace-deploy.js +0 -2
- package/dist/guards/no-env-credentials.js +9 -0
- package/dist/ingestion/index.d.ts +1 -1
- package/dist/ingestion/pptx.d.ts +68 -0
- package/dist/ingestion/pptx.js +660 -213
- package/dist/ingestion/website.d.ts +30 -0
- package/dist/integrations/plugin.js +9 -6
- package/dist/localization/default-messages.d.ts +59 -0
- package/dist/localization/default-messages.js +62 -2
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp-client/oauth-routes.d.ts +2 -0
- package/dist/mcp-client/oauth-routes.js +74 -7
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.js +49 -0
- package/dist/navigation/index.js +16 -5
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.js +11 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/store.js +15 -1
- package/dist/scripts/call-agent.js +45 -4
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/action-filters-a2a.d.ts +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +36 -1
- package/dist/server/agent-chat-plugin.js +29 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.js +55 -17
- package/dist/server/better-auth-instance.js +5 -3
- package/dist/server/core-routes-plugin.d.ts +8 -1
- package/dist/server/core-routes-plugin.js +28 -3
- package/dist/server/credential-provider.d.ts +3 -3
- package/dist/server/credential-provider.js +90 -4
- package/dist/server/identity-sso.js +2 -1
- package/dist/server/onboarding-html.d.ts +2 -0
- package/dist/server/onboarding-html.js +171 -62
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/self-dispatch.d.ts +5 -4
- package/dist/server/self-dispatch.js +25 -14
- package/dist/server/ssr-handler.d.ts +1 -1
- package/dist/server/ssr-handler.js +1 -1
- package/dist/server/workspace-provider-oauth.js +1 -1
- package/dist/setup-connections/onboarding.js +1 -1
- package/dist/shared/cache-control.d.ts +26 -2
- package/dist/shared/cache-control.js +43 -2
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/password-policy.d.ts +3 -0
- package/dist/shared/password-policy.js +3 -0
- package/dist/shared/sign-in-journey.d.ts +5 -2
- package/dist/shared/sign-in-journey.js +10 -4
- package/dist/sharing/actions/set-resource-visibility.js +16 -4
- package/dist/sharing/registry.d.ts +14 -0
- package/dist/styles/agent-native.css +6 -0
- package/dist/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/dist/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/dist/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/dist/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/dist/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/dist/templates/chat/app/root.tsx +1 -1
- package/dist/templates/chat/app/routes/agent.tsx +7 -7
- package/dist/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/dist/templates/chat/app/routes/settings.$.tsx +1 -0
- package/dist/templates/chat/app/routes/team.tsx +1 -1
- package/dist/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/dist/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/dist/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/dist/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/dist/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/dist/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/dist/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/dist/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/dist/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
- package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/dist/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/dist/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/dist/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/dist/templates/workspace-root/AGENTS.md +7 -0
- package/dist/user-profile/actions/change-password.js +8 -3
- package/dist/user-profile/actions/set-password.js +5 -1
- package/dist/vite/action-types-plugin.js +59 -0
- package/docs/content/authentication.mdx +4 -5
- package/docs/content/skills-guide.mdx +13 -0
- package/docs/content/template-analytics-developers.mdx +1 -1
- package/docs/content/template-calendar-developers.mdx +1 -1
- package/docs/content/template-chat-developers.mdx +2 -2
- package/docs/content/template-content-developers.mdx +1 -1
- package/docs/content/template-mail-developers.mdx +1 -1
- package/docs/content/template-plan-developers.mdx +1 -1
- package/docs/content/toolkit-command-navigation.mdx +6 -6
- package/docs/content/toolkit-org-team.mdx +1 -1
- package/docs/content/toolkit-settings.mdx +8 -8
- package/docs/content/toolkit-setup-connections.mdx +1 -1
- package/package.json +6 -2
- package/src/templates/chat/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/src/templates/chat/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/src/templates/chat/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/src/templates/chat/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/src/templates/chat/app/hooks/use-navigation-state.ts +1 -1
- package/src/templates/chat/app/root.tsx +1 -1
- package/src/templates/chat/app/routes/agent.tsx +7 -7
- package/src/templates/chat/app/routes/extensions._index.tsx +1 -1
- package/src/templates/chat/app/routes/settings.$.tsx +1 -0
- package/src/templates/chat/app/routes/team.tsx +1 -1
- package/src/templates/default/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/src/templates/default/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/src/templates/default/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/src/templates/default/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/src/templates/headless/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/src/templates/headless/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/src/templates/headless/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/src/templates/headless/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/src/templates/workspace-core/.agents/skills/adding-workspace-apps/SKILL.md +6 -0
- package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +37 -6
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +4 -2
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +51 -0
- package/src/templates/workspace-core/.agents/skills/turn-into-app/SKILL.md +296 -0
- package/src/templates/workspace-core/.agents/skills/turn-into-app/agents/openai.yaml +4 -0
- package/src/templates/workspace-core/.agents/skills/turn-into-skill/SKILL.md +112 -0
- package/src/templates/workspace-core/.agents/skills/turn-into-skill/agents/openai.yaml +4 -0
- package/src/templates/workspace-root/AGENTS.md +7 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/references/template-catalog-and-demo.md +0 -62
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +0 -369
- package/corpus/templates/analytics/actions/list-dashboard-templates.ts +0 -47
- package/corpus/templates/analytics/actions/list-native-v2-dashboard-templates.ts +0 -34
- package/corpus/templates/analytics/app/routes/catalog.tsx +0 -240
- package/src/templates/default/actions/navigate.spec.ts +0 -53
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import and validate the portable Agent Plugin format.
|
|
3
|
+
*
|
|
4
|
+
* The importer intentionally handles only local plugin directories. Skills are
|
|
5
|
+
* copied as inert files, and remote Streamable HTTP MCP entries are translated
|
|
6
|
+
* to the Agent-Native `mcp.config.json` shape without copying package headers.
|
|
7
|
+
* A plugin package is not a credential store and this command never executes
|
|
8
|
+
* a plugin's scripts or stdio servers.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { validateRemoteUrl } from "../mcp-client/remote-url.js";
|
|
14
|
+
import { getFrontmatterValue, parseFrontmatter, } from "../resources/metadata.js";
|
|
15
|
+
import { withFileLockSync, writeJsonFileAtomically, } from "./atomic-json-file.js";
|
|
16
|
+
export const AGENT_PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
17
|
+
export const AGENT_PLUGIN_MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
18
|
+
const PLUGIN_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9.-]{0,62}[a-z0-9])?$/;
|
|
19
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
20
|
+
const MAX_PLUGIN_DESCRIPTION_LENGTH = 1024;
|
|
21
|
+
const MAX_SKILL_DESCRIPTION_LENGTH = 1024;
|
|
22
|
+
const MAX_SKILL_FILES = 512;
|
|
23
|
+
const MAX_SKILL_FILE_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
const MAX_SKILL_BYTES = 10 * 1024 * 1024;
|
|
25
|
+
const PLUGIN_MANIFEST_FIELDS = new Set([
|
|
26
|
+
"$schema",
|
|
27
|
+
"name",
|
|
28
|
+
"version",
|
|
29
|
+
"description",
|
|
30
|
+
"author",
|
|
31
|
+
"homepage",
|
|
32
|
+
"repository",
|
|
33
|
+
"license",
|
|
34
|
+
"keywords",
|
|
35
|
+
"extensions",
|
|
36
|
+
]);
|
|
37
|
+
const HELP = `agent-native plugin
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
agent-native plugin import <path> [--into <workspace>] [--yes] [--force] [--dry-run] [--json]
|
|
41
|
+
|
|
42
|
+
Commands:
|
|
43
|
+
import Import a standard Agent Plugin's Skills and remote MCP servers into an Agent-Native workspace.
|
|
44
|
+
|
|
45
|
+
The importer copies Skills into a namespaced skills/<plugin>/<skill> path and
|
|
46
|
+
maps Streamable HTTP MCP servers into mcp.config.json. Package headers are
|
|
47
|
+
never imported as credentials; configure authentication through the app's
|
|
48
|
+
normal Connections flow. Stdio and SSE servers are reported and skipped.`;
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
51
|
+
}
|
|
52
|
+
function nonEmptyString(value) {
|
|
53
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
54
|
+
}
|
|
55
|
+
function pathExists(file) {
|
|
56
|
+
try {
|
|
57
|
+
fs.lstatSync(file);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function assertRegularFile(file, label) {
|
|
65
|
+
const stat = fs.lstatSync(file);
|
|
66
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
67
|
+
throw new Error(`${label} must be a regular file, not a symlink or directory.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validateName(value, label, pattern) {
|
|
71
|
+
const name = nonEmptyString(value);
|
|
72
|
+
if (!name || name.length > 64 || !pattern.test(name)) {
|
|
73
|
+
throw new Error(`${label} must be 1-64 lowercase letters, numbers, and allowed separators.`);
|
|
74
|
+
}
|
|
75
|
+
if (name.includes("--") || name.includes("..")) {
|
|
76
|
+
throw new Error(`${label} must not contain repeated separators.`);
|
|
77
|
+
}
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
function validateOptionalUrl(value, label) {
|
|
81
|
+
if (value === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const raw = nonEmptyString(value);
|
|
84
|
+
if (!raw)
|
|
85
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(raw);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error(`${label} must be a valid URL.`);
|
|
92
|
+
}
|
|
93
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
94
|
+
throw new Error(`${label} must use http:// or https://.`);
|
|
95
|
+
}
|
|
96
|
+
return url.href;
|
|
97
|
+
}
|
|
98
|
+
function validatePluginManifest(raw) {
|
|
99
|
+
if (!isRecord(raw))
|
|
100
|
+
throw new Error("plugin.json must contain an object.");
|
|
101
|
+
const name = validateName(raw.name, "plugin.json name", PLUGIN_NAME_PATTERN);
|
|
102
|
+
const warnings = [];
|
|
103
|
+
for (const key of Object.keys(raw)) {
|
|
104
|
+
if (!PLUGIN_MANIFEST_FIELDS.has(key)) {
|
|
105
|
+
warnings.push(`Ignored unsupported plugin.json field "${key}".`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (raw.$schema !== undefined && typeof raw.$schema !== "string") {
|
|
109
|
+
throw new Error("plugin.json $schema must be a string when provided.");
|
|
110
|
+
}
|
|
111
|
+
if (typeof raw.$schema === "string" && raw.$schema !== AGENT_PLUGIN_SCHEMA) {
|
|
112
|
+
warnings.push("plugin.json uses a schema URL different from Agent Plugin 1.0.");
|
|
113
|
+
}
|
|
114
|
+
const version = raw.version === undefined ? undefined : nonEmptyString(raw.version);
|
|
115
|
+
if (raw.version !== undefined && !version) {
|
|
116
|
+
throw new Error("plugin.json version must be a non-empty string.");
|
|
117
|
+
}
|
|
118
|
+
if (version && version.length > 128) {
|
|
119
|
+
throw new Error("plugin.json version is too long.");
|
|
120
|
+
}
|
|
121
|
+
const description = raw.description === undefined ? undefined : nonEmptyString(raw.description);
|
|
122
|
+
if (raw.description !== undefined && !description) {
|
|
123
|
+
throw new Error("plugin.json description must be a non-empty string.");
|
|
124
|
+
}
|
|
125
|
+
if (description && description.length > MAX_PLUGIN_DESCRIPTION_LENGTH) {
|
|
126
|
+
throw new Error("plugin.json description is too long.");
|
|
127
|
+
}
|
|
128
|
+
let author;
|
|
129
|
+
if (raw.author !== undefined) {
|
|
130
|
+
if (typeof raw.author === "string") {
|
|
131
|
+
author = nonEmptyString(raw.author);
|
|
132
|
+
if (!author)
|
|
133
|
+
throw new Error("plugin.json author must not be empty.");
|
|
134
|
+
}
|
|
135
|
+
else if (isRecord(raw.author)) {
|
|
136
|
+
const authorName = nonEmptyString(raw.author.name);
|
|
137
|
+
if (!authorName)
|
|
138
|
+
throw new Error("plugin.json author.name is required.");
|
|
139
|
+
author = {
|
|
140
|
+
name: authorName,
|
|
141
|
+
...(raw.author.url === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: {
|
|
144
|
+
url: validateOptionalUrl(raw.author.url, "plugin.json author.url"),
|
|
145
|
+
}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
throw new Error("plugin.json author must be a string or object.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const homepage = validateOptionalUrl(raw.homepage, "plugin.json homepage");
|
|
153
|
+
const repository = validateOptionalUrl(raw.repository, "plugin.json repository");
|
|
154
|
+
const license = raw.license === undefined ? undefined : nonEmptyString(raw.license);
|
|
155
|
+
if (raw.license !== undefined && !license) {
|
|
156
|
+
throw new Error("plugin.json license must be a non-empty string.");
|
|
157
|
+
}
|
|
158
|
+
let keywords;
|
|
159
|
+
if (raw.keywords !== undefined) {
|
|
160
|
+
if (!Array.isArray(raw.keywords)) {
|
|
161
|
+
throw new Error("plugin.json keywords must be an array of strings.");
|
|
162
|
+
}
|
|
163
|
+
keywords = raw.keywords.map((keyword, index) => {
|
|
164
|
+
const value = nonEmptyString(keyword);
|
|
165
|
+
if (!value)
|
|
166
|
+
throw new Error(`plugin.json keywords[${index}] is invalid.`);
|
|
167
|
+
return value;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (raw.extensions !== undefined && !isRecord(raw.extensions)) {
|
|
171
|
+
throw new Error("plugin.json extensions must be an object when provided.");
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
manifest: {
|
|
175
|
+
name,
|
|
176
|
+
...(version ? { version } : {}),
|
|
177
|
+
...(description ? { description } : {}),
|
|
178
|
+
...(author ? { author } : {}),
|
|
179
|
+
...(homepage ? { homepage } : {}),
|
|
180
|
+
...(repository ? { repository } : {}),
|
|
181
|
+
...(license ? { license } : {}),
|
|
182
|
+
...(keywords ? { keywords } : {}),
|
|
183
|
+
},
|
|
184
|
+
warnings,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function resolvePluginRoot(input) {
|
|
188
|
+
const requested = path.resolve(input);
|
|
189
|
+
if (!pathExists(requested)) {
|
|
190
|
+
throw new Error(`Agent Plugin path does not exist: ${requested}`);
|
|
191
|
+
}
|
|
192
|
+
const stat = fs.lstatSync(requested);
|
|
193
|
+
const candidate = stat.isFile() && path.basename(requested) === "plugin.json"
|
|
194
|
+
? path.dirname(requested)
|
|
195
|
+
: requested;
|
|
196
|
+
if (!fs.statSync(candidate).isDirectory()) {
|
|
197
|
+
throw new Error(`Agent Plugin path must be a directory: ${candidate}`);
|
|
198
|
+
}
|
|
199
|
+
return fs.realpathSync(candidate);
|
|
200
|
+
}
|
|
201
|
+
function readJsonObject(file, label) {
|
|
202
|
+
assertRegularFile(file, label);
|
|
203
|
+
let raw;
|
|
204
|
+
try {
|
|
205
|
+
raw = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(raw))
|
|
211
|
+
throw new Error(`${label} must contain an object.`);
|
|
212
|
+
return raw;
|
|
213
|
+
}
|
|
214
|
+
function isSensitivePathPart(value) {
|
|
215
|
+
const lower = value.toLowerCase();
|
|
216
|
+
return (lower === "node_modules" ||
|
|
217
|
+
lower === ".git" ||
|
|
218
|
+
lower === "secrets" ||
|
|
219
|
+
lower === "private" ||
|
|
220
|
+
lower === ".npmrc" ||
|
|
221
|
+
lower === ".dev.vars" ||
|
|
222
|
+
lower === ".env" ||
|
|
223
|
+
lower.startsWith(".env.") ||
|
|
224
|
+
lower.endsWith(".pem") ||
|
|
225
|
+
lower.endsWith(".key") ||
|
|
226
|
+
lower.endsWith(".crt") ||
|
|
227
|
+
lower.endsWith(".p12") ||
|
|
228
|
+
lower.endsWith(".pfx"));
|
|
229
|
+
}
|
|
230
|
+
function toPosixPath(value) {
|
|
231
|
+
return value.split(path.sep).join("/");
|
|
232
|
+
}
|
|
233
|
+
function readSkillFiles(directory, skillName, warnings) {
|
|
234
|
+
const files = [];
|
|
235
|
+
let totalBytes = 0;
|
|
236
|
+
const walk = (current, prefix) => {
|
|
237
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
240
|
+
const absolutePath = path.join(current, entry.name);
|
|
241
|
+
if (entry.isSymbolicLink()) {
|
|
242
|
+
throw new Error(`contains a symbolic link at ${relativePath}`);
|
|
243
|
+
}
|
|
244
|
+
if (isSensitivePathPart(entry.name)) {
|
|
245
|
+
warnings.push(`Skipped sensitive file from skill "${skillName}": ${relativePath}.`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
walk(absolutePath, relativePath);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (!entry.isFile()) {
|
|
253
|
+
throw new Error(`contains a non-regular file at ${relativePath}`);
|
|
254
|
+
}
|
|
255
|
+
if (files.length >= MAX_SKILL_FILES) {
|
|
256
|
+
throw new Error(`contains more than ${MAX_SKILL_FILES} files`);
|
|
257
|
+
}
|
|
258
|
+
const stat = fs.lstatSync(absolutePath);
|
|
259
|
+
if (stat.size > MAX_SKILL_FILE_BYTES) {
|
|
260
|
+
throw new Error(`contains a file larger than ${MAX_SKILL_FILE_BYTES} bytes at ${relativePath}`);
|
|
261
|
+
}
|
|
262
|
+
totalBytes += stat.size;
|
|
263
|
+
if (totalBytes > MAX_SKILL_BYTES) {
|
|
264
|
+
throw new Error(`exceeds the ${MAX_SKILL_BYTES}-byte size limit`);
|
|
265
|
+
}
|
|
266
|
+
files.push({
|
|
267
|
+
relativePath: toPosixPath(relativePath),
|
|
268
|
+
content: fs.readFileSync(absolutePath),
|
|
269
|
+
mode: stat.mode & 0o777,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
walk(directory, "");
|
|
274
|
+
return files.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
275
|
+
}
|
|
276
|
+
function discoverSkills(rootDir, warnings, skipped) {
|
|
277
|
+
const skillsDir = path.join(rootDir, "skills");
|
|
278
|
+
if (!pathExists(skillsDir))
|
|
279
|
+
return [];
|
|
280
|
+
if (fs.lstatSync(skillsDir).isSymbolicLink()) {
|
|
281
|
+
skipped.push({
|
|
282
|
+
component: "skill",
|
|
283
|
+
reason: "The plugin skills directory is a symbolic link.",
|
|
284
|
+
});
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
if (!fs.statSync(skillsDir).isDirectory()) {
|
|
288
|
+
skipped.push({
|
|
289
|
+
component: "skill",
|
|
290
|
+
reason: "The plugin skills path is not a directory.",
|
|
291
|
+
});
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
const skills = [];
|
|
295
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
296
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
297
|
+
warnings.push(`Ignored non-directory entry in skills/: ${entry.name}.`);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const directory = path.join(skillsDir, entry.name);
|
|
301
|
+
try {
|
|
302
|
+
const skillName = validateName(entry.name, `Skill directory ${entry.name}`, SKILL_NAME_PATTERN);
|
|
303
|
+
const skillFile = path.join(directory, "SKILL.md");
|
|
304
|
+
assertRegularFile(skillFile, `Skill ${skillName}/SKILL.md`);
|
|
305
|
+
const content = fs.readFileSync(skillFile, "utf-8");
|
|
306
|
+
const frontmatter = parseFrontmatter(content);
|
|
307
|
+
const declaredName = getFrontmatterValue(frontmatter, "name");
|
|
308
|
+
if (!declaredName)
|
|
309
|
+
throw new Error("SKILL.md frontmatter needs name");
|
|
310
|
+
if (declaredName !== skillName) {
|
|
311
|
+
throw new Error(`SKILL.md name must match its directory (${skillName})`);
|
|
312
|
+
}
|
|
313
|
+
const description = getFrontmatterValue(frontmatter, "description");
|
|
314
|
+
if (!description) {
|
|
315
|
+
throw new Error("SKILL.md frontmatter needs description");
|
|
316
|
+
}
|
|
317
|
+
if (description.length > MAX_SKILL_DESCRIPTION_LENGTH) {
|
|
318
|
+
throw new Error("SKILL.md description is too long");
|
|
319
|
+
}
|
|
320
|
+
const files = readSkillFiles(directory, skillName, warnings);
|
|
321
|
+
skills.push({
|
|
322
|
+
name: skillName,
|
|
323
|
+
description,
|
|
324
|
+
relativePath: `skills/${skillName}`,
|
|
325
|
+
directory,
|
|
326
|
+
files,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
331
|
+
skipped.push({ component: "skill", name: entry.name, reason });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return skills.sort((left, right) => left.name.localeCompare(right.name));
|
|
335
|
+
}
|
|
336
|
+
function validateMcpUrl(raw, label) {
|
|
337
|
+
const value = nonEmptyString(raw);
|
|
338
|
+
if (!value)
|
|
339
|
+
throw new Error(`${label} URL is required.`);
|
|
340
|
+
const result = validateRemoteUrl(value);
|
|
341
|
+
if (!result.ok || !result.url) {
|
|
342
|
+
throw new Error(`${label} URL is not allowed: ${result.error ?? "invalid URL"}.`);
|
|
343
|
+
}
|
|
344
|
+
if (result.url.username || result.url.password || result.url.hash) {
|
|
345
|
+
throw new Error(`${label} URL must not contain credentials or a fragment.`);
|
|
346
|
+
}
|
|
347
|
+
return result.url.href;
|
|
348
|
+
}
|
|
349
|
+
function headerCount(value, label) {
|
|
350
|
+
if (value === undefined)
|
|
351
|
+
return 0;
|
|
352
|
+
if (!isRecord(value))
|
|
353
|
+
throw new Error(`${label} headers must be an object.`);
|
|
354
|
+
for (const headerValue of Object.values(value)) {
|
|
355
|
+
if (typeof headerValue !== "string") {
|
|
356
|
+
throw new Error(`${label} headers must contain string values.`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return Object.keys(value).length;
|
|
360
|
+
}
|
|
361
|
+
function discoverMcpServers(rootDir, warnings, skipped) {
|
|
362
|
+
const file = path.join(rootDir, "mcp.json");
|
|
363
|
+
if (!pathExists(file))
|
|
364
|
+
return { servers: [] };
|
|
365
|
+
if (fs.lstatSync(file).isSymbolicLink()) {
|
|
366
|
+
skipped.push({
|
|
367
|
+
component: "mcp",
|
|
368
|
+
reason: "mcp.json is a symbolic link and was not read.",
|
|
369
|
+
});
|
|
370
|
+
return { servers: [] };
|
|
371
|
+
}
|
|
372
|
+
let parsed;
|
|
373
|
+
try {
|
|
374
|
+
parsed = readJsonObject(file, "mcp.json");
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
skipped.push({
|
|
378
|
+
component: "mcp",
|
|
379
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
380
|
+
});
|
|
381
|
+
return { file, servers: [] };
|
|
382
|
+
}
|
|
383
|
+
if (parsed.$schema !== undefined && typeof parsed.$schema !== "string") {
|
|
384
|
+
skipped.push({
|
|
385
|
+
component: "mcp",
|
|
386
|
+
reason: "mcp.json $schema must be a string when provided.",
|
|
387
|
+
});
|
|
388
|
+
return { file, servers: [] };
|
|
389
|
+
}
|
|
390
|
+
if (typeof parsed.$schema === "string" &&
|
|
391
|
+
parsed.$schema !== AGENT_PLUGIN_MCP_SCHEMA) {
|
|
392
|
+
warnings.push("mcp.json uses a schema URL different from Agent Plugin 1.0.");
|
|
393
|
+
}
|
|
394
|
+
if (!isRecord(parsed.mcpServers)) {
|
|
395
|
+
skipped.push({
|
|
396
|
+
component: "mcp",
|
|
397
|
+
reason: "mcp.json must contain an mcpServers object.",
|
|
398
|
+
});
|
|
399
|
+
return { file, servers: [] };
|
|
400
|
+
}
|
|
401
|
+
const servers = [];
|
|
402
|
+
for (const [name, raw] of Object.entries(parsed.mcpServers)) {
|
|
403
|
+
try {
|
|
404
|
+
if (!name.trim() || name.length > 128 || /[\u0000-\u001f]/.test(name)) {
|
|
405
|
+
throw new Error("server name is empty, too long, or contains control characters");
|
|
406
|
+
}
|
|
407
|
+
if (!isRecord(raw))
|
|
408
|
+
throw new Error("server entry must be an object");
|
|
409
|
+
const type = raw.type;
|
|
410
|
+
const headers = headerCount(raw.headers, `MCP server "${name}"`);
|
|
411
|
+
if (headers > 0) {
|
|
412
|
+
warnings.push(`MCP server "${name}" declares ${headers} package header(s); headers are not imported as credentials.`);
|
|
413
|
+
}
|
|
414
|
+
if (type === "streamable-http" || type === "sse") {
|
|
415
|
+
const url = validateMcpUrl(raw.url, `MCP server "${name}"`);
|
|
416
|
+
const server = {
|
|
417
|
+
name,
|
|
418
|
+
type,
|
|
419
|
+
url,
|
|
420
|
+
headerCount: headers,
|
|
421
|
+
};
|
|
422
|
+
servers.push(server);
|
|
423
|
+
if (type === "sse") {
|
|
424
|
+
skipped.push({
|
|
425
|
+
component: "mcp",
|
|
426
|
+
name,
|
|
427
|
+
reason: "SSE servers are not imported; only Streamable HTTP is supported.",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (type === "stdio") {
|
|
433
|
+
const command = nonEmptyString(raw.command);
|
|
434
|
+
if (!command || /\s|[;&|<>`$]/.test(command)) {
|
|
435
|
+
throw new Error("stdio command must be one executable token");
|
|
436
|
+
}
|
|
437
|
+
const args = raw.args;
|
|
438
|
+
if (args !== undefined &&
|
|
439
|
+
(!Array.isArray(args) || args.some((arg) => typeof arg !== "string"))) {
|
|
440
|
+
throw new Error("stdio args must be an array of strings");
|
|
441
|
+
}
|
|
442
|
+
if (raw.cwd !== undefined && typeof raw.cwd !== "string") {
|
|
443
|
+
throw new Error("stdio cwd must be a string when provided");
|
|
444
|
+
}
|
|
445
|
+
skipped.push({
|
|
446
|
+
component: "mcp",
|
|
447
|
+
name,
|
|
448
|
+
reason: "Stdio servers are not imported or executed by this command.",
|
|
449
|
+
});
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
throw new Error(`unsupported transport ${typeof type === "string" ? type : "(missing type)"}`);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
skipped.push({
|
|
456
|
+
component: "mcp",
|
|
457
|
+
name,
|
|
458
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
file,
|
|
464
|
+
servers: servers.filter((server) => server.type === "streamable-http"),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function hashPlugin(manifestFile, mcpFile, skills) {
|
|
468
|
+
const hash = createHash("sha256");
|
|
469
|
+
const addFile = (relativePath, content) => {
|
|
470
|
+
hash.update(relativePath);
|
|
471
|
+
hash.update("\u0000");
|
|
472
|
+
hash.update(content);
|
|
473
|
+
hash.update("\u0000");
|
|
474
|
+
};
|
|
475
|
+
addFile("plugin.json", fs.readFileSync(manifestFile));
|
|
476
|
+
if (mcpFile)
|
|
477
|
+
addFile("mcp.json", fs.readFileSync(mcpFile));
|
|
478
|
+
for (const skill of skills) {
|
|
479
|
+
for (const file of skill.files) {
|
|
480
|
+
addFile(`${skill.relativePath}/${file.relativePath}`, file.content);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return hash.digest("hex");
|
|
484
|
+
}
|
|
485
|
+
export function loadAgentPlugin(input) {
|
|
486
|
+
const rootDir = resolvePluginRoot(input);
|
|
487
|
+
const manifestFile = path.join(rootDir, "plugin.json");
|
|
488
|
+
if (!pathExists(manifestFile)) {
|
|
489
|
+
throw new Error(`Agent Plugin is missing required ${manifestFile}.`);
|
|
490
|
+
}
|
|
491
|
+
let rawManifest;
|
|
492
|
+
try {
|
|
493
|
+
rawManifest = readJsonObject(manifestFile, "plugin.json");
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
throw new Error(error instanceof Error ? error.message : String(error));
|
|
497
|
+
}
|
|
498
|
+
const normalized = validatePluginManifest(rawManifest);
|
|
499
|
+
const warnings = [...normalized.warnings];
|
|
500
|
+
const skipped = [];
|
|
501
|
+
const skills = discoverSkills(rootDir, warnings, skipped);
|
|
502
|
+
const mcp = discoverMcpServers(rootDir, warnings, skipped);
|
|
503
|
+
return {
|
|
504
|
+
rootDir,
|
|
505
|
+
manifestFile,
|
|
506
|
+
mcpFile: mcp.file,
|
|
507
|
+
manifest: normalized.manifest,
|
|
508
|
+
pluginHash: hashPlugin(manifestFile, mcp.file, skills),
|
|
509
|
+
skills,
|
|
510
|
+
mcpServers: mcp.servers,
|
|
511
|
+
skipped,
|
|
512
|
+
warnings,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function canonicalPathForComparison(input) {
|
|
516
|
+
const absolute = path.resolve(input);
|
|
517
|
+
if (pathExists(absolute))
|
|
518
|
+
return fs.realpathSync(absolute);
|
|
519
|
+
const missing = [];
|
|
520
|
+
let current = absolute;
|
|
521
|
+
while (!pathExists(current)) {
|
|
522
|
+
const parent = path.dirname(current);
|
|
523
|
+
if (parent === current)
|
|
524
|
+
return absolute;
|
|
525
|
+
missing.push(path.basename(current));
|
|
526
|
+
current = parent;
|
|
527
|
+
}
|
|
528
|
+
return path.resolve(fs.realpathSync(current), ...missing.reverse());
|
|
529
|
+
}
|
|
530
|
+
function isPathInside(root, target, includeRoot = false) {
|
|
531
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
532
|
+
if (!relative)
|
|
533
|
+
return includeRoot;
|
|
534
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
535
|
+
}
|
|
536
|
+
function assertPathInside(root, target, label) {
|
|
537
|
+
if (!isPathInside(root, target, true)) {
|
|
538
|
+
throw new Error(`${label} must remain inside ${root}.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function assertPathOutside(root, target, label) {
|
|
542
|
+
if (isPathInside(root, target, true)) {
|
|
543
|
+
throw new Error(`${label} must not be inside the imported plugin directory.`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function assertNoSymlinkInExistingPath(target, label) {
|
|
547
|
+
const absolute = path.resolve(target);
|
|
548
|
+
const parts = absolute.split(path.sep);
|
|
549
|
+
let current = parts[0] === "" ? path.sep : parts[0];
|
|
550
|
+
for (let index = 1; index < parts.length; index++) {
|
|
551
|
+
current = path.join(current, parts[index]);
|
|
552
|
+
if (!pathExists(current))
|
|
553
|
+
break;
|
|
554
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
555
|
+
throw new Error(`${label} contains a symbolic link: ${current}.`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function pluginSlug(name) {
|
|
560
|
+
const slug = name
|
|
561
|
+
.toLowerCase()
|
|
562
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
563
|
+
.replace(/^-+|-+$/g, "")
|
|
564
|
+
.slice(0, 56);
|
|
565
|
+
return slug || "plugin";
|
|
566
|
+
}
|
|
567
|
+
function serverSlug(name) {
|
|
568
|
+
const slug = name
|
|
569
|
+
.toLowerCase()
|
|
570
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
571
|
+
.replace(/^-+|-+$/g, "")
|
|
572
|
+
.slice(0, 40);
|
|
573
|
+
return slug || "server";
|
|
574
|
+
}
|
|
575
|
+
function shortHash(value) {
|
|
576
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 8);
|
|
577
|
+
}
|
|
578
|
+
export function importedMcpServerId(pluginName, serverName, existingIds = []) {
|
|
579
|
+
const base = `agent-plugin-${pluginSlug(pluginName)}-${serverSlug(serverName)}`.slice(0, 96);
|
|
580
|
+
const existing = new Set(existingIds);
|
|
581
|
+
if (!existing.has(base))
|
|
582
|
+
return base;
|
|
583
|
+
return `${base}-${shortHash(`${pluginName}\u0000${serverName}`)}`.slice(0, 110);
|
|
584
|
+
}
|
|
585
|
+
function listExistingSkillFiles(directory) {
|
|
586
|
+
const files = [];
|
|
587
|
+
const walk = (current, prefix) => {
|
|
588
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
589
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
590
|
+
const absolutePath = path.join(current, entry.name);
|
|
591
|
+
if (entry.isSymbolicLink()) {
|
|
592
|
+
throw new Error(`existing skill contains a symbolic link at ${relativePath}`);
|
|
593
|
+
}
|
|
594
|
+
if (entry.isDirectory())
|
|
595
|
+
walk(absolutePath, relativePath);
|
|
596
|
+
else if (entry.isFile())
|
|
597
|
+
files.push(toPosixPath(relativePath));
|
|
598
|
+
else
|
|
599
|
+
throw new Error(`existing skill contains a non-regular file at ${relativePath}`);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
walk(directory, "");
|
|
603
|
+
return files.sort();
|
|
604
|
+
}
|
|
605
|
+
function skillFilesMatch(source, destination) {
|
|
606
|
+
const sourcePaths = source.map((file) => file.relativePath).sort();
|
|
607
|
+
const destinationPaths = listExistingSkillFiles(destination);
|
|
608
|
+
if (sourcePaths.length !== destinationPaths.length ||
|
|
609
|
+
sourcePaths.some((file, index) => file !== destinationPaths[index])) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
return source.every((file) => {
|
|
613
|
+
const target = path.join(destination, ...file.relativePath.split("/"));
|
|
614
|
+
return (fs.readFileSync(target).equals(file.content) &&
|
|
615
|
+
(fs.lstatSync(target).mode & 0o777) === file.mode);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function readExistingMcpConfig(file) {
|
|
619
|
+
if (!pathExists(file)) {
|
|
620
|
+
return { fileExists: false, value: { servers: {} }, servers: {} };
|
|
621
|
+
}
|
|
622
|
+
assertRegularFile(file, "mcp.config.json");
|
|
623
|
+
let parsed;
|
|
624
|
+
try {
|
|
625
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
throw new Error(`mcp.config.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
+
}
|
|
630
|
+
if (!isRecord(parsed) || !isRecord(parsed.servers)) {
|
|
631
|
+
throw new Error("mcp.config.json must contain a servers object.");
|
|
632
|
+
}
|
|
633
|
+
for (const [id, entry] of Object.entries(parsed.servers)) {
|
|
634
|
+
if (!isRecord(entry)) {
|
|
635
|
+
throw new Error(`mcp.config.json server "${id}" must be an object.`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
fileExists: true,
|
|
640
|
+
value: parsed,
|
|
641
|
+
servers: parsed.servers,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function sameMcpEndpoint(value, url) {
|
|
645
|
+
return isRecord(value) && value.type === "http" && value.url === url;
|
|
646
|
+
}
|
|
647
|
+
function planMcpServers(plugin, existing, force) {
|
|
648
|
+
const planned = [];
|
|
649
|
+
const plannedIds = new Set();
|
|
650
|
+
const pluginName = plugin.manifest.name;
|
|
651
|
+
for (const server of plugin.mcpServers) {
|
|
652
|
+
const baseId = importedMcpServerId(pluginName, server.name).slice(0, 96);
|
|
653
|
+
let id = baseId;
|
|
654
|
+
const existingEntry = existing.servers[id];
|
|
655
|
+
const plannedCollision = plannedIds.has(id);
|
|
656
|
+
if (plannedCollision ||
|
|
657
|
+
(existingEntry && !sameMcpEndpoint(existingEntry, server.url))) {
|
|
658
|
+
if (!plannedCollision && existingEntry && force) {
|
|
659
|
+
// A caller explicitly approved replacing an existing entry with the
|
|
660
|
+
// stable id. Preserve the id so updates do not accumulate stale names.
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
const stem = `${baseId}-${shortHash(`${pluginName}\u0000${server.name}\u0000${server.url}`)}`.slice(0, 105);
|
|
664
|
+
id = stem;
|
|
665
|
+
for (let suffix = 2; plannedIds.has(id) ||
|
|
666
|
+
(existing.servers[id] &&
|
|
667
|
+
!sameMcpEndpoint(existing.servers[id], server.url)); suffix++) {
|
|
668
|
+
id = `${stem}-${suffix}`.slice(0, 110);
|
|
669
|
+
if (suffix > 20) {
|
|
670
|
+
throw new Error(`MCP server id collision for "${server.name}". Re-run with a different workspace or resolve the existing id first.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const current = existing.servers[id];
|
|
676
|
+
planned.push({
|
|
677
|
+
server,
|
|
678
|
+
id,
|
|
679
|
+
entry: {
|
|
680
|
+
type: "http",
|
|
681
|
+
url: server.url,
|
|
682
|
+
description: `Imported Agent Plugin: ${pluginName}/${server.name}`,
|
|
683
|
+
},
|
|
684
|
+
status: current
|
|
685
|
+
? sameMcpEndpoint(current, server.url)
|
|
686
|
+
? "unchanged"
|
|
687
|
+
: "overwritten"
|
|
688
|
+
: "imported",
|
|
689
|
+
});
|
|
690
|
+
plannedIds.add(id);
|
|
691
|
+
}
|
|
692
|
+
return planned;
|
|
693
|
+
}
|
|
694
|
+
function planSkills(plugin, targetDir, targetPluginSlug, force) {
|
|
695
|
+
return plugin.skills.map((skill) => {
|
|
696
|
+
const destination = path.join(targetDir, "skills", targetPluginSlug, skill.name);
|
|
697
|
+
assertPathInside(targetDir, destination, "Imported skill destination");
|
|
698
|
+
assertPathOutside(plugin.rootDir, destination, "Imported skill destination");
|
|
699
|
+
assertNoSymlinkInExistingPath(path.dirname(destination), "Imported skill destination");
|
|
700
|
+
if (!pathExists(destination)) {
|
|
701
|
+
return { skill, destination, status: "imported" };
|
|
702
|
+
}
|
|
703
|
+
const stat = fs.lstatSync(destination);
|
|
704
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
705
|
+
throw new Error(`Imported skill destination already exists and is not a regular directory: ${destination}`);
|
|
706
|
+
}
|
|
707
|
+
const unchanged = skillFilesMatch(skill.files, destination);
|
|
708
|
+
if (!unchanged && !force) {
|
|
709
|
+
throw new Error(`Imported skill destination differs: ${destination}. Re-run with --force to replace it.`);
|
|
710
|
+
}
|
|
711
|
+
return {
|
|
712
|
+
skill,
|
|
713
|
+
destination,
|
|
714
|
+
status: unchanged ? "unchanged" : "overwritten",
|
|
715
|
+
};
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function replaceDirectory(staging, destination, replaceExisting) {
|
|
719
|
+
const parent = path.dirname(destination);
|
|
720
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
721
|
+
if (!pathExists(destination)) {
|
|
722
|
+
fs.renameSync(staging, destination);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (!replaceExisting) {
|
|
726
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
727
|
+
throw new Error(`Refusing to replace existing directory: ${destination}`);
|
|
728
|
+
}
|
|
729
|
+
const backup = path.join(parent, `.${path.basename(destination)}.backup-${process.pid}-${randomUUID()}`);
|
|
730
|
+
fs.renameSync(destination, backup);
|
|
731
|
+
try {
|
|
732
|
+
fs.renameSync(staging, destination);
|
|
733
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
734
|
+
}
|
|
735
|
+
catch (error) {
|
|
736
|
+
if (!pathExists(destination) && pathExists(backup)) {
|
|
737
|
+
fs.renameSync(backup, destination);
|
|
738
|
+
}
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function writeSkill(planned, dryRun) {
|
|
743
|
+
if (dryRun || planned.status === "unchanged")
|
|
744
|
+
return;
|
|
745
|
+
const parent = path.dirname(planned.destination);
|
|
746
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
747
|
+
const staging = fs.mkdtempSync(path.join(parent, `.${path.basename(planned.destination)}.tmp-`));
|
|
748
|
+
try {
|
|
749
|
+
for (const file of planned.skill.files) {
|
|
750
|
+
const target = path.join(staging, ...file.relativePath.split("/"));
|
|
751
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
752
|
+
fs.writeFileSync(target, file.content, { mode: file.mode });
|
|
753
|
+
fs.chmodSync(target, file.mode);
|
|
754
|
+
}
|
|
755
|
+
replaceDirectory(staging, planned.destination, planned.status === "overwritten");
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
if (pathExists(staging))
|
|
759
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
760
|
+
throw error;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function safeManifestMetadata(manifest) {
|
|
764
|
+
const author = typeof manifest.author === "object" && manifest.author
|
|
765
|
+
? {
|
|
766
|
+
name: manifest.author.name,
|
|
767
|
+
...(manifest.author.url ? { url: manifest.author.url } : {}),
|
|
768
|
+
}
|
|
769
|
+
: manifest.author;
|
|
770
|
+
return {
|
|
771
|
+
name: manifest.name,
|
|
772
|
+
...(manifest.version ? { version: manifest.version } : {}),
|
|
773
|
+
...(manifest.description ? { description: manifest.description } : {}),
|
|
774
|
+
...(author ? { author } : {}),
|
|
775
|
+
...(manifest.homepage ? { homepage: manifest.homepage } : {}),
|
|
776
|
+
...(manifest.repository ? { repository: manifest.repository } : {}),
|
|
777
|
+
...(manifest.license ? { license: manifest.license } : {}),
|
|
778
|
+
...(manifest.keywords ? { keywords: manifest.keywords } : {}),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
function buildImportMetadata(plugin, targetDir, skillPlans, mcpPlans) {
|
|
782
|
+
return {
|
|
783
|
+
schemaVersion: 1,
|
|
784
|
+
source: "agent-plugin",
|
|
785
|
+
plugin: safeManifestMetadata(plugin.manifest),
|
|
786
|
+
pluginHash: plugin.pluginHash,
|
|
787
|
+
sourceDir: plugin.rootDir,
|
|
788
|
+
importedAt: new Date().toISOString(),
|
|
789
|
+
skills: skillPlans.map((plan) => ({
|
|
790
|
+
name: plan.skill.name,
|
|
791
|
+
sourcePath: plan.skill.relativePath,
|
|
792
|
+
path: toPosixPath(path.relative(targetDir, plan.destination)),
|
|
793
|
+
destination: plan.destination,
|
|
794
|
+
})),
|
|
795
|
+
mcpServers: mcpPlans.map((plan) => ({
|
|
796
|
+
sourceName: plan.server.name,
|
|
797
|
+
id: plan.id,
|
|
798
|
+
type: plan.server.type,
|
|
799
|
+
url: plan.server.url,
|
|
800
|
+
headersIgnored: plan.server.headerCount,
|
|
801
|
+
})),
|
|
802
|
+
skipped: plugin.skipped,
|
|
803
|
+
warnings: plugin.warnings,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function readExistingMetadata(file) {
|
|
807
|
+
if (!pathExists(file))
|
|
808
|
+
return undefined;
|
|
809
|
+
assertRegularFile(file, "Agent Plugin import metadata");
|
|
810
|
+
let parsed;
|
|
811
|
+
try {
|
|
812
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
813
|
+
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
throw new Error(`Agent Plugin import metadata is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
816
|
+
}
|
|
817
|
+
if (!isRecord(parsed)) {
|
|
818
|
+
throw new Error("Agent Plugin import metadata must contain an object.");
|
|
819
|
+
}
|
|
820
|
+
return parsed;
|
|
821
|
+
}
|
|
822
|
+
function metadataMatches(metadata, plugin) {
|
|
823
|
+
return (metadata?.pluginHash === plugin.pluginHash &&
|
|
824
|
+
isRecord(metadata.plugin) &&
|
|
825
|
+
metadata.plugin.name === plugin.manifest.name);
|
|
826
|
+
}
|
|
827
|
+
function applyMcpConfig(file, plans, force) {
|
|
828
|
+
if (plans.length === 0)
|
|
829
|
+
return;
|
|
830
|
+
withFileLockSync(file, () => {
|
|
831
|
+
const current = readExistingMcpConfig(file);
|
|
832
|
+
const nextServers = { ...current.servers };
|
|
833
|
+
for (const plan of plans) {
|
|
834
|
+
const existing = nextServers[plan.id];
|
|
835
|
+
if (existing && sameMcpEndpoint(existing, plan.entry.url))
|
|
836
|
+
continue;
|
|
837
|
+
if (existing && !force && plan.status === "overwritten") {
|
|
838
|
+
throw new Error(`MCP server id "${plan.id}" already points to another URL. Re-run with --force to replace it.`);
|
|
839
|
+
}
|
|
840
|
+
nextServers[plan.id] = plan.entry;
|
|
841
|
+
}
|
|
842
|
+
const changed = JSON.stringify(nextServers) !== JSON.stringify(current.servers);
|
|
843
|
+
if (changed || !current.fileExists) {
|
|
844
|
+
writeJsonFileAtomically(file, { ...current.value, servers: nextServers });
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
function verifyImport(plugin, skillPlans, mcpPlans, metadataFile, mcpFile) {
|
|
849
|
+
for (const plan of skillPlans) {
|
|
850
|
+
if (!pathExists(plan.destination)) {
|
|
851
|
+
throw new Error(`Imported skill is missing after write: ${plan.destination}`);
|
|
852
|
+
}
|
|
853
|
+
if (!skillFilesMatch(plan.skill.files, plan.destination)) {
|
|
854
|
+
throw new Error(`Imported skill verification failed: ${plan.destination}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (mcpPlans.length > 0) {
|
|
858
|
+
const config = readExistingMcpConfig(mcpFile);
|
|
859
|
+
for (const plan of mcpPlans) {
|
|
860
|
+
if (!sameMcpEndpoint(config.servers[plan.id], plan.entry.url)) {
|
|
861
|
+
throw new Error(`Imported MCP verification failed: ${plan.id}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const metadata = readExistingMetadata(metadataFile);
|
|
866
|
+
if (!metadataMatches(metadata, plugin)) {
|
|
867
|
+
throw new Error(`Imported Agent Plugin metadata verification failed: ${metadataFile}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
export function importAgentPlugin(input, options = {}) {
|
|
871
|
+
const plugin = loadAgentPlugin(input);
|
|
872
|
+
const targetDir = canonicalPathForComparison(options.targetDir ?? process.cwd());
|
|
873
|
+
if (pathExists(targetDir) && !fs.statSync(targetDir).isDirectory()) {
|
|
874
|
+
throw new Error(`Agent-Native import target must be a directory: ${targetDir}`);
|
|
875
|
+
}
|
|
876
|
+
if (isPathInside(plugin.rootDir, targetDir, true)) {
|
|
877
|
+
throw new Error("Agent Plugin import target must not be the plugin directory or one of its children.");
|
|
878
|
+
}
|
|
879
|
+
assertNoSymlinkInExistingPath(targetDir, "Agent-Native import target");
|
|
880
|
+
const slug = pluginSlug(plugin.manifest.name);
|
|
881
|
+
const skillsRoot = path.join(targetDir, "skills", slug);
|
|
882
|
+
const metadataFile = path.join(targetDir, ".agent-native", "plugins", `${slug}.json`);
|
|
883
|
+
const mcpFile = path.join(targetDir, "mcp.config.json");
|
|
884
|
+
assertPathInside(targetDir, skillsRoot, "Imported skills root");
|
|
885
|
+
assertPathInside(targetDir, metadataFile, "Imported metadata path");
|
|
886
|
+
assertPathInside(targetDir, mcpFile, "Imported MCP config path");
|
|
887
|
+
assertPathOutside(plugin.rootDir, skillsRoot, "Imported skills root");
|
|
888
|
+
assertPathOutside(plugin.rootDir, metadataFile, "Imported metadata path");
|
|
889
|
+
assertPathOutside(plugin.rootDir, mcpFile, "Imported MCP config path");
|
|
890
|
+
assertNoSymlinkInExistingPath(path.dirname(metadataFile), "Imported metadata path");
|
|
891
|
+
assertNoSymlinkInExistingPath(mcpFile, "Imported MCP config path");
|
|
892
|
+
const force = options.force ?? false;
|
|
893
|
+
const dryRun = options.dryRun ?? false;
|
|
894
|
+
const existingMcp = readExistingMcpConfig(mcpFile);
|
|
895
|
+
const skillPlans = planSkills(plugin, targetDir, slug, force);
|
|
896
|
+
const mcpPlans = planMcpServers(plugin, existingMcp, force);
|
|
897
|
+
const existingMetadata = readExistingMetadata(metadataFile);
|
|
898
|
+
const metadataIsSame = metadataMatches(existingMetadata, plugin);
|
|
899
|
+
if (existingMetadata && !metadataIsSame && !force) {
|
|
900
|
+
throw new Error(`Agent Plugin import metadata differs: ${metadataFile}. Re-run with --force to replace it.`);
|
|
901
|
+
}
|
|
902
|
+
const metadata = metadataIsSame
|
|
903
|
+
? existingMetadata
|
|
904
|
+
: buildImportMetadata(plugin, targetDir, skillPlans, mcpPlans);
|
|
905
|
+
const result = {
|
|
906
|
+
plugin: {
|
|
907
|
+
name: plugin.manifest.name,
|
|
908
|
+
...(plugin.manifest.version ? { version: plugin.manifest.version } : {}),
|
|
909
|
+
hash: plugin.pluginHash,
|
|
910
|
+
sourceDir: plugin.rootDir,
|
|
911
|
+
},
|
|
912
|
+
targetDir,
|
|
913
|
+
metadataPath: metadataFile,
|
|
914
|
+
mcpConfigPath: mcpFile,
|
|
915
|
+
skills: skillPlans.map((plan) => ({
|
|
916
|
+
name: plan.skill.name,
|
|
917
|
+
sourcePath: plan.skill.relativePath,
|
|
918
|
+
destination: plan.destination,
|
|
919
|
+
files: plan.skill.files.length,
|
|
920
|
+
status: dryRun
|
|
921
|
+
? plan.status === "overwritten"
|
|
922
|
+
? "would-overwrite"
|
|
923
|
+
: plan.status === "unchanged"
|
|
924
|
+
? "unchanged"
|
|
925
|
+
: "would-import"
|
|
926
|
+
: plan.status,
|
|
927
|
+
})),
|
|
928
|
+
mcpServers: mcpPlans.map((plan) => ({
|
|
929
|
+
name: plan.server.name,
|
|
930
|
+
id: plan.id,
|
|
931
|
+
url: plan.server.url,
|
|
932
|
+
headersIgnored: plan.server.headerCount,
|
|
933
|
+
status: dryRun
|
|
934
|
+
? plan.status === "overwritten"
|
|
935
|
+
? "would-overwrite"
|
|
936
|
+
: plan.status === "unchanged"
|
|
937
|
+
? "unchanged"
|
|
938
|
+
: "would-import"
|
|
939
|
+
: plan.status,
|
|
940
|
+
})),
|
|
941
|
+
skipped: plugin.skipped,
|
|
942
|
+
warnings: plugin.warnings,
|
|
943
|
+
dryRun,
|
|
944
|
+
};
|
|
945
|
+
if (dryRun)
|
|
946
|
+
return result;
|
|
947
|
+
for (const plan of skillPlans)
|
|
948
|
+
writeSkill(plan, false);
|
|
949
|
+
applyMcpConfig(mcpFile, mcpPlans, force);
|
|
950
|
+
if (!metadataIsSame || !pathExists(metadataFile)) {
|
|
951
|
+
writeJsonFileAtomically(metadataFile, metadata);
|
|
952
|
+
}
|
|
953
|
+
verifyImport(plugin, skillPlans, mcpPlans, metadataFile, mcpFile);
|
|
954
|
+
return result;
|
|
955
|
+
}
|
|
956
|
+
export function parseAgentPluginArgs(argv) {
|
|
957
|
+
const first = argv[0];
|
|
958
|
+
if (first === "help" || first === "--help" || first === "-h") {
|
|
959
|
+
return {
|
|
960
|
+
command: "help",
|
|
961
|
+
force: false,
|
|
962
|
+
dryRun: false,
|
|
963
|
+
printJson: false,
|
|
964
|
+
yes: false,
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
if (first !== "import") {
|
|
968
|
+
throw new Error(`Unknown plugin command: ${first ?? "(missing command)"}`);
|
|
969
|
+
}
|
|
970
|
+
const out = {
|
|
971
|
+
command: "import",
|
|
972
|
+
force: false,
|
|
973
|
+
dryRun: false,
|
|
974
|
+
printJson: false,
|
|
975
|
+
yes: false,
|
|
976
|
+
};
|
|
977
|
+
for (let index = 1; index < argv.length; index++) {
|
|
978
|
+
const arg = argv[index];
|
|
979
|
+
const eat = (flag) => {
|
|
980
|
+
if (arg === flag) {
|
|
981
|
+
const next = argv[++index];
|
|
982
|
+
if (!next || next.startsWith("-"))
|
|
983
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
984
|
+
return next;
|
|
985
|
+
}
|
|
986
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
987
|
+
const value = arg.slice(flag.length + 1);
|
|
988
|
+
if (!value)
|
|
989
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
990
|
+
return value;
|
|
991
|
+
}
|
|
992
|
+
return undefined;
|
|
993
|
+
};
|
|
994
|
+
const into = eat("--into");
|
|
995
|
+
if (into !== undefined)
|
|
996
|
+
out.into = into;
|
|
997
|
+
else if (arg === "--force")
|
|
998
|
+
out.force = true;
|
|
999
|
+
else if (arg === "--dry-run")
|
|
1000
|
+
out.dryRun = true;
|
|
1001
|
+
else if (arg === "--json")
|
|
1002
|
+
out.printJson = true;
|
|
1003
|
+
else if (arg === "--yes" || arg === "-y")
|
|
1004
|
+
out.yes = true;
|
|
1005
|
+
else if (arg.startsWith("-"))
|
|
1006
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
1007
|
+
else if (!out.source)
|
|
1008
|
+
out.source = arg;
|
|
1009
|
+
else
|
|
1010
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (!out.source)
|
|
1013
|
+
throw new Error("Missing Agent Plugin path to import.");
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
async function confirmPluginImport(plugin, targetDir, result) {
|
|
1017
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1018
|
+
throw new Error("Refusing to import an Agent Plugin without confirmation. Re-run with --yes to approve.");
|
|
1019
|
+
}
|
|
1020
|
+
const readline = await import("node:readline/promises");
|
|
1021
|
+
const rl = readline.createInterface({
|
|
1022
|
+
input: process.stdin,
|
|
1023
|
+
output: process.stdout,
|
|
1024
|
+
});
|
|
1025
|
+
try {
|
|
1026
|
+
const answer = await rl.question([
|
|
1027
|
+
`Import Agent Plugin ${plugin.manifest.name}?`,
|
|
1028
|
+
` Skills: ${result.skills.length}`,
|
|
1029
|
+
` Remote MCP servers: ${result.mcpServers.length}`,
|
|
1030
|
+
` Target: ${targetDir}`,
|
|
1031
|
+
"Package headers are ignored and are not stored as credentials.",
|
|
1032
|
+
"Proceed? [y/N] ",
|
|
1033
|
+
].join("\n"));
|
|
1034
|
+
if (!/^(y|yes)$/i.test(answer.trim()))
|
|
1035
|
+
throw new Error("Cancelled Agent Plugin import.");
|
|
1036
|
+
}
|
|
1037
|
+
finally {
|
|
1038
|
+
rl.close();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
export async function runAgentPlugin(argv) {
|
|
1042
|
+
try {
|
|
1043
|
+
const parsed = parseAgentPluginArgs(argv);
|
|
1044
|
+
if (parsed.command === "help") {
|
|
1045
|
+
process.stdout.write(`${HELP}\n`);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (!parsed.source)
|
|
1049
|
+
throw new Error("Missing Agent Plugin path to import.");
|
|
1050
|
+
const loaded = loadAgentPlugin(parsed.source);
|
|
1051
|
+
const preview = importAgentPlugin(parsed.source, {
|
|
1052
|
+
targetDir: parsed.into,
|
|
1053
|
+
force: parsed.force,
|
|
1054
|
+
dryRun: true,
|
|
1055
|
+
});
|
|
1056
|
+
if (!parsed.dryRun && !parsed.yes) {
|
|
1057
|
+
await confirmPluginImport(loaded, preview.targetDir, preview);
|
|
1058
|
+
}
|
|
1059
|
+
const result = parsed.dryRun
|
|
1060
|
+
? preview
|
|
1061
|
+
: importAgentPlugin(parsed.source, {
|
|
1062
|
+
targetDir: parsed.into,
|
|
1063
|
+
force: parsed.force,
|
|
1064
|
+
});
|
|
1065
|
+
if (parsed.printJson) {
|
|
1066
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const action = parsed.dryRun ? "Would import" : "Imported";
|
|
1070
|
+
process.stdout.write(`${action} ${result.plugin.name} into ${result.targetDir}: ${result.skills.length} skill(s), ${result.mcpServers.length} remote MCP server(s).\n`);
|
|
1071
|
+
for (const warning of result.warnings)
|
|
1072
|
+
process.stdout.write(`Warning: ${warning}\n`);
|
|
1073
|
+
for (const skipped of result.skipped) {
|
|
1074
|
+
process.stdout.write(`Skipped ${skipped.component}${skipped.name ? ` "${skipped.name}"` : ""}: ${skipped.reason}\n`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
1079
|
+
process.exitCode = 1;
|
|
1080
|
+
}
|
|
1081
|
+
}
|