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,183 @@
|
|
|
1
|
+
import { allFields, isAudioValue, isLocationValue, itemsTotals, locationText, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { listAsMarkdown } from "@engenty-wizards/shared/store";
|
|
3
|
+
const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
4
|
+
function money(n, currency) {
|
|
5
|
+
try {
|
|
6
|
+
return new Intl.NumberFormat("de-DE", { style: "currency", currency }).format(n);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return `${n.toFixed(2)} ${currency}`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Line items as a Markdown table with their totals — what a model needs to write a document. */
|
|
13
|
+
export function itemsAsMarkdown(field, value, values) {
|
|
14
|
+
const totals = itemsTotals(field, value, values);
|
|
15
|
+
const cols = field.columns ?? [];
|
|
16
|
+
if (!totals.rows.length) {
|
|
17
|
+
return "(no line items)";
|
|
18
|
+
}
|
|
19
|
+
const head = `| ${cols.map((c) => c.label).join(" | ")} | Amount |\n|${cols.map(() => " --- ").join("|")}| ---: |`;
|
|
20
|
+
const body = totals.rows.map((r) => `| ${cols
|
|
21
|
+
.map((c) => c.kind === "money" ? money(Number(r[c.id]) || 0, totals.currency) : String(r[c.id] ?? ""))
|
|
22
|
+
.join(" | ")} | ${money(r.amount, totals.currency)} |`);
|
|
23
|
+
const sums = [
|
|
24
|
+
`Net: ${money(totals.net, totals.currency)}`,
|
|
25
|
+
totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency)}` : null,
|
|
26
|
+
`Total: ${money(totals.gross, totals.currency)}`,
|
|
27
|
+
].filter(Boolean);
|
|
28
|
+
return [head, ...body, "", ...sums].join("\n");
|
|
29
|
+
}
|
|
30
|
+
/** Facts as lines a model reads: "Label: value". */
|
|
31
|
+
export function factLines(facts) {
|
|
32
|
+
return facts.map((f) => `${f.label}: ${f.value}`).join("\n");
|
|
33
|
+
}
|
|
34
|
+
export function colorLine(colors) {
|
|
35
|
+
return colors.map((c) => (c.name ? `${c.name} ${c.value}` : c.value)).join(", ");
|
|
36
|
+
}
|
|
37
|
+
function brandRef(brand, key) {
|
|
38
|
+
switch (key) {
|
|
39
|
+
case "name":
|
|
40
|
+
return brand.name ?? "";
|
|
41
|
+
case "about":
|
|
42
|
+
return brand.about ?? "";
|
|
43
|
+
// What the free "details" text once held is now the description and the facts.
|
|
44
|
+
case "details":
|
|
45
|
+
return [brand.about, factLines(brand.facts ?? [])].filter(Boolean).join("\n");
|
|
46
|
+
case "accent":
|
|
47
|
+
return brand.colors?.[0]?.value ?? "";
|
|
48
|
+
case "colors":
|
|
49
|
+
return colorLine(brand.colors ?? []);
|
|
50
|
+
default:
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function stringify(v) {
|
|
55
|
+
if (v === undefined || v === null) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
if (typeof v === "string") {
|
|
59
|
+
return v;
|
|
60
|
+
}
|
|
61
|
+
if (typeof v === "number" || typeof v === "boolean") {
|
|
62
|
+
return String(v);
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(v) && v.every((x) => typeof x !== "object")) {
|
|
65
|
+
return v.join(", ");
|
|
66
|
+
}
|
|
67
|
+
return JSON.stringify(v, null, 2);
|
|
68
|
+
}
|
|
69
|
+
export function resolveRef(ref, scope) {
|
|
70
|
+
const { def, state, brand } = scope;
|
|
71
|
+
const [head, ...rest] = ref.split(".");
|
|
72
|
+
if (head === "lists") {
|
|
73
|
+
const list = scope.lists?.[rest[0] ?? ""];
|
|
74
|
+
return list ? listAsMarkdown(list.def, list.rows) : "";
|
|
75
|
+
}
|
|
76
|
+
if (head === "today") {
|
|
77
|
+
return new Date().toISOString().slice(0, 10);
|
|
78
|
+
}
|
|
79
|
+
if (head === "item" || head === "index" || head === "count") {
|
|
80
|
+
const entry = scope.entry;
|
|
81
|
+
if (!entry) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
if (head === "index") {
|
|
85
|
+
return entry.index + 1;
|
|
86
|
+
}
|
|
87
|
+
if (head === "count") {
|
|
88
|
+
return entry.count;
|
|
89
|
+
}
|
|
90
|
+
let cur = entry.item;
|
|
91
|
+
for (const key of rest) {
|
|
92
|
+
cur = cur && typeof cur === "object" ? cur[key] : undefined;
|
|
93
|
+
}
|
|
94
|
+
return cur;
|
|
95
|
+
}
|
|
96
|
+
if (head === "brand") {
|
|
97
|
+
return brandRef(brand, rest[0] ?? "name");
|
|
98
|
+
}
|
|
99
|
+
if (head === "facts") {
|
|
100
|
+
const facts = brand.facts ?? [];
|
|
101
|
+
return rest[0] ? (facts.find((f) => f.key === rest[0])?.value ?? "") : factLines(facts);
|
|
102
|
+
}
|
|
103
|
+
if (head === "steps") {
|
|
104
|
+
const out = state.outputs[rest[0] ?? ""];
|
|
105
|
+
if (!out) {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
if (rest.length === 1) {
|
|
109
|
+
return out.json !== undefined ? out.json : (out.text ?? "");
|
|
110
|
+
}
|
|
111
|
+
let cur = out.json;
|
|
112
|
+
for (const key of rest.slice(1)) {
|
|
113
|
+
cur = cur && typeof cur === "object" ? cur[key] : undefined;
|
|
114
|
+
}
|
|
115
|
+
return cur;
|
|
116
|
+
}
|
|
117
|
+
const field = allFields(def).find((f) => f.id === head);
|
|
118
|
+
const value = state.values[head];
|
|
119
|
+
if (field?.kind === "items") {
|
|
120
|
+
if (!rest.length) {
|
|
121
|
+
return itemsAsMarkdown(field, value, state.values);
|
|
122
|
+
}
|
|
123
|
+
const totals = itemsTotals(field, value, state.values);
|
|
124
|
+
return totals[rest[0]];
|
|
125
|
+
}
|
|
126
|
+
if (field?.kind === "image" || field?.kind === "file") {
|
|
127
|
+
// The files themselves are read with read_document / scan_documents by these names.
|
|
128
|
+
const ids = (Array.isArray(value) ? value : [value]).filter((v) => typeof v === "string" && v.length > 0);
|
|
129
|
+
return ids.map((id) => `upload:${id}`).join(", ");
|
|
130
|
+
}
|
|
131
|
+
if (field?.kind === "location") {
|
|
132
|
+
if (!isLocationValue(value)) {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
switch (rest[0]) {
|
|
136
|
+
case undefined:
|
|
137
|
+
return locationText(value);
|
|
138
|
+
case "map":
|
|
139
|
+
return value.lat === undefined
|
|
140
|
+
? ""
|
|
141
|
+
: `https://www.openstreetmap.org/?mlat=${value.lat}&mlon=${value.lng}#map=17/${value.lat}/${value.lng}`;
|
|
142
|
+
default:
|
|
143
|
+
return value[rest[0]];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (field?.kind === "audio") {
|
|
147
|
+
if (!isAudioValue(value)) {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
if (rest[0] === "seconds") {
|
|
151
|
+
return value.seconds;
|
|
152
|
+
}
|
|
153
|
+
// A recording reads as what is said in it.
|
|
154
|
+
return value.transcript?.trim() || "(voice note without intelligible speech)";
|
|
155
|
+
}
|
|
156
|
+
if (field?.kind === "signature") {
|
|
157
|
+
// Documents place it as an image: <img src="asset://ID">.
|
|
158
|
+
return typeof value === "string" && value ? `asset://${value}` : "";
|
|
159
|
+
}
|
|
160
|
+
if (field?.kind === "list") {
|
|
161
|
+
const list = scope.lists?.[field.list ?? ""];
|
|
162
|
+
return list ? listAsMarkdown(list.def, list.rows) : "";
|
|
163
|
+
}
|
|
164
|
+
if (field?.kind === "toggle") {
|
|
165
|
+
return value ? "yes" : "no";
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
export function renderTemplate(template, scope) {
|
|
170
|
+
return template.replace(TEMPLATE_RE, (_m, ref) => stringify(resolveRef(ref.trim(), scope)));
|
|
171
|
+
}
|
|
172
|
+
/** Everything the person entered so far, readable — handed to agents as data. */
|
|
173
|
+
export function answersAsText(scope) {
|
|
174
|
+
const lines = [];
|
|
175
|
+
for (const field of allFields(scope.def)) {
|
|
176
|
+
const value = scope.state.values[field.id];
|
|
177
|
+
if (value === undefined || value === "" || (Array.isArray(value) && !value.length)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
lines.push(`### ${field.label}\n${stringify(resolveRef(field.id, scope))}`);
|
|
181
|
+
}
|
|
182
|
+
return lines.join("\n\n");
|
|
183
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
/** Load `.env.local` then `.env` without overriding the real environment. */
|
|
6
|
+
function loadEnvFiles() {
|
|
7
|
+
for (const name of [".env.local", ".env"]) {
|
|
8
|
+
const path = resolve(process.cwd(), name);
|
|
9
|
+
if (!existsSync(path)) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
13
|
+
const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
14
|
+
if (!match || process.env[match[1]] !== undefined) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
process.env[match[1]] = match[2].replace(/^(['"])(.*)\1$/, "$2");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
loadEnvFiles();
|
|
22
|
+
const str = (key, fallback = "") => process.env[key]?.trim() || fallback;
|
|
23
|
+
/**
|
|
24
|
+
* Without `SANDBOX`: Docker when the sandbox image is on this machine, else agentOS when its
|
|
25
|
+
* packages are installed (a checkout has them, the image and the desktop app do not), else none.
|
|
26
|
+
* The image is not published, so a fresh install would otherwise offer a tool that cannot start.
|
|
27
|
+
*/
|
|
28
|
+
function sandboxEngine(image) {
|
|
29
|
+
const engine = str("SANDBOX");
|
|
30
|
+
if (str("SANDBOX_ENABLED", "1") !== "1" || engine === "off") {
|
|
31
|
+
return "off";
|
|
32
|
+
}
|
|
33
|
+
if (engine) {
|
|
34
|
+
return engine === "agentos" ? "agentos" : "docker";
|
|
35
|
+
}
|
|
36
|
+
if (dockerHasImage(image)) {
|
|
37
|
+
return "docker";
|
|
38
|
+
}
|
|
39
|
+
return agentOsInstalled() ? "agentos" : "off";
|
|
40
|
+
}
|
|
41
|
+
function dockerHasImage(image) {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 5000 });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** agentOS ships its sidecar for macOS and Linux on arm64 and x64 only. */
|
|
51
|
+
function agentOsInstalled() {
|
|
52
|
+
if (!["darwin", "linux"].includes(process.platform) || !["arm64", "x64"].includes(process.arch)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
import.meta.resolve("@rivet-dev/agentos-core");
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const num = (key, fallback) => {
|
|
64
|
+
const v = Number(process.env[key]);
|
|
65
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
66
|
+
};
|
|
67
|
+
const dataDir = resolve(str("DATA_DIR", "data"));
|
|
68
|
+
/**
|
|
69
|
+
* Signs cookies and links and derives the store's encryption key. A runtime that runs alone
|
|
70
|
+
* makes its own once and keeps it in the data folder.
|
|
71
|
+
*/
|
|
72
|
+
function appSecret() {
|
|
73
|
+
const given = str("APP_SECRET");
|
|
74
|
+
if (given) {
|
|
75
|
+
return given;
|
|
76
|
+
}
|
|
77
|
+
mkdirSync(dataDir, { recursive: true });
|
|
78
|
+
const file = join(dataDir, "secret");
|
|
79
|
+
if (!existsSync(file)) {
|
|
80
|
+
writeFileSync(file, randomBytes(32).toString("hex"), { mode: 0o600 });
|
|
81
|
+
}
|
|
82
|
+
return readFileSync(file, "utf8").trim();
|
|
83
|
+
}
|
|
84
|
+
const manageUrl = str("MANAGE_URL").replace(/\/$/, "");
|
|
85
|
+
const port = num("API_PORT", 24368);
|
|
86
|
+
/** From source the Vite dev server on :5181 serves the pages; built, this server does. */
|
|
87
|
+
const fromSource = import.meta.url.endsWith(".ts");
|
|
88
|
+
const sandboxImage = str("SANDBOX_IMAGE", "engenty-sandbox:latest");
|
|
89
|
+
export const env = {
|
|
90
|
+
production: process.env.NODE_ENV === "production",
|
|
91
|
+
port,
|
|
92
|
+
/** Listen address; a runtime that runs alone stays on the loopback interface. */
|
|
93
|
+
host: str("API_HOST", manageUrl ? "0.0.0.0" : "127.0.0.1"),
|
|
94
|
+
/** The origin people open, e.g. https://wizards.localhost — auth callbacks and links use it. */
|
|
95
|
+
appUrl: str("APP_URL", fromSource ? "http://localhost:5181" : `http://localhost:${port}`).replace(/\/$/, ""),
|
|
96
|
+
/**
|
|
97
|
+
* Where a signed-out visitor of the start page is sent instead of the sign-in, e.g. a landing
|
|
98
|
+
* page while sign-up is closed. The sign-in stays at /sign-in. Empty: the start page signs in.
|
|
99
|
+
*/
|
|
100
|
+
signedOutUrl: str("SIGNED_OUT_URL"),
|
|
101
|
+
/** More host names this server answers under, besides APP_URL's and the loopback names. */
|
|
102
|
+
allowedHosts: str("ALLOWED_HOSTS")
|
|
103
|
+
.split(",")
|
|
104
|
+
.map((h) => h.trim())
|
|
105
|
+
.filter(Boolean),
|
|
106
|
+
dataDir,
|
|
107
|
+
/** The control database; empty = a libSQL file in the data folder. */
|
|
108
|
+
databaseUrl: str("DATABASE_URL", ""),
|
|
109
|
+
databaseAuthToken: str("DATABASE_AUTH_TOKEN"),
|
|
110
|
+
/** Tenant databases on a libSQL server: `{tenant}` is replaced by the tenant id. Empty = files. */
|
|
111
|
+
tenantDbUrlTemplate: str("TENANT_DB_URL_TEMPLATE"),
|
|
112
|
+
tenantDbAuthToken: str("TENANT_DB_AUTH_TOKEN", str("DATABASE_AUTH_TOKEN")),
|
|
113
|
+
authSecret: appSecret(),
|
|
114
|
+
/** Where OAuth providers send people back after connecting an account; default <APP_URL>/api/connect/callback. */
|
|
115
|
+
connectRedirectUrl: str("CONNECT_REDIRECT_URL"),
|
|
116
|
+
/** Encrypts connected accounts and kept browser sessions; falls back to the app secret. */
|
|
117
|
+
storeKey: str("STORE_ENC_KEY"),
|
|
118
|
+
devLogin: process.env.NODE_ENV !== "production" && str("DEV_LOGIN") === "1",
|
|
119
|
+
/**
|
|
120
|
+
* The Manage-App this runtime belongs to. Set = people sign in there, tenants come from its
|
|
121
|
+
* tokens and model calls go through its gateway. Empty = the runtime runs alone with one tenant.
|
|
122
|
+
*/
|
|
123
|
+
manage: {
|
|
124
|
+
url: manageUrl,
|
|
125
|
+
gatewayUrl: str("GATEWAY_URL").replace(/\/$/, ""),
|
|
126
|
+
clientId: str("MANAGE_CLIENT_ID"),
|
|
127
|
+
clientSecret: str("MANAGE_CLIENT_SECRET"),
|
|
128
|
+
/** Authenticates this runtime at the Manage-App and the gateway, and the Manage-App here. */
|
|
129
|
+
serviceKey: str("MANAGE_SERVICE_KEY"),
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* A runtime that runs alone answers the studio only with a cookie, set by opening
|
|
133
|
+
* `/api/local/enter?k=<key>` once. The desktop app passes the key; else one is printed at start.
|
|
134
|
+
*/
|
|
135
|
+
local: {
|
|
136
|
+
accessKey: str("LOCAL_ACCESS_KEY"),
|
|
137
|
+
/** The Manage-App a local runtime links an account to (credits, publishing). */
|
|
138
|
+
accountUrl: str("ACCOUNT_URL", "https://account.engenty.ai").replace(/\/$/, ""),
|
|
139
|
+
gatewayUrl: str("ACCOUNT_GATEWAY_URL", "https://gateway.engenty.ai").replace(/\/$/, ""),
|
|
140
|
+
/** The cloud runtime "publish to the cloud" sends wizards to. */
|
|
141
|
+
cloudUrl: str("CLOUD_URL", "https://engenty.ai").replace(/\/$/, ""),
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* The marketplace (docs/marketplace-contract.md): searched live, its starters and what people
|
|
145
|
+
* starred kept for offline use. `off` = none.
|
|
146
|
+
*/
|
|
147
|
+
marketplace: {
|
|
148
|
+
url: str("MARKETPLACE_URL", "https://engenty.ai/wizards")
|
|
149
|
+
.replace(/^off$/, "")
|
|
150
|
+
.replace(/\/+$/, ""),
|
|
151
|
+
},
|
|
152
|
+
aiGatewayKey: str("AI_GATEWAY_API_KEY"),
|
|
153
|
+
openaiKey: str("OPENAI_API_KEY"),
|
|
154
|
+
anthropicKey: str("ANTHROPIC_API_KEY"),
|
|
155
|
+
ollamaUrl: str("OLLAMA_URL", "http://127.0.0.1:11434/v1"),
|
|
156
|
+
/** What each model class runs on when this runtime resolves classes itself (own keys, Ollama). */
|
|
157
|
+
models: {
|
|
158
|
+
classifier: str("MODEL_CLASSIFIER", "anthropic/claude-haiku-4.5"),
|
|
159
|
+
standard: str("MODEL_STANDARD", "anthropic/claude-haiku-4.5"),
|
|
160
|
+
high: str("MODEL_HIGH", "anthropic/claude-sonnet-5.5"),
|
|
161
|
+
highest: str("MODEL_HIGHEST", "anthropic/claude-sonnet-5.5"),
|
|
162
|
+
image: str("MODEL_IMAGE", "google/gemini-3.1-flash-image"),
|
|
163
|
+
video: str("MODEL_VIDEO", "google/veo-3.1-fast-generate-001"),
|
|
164
|
+
audio: str("MODEL_AUDIO", "google/gemini-3.5-flash-lite"),
|
|
165
|
+
speech: str("MODEL_SPEECH", "google/gemini-3.8-flash-tts"),
|
|
166
|
+
/** Vectors for a project's document index; not a class a step names. */
|
|
167
|
+
embedding: str("MODEL_EMBEDDING", "openai/text-embedding-3-small"),
|
|
168
|
+
},
|
|
169
|
+
/**
|
|
170
|
+
* Reverse geocoder for location fields (Nominatim's `/reverse`); unset = coordinates only,
|
|
171
|
+
* nothing about where a person stands leaves this server.
|
|
172
|
+
*/
|
|
173
|
+
geocoderUrl: str("GEOCODER_URL"),
|
|
174
|
+
/** CDP endpoint of a Chromium (the engenty-browser image); else a local Chrome is launched. */
|
|
175
|
+
browserCdpUrl: str("BROWSER_CDP_URL"),
|
|
176
|
+
chromePath: str("CHROME_PATH", process.platform === "darwin"
|
|
177
|
+
? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
178
|
+
: "/usr/bin/chromium"),
|
|
179
|
+
/** Encodes widget animations to MP4; without it widgets offer no video. */
|
|
180
|
+
ffmpegPath: str("FFMPEG_PATH", "ffmpeg"),
|
|
181
|
+
/** What runs an agent's shell and code: a Docker container, an agentOS VM, or nothing. */
|
|
182
|
+
sandbox: sandboxEngine(sandboxImage),
|
|
183
|
+
sandboxImage,
|
|
184
|
+
/** Assets and workspace files in an S3-compatible bucket (R2); empty = the data folder. */
|
|
185
|
+
s3: {
|
|
186
|
+
endpoint: str("S3_ENDPOINT").replace(/\/$/, ""),
|
|
187
|
+
bucket: str("S3_BUCKET"),
|
|
188
|
+
region: str("S3_REGION", "auto"),
|
|
189
|
+
accessKeyId: str("S3_ACCESS_KEY_ID"),
|
|
190
|
+
secretAccessKey: str("S3_SECRET_ACCESS_KEY"),
|
|
191
|
+
},
|
|
192
|
+
turnstile: { siteKey: str("TURNSTILE_SITE_KEY"), secret: str("TURNSTILE_SECRET_KEY") },
|
|
193
|
+
limits: {
|
|
194
|
+
visitorRunsPerHour: num("LIMIT_VISITOR_RUNS_PER_HOUR", 6),
|
|
195
|
+
defaultDailyRuns: num("LIMIT_DEFAULT_DAILY_RUNS", 50),
|
|
196
|
+
/** Runs of one tenant that work at the same time; the Manage-App can set another number per tenant. */
|
|
197
|
+
concurrentRuns: num("LIMIT_CONCURRENT_RUNS", 4),
|
|
198
|
+
/**
|
|
199
|
+
* Projects a tenant works with. One: the studio shows no project switcher. The Manage-App
|
|
200
|
+
* can set another number per tenant; the database holds any number either way.
|
|
201
|
+
*/
|
|
202
|
+
projects: num("LIMIT_PROJECTS", 1),
|
|
203
|
+
/** End-user runs (no account) and their shared links are deleted after this many days. */
|
|
204
|
+
resultTtlDays: num("RESULT_TTL_DAYS", 7),
|
|
205
|
+
/** What a wizard keeps for a person (lists, files, accounts) goes when unused this long. */
|
|
206
|
+
storeTtlDays: num("STORE_TTL_DAYS", 400),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* The path APP_URL ends in: `/wizards` for https://example.com/wizards, empty for an origin alone. The proxy
|
|
211
|
+
* may strip it or not, so the server takes both; what it writes into a cookie or a redirect
|
|
212
|
+
* carries it.
|
|
213
|
+
*/
|
|
214
|
+
export const basePath = new URL(env.appUrl).pathname.replace(/\/+$/, "");
|
|
215
|
+
if (env.production) {
|
|
216
|
+
const required = [
|
|
217
|
+
"APP_URL",
|
|
218
|
+
...(manageUrl ? ["APP_SECRET", "MANAGE_SERVICE_KEY", "MANAGE_CLIENT_ID"] : []),
|
|
219
|
+
];
|
|
220
|
+
const missing = required.filter((k) => !process.env[k]?.trim());
|
|
221
|
+
if (missing.length) {
|
|
222
|
+
throw new Error(`Missing required environment in production: ${missing.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { objects } from "./objects.js";
|
|
3
|
+
/** Content-addressed file store for workspaces: a blob never changes, so snapshots are just lists of hashes. */
|
|
4
|
+
const blobKey = (hash) => `blobs/${hash.slice(0, 2)}/${hash}`;
|
|
5
|
+
export async function putBlob(data) {
|
|
6
|
+
const hash = createHash("sha256").update(data).digest("hex");
|
|
7
|
+
const key = blobKey(hash);
|
|
8
|
+
if (!(await objects.has(key))) {
|
|
9
|
+
await objects.put(key, data);
|
|
10
|
+
}
|
|
11
|
+
return hash;
|
|
12
|
+
}
|
|
13
|
+
export async function getBlob(hash) {
|
|
14
|
+
if (!/^[a-f0-9]{64}$/.test(hash)) {
|
|
15
|
+
throw new Error("bad blob hash");
|
|
16
|
+
}
|
|
17
|
+
const data = await objects.get(blobKey(hash));
|
|
18
|
+
if (!data) {
|
|
19
|
+
throw new Error("blob missing");
|
|
20
|
+
}
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { AwsClient } from "aws4fetch";
|
|
6
|
+
import { env } from "../env.js";
|
|
7
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
8
|
+
function fileStore(root) {
|
|
9
|
+
const at = (key) => join(root, key);
|
|
10
|
+
return {
|
|
11
|
+
async put(key, data) {
|
|
12
|
+
const path = at(key);
|
|
13
|
+
await mkdir(dirname(path), { recursive: true });
|
|
14
|
+
// Two writers of the same key (the same blob, at the same moment) each need their own.
|
|
15
|
+
const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
16
|
+
await writeFile(tmp, data);
|
|
17
|
+
await rename(tmp, path);
|
|
18
|
+
},
|
|
19
|
+
get: (key) => readFile(at(key)).catch(() => null),
|
|
20
|
+
has: async (key) => existsSync(at(key)),
|
|
21
|
+
remove: (key) => rm(at(key), { force: true }),
|
|
22
|
+
removePrefix: (prefix) => rm(at(prefix), { recursive: true, force: true }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function s3Store() {
|
|
26
|
+
const aws = new AwsClient({
|
|
27
|
+
accessKeyId: env.s3.accessKeyId,
|
|
28
|
+
secretAccessKey: env.s3.secretAccessKey,
|
|
29
|
+
region: env.s3.region,
|
|
30
|
+
service: "s3",
|
|
31
|
+
});
|
|
32
|
+
const base = `${env.s3.endpoint}/${env.s3.bucket}`;
|
|
33
|
+
const url = (key) => `${base}/${key.split("/").map(encodeURIComponent).join("/")}`;
|
|
34
|
+
const check = (res, what) => {
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`Object store ${what} failed: ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
async put(key, data) {
|
|
41
|
+
check(await aws.fetch(url(key), { method: "PUT", body: data }), "put");
|
|
42
|
+
},
|
|
43
|
+
async get(key) {
|
|
44
|
+
const res = await aws.fetch(url(key));
|
|
45
|
+
if (res.status === 404) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
check(res, "get");
|
|
49
|
+
return Buffer.from(await res.arrayBuffer());
|
|
50
|
+
},
|
|
51
|
+
async has(key) {
|
|
52
|
+
return (await aws.fetch(url(key), { method: "HEAD" })).ok;
|
|
53
|
+
},
|
|
54
|
+
async remove(key) {
|
|
55
|
+
check(await aws.fetch(url(key), { method: "DELETE" }), "delete");
|
|
56
|
+
},
|
|
57
|
+
async removePrefix(prefix) {
|
|
58
|
+
for (let token = ""; token !== null;) {
|
|
59
|
+
const list = new URL(base);
|
|
60
|
+
list.searchParams.set("list-type", "2");
|
|
61
|
+
list.searchParams.set("prefix", prefix);
|
|
62
|
+
if (token) {
|
|
63
|
+
list.searchParams.set("continuation-token", token);
|
|
64
|
+
}
|
|
65
|
+
const res = await aws.fetch(list.toString());
|
|
66
|
+
check(res, "list");
|
|
67
|
+
const xml = await res.text();
|
|
68
|
+
const keys = [...xml.matchAll(/<Key>([^<]+)<\/Key>/g)].map((m) => m[1]);
|
|
69
|
+
await Promise.all(keys.map((key) => aws.fetch(url(key), { method: "DELETE" })));
|
|
70
|
+
token = xml.match(/<NextContinuationToken>([^<]+)</)?.[1] ?? null;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const store = env.s3.endpoint && env.s3.bucket ? s3Store() : fileStore(join(env.dataDir, "objects"));
|
|
76
|
+
const tenantKey = (key) => `t/${currentTenant()}/${key}`;
|
|
77
|
+
export const objects = {
|
|
78
|
+
put: (key, data) => store.put(tenantKey(key), data),
|
|
79
|
+
get: (key) => store.get(tenantKey(key)),
|
|
80
|
+
has: (key) => store.has(tenantKey(key)),
|
|
81
|
+
remove: (key) => store.remove(tenantKey(key)),
|
|
82
|
+
};
|
|
83
|
+
/** Deletes every file of a tenant. */
|
|
84
|
+
export function removeTenantObjects(tenantId) {
|
|
85
|
+
return store.removePrefix(`t/${tenantId}/`);
|
|
86
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { db, schema } from "../db/client.js";
|
|
4
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
5
|
+
import { objects } from "./objects.js";
|
|
6
|
+
const assetKey = (file) => `assets/${file}`;
|
|
7
|
+
const EXT = {
|
|
8
|
+
"image/png": "png",
|
|
9
|
+
"image/jpeg": "jpg",
|
|
10
|
+
"image/webp": "webp",
|
|
11
|
+
"video/mp4": "mp4",
|
|
12
|
+
"video/webm": "webm",
|
|
13
|
+
"video/quicktime": "mov",
|
|
14
|
+
"audio/webm": "weba",
|
|
15
|
+
"audio/mp4": "m4a",
|
|
16
|
+
"audio/mpeg": "mp3",
|
|
17
|
+
"audio/ogg": "ogg",
|
|
18
|
+
"audio/wav": "wav",
|
|
19
|
+
"text/html": "html",
|
|
20
|
+
"text/markdown": "md",
|
|
21
|
+
"text/plain": "txt",
|
|
22
|
+
"application/json": "json",
|
|
23
|
+
"application/pdf": "pdf",
|
|
24
|
+
};
|
|
25
|
+
export function extFor(mime) {
|
|
26
|
+
return EXT[mime] ?? "bin";
|
|
27
|
+
}
|
|
28
|
+
export async function saveAsset(input) {
|
|
29
|
+
const id = nanoid(16);
|
|
30
|
+
const bytes = typeof input.data === "string" ? Buffer.from(input.data, "utf8") : Buffer.from(input.data);
|
|
31
|
+
const file = `${id}.${extFor(input.mime)}`;
|
|
32
|
+
await objects.put(assetKey(file), bytes);
|
|
33
|
+
await db.insert(schema.asset).values({
|
|
34
|
+
id,
|
|
35
|
+
tenantId: currentTenant(),
|
|
36
|
+
runId: input.runId ?? null,
|
|
37
|
+
stepId: input.stepId ?? null,
|
|
38
|
+
kind: input.kind,
|
|
39
|
+
mime: input.mime,
|
|
40
|
+
name: input.name,
|
|
41
|
+
path: file,
|
|
42
|
+
size: bytes.byteLength,
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
id,
|
|
46
|
+
kind: input.kind,
|
|
47
|
+
mime: input.mime,
|
|
48
|
+
name: input.name,
|
|
49
|
+
...(input.ai ? { ai: input.ai } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function loadAsset(id) {
|
|
53
|
+
const row = await db.query.asset.findFirst({ where: eq(schema.asset.id, id) });
|
|
54
|
+
if (!row) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const data = await objects.get(assetKey(row.path));
|
|
58
|
+
return data ? { row, data } : null;
|
|
59
|
+
}
|
|
60
|
+
export async function loadAssetText(id) {
|
|
61
|
+
const found = await loadAsset(id);
|
|
62
|
+
return found ? found.data.toString("utf8") : "";
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generated HTML points at images as `asset://ID`; previews and downloads get them
|
|
66
|
+
* inlined as data URIs, so every file stands on its own. Only the tenant's assets resolve.
|
|
67
|
+
*/
|
|
68
|
+
export async function inlineAssetRefs(html) {
|
|
69
|
+
const ids = [...new Set([...html.matchAll(/asset:\/\/([A-Za-z0-9_-]{6,40})/g)].map((m) => m[1]))];
|
|
70
|
+
let out = html;
|
|
71
|
+
for (const id of ids) {
|
|
72
|
+
const found = await loadAsset(id);
|
|
73
|
+
const uri = found?.row.mime.startsWith("image/")
|
|
74
|
+
? `data:${found.row.mime};base64,${found.data.toString("base64")}`
|
|
75
|
+
: "";
|
|
76
|
+
out = out.split(`asset://${id}`).join(uri);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** Deletes one asset: its file and its row. */
|
|
81
|
+
export async function removeAsset(id) {
|
|
82
|
+
const row = await db.query.asset.findFirst({ where: eq(schema.asset.id, id) });
|
|
83
|
+
if (row) {
|
|
84
|
+
await objects.remove(assetKey(row.path));
|
|
85
|
+
await db.delete(schema.asset).where(eq(schema.asset.id, id));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Deletes every file a run made (uploads, results, renders) and their rows. */
|
|
89
|
+
export async function removeAssetFiles(runId) {
|
|
90
|
+
const rows = await db.query.asset.findMany({ where: eq(schema.asset.runId, runId) });
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
await objects.remove(assetKey(row.path));
|
|
93
|
+
}
|
|
94
|
+
await db.delete(schema.asset).where(eq(schema.asset.runId, runId));
|
|
95
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { env } from "./env.js";
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
let queue = Promise.resolve();
|
|
4
|
+
let lastCall = 0;
|
|
5
|
+
/** "Herrengasse 16, 8010 Graz" from a reverse-geocoding answer; null when it names nothing. */
|
|
6
|
+
export function placeLabel(result) {
|
|
7
|
+
const a = result.address;
|
|
8
|
+
if (!a) {
|
|
9
|
+
return result.display_name?.split(",").slice(0, 3).join(",").trim() || null;
|
|
10
|
+
}
|
|
11
|
+
const street = [a.road ?? a.pedestrian ?? a.footway ?? a.path, a.house_number]
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.join(" ");
|
|
14
|
+
const town = a.city ?? a.town ?? a.village ?? a.municipality ?? a.hamlet ?? a.county;
|
|
15
|
+
const place = [a.postcode, town].filter(Boolean).join(" ");
|
|
16
|
+
return [street, place].filter(Boolean).join(", ") || null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The address of a position, when the server has a geocoder (`GEOCODER_URL`). Calls go out one
|
|
20
|
+
* at a time and at most one a second — what a public Nominatim asks of its users.
|
|
21
|
+
*/
|
|
22
|
+
export function reverseGeocode(lat, lng, language = "de") {
|
|
23
|
+
if (!env.geocoderUrl) {
|
|
24
|
+
return Promise.resolve(null);
|
|
25
|
+
}
|
|
26
|
+
// ~10 m: the same spot asked twice is one call.
|
|
27
|
+
const key = `${lat.toFixed(4)},${lng.toFixed(4)},${language}`;
|
|
28
|
+
if (cache.has(key)) {
|
|
29
|
+
return Promise.resolve(cache.get(key) ?? null);
|
|
30
|
+
}
|
|
31
|
+
const call = queue.then(async () => {
|
|
32
|
+
await new Promise((r) => setTimeout(r, Math.max(0, lastCall + 1000 - Date.now())));
|
|
33
|
+
lastCall = Date.now();
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(env.geocoderUrl);
|
|
36
|
+
url.searchParams.set("format", "jsonv2");
|
|
37
|
+
url.searchParams.set("lat", String(lat));
|
|
38
|
+
url.searchParams.set("lon", String(lng));
|
|
39
|
+
url.searchParams.set("zoom", "18");
|
|
40
|
+
url.searchParams.set("addressdetails", "1");
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
headers: { "accept-language": language, "user-agent": `engenty-wizards (${env.appUrl})` },
|
|
43
|
+
signal: AbortSignal.timeout(4000),
|
|
44
|
+
});
|
|
45
|
+
const label = res.ok ? placeLabel((await res.json())) : null;
|
|
46
|
+
if (cache.size > 500) {
|
|
47
|
+
cache.clear();
|
|
48
|
+
}
|
|
49
|
+
cache.set(key, label);
|
|
50
|
+
return label;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
queue = call;
|
|
57
|
+
return call;
|
|
58
|
+
}
|