wizards 0.1.9
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/LICENSE +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { refreshConnector, resolveConnector } from "../connectors/external.js";
|
|
4
|
+
import { connectionContext } from "../connectors/index.js";
|
|
5
|
+
import { attempt, clip } from "./shared.js";
|
|
6
|
+
import { resolveFile } from "./store.js";
|
|
7
|
+
const FILE_NOTE = " File content never passes through you: returned content is kept as a file and you get its path; to send a kept file, give `file:<path>` where an input asks for `*_base64`.";
|
|
8
|
+
/** Above this many actions a connector is offered as "find an action, then call it". */
|
|
9
|
+
const DIRECT_TOOLS = 24;
|
|
10
|
+
const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
11
|
+
/**
|
|
12
|
+
* The actions of imported connectors a step may use, as tools. Reading is free; anything that
|
|
13
|
+
* changes the person's account waits for their yes — engenty's default policy for connectors
|
|
14
|
+
* (read: allow, write and destructive: ask), asked of the person who is running the wizard.
|
|
15
|
+
*/
|
|
16
|
+
export async function connectorTools(step, ctx, files) {
|
|
17
|
+
const tools = {};
|
|
18
|
+
const connections = (ctx.def.connections ?? []).filter((c) => c.connector && step.connections?.includes(c.id));
|
|
19
|
+
for (const connection of connections) {
|
|
20
|
+
Object.assign(tools, await toolsOf(connection, ctx, files));
|
|
21
|
+
}
|
|
22
|
+
return tools;
|
|
23
|
+
}
|
|
24
|
+
const MAGIC = [
|
|
25
|
+
["%PDF", "application/pdf", "pdf"],
|
|
26
|
+
["\x89PNG", "image/png", "png"],
|
|
27
|
+
["\xff\xd8\xff", "image/jpeg", "jpg"],
|
|
28
|
+
["GIF8", "image/gif", "gif"],
|
|
29
|
+
["PK\x03\x04", "application/zip", "zip"],
|
|
30
|
+
];
|
|
31
|
+
/** What a file is, by its first bytes — connectors often hand over content without a name. */
|
|
32
|
+
function sniff(data) {
|
|
33
|
+
const head = data.subarray(0, 8).toString("latin1");
|
|
34
|
+
const hit = MAGIC.find(([magic]) => head.startsWith(magic));
|
|
35
|
+
return hit ? { mime: hit[1], ext: hit[2] } : null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A model cannot carry a file: content an action returns as base64 is kept in the wizard's
|
|
39
|
+
* files and the result names the path instead.
|
|
40
|
+
*/
|
|
41
|
+
async function keepContent(result, action, service, files) {
|
|
42
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
const out = { ...result };
|
|
46
|
+
for (const [key, value] of Object.entries(out)) {
|
|
47
|
+
if (!key.endsWith("_base64") || typeof value !== "string" || !value) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const data = Buffer.from(value, "base64");
|
|
51
|
+
const kind = sniff(data);
|
|
52
|
+
const given = [out.name, out.filename, out.file_name].find((v) => typeof v === "string" && v.length > 0);
|
|
53
|
+
const name = given ?? `${action.id}-${Date.now().toString(36)}.${kind?.ext ?? "bin"}`;
|
|
54
|
+
const declared = [out.mime_type, out.mimeType, out.content_type].find((v) => typeof v === "string");
|
|
55
|
+
const kept = await files.keep(`${service}/${name}`, data, {
|
|
56
|
+
mime: declared ?? kind?.mime,
|
|
57
|
+
source: `${service}: ${action.summary || action.id}`,
|
|
58
|
+
});
|
|
59
|
+
delete out[key];
|
|
60
|
+
out.saved = kept.path;
|
|
61
|
+
out.note = "The content is kept as a file; read it with read_document.";
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** The other way round: `file:<path>` in a `*_base64` input sends a kept file or an upload. */
|
|
66
|
+
async function sendContent(input, ctx) {
|
|
67
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
const out = { ...input };
|
|
71
|
+
for (const [key, value] of Object.entries(out)) {
|
|
72
|
+
if (key.endsWith("_base64") && typeof value === "string" && value.startsWith("file:")) {
|
|
73
|
+
const file = await resolveFile(ctx, value.slice("file:".length));
|
|
74
|
+
out[key] = Buffer.from(file.data).toString("base64");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether the person is asked before an action runs. The wizard's author can settle it per
|
|
81
|
+
* action (a server may call a plain lookup a change); deleting is never taken off the person.
|
|
82
|
+
*/
|
|
83
|
+
function asks(connection, action) {
|
|
84
|
+
if (action.group === "destructive") {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return (connection.policy?.[action.id] ?? (action.group === "read" ? "allow" : "ask")) === "ask";
|
|
88
|
+
}
|
|
89
|
+
async function toolsOf(connection, ctx, files) {
|
|
90
|
+
const projectId = ctx.project.id;
|
|
91
|
+
const id = connection.connector ?? "";
|
|
92
|
+
let resolved = await resolveConnector(projectId, id);
|
|
93
|
+
if (!resolved) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
const account = () => connectionContext(ctx.store, connection, projectId);
|
|
97
|
+
// An MCP server that lists its tools only to a signed-in account: ask it now that there is one.
|
|
98
|
+
if (resolved.imported && !resolved.connector.actions.length) {
|
|
99
|
+
const found = await account();
|
|
100
|
+
if (found) {
|
|
101
|
+
await refreshConnector(projectId, id, found.ctx.accessToken).catch(() => undefined);
|
|
102
|
+
resolved = (await resolveConnector(projectId, id)) ?? resolved;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const { connector, inputSchema } = resolved;
|
|
106
|
+
const usable = connection.actions
|
|
107
|
+
? connector.actions.filter((a) => connection.actions?.includes(a.id))
|
|
108
|
+
: connector.actions.filter((a) => a.group === "read");
|
|
109
|
+
const service = connection.title ?? connector.name;
|
|
110
|
+
const call = (action, input) => attempt(async () => {
|
|
111
|
+
const found = await account();
|
|
112
|
+
if (!found) {
|
|
113
|
+
return {
|
|
114
|
+
error: `The person has not connected ${service}. Go on without it and say so in your result.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const key = `${connection.id}:${action.id}`;
|
|
118
|
+
if (asks(connection, action) && !ctx.resources.allowed.has(key)) {
|
|
119
|
+
const answer = await ctx.ask({
|
|
120
|
+
kind: "confirm",
|
|
121
|
+
reason: action.summary || action.id,
|
|
122
|
+
service,
|
|
123
|
+
action: {
|
|
124
|
+
id: action.id,
|
|
125
|
+
summary: action.summary,
|
|
126
|
+
destructive: action.group === "destructive",
|
|
127
|
+
},
|
|
128
|
+
input: JSON.stringify(input ?? {}, null, 2).slice(0, 4000),
|
|
129
|
+
});
|
|
130
|
+
if (!answer) {
|
|
131
|
+
return {
|
|
132
|
+
error: "Nobody is there to allow this change, so it was not made. Report it as open in your result.",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (answer.type !== "done") {
|
|
136
|
+
return {
|
|
137
|
+
error: "The person did not allow this change. Do not try it again; say in your result that it was not made.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (answer.remember) {
|
|
141
|
+
ctx.resources.allowed.add(key);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await ctx.emit("tool", `${service}: ${action.summary || action.id}`.slice(0, 120));
|
|
145
|
+
const result = await keepContent(await action.handler(await sendContent(input ?? {}, ctx), found.ctx), action, service, files);
|
|
146
|
+
const text = typeof result === "string" ? result : JSON.stringify(result);
|
|
147
|
+
return { result: clip(text ?? "", 8000) };
|
|
148
|
+
});
|
|
149
|
+
const prefix = connector.toolPrefix;
|
|
150
|
+
const direct = usable.length <= DIRECT_TOOLS && usable.every((a) => TOOL_NAME.test(`${prefix}_${a.id}`));
|
|
151
|
+
if (direct) {
|
|
152
|
+
return Object.fromEntries(usable.map((action) => [
|
|
153
|
+
`${prefix}_${action.id}`,
|
|
154
|
+
createTool({
|
|
155
|
+
id: `${prefix}_${action.id}`,
|
|
156
|
+
description: `${service}: ${action.description || action.summary}${asks(connection, action) ? " — the person is asked before this runs." : ""}${FILE_NOTE}`.slice(0, 1100),
|
|
157
|
+
inputSchema: action.inputSchema,
|
|
158
|
+
execute: (input) => call(action, input),
|
|
159
|
+
}),
|
|
160
|
+
]));
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
[`${prefix}_actions`]: createTool({
|
|
164
|
+
id: `${prefix}_actions`,
|
|
165
|
+
description: `${service} offers ${usable.length} actions. Find the ones you need by words from their name or purpose; you get each action's id, what it does and its input. Then call ${prefix}_call.`,
|
|
166
|
+
inputSchema: z.object({
|
|
167
|
+
query: z.string().describe("Words to look for, e.g. 'search pages' or 'invoice list'."),
|
|
168
|
+
}),
|
|
169
|
+
execute: ({ query }) => attempt(async () => {
|
|
170
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
171
|
+
const scored = usable
|
|
172
|
+
.map((a) => {
|
|
173
|
+
const text = `${a.id} ${a.summary} ${a.description}`.toLowerCase();
|
|
174
|
+
return { a, score: words.filter((w) => text.includes(w)).length };
|
|
175
|
+
})
|
|
176
|
+
.filter((x) => x.score > 0)
|
|
177
|
+
.sort((x, y) => y.score - x.score)
|
|
178
|
+
.slice(0, 12);
|
|
179
|
+
return {
|
|
180
|
+
actions: scored.map(({ a }) => ({
|
|
181
|
+
id: a.id,
|
|
182
|
+
does: a.summary || a.description.slice(0, 200),
|
|
183
|
+
asks_person_first: asks(connection, a),
|
|
184
|
+
input: inputSchema(a.id),
|
|
185
|
+
})),
|
|
186
|
+
};
|
|
187
|
+
}),
|
|
188
|
+
}),
|
|
189
|
+
[`${prefix}_call`]: createTool({
|
|
190
|
+
id: `${prefix}_call`,
|
|
191
|
+
description: `Run one ${service} action found with ${prefix}_actions. Some wait for the person's yes first.${FILE_NOTE}`,
|
|
192
|
+
inputSchema: z.object({
|
|
193
|
+
action: z.string(),
|
|
194
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
195
|
+
}),
|
|
196
|
+
execute: ({ action: id, input }) => {
|
|
197
|
+
const action = usable.find((a) => a.id === id);
|
|
198
|
+
return action
|
|
199
|
+
? call(action, input)
|
|
200
|
+
: Promise.resolve({ error: `No action "${id}". Use ${prefix}_actions to find one.` });
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
+
import { isTextMime } from "@engenty-wizards/shared/workspace";
|
|
3
|
+
import { createTool } from "@mastra/core/tools";
|
|
4
|
+
import { MCPClient } from "@mastra/mcp";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { generateImageMedia } from "../media/generate.js";
|
|
7
|
+
import { markMedia } from "../media/marking.js";
|
|
8
|
+
import { gatewayTools, isHarnessVendor } from "../models.js";
|
|
9
|
+
import { htmlToMarkdown } from "../render/convert.js";
|
|
10
|
+
import { pythonToolDescription, sandboxCapabilities, shellToolDescription, } from "../sandbox/index.js";
|
|
11
|
+
import { snapshotFile } from "../services/files.js";
|
|
12
|
+
import { browserTools } from "./browser.js";
|
|
13
|
+
import { connectorTools } from "./connector.js";
|
|
14
|
+
import { mailTools } from "./mail.js";
|
|
15
|
+
import { assertPublicUrl, safeFetch } from "./net-guard.js";
|
|
16
|
+
import { projectTools } from "./project.js";
|
|
17
|
+
import { clip, FileKeeper } from "./shared.js";
|
|
18
|
+
import { storeTools } from "./store.js";
|
|
19
|
+
/** The tools an agent step may use: exactly what its definition allowlists. */
|
|
20
|
+
export async function buildStepTools(step, ctx, resolved, uploads) {
|
|
21
|
+
const tools = {};
|
|
22
|
+
const assets = [];
|
|
23
|
+
const allowed = new Set(step.tools);
|
|
24
|
+
let mcp = null;
|
|
25
|
+
if (allowed.has("web_search")) {
|
|
26
|
+
// Anthropic models search natively; other models search through the AI Gateway's tool.
|
|
27
|
+
if (resolved.vendor === "anthropic") {
|
|
28
|
+
tools.web_search = anthropic.tools.webSearch_20250305({ maxUses: 8 });
|
|
29
|
+
}
|
|
30
|
+
else if (isHarnessVendor(resolved.vendor)) {
|
|
31
|
+
// An installed client searches with its own tool; naming it here switches it on.
|
|
32
|
+
tools.web_search = createTool({
|
|
33
|
+
id: "web_search",
|
|
34
|
+
description: "Search the web.",
|
|
35
|
+
inputSchema: z.object({ query: z.string() }),
|
|
36
|
+
execute: async () => ({ error: "The client searches itself." }),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else if (resolved.gateway) {
|
|
40
|
+
tools.web_search = gatewayTools.perplexitySearch({ maxResults: 8 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (allowed.has("web_fetch") || allowed.has("web_search")) {
|
|
44
|
+
tools.web_fetch = createTool({
|
|
45
|
+
id: "web_fetch",
|
|
46
|
+
description: "Fetch a public web page or file and return its readable text (Markdown).",
|
|
47
|
+
inputSchema: z.object({ url: z.string().describe("Absolute http(s) URL") }),
|
|
48
|
+
execute: async ({ url }) => {
|
|
49
|
+
const safe = await assertPublicUrl(url);
|
|
50
|
+
await ctx.emit("tool", `Liest ${safe.hostname}`);
|
|
51
|
+
const res = await safeFetch(safe.toString(), {
|
|
52
|
+
signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(20_000)]),
|
|
53
|
+
headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
|
|
54
|
+
});
|
|
55
|
+
const type = res.headers.get("content-type") ?? "";
|
|
56
|
+
const body = await res.text();
|
|
57
|
+
const text = type.includes("html") ? htmlToMarkdown(body) : body;
|
|
58
|
+
return { status: res.status, url: res.url, content: clip(text) };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (allowed.has("http")) {
|
|
63
|
+
tools.http_request = createTool({
|
|
64
|
+
id: "http_request",
|
|
65
|
+
description: "Call a public HTTP API. Use for REST endpoints the instructions name.",
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
|
|
68
|
+
url: z.string(),
|
|
69
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
70
|
+
json: z.unknown().optional().describe("JSON body"),
|
|
71
|
+
}),
|
|
72
|
+
execute: async ({ method, url, headers, json }) => {
|
|
73
|
+
const safe = await assertPublicUrl(url);
|
|
74
|
+
await ctx.emit("tool", `${method} ${safe.hostname}${safe.pathname}`);
|
|
75
|
+
const res = await safeFetch(safe.toString(), {
|
|
76
|
+
method,
|
|
77
|
+
signal: AbortSignal.any([ctx.signal, AbortSignal.timeout(30_000)]),
|
|
78
|
+
headers: {
|
|
79
|
+
...(json !== undefined ? { "content-type": "application/json" } : {}),
|
|
80
|
+
...headers,
|
|
81
|
+
},
|
|
82
|
+
body: json !== undefined ? JSON.stringify(json) : undefined,
|
|
83
|
+
});
|
|
84
|
+
return { status: res.status, body: clip(await res.text(), 8000) };
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const files = new FileKeeper(ctx, assets);
|
|
89
|
+
if (allowed.has("browser")) {
|
|
90
|
+
Object.assign(tools, browserTools(ctx, assets, files));
|
|
91
|
+
}
|
|
92
|
+
// The wizard's lists and files, the documents the person gave and the project's documents are
|
|
93
|
+
// always at hand.
|
|
94
|
+
Object.assign(tools, storeTools(ctx, uploads, files));
|
|
95
|
+
Object.assign(tools, projectTools(ctx));
|
|
96
|
+
Object.assign(tools, mailTools(step, ctx, files));
|
|
97
|
+
Object.assign(tools, await connectorTools(step, ctx, files));
|
|
98
|
+
const sandbox = sandboxCapabilities();
|
|
99
|
+
if (allowed.has("sandbox") && sandbox) {
|
|
100
|
+
const timeoutMs = (seconds) => Math.min((seconds ?? 120) * 1000, 600_000);
|
|
101
|
+
const report = (result) => ({
|
|
102
|
+
exitCode: result.exitCode,
|
|
103
|
+
stdout: clip(result.stdout, 8000),
|
|
104
|
+
stderr: clip(result.stderr, 4000),
|
|
105
|
+
});
|
|
106
|
+
tools.run_command = createTool({
|
|
107
|
+
id: "run_command",
|
|
108
|
+
description: shellToolDescription(sandbox),
|
|
109
|
+
inputSchema: z.object({ command: z.string(), timeoutSeconds: z.number().optional() }),
|
|
110
|
+
execute: async ({ command, timeoutSeconds }) => {
|
|
111
|
+
await ctx.emit("tool", `Führt aus: ${command.slice(0, 80)}`);
|
|
112
|
+
return report(await ctx.resources
|
|
113
|
+
.sandboxHandle()
|
|
114
|
+
.exec(command, { timeoutMs: timeoutMs(timeoutSeconds) }));
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (sandbox.python === "tool") {
|
|
118
|
+
tools.run_python = createTool({
|
|
119
|
+
id: "run_python",
|
|
120
|
+
description: pythonToolDescription(sandbox),
|
|
121
|
+
inputSchema: z.object({
|
|
122
|
+
code: z.string(),
|
|
123
|
+
packages: z.array(z.string()).optional().describe("PyPI packages the code imports"),
|
|
124
|
+
timeoutSeconds: z.number().optional(),
|
|
125
|
+
}),
|
|
126
|
+
execute: async ({ code, packages, timeoutSeconds }) => {
|
|
127
|
+
await ctx.emit("tool", "Führt Python aus");
|
|
128
|
+
return report(await ctx.resources.sandboxHandle().runPython(code, {
|
|
129
|
+
packages,
|
|
130
|
+
timeoutMs: timeoutMs(timeoutSeconds),
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
tools.export_file = createTool({
|
|
136
|
+
id: "export_file",
|
|
137
|
+
description: "Hand a file from the sandbox to the person as a result (e.g. a chart PNG, a CSV).",
|
|
138
|
+
inputSchema: z.object({
|
|
139
|
+
path: z.string(),
|
|
140
|
+
mime: z.string().describe("e.g. image/png, text/csv"),
|
|
141
|
+
}),
|
|
142
|
+
execute: async ({ path, mime }) => {
|
|
143
|
+
let bytes;
|
|
144
|
+
try {
|
|
145
|
+
bytes = await ctx.resources.sandboxHandle().readFile(path);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return { error: err.message || "file not found" };
|
|
149
|
+
}
|
|
150
|
+
const ref = await ctx.saveAsset({
|
|
151
|
+
kind: mime.startsWith("image/") ? "image" : "file",
|
|
152
|
+
mime,
|
|
153
|
+
name: path.split("/").pop() ?? "file",
|
|
154
|
+
data: Buffer.from(bytes),
|
|
155
|
+
});
|
|
156
|
+
assets.push(ref);
|
|
157
|
+
return { saved: true, assetId: ref.id };
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (allowed.has("image")) {
|
|
162
|
+
tools.generate_image = createTool({
|
|
163
|
+
id: "generate_image",
|
|
164
|
+
description: "Draw an image from a detailed visual prompt. The image is shown to the person.",
|
|
165
|
+
inputSchema: z.object({
|
|
166
|
+
prompt: z.string(),
|
|
167
|
+
aspectRatio: z.enum(["1:1", "16:9", "9:16", "4:5", "3:2", "2:3"]).optional(),
|
|
168
|
+
}),
|
|
169
|
+
execute: async ({ prompt, aspectRatio }) => {
|
|
170
|
+
await ctx.emit("tool", "Zeichnet ein Bild");
|
|
171
|
+
const media = await generateImageMedia({
|
|
172
|
+
call: ctx.call,
|
|
173
|
+
prompt,
|
|
174
|
+
aspectRatio,
|
|
175
|
+
abortSignal: ctx.signal,
|
|
176
|
+
});
|
|
177
|
+
await ctx.chargeUsd(media.costUsd);
|
|
178
|
+
const ref = await ctx.saveAsset({
|
|
179
|
+
kind: "image",
|
|
180
|
+
mime: media.mime,
|
|
181
|
+
name: "image.png",
|
|
182
|
+
data: markMedia(media.bytes, media.mime, { origin: "generated", system: media.system }),
|
|
183
|
+
ai: "generated",
|
|
184
|
+
});
|
|
185
|
+
assets.push(ref);
|
|
186
|
+
return { saved: true, assetId: ref.id };
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// The wizard's workspace (reference data, price lists, templates) is always readable.
|
|
191
|
+
if (ctx.files.length) {
|
|
192
|
+
tools.read_workspace_file = createTool({
|
|
193
|
+
id: "read_workspace_file",
|
|
194
|
+
description: `Read a file from the wizard's workspace. Files: ${ctx.files
|
|
195
|
+
.slice(0, 60)
|
|
196
|
+
.map((f) => f.path)
|
|
197
|
+
.join(", ")}`,
|
|
198
|
+
inputSchema: z.object({ path: z.string() }),
|
|
199
|
+
execute: async ({ path }) => {
|
|
200
|
+
const found = await snapshotFile(ctx.files, path);
|
|
201
|
+
if (!found) {
|
|
202
|
+
return { error: `No file ${path}` };
|
|
203
|
+
}
|
|
204
|
+
return isTextMime(found.file.mime)
|
|
205
|
+
? { path, text: clip(found.data.toString("utf8"), 40_000) }
|
|
206
|
+
: { path, mime: found.file.mime, size: found.file.size, note: "binary file" };
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const servers = ctx.project.mcpServers.filter((s) => step.mcp?.includes(s.id));
|
|
211
|
+
if (servers.length) {
|
|
212
|
+
mcp = new MCPClient({
|
|
213
|
+
id: `run-${ctx.runId}-${step.id}`,
|
|
214
|
+
servers: Object.fromEntries(servers.map((s) => [
|
|
215
|
+
s.id,
|
|
216
|
+
{ url: new URL(s.url), requestInit: { headers: s.headers ?? {} }, timeout: 60_000 },
|
|
217
|
+
])),
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
Object.assign(tools, await mcp.listTools());
|
|
221
|
+
await ctx.emit("tool", `Verbunden mit ${servers.map((s) => s.name).join(", ")}`);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
await ctx.emit("info", `MCP-Server nicht erreichbar: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
tools,
|
|
229
|
+
assets,
|
|
230
|
+
close: async () => {
|
|
231
|
+
await mcp?.disconnect().catch(() => undefined);
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { connectionContext } from "../connectors/index.js";
|
|
4
|
+
import { mailAction, mailSearchInput, } from "../connectors/mail.js";
|
|
5
|
+
import { htmlToPdf } from "../render/convert.js";
|
|
6
|
+
import { guardHtml } from "../render/guard.js";
|
|
7
|
+
import { attempt, clip } from "./shared.js";
|
|
8
|
+
const NOT_CONNECTED = "The person has not connected a mailbox. Go on without mail and say in your result that the mailbox is not connected.";
|
|
9
|
+
/**
|
|
10
|
+
* Read-only mail for a step that names a mail connection. The tools are the same whichever
|
|
11
|
+
* provider the person connected; attachments never pass through the model — they are saved.
|
|
12
|
+
*/
|
|
13
|
+
export function mailTools(step, ctx, files) {
|
|
14
|
+
const connection = (ctx.def.connections ?? []).find((c) => c.kind === "mail" && step.connections?.includes(c.id));
|
|
15
|
+
if (!connection) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const open = async () => {
|
|
19
|
+
const found = await connectionContext(ctx.store, connection, ctx.project.id);
|
|
20
|
+
if (!found) {
|
|
21
|
+
throw new Error(NOT_CONNECTED);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
};
|
|
25
|
+
const tools = {};
|
|
26
|
+
tools.mail_search = createTool({
|
|
27
|
+
id: "mail_search",
|
|
28
|
+
description: "Search the person's mailbox (read-only), newest first. Returns id, date, sender, subject, snippet and attachments (id, name, size) of each mail. Search broadly with several terms at once rather than one call per word.",
|
|
29
|
+
inputSchema: mailSearchInput,
|
|
30
|
+
execute: (input) => attempt(async () => {
|
|
31
|
+
const { connector, ctx: actionCtx, label } = await open();
|
|
32
|
+
await ctx.emit("tool", `Durchsucht ${label}`);
|
|
33
|
+
const { mails } = await mailAction(connector, "search", input, actionCtx);
|
|
34
|
+
return {
|
|
35
|
+
mailbox: label,
|
|
36
|
+
total: mails.length,
|
|
37
|
+
mails: mails.map((m) => ({ ...m, snippet: m.snippet.slice(0, 200) })),
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
tools.mail_read = createTool({
|
|
42
|
+
id: "mail_read",
|
|
43
|
+
description: "Read one mail in full: sender, recipients, text, attachments.",
|
|
44
|
+
inputSchema: z.object({ id: z.string() }),
|
|
45
|
+
execute: ({ id }) => attempt(async () => {
|
|
46
|
+
const { connector, ctx: actionCtx } = await open();
|
|
47
|
+
const mail = await mailAction(connector, "read", { id }, actionCtx);
|
|
48
|
+
const { html: _html, ...rest } = mail;
|
|
49
|
+
return { ...rest, text: clip(mail.text, 6000) };
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
tools.mail_save = createTool({
|
|
53
|
+
id: "mail_save",
|
|
54
|
+
description: "Keep mail attachments (invoice PDFs) as files, several at once: each is stored under `path` in the wizard's files and added to this step's result. Leave attachment_id out to keep the mail itself as a PDF — for receipts that only exist as mail text. Then read them with scan_documents or read_document.",
|
|
55
|
+
inputSchema: z.object({
|
|
56
|
+
items: z
|
|
57
|
+
.array(z.object({
|
|
58
|
+
id: z.string().describe("Mail id from mail_search."),
|
|
59
|
+
attachment_id: z.string().optional(),
|
|
60
|
+
path: z.string().describe("e.g. invoices/2026-09/2026-09-03_Notion_INV-123.pdf"),
|
|
61
|
+
}))
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(40),
|
|
64
|
+
}),
|
|
65
|
+
execute: ({ items }) => attempt(async () => {
|
|
66
|
+
const { connector, ctx: actionCtx, label } = await open();
|
|
67
|
+
await ctx.emit("tool", `Speichert ${items.length} Belege aus ${label}`);
|
|
68
|
+
const saved = [];
|
|
69
|
+
for (const item of items) {
|
|
70
|
+
try {
|
|
71
|
+
if (item.attachment_id) {
|
|
72
|
+
const a = await mailAction(connector, "get_attachment", { id: item.id, attachment_id: item.attachment_id }, actionCtx);
|
|
73
|
+
const kept = await files.keep(item.path, Buffer.from(a.data_base64, "base64"), {
|
|
74
|
+
mime: a.mime,
|
|
75
|
+
source: `E-Mail-Anhang „${a.name}“ aus ${label}`,
|
|
76
|
+
});
|
|
77
|
+
saved.push({ id: item.id, saved: kept.path, size: kept.size });
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const mail = await mailAction(connector, "read", { id: item.id }, actionCtx);
|
|
81
|
+
const page = mail.html ??
|
|
82
|
+
`<pre style="white-space:pre-wrap;font:14px system-ui">${escapeHtml(mail.text)}</pre>`;
|
|
83
|
+
const head = `<div style="font:13px system-ui;color:#444;border-bottom:1px solid #ddd;padding-bottom:8px;margin-bottom:12px">${escapeHtml(mail.from ?? "")}<br>${escapeHtml(mail.date ?? "")}<br><b>${escapeHtml(mail.subject ?? "")}</b></div>`;
|
|
84
|
+
// The mail's own HTML is rendered offline: its remote images and scripts never load.
|
|
85
|
+
const pdf = await htmlToPdf(guardHtml(`<!doctype html><html><body>${head}${page}</body></html>`));
|
|
86
|
+
const kept = await files.keep(item.path.replace(/(\.pdf)?$/i, ".pdf"), pdf, {
|
|
87
|
+
mime: "application/pdf",
|
|
88
|
+
source: `E-Mail „${mail.subject ?? ""}“ aus ${label}`,
|
|
89
|
+
});
|
|
90
|
+
saved.push({ id: item.id, saved: kept.path, size: kept.size });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
saved.push({ id: item.id, error: String(err.message).slice(0, 200) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { saved };
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
return tools;
|
|
101
|
+
}
|
|
102
|
+
function escapeHtml(s) {
|
|
103
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
104
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { lookup } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
const ALLOW_PRIVATE = process.env.ALLOW_PRIVATE_FETCH === "1";
|
|
4
|
+
function privateV4(ip) {
|
|
5
|
+
const [a, b] = ip.split(".").map(Number);
|
|
6
|
+
return (a === 10 ||
|
|
7
|
+
a === 127 ||
|
|
8
|
+
a === 0 ||
|
|
9
|
+
(a === 169 && b === 254) ||
|
|
10
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
11
|
+
(a === 192 && b === 168) ||
|
|
12
|
+
(a === 100 && b >= 64 && b <= 127));
|
|
13
|
+
}
|
|
14
|
+
function privateV6(ip) {
|
|
15
|
+
const s = ip.toLowerCase();
|
|
16
|
+
return (s === "::1" || s === "::" || s.startsWith("fc") || s.startsWith("fd") || s.startsWith("fe80"));
|
|
17
|
+
}
|
|
18
|
+
/** Agents may reach the public internet, never this server's own network. */
|
|
19
|
+
export async function assertPublicUrl(raw) {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`Not a valid URL: ${raw}`);
|
|
26
|
+
}
|
|
27
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
28
|
+
throw new Error("Only http and https URLs are allowed.");
|
|
29
|
+
}
|
|
30
|
+
if (ALLOW_PRIVATE) {
|
|
31
|
+
return url;
|
|
32
|
+
}
|
|
33
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
34
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) {
|
|
35
|
+
throw new Error("Private addresses are not reachable from a wizard.");
|
|
36
|
+
}
|
|
37
|
+
const addresses = isIP(host)
|
|
38
|
+
? [{ address: host, family: isIP(host) }]
|
|
39
|
+
: await lookup(host, { all: true });
|
|
40
|
+
for (const a of addresses) {
|
|
41
|
+
if (a.family === 4 ? privateV4(a.address) : privateV6(a.address)) {
|
|
42
|
+
throw new Error("Private addresses are not reachable from a wizard.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
/** fetch that re-checks every redirect hop, so a public URL cannot bounce into the private network. */
|
|
48
|
+
export async function safeFetch(raw, init = {}) {
|
|
49
|
+
let url = await assertPublicUrl(raw);
|
|
50
|
+
for (let hop = 0; hop < 6; hop++) {
|
|
51
|
+
const res = await fetch(url, { ...init, redirect: "manual" });
|
|
52
|
+
const location = res.headers.get("location");
|
|
53
|
+
if (res.status < 300 || res.status >= 400 || !location) {
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
url = await assertPublicUrl(new URL(location, url).toString());
|
|
57
|
+
}
|
|
58
|
+
throw new Error("Too many redirects.");
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createTool } from "@mastra/core/tools";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { documentText, searchProject } from "../services/project-index.js";
|
|
4
|
+
import { attempt, clip } from "./shared.js";
|
|
5
|
+
/** The project's documents, for every agent step of its wizards: search the index, read one whole. */
|
|
6
|
+
export function projectTools(ctx) {
|
|
7
|
+
const docs = ctx.projectFiles.filter((f) => f.kind === "document");
|
|
8
|
+
if (!docs.length) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
project_search: createTool({
|
|
13
|
+
id: "project_search",
|
|
14
|
+
description: "Search the project's documents (the knowledge its owner gave for all wizards) and get the passages that fit best. Ask in the documents' language, with the words they would use.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
query: z.string().describe("What to look for: a question or keywords"),
|
|
17
|
+
limit: z.number().int().min(1).max(12).optional(),
|
|
18
|
+
}),
|
|
19
|
+
execute: ({ query, limit }) => attempt(async () => {
|
|
20
|
+
await ctx.emit("tool", `Sucht in den Dokumenten: ${query.slice(0, 60)}`);
|
|
21
|
+
const hits = await searchProject(ctx.project.id, query, limit ?? 6);
|
|
22
|
+
return {
|
|
23
|
+
passages: hits.map((h) => ({ document: h.name, part: h.index, text: h.text })),
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
project_document: createTool({
|
|
28
|
+
id: "project_document",
|
|
29
|
+
description: "Read one of the project's documents as text. Long documents come in parts: pass `from` to continue.",
|
|
30
|
+
inputSchema: z.object({
|
|
31
|
+
name: z.string().describe("The document's name, as listed"),
|
|
32
|
+
from: z.number().int().min(0).optional().describe("Character to start at"),
|
|
33
|
+
}),
|
|
34
|
+
execute: ({ name, from }) => attempt(async () => {
|
|
35
|
+
const doc = docs.find((d) => d.name === name) ??
|
|
36
|
+
docs.find((d) => d.name.toLowerCase().includes(name.toLowerCase()));
|
|
37
|
+
if (!doc) {
|
|
38
|
+
throw new Error(`No document "${name}". There are: ${docs.map((d) => d.name).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
await ctx.emit("tool", `Liest ${doc.name}`);
|
|
41
|
+
const text = await documentText(doc);
|
|
42
|
+
const start = Math.min(from ?? 0, text.length);
|
|
43
|
+
const part = text.slice(start, start + 12_000);
|
|
44
|
+
return {
|
|
45
|
+
document: doc.name,
|
|
46
|
+
text: clip(part, 12_000),
|
|
47
|
+
next: start + part.length < text.length ? start + part.length : null,
|
|
48
|
+
length: text.length,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|