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,202 @@
|
|
|
1
|
+
import { cleanPath, WORKSPACE_LIMITS } from "@engenty-wizards/shared/workspace";
|
|
2
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { listConnectors } from "../connectors/external.js";
|
|
5
|
+
import { getBlob } from "../files/blobs.js";
|
|
6
|
+
import { ServiceError } from "./errors.js";
|
|
7
|
+
import { draftFiles, writeFile } from "./files.js";
|
|
8
|
+
import { ownedProject } from "./projects.js";
|
|
9
|
+
import { createWizard, deleteWizard, draftIssues, ownedWizard, parseDraft } from "./wizards.js";
|
|
10
|
+
/**
|
|
11
|
+
* A wizard as a file, `<title>.wizard`: a zip with `wizard.json` — the definition and what it
|
|
12
|
+
* needs from its project — and the workspace under `files/`. What belongs to people (runs, lists, connected
|
|
13
|
+
* accounts) and to the project (brand, credentials) stays where it is.
|
|
14
|
+
*/
|
|
15
|
+
const FORMAT = "engenty-wizard";
|
|
16
|
+
const MANIFEST = "wizard.json";
|
|
17
|
+
const FILES = "files/";
|
|
18
|
+
export const PACKAGE_EXTENSION = ".wizard";
|
|
19
|
+
/** A type of its own: under `application/zip` a browser may save the file as `.zip`. */
|
|
20
|
+
export const PACKAGE_MIME = "application/vnd.engenty.wizard+zip";
|
|
21
|
+
/** The workspace's 25 MB, with room for the manifest and the zip's own overhead. */
|
|
22
|
+
export const PACKAGE_MAX_BYTES = 30_000_000;
|
|
23
|
+
const manifestSchema = z.object({
|
|
24
|
+
format: z.literal(FORMAT),
|
|
25
|
+
version: z.literal(1),
|
|
26
|
+
exportedAt: z.string().optional(),
|
|
27
|
+
definition: z.unknown(),
|
|
28
|
+
/** Imported connectors the wizard's connections name. No address: one pasted by hand can hold a secret. */
|
|
29
|
+
connectors: z
|
|
30
|
+
.array(z.object({
|
|
31
|
+
id: z.string().max(60),
|
|
32
|
+
name: z.string().max(120),
|
|
33
|
+
domain: z.string().max(253).optional(),
|
|
34
|
+
sourceKind: z.enum(["openapi", "mcp"]).optional(),
|
|
35
|
+
}))
|
|
36
|
+
.max(50)
|
|
37
|
+
.default([]),
|
|
38
|
+
/** Project MCP servers the wizard's steps name — by name only, their address and headers are credentials. */
|
|
39
|
+
mcpServers: z
|
|
40
|
+
.array(z.object({ id: z.string().max(31), name: z.string().max(60) }))
|
|
41
|
+
.max(10)
|
|
42
|
+
.default([]),
|
|
43
|
+
});
|
|
44
|
+
function fileName(title) {
|
|
45
|
+
const slug = title
|
|
46
|
+
.normalize("NFKD")
|
|
47
|
+
.replace(/[̀-ͯ]/g, "")
|
|
48
|
+
.toLowerCase()
|
|
49
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
50
|
+
.replace(/^-+|-+$/g, "")
|
|
51
|
+
.slice(0, 60);
|
|
52
|
+
return `${slug || "wizard"}${PACKAGE_EXTENSION}`;
|
|
53
|
+
}
|
|
54
|
+
export async function exportWizard(userId, wizardId) {
|
|
55
|
+
const w = await ownedWizard(userId, wizardId);
|
|
56
|
+
const project = await ownedProject(userId, w.projectId);
|
|
57
|
+
const named = new Set((w.draft.connections ?? []).map((c) => c.connector));
|
|
58
|
+
const servers = new Set(w.draft.steps.flatMap((s) => (s.type === "agent" ? (s.mcp ?? []) : [])));
|
|
59
|
+
const manifest = {
|
|
60
|
+
format: FORMAT,
|
|
61
|
+
version: 1,
|
|
62
|
+
exportedAt: new Date().toISOString(),
|
|
63
|
+
definition: w.draft,
|
|
64
|
+
connectors: (await listConnectors(project.id))
|
|
65
|
+
.filter((c) => c.sourceKind !== "builtin" && named.has(c.id))
|
|
66
|
+
.map((c) => ({
|
|
67
|
+
id: c.id,
|
|
68
|
+
name: c.name,
|
|
69
|
+
domain: c.domain,
|
|
70
|
+
sourceKind: c.sourceKind,
|
|
71
|
+
})),
|
|
72
|
+
mcpServers: project.mcpServers
|
|
73
|
+
.filter((s) => servers.has(s.id))
|
|
74
|
+
.map((s) => ({ id: s.id, name: s.name })),
|
|
75
|
+
};
|
|
76
|
+
const entries = {
|
|
77
|
+
[MANIFEST]: strToU8(JSON.stringify(manifest, null, 2)),
|
|
78
|
+
};
|
|
79
|
+
for (const f of await draftFiles(w.id)) {
|
|
80
|
+
entries[FILES + f.path] = new Uint8Array(await getBlob(f.hash));
|
|
81
|
+
}
|
|
82
|
+
return { name: fileName(w.draft.title), zip: zipSync(entries, { level: 6 }) };
|
|
83
|
+
}
|
|
84
|
+
const invalid = (message) => new ServiceError("invalid", message);
|
|
85
|
+
function json(data) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(strFromU8(data));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw invalid("Das ist kein Wizard-Paket: wizard.json ist kein gültiges JSON.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A manifest, or a bare definition: a wizard without workspace is fully told by its JSON. */
|
|
94
|
+
function readManifest(raw) {
|
|
95
|
+
if (raw?.format !== FORMAT) {
|
|
96
|
+
return { format: FORMAT, version: 1, definition: raw, connectors: [], mcpServers: [] };
|
|
97
|
+
}
|
|
98
|
+
const parsed = manifestSchema.safeParse(raw);
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
throw invalid("Dieses Wizard-Paket stammt aus einer neueren Version oder ist beschädigt.");
|
|
101
|
+
}
|
|
102
|
+
return parsed.data;
|
|
103
|
+
}
|
|
104
|
+
/** The package's content, checked against the workspace limits before anything is unpacked. */
|
|
105
|
+
function readPackage(data) {
|
|
106
|
+
const zipped = data[0] === 0x50 && data[1] === 0x4b;
|
|
107
|
+
if (!zipped) {
|
|
108
|
+
return { manifest: readManifest(json(data)), files: [] };
|
|
109
|
+
}
|
|
110
|
+
let total = 0;
|
|
111
|
+
let count = 0;
|
|
112
|
+
let entries;
|
|
113
|
+
try {
|
|
114
|
+
entries = unzipSync(data, {
|
|
115
|
+
filter: (file) => {
|
|
116
|
+
// Folders, and what the Finder adds when a package is zipped by hand.
|
|
117
|
+
if (file.name.endsWith("/") || /(^|\/)(__MACOSX\/|\.DS_Store$)/.test(file.name)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
total += file.originalSize;
|
|
121
|
+
count += 1;
|
|
122
|
+
if (file.originalSize > WORKSPACE_LIMITS.fileBytes) {
|
|
123
|
+
throw invalid(`${file.name} ist größer als 5 MB.`);
|
|
124
|
+
}
|
|
125
|
+
if (count > WORKSPACE_LIMITS.files + 1 || total > PACKAGE_MAX_BYTES) {
|
|
126
|
+
throw invalid("Das Paket ist größer als ein Workspace sein darf (200 Dateien, 25 MB).");
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw err instanceof ServiceError ? err : invalid("Das Paket lässt sich nicht entpacken.");
|
|
134
|
+
}
|
|
135
|
+
// A package unpacked and zipped again often sits in a folder of its own.
|
|
136
|
+
const manifestPath = Object.keys(entries)
|
|
137
|
+
.filter((name) => name === MANIFEST || name.endsWith(`/${MANIFEST}`))
|
|
138
|
+
.sort((a, b) => a.length - b.length)[0];
|
|
139
|
+
if (manifestPath === undefined) {
|
|
140
|
+
throw invalid("Das ist kein Wizard-Paket: wizard.json fehlt.");
|
|
141
|
+
}
|
|
142
|
+
const root = manifestPath.slice(0, -MANIFEST.length);
|
|
143
|
+
const files = Object.entries(entries)
|
|
144
|
+
.filter(([name]) => name.startsWith(root + FILES))
|
|
145
|
+
.map(([name, content]) => {
|
|
146
|
+
const path = cleanPath(name.slice(root.length + FILES.length));
|
|
147
|
+
if (!path) {
|
|
148
|
+
throw invalid(`"${name}" ist kein gültiger Dateipfad.`);
|
|
149
|
+
}
|
|
150
|
+
return { path, data: content };
|
|
151
|
+
});
|
|
152
|
+
if (files.reduce((n, f) => n + f.data.byteLength, 0) > WORKSPACE_LIMITS.totalBytes) {
|
|
153
|
+
throw invalid("Der Workspace im Paket ist größer als 25 MB.");
|
|
154
|
+
}
|
|
155
|
+
return { manifest: readManifest(json(entries[manifestPath])), files };
|
|
156
|
+
}
|
|
157
|
+
/** What the wizard names that the project does not have, in words for the chat. */
|
|
158
|
+
async function missing(userId, projectId, draft, manifest) {
|
|
159
|
+
const project = await ownedProject(userId, projectId);
|
|
160
|
+
const connectors = await listConnectors(project.id);
|
|
161
|
+
const wanted = [...new Set((draft.connections ?? []).flatMap((c) => c.connector ?? []))];
|
|
162
|
+
const servers = [
|
|
163
|
+
...new Set(draft.steps.flatMap((s) => (s.type === "agent" ? (s.mcp ?? []) : []))),
|
|
164
|
+
];
|
|
165
|
+
return [
|
|
166
|
+
...wanted
|
|
167
|
+
.filter((id) => !connectors.some((c) => c.id === id))
|
|
168
|
+
.map((id) => {
|
|
169
|
+
const told = manifest.connectors.find((c) => c.id === id);
|
|
170
|
+
return `Connector „${told?.name ?? id}“${told?.domain ? ` (${told.domain})` : ""}`;
|
|
171
|
+
}),
|
|
172
|
+
...servers
|
|
173
|
+
.filter((id) => !project.mcpServers.some((s) => s.id === id))
|
|
174
|
+
.map((id) => `MCP-Server „${manifest.mcpServers.find((s) => s.id === id)?.name ?? id}“`),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
/** Makes a new wizard of a package in the project: the draft and its workspace, not published. */
|
|
178
|
+
export async function importWizard(userId, projectId, data, name) {
|
|
179
|
+
const { manifest, files } = readPackage(data);
|
|
180
|
+
// The shape is checked before anything is written; a wrong one leaves no wizard behind.
|
|
181
|
+
const { draft } = parseDraft(manifest.definition, files.map((f) => f.path));
|
|
182
|
+
const lacks = await missing(userId, projectId, draft, manifest);
|
|
183
|
+
const note = [
|
|
184
|
+
`Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`,
|
|
185
|
+
lacks.length
|
|
186
|
+
? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
|
|
187
|
+
: "",
|
|
188
|
+
]
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
.join("\n\n");
|
|
191
|
+
const { id } = await createWizard(userId, { projectId, definition: draft, note });
|
|
192
|
+
try {
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
await writeFile(userId, id, file.path, file.data);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
await deleteWizard(userId, id);
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
return { id, issues: await draftIssues(await ownedWizard(userId, id)) };
|
|
202
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { managed } from "../manage.js";
|
|
3
|
+
import { readSetting, writeSetting } from "../settings.js";
|
|
4
|
+
export const profileSchema = z.object({
|
|
5
|
+
name: z.string().trim().max(80),
|
|
6
|
+
about: z.string().trim().max(2000),
|
|
7
|
+
email: z.string().trim().max(200),
|
|
8
|
+
phone: z.string().trim().max(40),
|
|
9
|
+
});
|
|
10
|
+
const key = (user) => `profile:${user.tenantId}:${user.id}`;
|
|
11
|
+
export async function userProfile(user) {
|
|
12
|
+
const stored = (await readSetting(key(user))) ?? {};
|
|
13
|
+
return {
|
|
14
|
+
name: (managed ? "" : stored.name) || user.name,
|
|
15
|
+
about: stored.about ?? "",
|
|
16
|
+
email: (managed ? "" : stored.email) || user.email,
|
|
17
|
+
phone: stored.phone ?? "",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function saveProfile(user, profile) {
|
|
21
|
+
await writeSetting(key(user), profile);
|
|
22
|
+
return userProfile(user);
|
|
23
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { PROJECT_LIMITS, projectFileLimit, } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import { and, asc, count, eq, max } from "drizzle-orm";
|
|
4
|
+
import { db, schema } from "../db/client.js";
|
|
5
|
+
import { documentMime, UnreadableDocument } from "../documents/parse.js";
|
|
6
|
+
import { loadAsset, removeAsset, saveAsset } from "../files/storage.js";
|
|
7
|
+
import { textModel } from "../models.js";
|
|
8
|
+
import { dropLinks, putLink } from "../tenants/control.js";
|
|
9
|
+
import { notFound, ServiceError } from "./errors.js";
|
|
10
|
+
import { dropIndex, indexDocument } from "./project-index.js";
|
|
11
|
+
/** Reading a file takes seconds to a few minutes; a row still `pending` after this was cut off. */
|
|
12
|
+
const STALE_MS = 15 * 60_000;
|
|
13
|
+
const SEEN_BY_MODEL = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
14
|
+
export function fileView(row) {
|
|
15
|
+
const stale = row.status === "pending" && Date.now() - row.updatedAt.getTime() > STALE_MS;
|
|
16
|
+
return {
|
|
17
|
+
id: row.id,
|
|
18
|
+
kind: row.kind,
|
|
19
|
+
name: row.name,
|
|
20
|
+
mime: row.mime,
|
|
21
|
+
size: row.size,
|
|
22
|
+
description: row.description,
|
|
23
|
+
source: row.source,
|
|
24
|
+
status: stale ? "failed" : row.status,
|
|
25
|
+
error: stale ? "Unterbrochen. Bitte neu einlesen." : row.error,
|
|
26
|
+
indexed: row.indexed,
|
|
27
|
+
pages: row.pages,
|
|
28
|
+
chars: row.chars,
|
|
29
|
+
createdAt: row.createdAt.toISOString(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function projectFiles(projectId, kind) {
|
|
33
|
+
return db.query.projectFile.findMany({
|
|
34
|
+
where: kind
|
|
35
|
+
? and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, kind))
|
|
36
|
+
: eq(schema.projectFile.projectId, projectId),
|
|
37
|
+
orderBy: [asc(schema.projectFile.position), asc(schema.projectFile.createdAt)],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function projectFile(projectId, fileId) {
|
|
41
|
+
const row = await db.query.projectFile.findFirst({
|
|
42
|
+
where: and(eq(schema.projectFile.id, fileId), eq(schema.projectFile.projectId, projectId)),
|
|
43
|
+
});
|
|
44
|
+
if (!row) {
|
|
45
|
+
throw notFound();
|
|
46
|
+
}
|
|
47
|
+
return row;
|
|
48
|
+
}
|
|
49
|
+
export async function projectFileContent(projectId, fileId) {
|
|
50
|
+
const row = await projectFile(projectId, fileId);
|
|
51
|
+
const found = await loadAsset(row.id);
|
|
52
|
+
if (!found) {
|
|
53
|
+
throw notFound();
|
|
54
|
+
}
|
|
55
|
+
return { row, data: found.data };
|
|
56
|
+
}
|
|
57
|
+
function accepts(kind, mime) {
|
|
58
|
+
if (kind === "logo") {
|
|
59
|
+
return mime.startsWith("image/");
|
|
60
|
+
}
|
|
61
|
+
if (kind === "asset") {
|
|
62
|
+
return /^(image|video|audio)\//.test(mime);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
export async function addProjectFile(projectId, input) {
|
|
67
|
+
const mime = documentMime(input.name, input.mime) || "application/octet-stream";
|
|
68
|
+
if (!accepts(input.kind, mime)) {
|
|
69
|
+
throw new ServiceError("invalid", input.kind === "logo"
|
|
70
|
+
? "Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP)."
|
|
71
|
+
: "Hierher gehören Bilder, Grafiken, Videos und Audio. Anderes bitte zu den Dokumenten.");
|
|
72
|
+
}
|
|
73
|
+
const limit = projectFileLimit(input.kind, mime);
|
|
74
|
+
if (!input.data.byteLength || input.data.byteLength > limit) {
|
|
75
|
+
throw new ServiceError("invalid", `Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`);
|
|
76
|
+
}
|
|
77
|
+
const scope = and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, input.kind));
|
|
78
|
+
const [{ n, last }] = await db
|
|
79
|
+
.select({ n: count(), last: max(schema.projectFile.position) })
|
|
80
|
+
.from(schema.projectFile)
|
|
81
|
+
.where(scope);
|
|
82
|
+
if (n >= PROJECT_LIMITS.files[input.kind]) {
|
|
83
|
+
throw new ServiceError("refused", `Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`);
|
|
84
|
+
}
|
|
85
|
+
const name = input.name.slice(0, 120) || "datei";
|
|
86
|
+
const ref = await saveAsset({
|
|
87
|
+
kind: input.kind === "logo" ? "logo" : "project",
|
|
88
|
+
mime,
|
|
89
|
+
name,
|
|
90
|
+
data: input.data,
|
|
91
|
+
});
|
|
92
|
+
const description = input.description?.trim().slice(0, 600) ?? "";
|
|
93
|
+
const work = input.kind === "document" || (!description && SEEN_BY_MODEL.includes(mime));
|
|
94
|
+
const [row] = await db
|
|
95
|
+
.insert(schema.projectFile)
|
|
96
|
+
.values({
|
|
97
|
+
id: ref.id,
|
|
98
|
+
projectId,
|
|
99
|
+
kind: input.kind,
|
|
100
|
+
name,
|
|
101
|
+
mime,
|
|
102
|
+
size: input.data.byteLength,
|
|
103
|
+
description,
|
|
104
|
+
source: input.source ?? null,
|
|
105
|
+
position: (last ?? -1) + 1,
|
|
106
|
+
status: work ? "pending" : "ready",
|
|
107
|
+
})
|
|
108
|
+
.returning();
|
|
109
|
+
if (input.kind === "logo") {
|
|
110
|
+
// Logos are shown on public pages, where only the control database knows the tenant.
|
|
111
|
+
await putLink(ref.id, "logo", ref.id);
|
|
112
|
+
}
|
|
113
|
+
if (work) {
|
|
114
|
+
void prepare(row);
|
|
115
|
+
}
|
|
116
|
+
return row;
|
|
117
|
+
}
|
|
118
|
+
// --- reading, describing, indexing -------------------------------------------------
|
|
119
|
+
/** Files are worked through two at a time: each costs model calls. */
|
|
120
|
+
let running = 0;
|
|
121
|
+
const waiting = [];
|
|
122
|
+
async function slot(run) {
|
|
123
|
+
if (running >= 2) {
|
|
124
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
125
|
+
}
|
|
126
|
+
running++;
|
|
127
|
+
try {
|
|
128
|
+
return await run();
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
running--;
|
|
132
|
+
waiting.shift()?.();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function projectLine(projectId) {
|
|
136
|
+
const p = await db.query.project.findFirst({ where: eq(schema.project.id, projectId) });
|
|
137
|
+
return [p?.brand.name || p?.name, p?.brand.about?.slice(0, 300)].filter(Boolean).join(" — ");
|
|
138
|
+
}
|
|
139
|
+
/** One sentence about a picture, for whoever picks among the project's files later. */
|
|
140
|
+
async function describeImage(row, data) {
|
|
141
|
+
const seer = await textModel("standard");
|
|
142
|
+
const result = await generateText({
|
|
143
|
+
model: seer.model,
|
|
144
|
+
maxOutputTokens: 300,
|
|
145
|
+
system: [
|
|
146
|
+
"You catalogue the files of a project. Describe the picture in ONE sentence of at most 25 words: what it shows, its style and colours, and what it suits (a logo on dark ground, a header image, a team photo, a product shot).",
|
|
147
|
+
"Write in the language of the project's description; if unclear, German. No preamble, no quotes.",
|
|
148
|
+
].join("\n"),
|
|
149
|
+
messages: [
|
|
150
|
+
{
|
|
151
|
+
role: "user",
|
|
152
|
+
content: [
|
|
153
|
+
{ type: "file", data, mediaType: row.mime, filename: row.name },
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Project: ${await projectLine(row.projectId)}\nFile: ${row.name} (${row.kind})`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
return result.text.trim().slice(0, 600);
|
|
163
|
+
}
|
|
164
|
+
async function describeDocument(row, text) {
|
|
165
|
+
const reader = await textModel("classifier");
|
|
166
|
+
const result = await generateText({
|
|
167
|
+
model: reader.model,
|
|
168
|
+
maxOutputTokens: 300,
|
|
169
|
+
system: [
|
|
170
|
+
"You catalogue the documents of a project. Say in ONE sentence of at most 30 words what this document is and what can be looked up in it.",
|
|
171
|
+
"Write in the document's language. No preamble, no quotes.",
|
|
172
|
+
].join("\n"),
|
|
173
|
+
prompt: `File: ${row.name}\n\n${text.slice(0, 6000)}`,
|
|
174
|
+
});
|
|
175
|
+
return result.text.trim().slice(0, 600);
|
|
176
|
+
}
|
|
177
|
+
/** Reads a new file: a document goes into the index, and a file nobody described is described. */
|
|
178
|
+
async function prepare(row) {
|
|
179
|
+
await slot(async () => {
|
|
180
|
+
const set = (values) => db
|
|
181
|
+
.update(schema.projectFile)
|
|
182
|
+
.set({ ...values, updatedAt: new Date() })
|
|
183
|
+
.where(eq(schema.projectFile.id, row.id));
|
|
184
|
+
try {
|
|
185
|
+
const found = await loadAsset(row.id);
|
|
186
|
+
if (!found) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const data = new Uint8Array(found.data);
|
|
190
|
+
if (row.kind !== "document") {
|
|
191
|
+
const description = await describeImage(row, data).catch(() => "");
|
|
192
|
+
await set({ status: "ready", ...(description ? { description } : {}) });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const { text, ...indexed } = await indexDocument(row, data);
|
|
196
|
+
const description = row.description || (text ? await describeDocument(row, text).catch(() => "") : "");
|
|
197
|
+
await set({ ...indexed, status: "ready", error: null, description });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.error("[project-file]", row.name, err);
|
|
201
|
+
await set({
|
|
202
|
+
status: "failed",
|
|
203
|
+
error: err instanceof UnreadableDocument
|
|
204
|
+
? err.message
|
|
205
|
+
: "Das Lesen hat nicht geklappt. Bitte noch einmal versuchen.",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
export async function reindexProjectFile(projectId, fileId) {
|
|
211
|
+
const row = await projectFile(projectId, fileId);
|
|
212
|
+
if (row.kind !== "document") {
|
|
213
|
+
throw new ServiceError("invalid", "Nur Dokumente stehen im Index.");
|
|
214
|
+
}
|
|
215
|
+
const [pending] = await db
|
|
216
|
+
.update(schema.projectFile)
|
|
217
|
+
.set({ status: "pending", error: null, updatedAt: new Date() })
|
|
218
|
+
.where(eq(schema.projectFile.id, row.id))
|
|
219
|
+
.returning();
|
|
220
|
+
void prepare(pending);
|
|
221
|
+
return pending;
|
|
222
|
+
}
|
|
223
|
+
export async function updateProjectFile(projectId, fileId, patch) {
|
|
224
|
+
const row = await projectFile(projectId, fileId);
|
|
225
|
+
const [updated] = await db
|
|
226
|
+
.update(schema.projectFile)
|
|
227
|
+
.set({
|
|
228
|
+
...(patch.name?.trim() ? { name: patch.name.trim().slice(0, 120) } : {}),
|
|
229
|
+
...(patch.description !== undefined
|
|
230
|
+
? { description: patch.description.trim().slice(0, 600) }
|
|
231
|
+
: {}),
|
|
232
|
+
})
|
|
233
|
+
.where(eq(schema.projectFile.id, row.id))
|
|
234
|
+
.returning();
|
|
235
|
+
return updated;
|
|
236
|
+
}
|
|
237
|
+
/** Puts the files of one kind into the order of `ids`; the first logo is the one end users see. */
|
|
238
|
+
export async function orderProjectFiles(projectId, kind, ids) {
|
|
239
|
+
const rows = await projectFiles(projectId, kind);
|
|
240
|
+
const order = [...ids.filter((id) => rows.some((r) => r.id === id))];
|
|
241
|
+
for (const r of rows) {
|
|
242
|
+
if (!order.includes(r.id)) {
|
|
243
|
+
order.push(r.id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const [position, id] of order.entries()) {
|
|
247
|
+
await db.update(schema.projectFile).set({ position }).where(eq(schema.projectFile.id, id));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function removeRow(row) {
|
|
251
|
+
await dropIndex(row.id);
|
|
252
|
+
await db.delete(schema.projectFile).where(eq(schema.projectFile.id, row.id));
|
|
253
|
+
await removeAsset(row.id);
|
|
254
|
+
if (row.kind === "logo") {
|
|
255
|
+
await dropLinks([row.id]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function removeProjectFile(projectId, fileId) {
|
|
259
|
+
await removeRow(await projectFile(projectId, fileId));
|
|
260
|
+
}
|
|
261
|
+
/** Before a project goes: its files leave the object store and the public links. */
|
|
262
|
+
export async function removeProjectFiles(projectId) {
|
|
263
|
+
for (const row of await projectFiles(projectId)) {
|
|
264
|
+
await removeRow(row);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { embedMany } from "ai";
|
|
2
|
+
import { eq, inArray, sql } from "drizzle-orm";
|
|
3
|
+
import { db, schema } from "../db/client.js";
|
|
4
|
+
import { parseDocument } from "../documents/parse.js";
|
|
5
|
+
import { createSearchChunks } from "../engenty/search-index/chunks.js";
|
|
6
|
+
import { getBlob, putBlob } from "../files/blobs.js";
|
|
7
|
+
import { embeddingModel } from "../models.js";
|
|
8
|
+
const CHUNK = { mode: "paragraph", max_chunk_length: 1200, overlap: 120 };
|
|
9
|
+
/** Passages of one document; a longer one keeps its start. */
|
|
10
|
+
const MAX_CHUNKS = 2000;
|
|
11
|
+
const vectorBytes = (v) => Buffer.from(new Float32Array(v).buffer);
|
|
12
|
+
/** The page marks of the document converter, as a line a reader understands. */
|
|
13
|
+
function withPageLines(markdown) {
|
|
14
|
+
return markdown.replace(/<page-break number="(\d+)"[^>]*><\/page-break>/g, (_m, n) => `\n\n[Seite ${n}]\n\n`);
|
|
15
|
+
}
|
|
16
|
+
async function embed(texts) {
|
|
17
|
+
const resolved = await embeddingModel();
|
|
18
|
+
if (!resolved || !texts.length) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const result = await embedMany({ model: resolved.model, values: texts, maxParallelCalls: 4 });
|
|
22
|
+
return { vectors: result.embeddings.map((e) => Array.from(e)), model: resolved.ref };
|
|
23
|
+
}
|
|
24
|
+
export async function dropIndex(fileId) {
|
|
25
|
+
await db.delete(schema.projectChunk).where(eq(schema.projectChunk.fileId, fileId));
|
|
26
|
+
}
|
|
27
|
+
/** Reads a document and puts it into the index; what it found goes back onto the file's row. */
|
|
28
|
+
export async function indexDocument(file, data) {
|
|
29
|
+
const parsed = await parseDocument({ data, name: file.name, mime: file.mime });
|
|
30
|
+
const text = withPageLines(parsed.markdown).trim();
|
|
31
|
+
const chunks = createSearchChunks({ doc_id: file.id, text, ...CHUNK }).slice(0, MAX_CHUNKS);
|
|
32
|
+
// Without an embedding model, or when it fails, the keywords still find the passage.
|
|
33
|
+
const embedded = await embed(chunks.map((c) => c.text)).catch((err) => {
|
|
34
|
+
console.error("[project-index] embedding failed", err);
|
|
35
|
+
return null;
|
|
36
|
+
});
|
|
37
|
+
await dropIndex(file.id);
|
|
38
|
+
for (let i = 0; i < chunks.length; i += 100) {
|
|
39
|
+
await db.insert(schema.projectChunk).values(chunks.slice(i, i + 100).map((c, j) => ({
|
|
40
|
+
projectId: file.projectId,
|
|
41
|
+
fileId: file.id,
|
|
42
|
+
idx: c.chunk_index,
|
|
43
|
+
text: c.text,
|
|
44
|
+
embedding: embedded ? vectorBytes(embedded.vectors[i + j]) : null,
|
|
45
|
+
model: embedded?.model ?? null,
|
|
46
|
+
})));
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
textHash: await putBlob(Buffer.from(text, "utf8")),
|
|
50
|
+
pages: parsed.pages,
|
|
51
|
+
chars: text.length,
|
|
52
|
+
indexed: chunks.length ? (embedded ? "embeddings" : "keywords") : null,
|
|
53
|
+
text,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function documentText(file) {
|
|
57
|
+
return file.textHash ? (await getBlob(file.textHash)).toString("utf8") : "";
|
|
58
|
+
}
|
|
59
|
+
const STOP = new Set("der die das den dem des ein eine einen einem einer und oder aber ist sind war wie was wer wo für von mit auf aus bei zu zum zur im in an am es ich du wir ihr sie nicht auch the a an of to and or is are was for with on at by it this that".split(" "));
|
|
60
|
+
/** The question's words as an FTS5 query: any of them, also as the start of a longer word. */
|
|
61
|
+
function keywordQuery(query) {
|
|
62
|
+
const words = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [])]
|
|
63
|
+
.filter((w) => !STOP.has(w))
|
|
64
|
+
.slice(0, 16);
|
|
65
|
+
return words.length ? words.map((w) => `"${w}"*`).join(" OR ") : null;
|
|
66
|
+
}
|
|
67
|
+
/** The passages of the project's documents that fit a question best. */
|
|
68
|
+
export async function searchProject(projectId, query, limit = 6) {
|
|
69
|
+
const lists = [];
|
|
70
|
+
const keywords = keywordQuery(query);
|
|
71
|
+
if (keywords) {
|
|
72
|
+
lists.push(await db.all(sql `
|
|
73
|
+
SELECT c.id, c.file_id, c.idx, c.text
|
|
74
|
+
FROM project_chunk_fts f JOIN project_chunk c ON c.id = f.rowid
|
|
75
|
+
WHERE project_chunk_fts MATCH ${keywords} AND c.project_id = ${projectId}
|
|
76
|
+
ORDER BY bm25(project_chunk_fts) LIMIT 24`));
|
|
77
|
+
}
|
|
78
|
+
const embedded = await embed([query]).catch(() => null);
|
|
79
|
+
if (embedded) {
|
|
80
|
+
lists.push(await db.all(sql `
|
|
81
|
+
SELECT id, file_id, idx, text
|
|
82
|
+
FROM project_chunk
|
|
83
|
+
WHERE project_id = ${projectId} AND model = ${embedded.model}
|
|
84
|
+
ORDER BY vector_distance_cos(embedding, ${vectorBytes(embedded.vectors[0])}) LIMIT 24`));
|
|
85
|
+
}
|
|
86
|
+
// Reciprocal rank fusion: a passage both lists rank high comes first.
|
|
87
|
+
const scored = new Map();
|
|
88
|
+
for (const list of lists) {
|
|
89
|
+
list.forEach((hit, rank) => {
|
|
90
|
+
const entry = scored.get(hit.id) ?? { hit, score: 0 };
|
|
91
|
+
entry.score += 1 / (60 + rank);
|
|
92
|
+
scored.set(hit.id, entry);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const top = [...scored.values()].sort((a, b) => b.score - a.score).slice(0, limit);
|
|
96
|
+
if (!top.length) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const files = await db.query.projectFile.findMany({
|
|
100
|
+
where: inArray(schema.projectFile.id, [...new Set(top.map((t) => t.hit.file_id))]),
|
|
101
|
+
});
|
|
102
|
+
return top.map(({ hit, score }) => ({
|
|
103
|
+
fileId: hit.file_id,
|
|
104
|
+
name: files.find((f) => f.id === hit.file_id)?.name ?? "",
|
|
105
|
+
index: hit.idx,
|
|
106
|
+
text: hit.text,
|
|
107
|
+
score: Math.round(score * 10_000) / 10_000,
|
|
108
|
+
}));
|
|
109
|
+
}
|