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,106 @@
|
|
|
1
|
+
import { and, count, eq, gte, inArray, or } from "drizzle-orm";
|
|
2
|
+
import { control, controlDb } from "../db/client.js";
|
|
3
|
+
import { env } from "../env.js";
|
|
4
|
+
import { currentTenant } from "./tenant.js";
|
|
5
|
+
/** Remembers which tenant a public token belongs to. */
|
|
6
|
+
export async function putLink(token, kind, ref) {
|
|
7
|
+
await controlDb
|
|
8
|
+
.insert(control.link)
|
|
9
|
+
.values({ token, tenantId: currentTenant(), kind, ref })
|
|
10
|
+
.onConflictDoUpdate({
|
|
11
|
+
target: control.link.token,
|
|
12
|
+
set: { tenantId: currentTenant(), kind, ref },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function dropLinks(tokens) {
|
|
16
|
+
if (tokens.length) {
|
|
17
|
+
await controlDb.delete(control.link).where(inArray(control.link.token, tokens));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function tenantOfLink(token, kind) {
|
|
21
|
+
const row = await controlDb.query.link.findFirst({
|
|
22
|
+
where: and(eq(control.link.token, token), eq(control.link.kind, kind)),
|
|
23
|
+
});
|
|
24
|
+
return row?.tenantId ?? null;
|
|
25
|
+
}
|
|
26
|
+
// --- runs ----------------------------------------------------------------------
|
|
27
|
+
const runTenants = new Map();
|
|
28
|
+
export async function indexRun(input) {
|
|
29
|
+
const tenantId = currentTenant();
|
|
30
|
+
runTenants.set(input.runId, tenantId);
|
|
31
|
+
await controlDb.insert(control.runIndex).values({
|
|
32
|
+
runId: input.runId,
|
|
33
|
+
tenantId,
|
|
34
|
+
visitorId: input.visitorId ?? null,
|
|
35
|
+
ipHash: input.ipHash ?? null,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function tenantOfRun(runId) {
|
|
39
|
+
const known = runTenants.get(runId);
|
|
40
|
+
if (known) {
|
|
41
|
+
return known;
|
|
42
|
+
}
|
|
43
|
+
const row = await controlDb.query.runIndex.findFirst({
|
|
44
|
+
where: eq(control.runIndex.runId, runId),
|
|
45
|
+
});
|
|
46
|
+
if (row) {
|
|
47
|
+
runTenants.set(runId, row.tenantId);
|
|
48
|
+
if (runTenants.size > 5000) {
|
|
49
|
+
runTenants.delete(runTenants.keys().next().value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return row?.tenantId ?? null;
|
|
53
|
+
}
|
|
54
|
+
export async function dropRuns(runIds) {
|
|
55
|
+
for (const id of runIds) {
|
|
56
|
+
runTenants.delete(id);
|
|
57
|
+
}
|
|
58
|
+
for (let i = 0; i < runIds.length; i += 200) {
|
|
59
|
+
await controlDb
|
|
60
|
+
.delete(control.runIndex)
|
|
61
|
+
.where(inArray(control.runIndex.runId, runIds.slice(i, i + 200)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Marks a run as working, so a restart finds it again. */
|
|
65
|
+
export async function markRunActive(runId, active) {
|
|
66
|
+
await controlDb.update(control.runIndex).set({ active }).where(eq(control.runIndex.runId, runId));
|
|
67
|
+
}
|
|
68
|
+
/** The runs that were working when the process stopped, with their tenants. */
|
|
69
|
+
export async function activeRuns() {
|
|
70
|
+
return controlDb
|
|
71
|
+
.select({ runId: control.runIndex.runId, tenantId: control.runIndex.tenantId })
|
|
72
|
+
.from(control.runIndex)
|
|
73
|
+
.where(eq(control.runIndex.active, true));
|
|
74
|
+
}
|
|
75
|
+
/** A visitor's runs are counted across every wizard of every tenant. */
|
|
76
|
+
export async function visitorOverLimit(visitorId, ipHash) {
|
|
77
|
+
const since = new Date(Date.now() - 3600_000);
|
|
78
|
+
const who = ipHash
|
|
79
|
+
? or(eq(control.runIndex.visitorId, visitorId), eq(control.runIndex.ipHash, ipHash))
|
|
80
|
+
: eq(control.runIndex.visitorId, visitorId);
|
|
81
|
+
const [{ n }] = await controlDb
|
|
82
|
+
.select({ n: count() })
|
|
83
|
+
.from(control.runIndex)
|
|
84
|
+
.where(and(who, gte(control.runIndex.createdAt, since)));
|
|
85
|
+
return n >= env.limits.visitorRunsPerHour;
|
|
86
|
+
}
|
|
87
|
+
// --- tenants ---------------------------------------------------------------------
|
|
88
|
+
export async function tenantStatus(id) {
|
|
89
|
+
const row = await controlDb.query.tenant.findFirst({ where: eq(control.tenant.id, id) });
|
|
90
|
+
return row?.status ?? null;
|
|
91
|
+
}
|
|
92
|
+
export async function setTenantStatus(id, status) {
|
|
93
|
+
await controlDb.update(control.tenant).set({ status }).where(eq(control.tenant.id, id));
|
|
94
|
+
}
|
|
95
|
+
/** Everything the control database knows about a tenant goes with it. */
|
|
96
|
+
export async function forgetTenantRows(id) {
|
|
97
|
+
await controlDb.delete(control.link).where(eq(control.link.tenantId, id));
|
|
98
|
+
await controlDb.delete(control.runIndex).where(eq(control.runIndex.tenantId, id));
|
|
99
|
+
await controlDb.delete(control.session).where(eq(control.session.tenantId, id));
|
|
100
|
+
await controlDb.delete(control.tenant).where(eq(control.tenant.id, id));
|
|
101
|
+
for (const [runId, tenantId] of runTenants) {
|
|
102
|
+
if (tenantId === id) {
|
|
103
|
+
runTenants.delete(runId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/** The tenant a request, a run or a background job works for. Set once per entry, never from a request body. */
|
|
3
|
+
const current = new AsyncLocalStorage();
|
|
4
|
+
export class NoTenantError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("No tenant in context: tenant tables are reached only inside inTenant().");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function inTenant(tenantId, fn) {
|
|
10
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(tenantId)) {
|
|
11
|
+
throw new Error("bad tenant id");
|
|
12
|
+
}
|
|
13
|
+
return current.run({ tenantId }, fn);
|
|
14
|
+
}
|
|
15
|
+
export function currentTenant() {
|
|
16
|
+
const store = current.getStore();
|
|
17
|
+
if (!store) {
|
|
18
|
+
throw new NoTenantError();
|
|
19
|
+
}
|
|
20
|
+
return store.tenantId;
|
|
21
|
+
}
|
|
22
|
+
export function currentTenantOrNull() {
|
|
23
|
+
return current.getStore()?.tenantId ?? null;
|
|
24
|
+
}
|
|
25
|
+
/** The one tenant of a runtime that runs alone (desktop app, own machine). */
|
|
26
|
+
export const LOCAL_TENANT = "local";
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createTool } from "@mastra/core/tools";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { assertPublicUrl } from "./net-guard.js";
|
|
5
|
+
import { attempt, clip } from "./shared.js";
|
|
6
|
+
const ref = (n) => `[data-wz-ref="${n}"]`;
|
|
7
|
+
/** The page as an agent reads it: its text and its interactive elements, numbered. */
|
|
8
|
+
export async function pageSnapshot(page) {
|
|
9
|
+
const data = await page.evaluate(() => {
|
|
10
|
+
const els = Array.from(document.querySelectorAll("a[href], button, input, textarea, select, [role=button], [role=link], [contenteditable=true]")).filter((el) => {
|
|
11
|
+
const r = el.getBoundingClientRect();
|
|
12
|
+
return r.width > 0 && r.height > 0 && el.type !== "hidden";
|
|
13
|
+
});
|
|
14
|
+
for (const old of Array.from(document.querySelectorAll("[data-wz-ref]"))) {
|
|
15
|
+
old.removeAttribute("data-wz-ref");
|
|
16
|
+
}
|
|
17
|
+
const items = els.slice(0, 120).map((el, i) => {
|
|
18
|
+
el.setAttribute("data-wz-ref", String(i));
|
|
19
|
+
const input = el;
|
|
20
|
+
const button = ["submit", "button", "reset"].includes(input.type);
|
|
21
|
+
const field = (el.tagName === "INPUT" && !button) || el.tagName === "TEXTAREA";
|
|
22
|
+
// What stands in a field may be something the person typed: the model learns that a
|
|
23
|
+
// field is filled, never with what.
|
|
24
|
+
const label = el.getAttribute("aria-label") ||
|
|
25
|
+
input.placeholder ||
|
|
26
|
+
(field ? "" : el.innerText || (button ? input.value : "")) ||
|
|
27
|
+
el.getAttribute("name") ||
|
|
28
|
+
"";
|
|
29
|
+
const tag = el.tagName.toLowerCase();
|
|
30
|
+
const kind = tag === "input" ? `input(${input.type || "text"})` : tag;
|
|
31
|
+
const filled = field && input.value ? " (filled)" : "";
|
|
32
|
+
return `[${i}] ${kind} ${label.trim().replace(/\s+/g, " ").slice(0, 80)}${filled}`;
|
|
33
|
+
});
|
|
34
|
+
return { text: document.body?.innerText ?? "", items };
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
url: page.url(),
|
|
38
|
+
title: await page.title(),
|
|
39
|
+
text: clip(data.text, 6000),
|
|
40
|
+
elements: data.items.join("\n"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function originOf(url) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(url);
|
|
46
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.origin : null;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** A wait that timed out is a rejection, so waits can be raced. */
|
|
53
|
+
function found(wait) {
|
|
54
|
+
const p = wait.then((v) => v ?? Promise.reject(new Error("none")));
|
|
55
|
+
p.catch(() => undefined);
|
|
56
|
+
return p;
|
|
57
|
+
}
|
|
58
|
+
const settle = (page) => page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => undefined);
|
|
59
|
+
/**
|
|
60
|
+
* Fetches a URL with the browser's sign-in, outside any page. Page requests pass the context's
|
|
61
|
+
* host guard; this one does not, so every redirect hop is checked here.
|
|
62
|
+
*/
|
|
63
|
+
async function signedInGet(page, url) {
|
|
64
|
+
let next = url;
|
|
65
|
+
for (let hop = 0; hop < 6; hop++) {
|
|
66
|
+
const safe = await assertPublicUrl(next);
|
|
67
|
+
const res = await page
|
|
68
|
+
.context()
|
|
69
|
+
.request.get(safe.toString(), { timeout: 60_000, maxRedirects: 0 });
|
|
70
|
+
const location = res.headers().location;
|
|
71
|
+
if (res.status() < 300 || res.status() >= 400 || !location) {
|
|
72
|
+
return res;
|
|
73
|
+
}
|
|
74
|
+
next = new URL(location, safe).toString();
|
|
75
|
+
}
|
|
76
|
+
throw new Error("Too many redirects.");
|
|
77
|
+
}
|
|
78
|
+
const credentialField = z.object({
|
|
79
|
+
ref: z.number().describe("The field's [number] from your latest page snapshot."),
|
|
80
|
+
label: z.string().min(1).max(60).describe("What the person sees, e.g. E-Mail."),
|
|
81
|
+
kind: z
|
|
82
|
+
.enum(["username", "password", "otp", "text"])
|
|
83
|
+
.describe("password only fills a password input."),
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Types the person's values into the page. A password goes only into a password input, and
|
|
87
|
+
* only while the page is still the site the person was shown. Failures are codes: a browser
|
|
88
|
+
* error message could quote the value it failed to type.
|
|
89
|
+
*/
|
|
90
|
+
async function fillCredentials(page, origin, fields, values) {
|
|
91
|
+
if (originOf(page.url()) !== origin) {
|
|
92
|
+
return { error: "origin_changed" };
|
|
93
|
+
}
|
|
94
|
+
const filled = [];
|
|
95
|
+
for (const [index, field] of fields.entries()) {
|
|
96
|
+
const value = values[`field_${index + 1}`];
|
|
97
|
+
if (typeof value !== "string" || !value) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const locator = page.locator(ref(field.ref)).first();
|
|
101
|
+
const element = await locator
|
|
102
|
+
.evaluate((el) => ({ tag: el.tagName, type: el.type ?? "" }), undefined, {
|
|
103
|
+
timeout: 5000,
|
|
104
|
+
})
|
|
105
|
+
.catch(() => null);
|
|
106
|
+
if (!element) {
|
|
107
|
+
return { error: "field_missing" };
|
|
108
|
+
}
|
|
109
|
+
if (element.tag !== "INPUT" && element.tag !== "TEXTAREA") {
|
|
110
|
+
return { error: "not_an_input" };
|
|
111
|
+
}
|
|
112
|
+
if (field.kind === "password" && element.type !== "password") {
|
|
113
|
+
return { error: "not_a_password_field" };
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await locator.fill(value, { timeout: 10_000 });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { error: "fill_failed" };
|
|
120
|
+
}
|
|
121
|
+
filled.push(field.label);
|
|
122
|
+
}
|
|
123
|
+
return { filled };
|
|
124
|
+
}
|
|
125
|
+
const NOBODY = "Nobody is at the keyboard for this run, so a sign-in cannot be asked for. Go on without it and report what the person has to do.";
|
|
126
|
+
const DECLINED = "The person did not sign in. Do not ask again for this site; go on without it and say in your result what is missing and what the person can do.";
|
|
127
|
+
export function browserTools(ctx, assets, files) {
|
|
128
|
+
const page = () => ctx.resources.browserPage();
|
|
129
|
+
const tools = {};
|
|
130
|
+
// There is one page. A model may ask for several browser actions at once; they run one after
|
|
131
|
+
// the other, in the order asked — else a navigation would cut a download short.
|
|
132
|
+
let queue = Promise.resolve();
|
|
133
|
+
const inTurn = (run) => {
|
|
134
|
+
const next = queue.then(() => attempt(run));
|
|
135
|
+
queue = next;
|
|
136
|
+
return next;
|
|
137
|
+
};
|
|
138
|
+
tools.browser_open = createTool({
|
|
139
|
+
id: "browser_open",
|
|
140
|
+
description: "Open a URL in the browser and return its text plus numbered interactive elements. Sites the person signed in to on an earlier run may still be signed in.",
|
|
141
|
+
inputSchema: z.object({ url: z.string() }),
|
|
142
|
+
execute: ({ url }) => inTurn(async () => {
|
|
143
|
+
const safe = await assertPublicUrl(url);
|
|
144
|
+
await ctx.emit("tool", `Öffnet ${safe.hostname}`);
|
|
145
|
+
const p = await page();
|
|
146
|
+
await p.goto(safe.toString(), { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
147
|
+
await p.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => undefined);
|
|
148
|
+
return pageSnapshot(p);
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
tools.browser_click = createTool({
|
|
152
|
+
id: "browser_click",
|
|
153
|
+
description: "Click an element by its [number] from the last snapshot.",
|
|
154
|
+
inputSchema: z.object({ ref: z.number() }),
|
|
155
|
+
execute: ({ ref: n }) => inTurn(async () => {
|
|
156
|
+
const p = await page();
|
|
157
|
+
await ctx.emit("tool", "Klickt im Browser");
|
|
158
|
+
const popup = p
|
|
159
|
+
.context()
|
|
160
|
+
.waitForEvent("page", { timeout: 3000 })
|
|
161
|
+
.catch(() => null);
|
|
162
|
+
await p.locator(ref(n)).first().click({ timeout: 10_000 });
|
|
163
|
+
const opened = await popup;
|
|
164
|
+
if (opened) {
|
|
165
|
+
ctx.resources.adopt(opened);
|
|
166
|
+
await settle(opened);
|
|
167
|
+
return pageSnapshot(opened);
|
|
168
|
+
}
|
|
169
|
+
await settle(p);
|
|
170
|
+
return pageSnapshot(p);
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
tools.browser_type = createTool({
|
|
174
|
+
id: "browser_type",
|
|
175
|
+
description: "Type text into an input by its [number]; set submit to press Enter afterwards. Never for passwords or codes — those the person enters through browser_request_credentials.",
|
|
176
|
+
inputSchema: z.object({ ref: z.number(), text: z.string(), submit: z.boolean().optional() }),
|
|
177
|
+
execute: ({ ref: n, text, submit }) => inTurn(async () => {
|
|
178
|
+
const p = await page();
|
|
179
|
+
await ctx.emit("tool", "Tippt im Browser");
|
|
180
|
+
const el = p.locator(ref(n)).first();
|
|
181
|
+
await el.fill(text, { timeout: 10_000 });
|
|
182
|
+
if (submit) {
|
|
183
|
+
await el.press("Enter");
|
|
184
|
+
await settle(p);
|
|
185
|
+
}
|
|
186
|
+
return pageSnapshot(p);
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
tools.browser_screenshot = createTool({
|
|
190
|
+
id: "browser_screenshot",
|
|
191
|
+
description: "Look at the current page: returns a picture of it to you. Use it when the text snapshot is not enough (charts, a layout you cannot make sense of, a captcha or cookie wall). Set save to also hand the picture to the person as part of the result.",
|
|
192
|
+
inputSchema: z.object({ save: z.boolean().optional(), name: z.string().optional() }),
|
|
193
|
+
execute: ({ save, name }) => inTurn(async () => {
|
|
194
|
+
const p = await page();
|
|
195
|
+
await ctx.emit("tool", "Sieht sich die Seite an");
|
|
196
|
+
const png = await p.screenshot({ type: "png" });
|
|
197
|
+
let saved;
|
|
198
|
+
if (save) {
|
|
199
|
+
const asset = await ctx.saveAsset({
|
|
200
|
+
kind: "image",
|
|
201
|
+
mime: "image/png",
|
|
202
|
+
name: name ?? "screenshot.png",
|
|
203
|
+
data: png,
|
|
204
|
+
});
|
|
205
|
+
assets.push(asset);
|
|
206
|
+
saved = asset.id;
|
|
207
|
+
}
|
|
208
|
+
return { url: p.url(), saved, screenshot: Buffer.from(png).toString("base64") };
|
|
209
|
+
}),
|
|
210
|
+
toModelOutput: (out) => {
|
|
211
|
+
if (!out?.screenshot) {
|
|
212
|
+
return { type: "json", value: out };
|
|
213
|
+
}
|
|
214
|
+
const { screenshot, ...rest } = out;
|
|
215
|
+
return {
|
|
216
|
+
type: "content",
|
|
217
|
+
value: [
|
|
218
|
+
{ type: "text", text: JSON.stringify(rest) },
|
|
219
|
+
{ type: "image-data", data: screenshot, mediaType: "image/png" },
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const handOver = async (input) => {
|
|
225
|
+
const p = await page();
|
|
226
|
+
const origin = originOf(p.url());
|
|
227
|
+
if (!origin) {
|
|
228
|
+
return { error: "no_site", note: "Open the site's sign-in page first." };
|
|
229
|
+
}
|
|
230
|
+
for (const field of input.fields) {
|
|
231
|
+
if (!(await p.locator(ref(field.ref)).count())) {
|
|
232
|
+
return {
|
|
233
|
+
error: "fields_not_found",
|
|
234
|
+
note: `[${field.ref}] is not on the page any more. Open or re-read the page and ask again with its numbers.`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
await ctx.emit("tool", `Wartet auf deine Anmeldung bei ${new URL(origin).hostname}`);
|
|
239
|
+
const answer = await ctx.ask({
|
|
240
|
+
kind: "login",
|
|
241
|
+
reason: input.reason,
|
|
242
|
+
site: { host: new URL(origin).hostname, title: await p.title() },
|
|
243
|
+
fields: input.fields.map((f, i) => ({
|
|
244
|
+
id: `field_${i + 1}`,
|
|
245
|
+
label: f.label,
|
|
246
|
+
secret: f.kind === "password",
|
|
247
|
+
})),
|
|
248
|
+
});
|
|
249
|
+
if (!answer) {
|
|
250
|
+
return { status: "needs_person", note: NOBODY };
|
|
251
|
+
}
|
|
252
|
+
if (answer.type === "skip" || answer.type === "timeout") {
|
|
253
|
+
return { status: "declined", note: DECLINED };
|
|
254
|
+
}
|
|
255
|
+
// The person may have gone on in a tab the site opened.
|
|
256
|
+
const current = ctx.resources.openPage() ?? p;
|
|
257
|
+
let note = "The person signed in on the page themselves.";
|
|
258
|
+
if (answer.type === "fill") {
|
|
259
|
+
const result = await fillCredentials(current, origin, input.fields, answer.values);
|
|
260
|
+
if ("error" in result) {
|
|
261
|
+
return {
|
|
262
|
+
error: result.error,
|
|
263
|
+
note: "Nothing you can read was entered. Re-read the page and ask again with its current numbers.",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (result.filled.length && input.submitRef !== undefined) {
|
|
267
|
+
await current
|
|
268
|
+
.locator(ref(input.submitRef))
|
|
269
|
+
.first()
|
|
270
|
+
.click({ timeout: 10_000 })
|
|
271
|
+
.catch(() => undefined);
|
|
272
|
+
}
|
|
273
|
+
await settle(current);
|
|
274
|
+
await current.waitForLoadState("networkidle", { timeout: 5000 }).catch(() => undefined);
|
|
275
|
+
note = `The person entered ${result.filled.join(", ")} straight into the page${input.submitRef !== undefined ? " and it was submitted" : ""}. You do not see what they typed and must not try to read it back.`;
|
|
276
|
+
}
|
|
277
|
+
if (answer.remember) {
|
|
278
|
+
await ctx.resources.keepSession().catch(() => undefined);
|
|
279
|
+
}
|
|
280
|
+
return { status: "done", note, page: await pageSnapshot(current) };
|
|
281
|
+
};
|
|
282
|
+
tools.browser_request_credentials = createTool({
|
|
283
|
+
id: "browser_request_credentials",
|
|
284
|
+
description: "THE way to get a login into a page: ask the person to enter a username, e-mail, password or one-time code into fields on your current page, WITHOUT you seeing it. The person gets a picture of the page, the site's address and a form; what they type goes straight into the page, never to you. Give each field's [number] from your latest snapshot, and submit_ref to press the sign-in or next button afterwards. A login spread over several pages (e-mail, then password, then code) is one call per page. WAITS for the person. Never ask for a password any other way.",
|
|
285
|
+
inputSchema: z.object({
|
|
286
|
+
reason: z
|
|
287
|
+
.string()
|
|
288
|
+
.min(1)
|
|
289
|
+
.max(300)
|
|
290
|
+
.describe("One sentence for the person, in their language: which site and why."),
|
|
291
|
+
fields: z.array(credentialField).min(1).max(4),
|
|
292
|
+
submit_ref: z
|
|
293
|
+
.number()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe("[number] of the button to press once the fields are filled."),
|
|
296
|
+
}),
|
|
297
|
+
execute: ({ reason, fields, submit_ref }) => inTurn(() => handOver({ reason, fields, submitRef: submit_ref })),
|
|
298
|
+
});
|
|
299
|
+
tools.browser_request_user = createTool({
|
|
300
|
+
id: "browser_request_user",
|
|
301
|
+
description: "Hand the page to the person for something no form can do: a captcha, 'continue with Google', a passkey, a cookie wall you cannot get past. They click and type in a picture of the page and say when they are done. For a plain login form use browser_request_credentials instead. WAITS for the person.",
|
|
302
|
+
inputSchema: z.object({
|
|
303
|
+
reason: z
|
|
304
|
+
.string()
|
|
305
|
+
.min(1)
|
|
306
|
+
.max(300)
|
|
307
|
+
.describe("One sentence for the person, in their language: what to do on the page."),
|
|
308
|
+
}),
|
|
309
|
+
execute: ({ reason }) => inTurn(() => handOver({ reason, fields: [] })),
|
|
310
|
+
});
|
|
311
|
+
tools.browser_download = createTool({
|
|
312
|
+
id: "browser_download",
|
|
313
|
+
description: "Download a file (an invoice PDF, an export) with the browser's sign-in and keep it: click the element with this [number], or fetch this URL. The file is stored under `path` in the wizard's files and added to this step's result.",
|
|
314
|
+
inputSchema: z.object({
|
|
315
|
+
ref: z.number().optional().describe("[number] of the download link or button."),
|
|
316
|
+
url: z.string().optional().describe("Direct URL of the file, instead of ref."),
|
|
317
|
+
path: z
|
|
318
|
+
.string()
|
|
319
|
+
.describe("Where to keep it, e.g. invoices/2026-09/2026-09-03_Notion_INV-123.pdf"),
|
|
320
|
+
source: z.string().optional().describe("Where it came from, in words, for the person."),
|
|
321
|
+
}),
|
|
322
|
+
execute: ({ ref: n, url, path, source }) => inTurn(async () => {
|
|
323
|
+
const p = await page();
|
|
324
|
+
await ctx.emit("tool", `Lädt ${path.split("/").pop()} herunter`);
|
|
325
|
+
let data;
|
|
326
|
+
let mime;
|
|
327
|
+
if (url) {
|
|
328
|
+
const res = await signedInGet(p, new URL(url, p.url()).toString());
|
|
329
|
+
if (!res.ok()) {
|
|
330
|
+
return { error: `The server answered ${res.status()}.` };
|
|
331
|
+
}
|
|
332
|
+
data = await res.body();
|
|
333
|
+
mime = res.headers()["content-type"]?.split(";")[0];
|
|
334
|
+
}
|
|
335
|
+
else if (n !== undefined) {
|
|
336
|
+
// A click may start a download, open the file in a new tab, or both.
|
|
337
|
+
const download = found(p.waitForEvent("download", { timeout: 12_000 }));
|
|
338
|
+
const popup = found(p.context().waitForEvent("page", { timeout: 5000 }));
|
|
339
|
+
await p.locator(ref(n)).first().click({ timeout: 10_000 });
|
|
340
|
+
const opened = await Promise.race([popup, download.then(() => null)]).catch(() => null);
|
|
341
|
+
const got = await Promise.any([
|
|
342
|
+
download,
|
|
343
|
+
...(opened ? [found(opened.waitForEvent("download", { timeout: 8000 }))] : []),
|
|
344
|
+
]).catch(() => null);
|
|
345
|
+
if (got) {
|
|
346
|
+
data = await readFile(await got.path());
|
|
347
|
+
await opened?.close().catch(() => undefined);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// No download event: the click opened the file in a tab (a PDF viewer).
|
|
351
|
+
const target = opened ?? p;
|
|
352
|
+
await settle(target);
|
|
353
|
+
const res = await signedInGet(p, target.url());
|
|
354
|
+
mime = res.headers()["content-type"]?.split(";")[0];
|
|
355
|
+
if (!res.ok() || mime === "text/html") {
|
|
356
|
+
await opened?.close().catch(() => undefined);
|
|
357
|
+
return {
|
|
358
|
+
error: "The click did not give a file.",
|
|
359
|
+
page: await pageSnapshot(p),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
data = await res.body();
|
|
363
|
+
await opened?.close().catch(() => undefined);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
return { error: "Give ref or url." };
|
|
368
|
+
}
|
|
369
|
+
if (mime === "text/html") {
|
|
370
|
+
return { error: "That is a web page, not a file — you may not be signed in." };
|
|
371
|
+
}
|
|
372
|
+
const kept = await files.keep(path, data, {
|
|
373
|
+
mime,
|
|
374
|
+
source: source ?? `Download von ${new URL(p.url()).hostname}`,
|
|
375
|
+
});
|
|
376
|
+
return { saved: kept.path, mime: kept.mime, size: kept.size };
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
return tools;
|
|
380
|
+
}
|