From a8410e632562fe1c1bdeb8c3e65b71de7ad27aad Mon Sep 17 00:00:00 2001 From: ivanberry Date: Thu, 12 Mar 2026 07:36:44 +0800 Subject: [PATCH] feat: initial commit --- .forgejo/workflows/register-skill-release.yml | 125 +++++++++ .gitignore | 4 + SKILL.md | 66 +++++ agents/openai.yaml | 12 + bun.lock | 246 ++++++++++++++++++ output_schema.json | 35 +++ package.json | 24 ++ scripts/export.ts | 20 ++ scripts/fetch.ts | 51 ++++ scripts/test.ts | 16 ++ src/eml.ts | 97 +++++++ src/index.ts | 145 +++++++++++ src/lead-dataset.ts | 50 ++++ src/types.ts | 72 +++++ src/zip.ts | 98 +++++++ 15 files changed, 1061 insertions(+) create mode 100644 .forgejo/workflows/register-skill-release.yml create mode 100644 .gitignore create mode 100644 SKILL.md create mode 100644 agents/openai.yaml create mode 100644 bun.lock create mode 100644 output_schema.json create mode 100644 package.json create mode 100644 scripts/export.ts create mode 100644 scripts/fetch.ts create mode 100644 scripts/test.ts create mode 100644 src/eml.ts create mode 100644 src/index.ts create mode 100644 src/lead-dataset.ts create mode 100644 src/types.ts create mode 100644 src/zip.ts diff --git a/.forgejo/workflows/register-skill-release.yml b/.forgejo/workflows/register-skill-release.yml new file mode 100644 index 0000000..38bc37a --- /dev/null +++ b/.forgejo/workflows/register-skill-release.yml @@ -0,0 +1,125 @@ +name: register-skill-release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + skill_slug: + description: Skill slug override (optional) + required: false + skill_subpath: + description: Skill folder path override (optional) + required: false + skill_doc_path: + description: Skill doc path override + required: false + default: SKILL.md + skill_version: + description: Version override (default tag name) + required: false + +jobs: + register-skill-version: + runs-on: ubuntu-latest + env: + API_BASE: ${{ vars.API_BASE || secrets.API_BASE }} + CLIENT_KEY: ${{ secrets.CLIENT_KEY }} + SKILL_VERSION: ${{ github.event.inputs.skill_version || github.ref_name }} + SKILL_SUBPATH: ${{ github.event.inputs.skill_subpath || vars.SKILL_SUBPATH || secrets.SKILL_SUBPATH }} + SKILL_DOC_PATH: ${{ github.event.inputs.skill_doc_path || vars.SKILL_DOC_PATH || secrets.SKILL_DOC_PATH || 'SKILL.md' }} + SKILL_SLUG: ${{ github.event.inputs.skill_slug || vars.SKILL_SLUG || secrets.SKILL_SLUG }} + RELEASE_NOTE: ${{ github.event.release.body }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Load skill doc content + shell: bash + run: | + set -euo pipefail + DOC_ABS_PATH="${SKILL_SUBPATH:+$SKILL_SUBPATH/}${SKILL_DOC_PATH}" + if [ ! -f "$DOC_ABS_PATH" ]; then + if [ -f "${SKILL_SUBPATH:+$SKILL_SUBPATH/}README.md" ]; then + DOC_ABS_PATH="${SKILL_SUBPATH:+$SKILL_SUBPATH/}README.md" + export SKILL_DOC_PATH="README.md" + else + echo "skill doc not found: $DOC_ABS_PATH" + exit 1 + fi + fi + + jq -Rs . < "$DOC_ABS_PATH" > /tmp/skill_doc.json + + - name: Register version to business system + shell: bash + run: | + set -euo pipefail + + if [ -z "${API_BASE:-}" ]; then + echo "API_BASE is required (global/repo var or secret)." + exit 1 + fi + if [ -z "${CLIENT_KEY:-}" ]; then + echo "CLIENT_KEY is required (secret)." + exit 1 + fi + + SKILL_BASE_DIR="${SKILL_SUBPATH:-.}" + + if [ -z "${SKILL_SLUG:-}" ]; then + if [ -f "${SKILL_BASE_DIR}/package.json" ]; then + PKG_NAME=$(jq -r '.name // empty' "${SKILL_BASE_DIR}/package.json") + if [ -n "$PKG_NAME" ]; then + # Strip npm scope: @scope/skill-name -> skill-name + SKILL_SLUG="${PKG_NAME##*/}" + fi + fi + fi + + if [ -z "${SKILL_SLUG:-}" ]; then + if [ -f "${SKILL_BASE_DIR}/pyproject.toml" ]; then + PYPROJECT_NAME=$(python3 -c "import sys,tomllib; p=sys.argv[1]; d=tomllib.load(open(p,'rb')); print((d.get('project',{}).get('name') or d.get('tool',{}).get('poetry',{}).get('name') or ''))" "${SKILL_BASE_DIR}/pyproject.toml" 2>/dev/null || true) + if [ -n "$PYPROJECT_NAME" ]; then + SKILL_SLUG="${PYPROJECT_NAME##*/}" + fi + fi + fi + + if [ -z "${SKILL_SLUG:-}" ]; then + SKILL_SLUG="${GITHUB_REPOSITORY##*/}" + fi + + SESSION_RES=$(curl -sS -X POST "${API_BASE}/auth/skill-credit/session" \ + -H "Content-Type: application/json" \ + -d "{\"clientKey\":\"${CLIENT_KEY}\"}") + ACCESS_TOKEN=$(printf '%s' "$SESSION_RES" | jq -r '.accessToken // empty') + if [ -z "$ACCESS_TOKEN" ]; then + echo "failed to exchange access token from client key" + echo "$SESSION_RES" + exit 1 + fi + + RUNTIME_META=$(jq -nc --arg entry "${SKILL_SUBPATH:+$SKILL_SUBPATH/}scripts" '{entry_hint:$entry, provider:"forgejo"}') + + cat > /tmp/register_payload.json < +``` + +Returns: JSON with businesses (name, website, reviews, emails) and summary stats. + +### 2. Compose (LLM) +The LLM agent reads fetch output and composes a personalized email per business. +Save drafts as a JSON array file. Each draft must have: +- `recipient_email` (string) +- `recipient_name` (string | null) +- `subject` (string, under 60 chars) +- `body_html` (string, professional HTML with inline styles) +- `body_text` (string, plain text fallback) + +#### Email Composition Rules +- Reference the business name and specific review insights +- Mention pain points or strengths identified from reviews +- Tone: professional, consultative, not salesy +- Save the drafts array to a temp JSON file, e.g. `/tmp/drafts-.json` + +### 3. Export (scripts/export.ts) +Converts drafts to RFC 5322 EML files, uploads individual EMLs + ZIP bundle to R2. + +```bash +cd ~/clawd/skills/email-content-compose +bun run export -- --drafts=/tmp/drafts-.json --workflow-id= [--from=] [--dry-run] +``` + +Returns: JSON with per-file R2 URLs and a bundle.zip URL. + +## Full Pipeline Example + +```bash +cd ~/clawd/skills/email-content-compose + +# 1. Fetch leads +bun run fetch -- --workflow-id=outreach-xxx > /tmp/leads.json + +# 2. LLM composes drafts → saves to /tmp/drafts-outreach-xxx.json + +# 3. Export EML + upload to R2 +bun run export -- --drafts=/tmp/drafts-outreach-xxx.json --workflow-id=outreach-xxx +``` + +## Environment (.env.local) +Already configured. Do NOT modify. +- AUTH_BASE, CLIENT_KEY: for lead-dataset API auth +- CLOUDFLARE_*: R2 upload credentials +- SENDER_EMAIL: From header in EML files diff --git a/agents/openai.yaml b/agents/openai.yaml new file mode 100644 index 0000000..77cfd65 --- /dev/null +++ b/agents/openai.yaml @@ -0,0 +1,12 @@ +interface: + display_name: "Email Content Compose" + short_description: "Fetch leads, compose personalized emails, export EML to R2" + default_prompt: > + [skill:email-content-compose] + This skill uses Bun+TypeScript CLI scripts. Do NOT create Python scripts or custom upload logic. + + Step 1: cd ~/clawd/skills/email-content-compose && bun run fetch -- --workflow-id= + Step 2: Read the fetch output. For each business with emails, compose a personalized EmailDraft (recipient_email, recipient_name, subject, body_html, body_text). Save all drafts as a JSON array to /tmp/drafts-.json + Step 3: bun run export -- --drafts=/tmp/drafts-.json --workflow-id= + + Return the export result JSON (file URLs + bundle URL). Read SKILL.md for composition rules. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c50c40f --- /dev/null +++ b/bun.lock @@ -0,0 +1,246 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "email-content-compose", + "dependencies": { + "@aws-sdk/client-s3": "^3.1004.0", + "@clawd/auth-runtime": "file:../_shared/auth-runtime", + "@clawd/r2-upload": "file:../r2-upload", + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="], + + "@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1004.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-node": "^3.972.18", "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", "@aws-sdk/middleware-expect-continue": "^3.972.7", "@aws-sdk/middleware-flexible-checksums": "^3.973.4", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-location-constraint": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/middleware-ssec": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/signature-v4-multi-region": "^3.996.6", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/eventstream-serde-browser": "^4.2.11", "@smithy/eventstream-serde-config-resolver": "^4.3.11", "@smithy/eventstream-serde-node": "^4.2.11", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-blob-browser": "^4.2.12", "@smithy/hash-node": "^4.2.11", "@smithy/hash-stream-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/md5-js": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.973.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/xml-builder": "^3.972.10", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA=="], + + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-login": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.18", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.16", "@aws-sdk/credential-provider-http": "^3.972.18", "@aws-sdk/credential-provider-ini": "^3.972.17", "@aws-sdk/credential-provider-process": "^3.972.16", "@aws-sdk/credential-provider-sso": "^3.972.17", "@aws-sdk/credential-provider-web-identity": "^3.972.17", "@aws-sdk/types": "^3.973.5", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.16", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/token-providers": "3.1004.0", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.17", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA=="], + + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA=="], + + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ=="], + + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.973.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/crc64-nvme": "^3.972.4", "@aws-sdk/types": "^3.973.5", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ=="], + + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ=="], + + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.18", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.8", "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw=="], + + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.19", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@smithy/core": "^3.23.8", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.7", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.18", "@aws-sdk/middleware-host-header": "^3.972.7", "@aws-sdk/middleware-logger": "^3.972.7", "@aws-sdk/middleware-recursion-detection": "^3.972.7", "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/region-config-resolver": "^3.972.7", "@aws-sdk/types": "^3.973.5", "@aws-sdk/util-endpoints": "^3.996.4", "@aws-sdk/util-user-agent-browser": "^3.972.7", "@aws-sdk/util-user-agent-node": "^3.973.4", "@smithy/config-resolver": "^4.4.10", "@smithy/core": "^3.23.8", "@smithy/fetch-http-handler": "^5.3.13", "@smithy/hash-node": "^4.2.11", "@smithy/invalid-dependency": "^4.2.11", "@smithy/middleware-content-length": "^4.2.11", "@smithy/middleware-endpoint": "^4.4.22", "@smithy/middleware-retry": "^4.4.39", "@smithy/middleware-serde": "^4.2.12", "@smithy/middleware-stack": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/node-http-handler": "^4.4.14", "@smithy/protocol-http": "^5.3.11", "@smithy/smithy-client": "^4.12.2", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.38", "@smithy/util-defaults-mode-node": "^4.2.41", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/config-resolver": "^4.4.10", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.6", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.18", "@aws-sdk/types": "^3.973.5", "@smithy/protocol-http": "^5.3.11", "@smithy/signature-v4": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1004.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.18", "@aws-sdk/nested-clients": "^3.996.7", "@aws-sdk/types": "^3.973.5", "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ=="], + + "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.4", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" } }, "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.5", "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.4", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.19", "@aws-sdk/types": "^3.973.5", "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" } }, "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + + "@clawd/auth-runtime": ["@clawd/auth-runtime@file:../_shared/auth-runtime", { "devDependencies": { "@types/node": "^25.3.3", "typescript": "^5.9.3" } }], + + "@clawd/r2-upload": ["@clawd/r2-upload@file:../r2-upload", { "dependencies": { "@aws-sdk/client-s3": "^3.705.0" }, "devDependencies": { "@types/bun": "latest", "typescript": "^5.0.0" } }], + + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ=="], + + "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], + + "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.2", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg=="], + + "@smithy/core": ["@smithy/core@3.23.9", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.12", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-stream": "^4.5.17", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.11", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.11", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.11", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.13", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ=="], + + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.12", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A=="], + + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], + + "@smithy/md5-js": ["@smithy/md5-js@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.23", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-serde": "^4.2.12", "@smithy/node-config-provider": "^4.3.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.11", "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" } }, "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.40", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/protocol-http": "^5.3.11", "@smithy/service-error-classification": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.11", "@smithy/util-retry": "^4.2.11", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.11", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/shared-ini-file-loader": "^4.4.6", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.14", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/querystring-builder": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.6", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.11", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.11", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.3", "", { "dependencies": { "@smithy/core": "^3.23.9", "@smithy/middleware-endpoint": "^4.4.23", "@smithy/middleware-stack": "^4.2.11", "@smithy/protocol-http": "^5.3.11", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" } }, "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw=="], + + "@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.11", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.39", "", { "dependencies": { "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.42", "", { "dependencies": { "@smithy/config-resolver": "^4.4.10", "@smithy/credential-provider-imds": "^4.2.11", "@smithy/node-config-provider": "^4.3.11", "@smithy/property-provider": "^4.2.11", "@smithy/smithy-client": "^4.12.3", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.11", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.11", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.17", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.13", "@smithy/node-http-handler": "^4.4.14", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], + + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.11", "", { "dependencies": { "@smithy/abort-controller": "^4.2.11", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "fast-xml-builder": ["fast-xml-builder@1.0.0", "", {}, "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ=="], + + "fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], + + "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + } +} diff --git a/output_schema.json b/output_schema.json new file mode 100644 index 0000000..bc95ca8 --- /dev/null +++ b/output_schema.json @@ -0,0 +1,35 @@ +{ + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["success", "failed"] }, + "error": { "type": ["string", "null"] }, + "workflowId": { "type": "string" }, + "workflowStatus": { "type": "string" }, + "query": { "type": "string" }, + "country": { "type": "string" }, + "productKeywords": { "type": ["string", "null"] }, + "completedAt": { "type": ["string", "null"] }, + "summary": { + "type": ["object", "null"], + "properties": { + "businesses_count": { "type": "number" }, + "contacts_count": { "type": "number" }, + "businesses_with_reviews": { "type": "number" }, + "businesses_with_email": { "type": "number" } + } + }, + "businesses": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "domain": { "type": ["string", "null"] }, + "emails": { "type": "array", "items": { "type": "string" } }, + "reviews_data": { "type": ["string", "null"] } + } + } + } + }, + "required": ["status", "error", "workflowId", "workflowStatus", "businesses"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..efaf610 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "email-content-compose", + "version": "1.1.0", + "description": "Fetch lead-dataset, compose personalized outreach emails, export as EML and upload to R2", + "type": "module", + "main": "src/index.ts", + "scripts": { + "fetch": "bun run scripts/fetch.ts", + "export": "bun run scripts/export.ts", + "test": "bun run scripts/test.ts" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1004.0", + "@clawd/auth-runtime": "git+http://192.168.0.108:3030/agent-skills/auth-runtime.git", + "@clawd/r2-upload": "git+http://192.168.0.108:3030/agent-skills/r2-upload.git" + } +} diff --git a/scripts/export.ts b/scripts/export.ts new file mode 100644 index 0000000..d60c9db --- /dev/null +++ b/scripts/export.ts @@ -0,0 +1,20 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +import { readDraftsFile, exportDrafts } from "../src/index.js"; + +const args = process.argv.slice(2); +const draftsFile = args.find(a => a.startsWith("--drafts="))?.split("=")[1]; +const workflowId = args.find(a => a.startsWith("--workflow-id="))?.split("=")[1] || "unknown"; +const from = args.find(a => a.startsWith("--from="))?.split("=")[1]; +const dryRun = args.includes("--dry-run"); + +if (!draftsFile) { + console.error("Usage: bun run export -- --drafts= --workflow-id= [--from=] [--dry-run]"); + process.exit(1); +} + +const drafts = await readDraftsFile(draftsFile); +console.log(`Exporting ${drafts.length} drafts...`); + +const result = await exportDrafts(drafts, workflowId, { from, dryRun }); +console.log(JSON.stringify(result, null, 2)); diff --git a/scripts/fetch.ts b/scripts/fetch.ts new file mode 100644 index 0000000..771090d --- /dev/null +++ b/scripts/fetch.ts @@ -0,0 +1,51 @@ +import { existsSync, readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import { fetchLeads } from "../src/index.js"; + +function loadEnvLocal(): void { + const scriptDir = dirname(fileURLToPath(import.meta.url)); + const envPath = join(scriptDir, "../.env.local"); + if (!existsSync(envPath)) return; + for (const line of readFileSync(envPath, "utf-8").split("\n")) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + let val = trimmed.slice(eq + 1).trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (!process.env[key]) process.env[key] = val; + } +} + +function parseArgs(args: string[]): { workflowId: string; dryRun: boolean } { + let dryRun = false; + let workflowId = ""; + + for (const arg of args) { + if (arg === "--dry-run") { dryRun = true; continue; } + if (arg === "-h" || arg === "--help") { + console.error("Usage: bun run fetch -- --workflow-id= [--dry-run]"); + process.exit(0); + } + if (arg.startsWith("--workflow-id=")) { workflowId = arg.slice("--workflow-id=".length).trim(); continue; } + if (!workflowId) workflowId = arg; + } + return { workflowId, dryRun }; +} + +async function main(): Promise { + loadEnvLocal(); + const args = parseArgs(process.argv.slice(2)); + const result = await fetchLeads(args.workflowId, args.dryRun); + console.log(JSON.stringify(result, null, 2)); + if (result.status === "failed") process.exit(1); +} + +main().catch((err) => { + console.error(JSON.stringify({ status: "failed", error: err instanceof Error ? err.message : String(err) })); + process.exit(1); +}); diff --git a/scripts/test.ts b/scripts/test.ts new file mode 100644 index 0000000..ee97ee0 --- /dev/null +++ b/scripts/test.ts @@ -0,0 +1,16 @@ +import { fetchLeads } from "../src/index.js"; + +const workflowId = process.argv[2] || "outreach-test-123"; +const mode = process.argv[3] || "--dry-run"; +const clientKey = process.argv[4] || ""; + +if (mode === "--live" && clientKey) { + process.env.CLIENT_KEY = clientKey; +} + +console.log("=== Email Content Compose - Test ==="); +console.log(`Workflow: ${workflowId} Mode: ${mode}\n`); + +const isDryRun = mode !== "--live"; +const result = await fetchLeads(workflowId, isDryRun); +console.log(JSON.stringify(result, null, 2)); diff --git a/src/eml.ts b/src/eml.ts new file mode 100644 index 0000000..ed8290c --- /dev/null +++ b/src/eml.ts @@ -0,0 +1,97 @@ +import type { EmailDraft } from "./types.js"; + +/** + * Convert a draft to RFC 5322 EML format with MIME multipart/alternative. + */ +export function draftToEml(draft: EmailDraft, from: string): string { + const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).slice(2)}`; + const date = new Date().toUTCString(); + const messageId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@outreach.local>`; + + const toHeader = draft.recipient_name + ? `"${draft.recipient_name}" <${draft.recipient_email}>` + : draft.recipient_email; + + const headers = [ + `From: ${from}`, + `To: ${toHeader}`, + `Subject: ${encodeSubject(draft.subject)}`, + `Date: ${date}`, + `Message-ID: ${messageId}`, + `MIME-Version: 1.0`, + `Content-Type: multipart/alternative; boundary="${boundary}"`, + `X-Mailer: email-export-skill`, + ]; + + const textPart = [ + `--${boundary}`, + `Content-Type: text/plain; charset="utf-8"`, + `Content-Transfer-Encoding: quoted-printable`, + ``, + quotedPrintableEncode(draft.body_text), + ].join("\r\n"); + + const htmlPart = [ + `--${boundary}`, + `Content-Type: text/html; charset="utf-8"`, + `Content-Transfer-Encoding: quoted-printable`, + ``, + quotedPrintableEncode(draft.body_html), + ].join("\r\n"); + + return [ + headers.join("\r\n"), + ``, + textPart, + ``, + htmlPart, + ``, + `--${boundary}--`, + ``, + ].join("\r\n"); +} + +/** RFC 2047 encoded-word for non-ASCII subjects */ +function encodeSubject(subject: string): string { + if (/^[\x20-\x7E]*$/.test(subject)) return subject; + const encoded = Buffer.from(subject, "utf-8").toString("base64"); + return `=?UTF-8?B?${encoded}?=`; +} + +/** Simple quoted-printable encoding */ +function quotedPrintableEncode(text: string): string { + const lines: string[] = []; + let current = ""; + + for (const char of text) { + const code = char.charCodeAt(0); + let encoded: string; + + if (char === "\r" || char === "\n") { + lines.push(current); + current = ""; + continue; + } else if (code === 9 || (code >= 32 && code <= 126 && char !== "=")) { + encoded = char; + } else { + const buf = Buffer.from(char, "utf-8"); + encoded = Array.from(buf).map(b => `=${b.toString(16).toUpperCase().padStart(2, "0")}`).join(""); + } + + if (current.length + encoded.length > 75) { + lines.push(current + "="); + current = encoded; + } else { + current += encoded; + } + } + if (current) lines.push(current); + return lines.join("\r\n"); +} + +/** Generate a safe filename from draft */ +export function emlFilename(draft: EmailDraft, index: number): string { + const domain = draft.recipient_email.split("@")[1] || "unknown"; + const safe = domain.replace(/[^a-zA-Z0-9.-]/g, "_"); + return `${String(index + 1).padStart(3, "0")}_${safe}.eml`; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..08d426c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,145 @@ +import type { EmailDraft, ExportResult, ExportedFile, FetchResult } from './types.js'; +import { createEnvConfig as createBaseEnvConfig, getAccessToken } from '@clawd/auth-runtime'; +import { fetchLeadDataset } from './lead-dataset.js'; +import { draftToEml, emlFilename } from './eml.js'; +import { loadR2Config, uploadToR2 } from '@clawd/r2-upload'; + +/** + * Fetch lead-dataset from completed workflow. + * Returns businesses with emails and reviews for LLM to compose emails. + */ +export async function fetchLeads( + workflowId: string, + dryRun: boolean = false, +): Promise { + if (!workflowId) { + return failedFetch('', 'missing workflow-id'); + } + + if (dryRun) { + return { + status: 'success', + error: null, + workflowId, + workflowStatus: 'dry_run', + query: 'dry-run-query', + country: 'us', + productKeywords: null, + completedAt: null, + summary: { businesses_count: 3, contacts_count: 5, businesses_with_reviews: 2, businesses_with_email: 3 }, + businesses: [], + }; + } + + const config = createBaseEnvConfig(); + + let accessToken: string; + try { + accessToken = await getAccessToken(false, config); + } catch (err) { + return failedFetch(workflowId, err instanceof Error ? err.message : 'failed to exchange token'); + } + + const { data, error } = await fetchLeadDataset(config, accessToken, workflowId, false); + if (!data) { + return failedFetch(workflowId, error); + } + + return { + status: 'success', + error: null, + workflowId: data.workflow_id, + workflowStatus: data.status, + query: data.query, + country: data.country, + productKeywords: data.product_keywords, + completedAt: data.completed_at, + summary: data.summary, + businesses: data.businesses, + }; +} + +/** + * Export drafts: convert to EML, upload to R2, return URLs. + */ +export async function exportDrafts( + drafts: EmailDraft[], + workflowId: string, + options: { + from?: string; + prefix?: string; + dryRun?: boolean; + } = {}, +): Promise { + const from = options.from || process.env.SENDER_EMAIL || 'outreach@company.com'; + const prefix = options.prefix || `outreach/eml/${workflowId}/${Date.now()}`; + + if (!drafts.length) { + return { status: 'failed', error: 'no drafts to export', workflowId, totalDrafts: 0, exportedCount: 0, bundleUrl: null, files: [] }; + } + + // Convert all drafts to EML + const emlFiles: { filename: string; content: string; draft: EmailDraft }[] = []; + for (let i = 0; i < drafts.length; i++) { + const draft = drafts[i]; + const filename = emlFilename(draft, i); + const content = draftToEml(draft, from); + emlFiles.push({ filename, content, draft }); + } + + if (options.dryRun) { + const files: ExportedFile[] = emlFiles.map(f => ({ + filename: f.filename, + url: `(dry-run) ${prefix}/${f.filename}`, + recipient: f.draft.recipient_email, + subject: f.draft.subject, + })); + return { status: 'success', error: null, workflowId, totalDrafts: drafts.length, exportedCount: files.length, bundleUrl: `(dry-run) ${prefix}/bundle.zip`, files }; + } + + // Upload to R2 + const r2Config = loadR2Config(); + const files: ExportedFile[] = []; + + for (const eml of emlFiles) { + const key = `${prefix}/${eml.filename}`; + const url = await uploadToR2(r2Config, key, eml.content, 'message/rfc822'); + files.push({ + filename: eml.filename, + url, + recipient: eml.draft.recipient_email, + subject: eml.draft.subject, + }); + } + + // Create and upload zip bundle + let bundleUrl: string | null = null; + try { + const { createZipBundle } = await import('./zip.js'); + const zipBuffer = await createZipBundle(emlFiles.map(f => ({ name: f.filename, content: f.content }))); + bundleUrl = await uploadToR2(r2Config, `${prefix}/bundle.zip`, zipBuffer, 'application/zip'); + } catch { + // zip is optional + } + + return { status: 'success', error: null, workflowId, totalDrafts: drafts.length, exportedCount: files.length, bundleUrl, files }; +} + +/** + * Read drafts from a JSON file. + */ +export async function readDraftsFile(path: string): Promise { + const file = Bun.file(path); + if (!await file.exists()) { + throw new Error(`file not found: ${path}`); + } + const raw = await file.json(); + return Array.isArray(raw) ? raw : (raw?.drafts ?? []); +} + +function failedFetch(workflowId: string, error: string): FetchResult { + return { + status: 'failed', error, workflowId, workflowStatus: '', query: '', country: '', + productKeywords: null, completedAt: null, summary: null, businesses: [], + }; +} diff --git a/src/lead-dataset.ts b/src/lead-dataset.ts new file mode 100644 index 0000000..b670b50 --- /dev/null +++ b/src/lead-dataset.ts @@ -0,0 +1,50 @@ +import type { LeadDatasetResponse } from './types.js'; +import { requestApiWithAutoRefresh } from '@clawd/auth-runtime'; +import type { EnvConfig } from '@clawd/auth-runtime'; + +/** + * Fetch lead-dataset from a completed cold-outreach workflow. + */ +export async function fetchLeadDataset( + config: EnvConfig, + accessToken: string, + workflowId: string, + dryRun: boolean, +): Promise<{ data: LeadDatasetResponse | null; error: string }> { + const url = `${config.authBase}/ecom/cold-outreach/${workflowId}/lead-dataset`; + const result = await requestApiWithAutoRefresh( + 'GET', + url, + dryRun, + config, + undefined, + accessToken, + ); + + if (result.status < 200 || result.status >= 300) { + const msg = parseError(result.body); + return { + data: null, + error: msg || `lead-dataset fetch failed: HTTP ${result.status}`, + }; + } + + try { + const data = JSON.parse(result.body) as LeadDatasetResponse; + if (!data.success) { + return { data: null, error: data.message || 'lead-dataset returned success=false' }; + } + return { data, error: '' }; + } catch { + return { data: null, error: 'failed to parse lead-dataset response' }; + } +} + +function parseError(body: string): string { + try { + const data = JSON.parse(body); + return data.message || data.error || ''; + } catch { + return ''; + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..528000d --- /dev/null +++ b/src/types.ts @@ -0,0 +1,72 @@ +export interface Business { + id: string; + name: string; + website: string | null; + domain: string | null; + address: string | null; + phone: string | null; + rating: number | null; + reviews_count: number | null; + category: string | null; + reviews_data: string | null; + emails: string[]; +} + +export interface LeadSummary { + businesses_count: number; + contacts_count: number; + businesses_with_reviews: number; + businesses_with_email: number; +} + +export interface LeadDatasetResponse { + success: boolean; + workflow_id: string; + status: string; + query: string; + country: string; + product_keywords: string | null; + completed_at: string | null; + summary: LeadSummary; + businesses: Business[]; + message?: string; +} + +export interface EmailDraft { + recipient_email: string; + recipient_name: string | null; + subject: string; + body_html: string; + body_text: string; + personalization_context?: Record; +} + +export interface ExportedFile { + filename: string; + url: string; + recipient: string; + subject: string; +} + +export interface FetchResult { + status: "success" | "failed"; + error: string | null; + workflowId: string; + workflowStatus: string; + query: string; + country: string; + productKeywords: string | null; + completedAt: string | null; + summary: LeadSummary | null; + businesses: Business[]; +} + +export interface ExportResult { + status: "success" | "failed"; + error: string | null; + workflowId: string; + totalDrafts: number; + exportedCount: number; + bundleUrl: string | null; + files: ExportedFile[]; +} diff --git a/src/zip.ts b/src/zip.ts new file mode 100644 index 0000000..1f97163 --- /dev/null +++ b/src/zip.ts @@ -0,0 +1,98 @@ +/** + * Create a zip buffer from named text files. + * Uses Bun built-in zlib for deflate. + */ +export async function createZipBundle( + files: { name: string; content: string }[], +): Promise { + // Minimal ZIP format implementation (store method, no compression for simplicity) + const entries: { name: Buffer; data: Buffer; offset: number }[] = []; + const parts: Buffer[] = []; + let offset = 0; + + for (const file of files) { + const name = Buffer.from(file.name, "utf-8"); + const data = Buffer.from(file.content, "utf-8"); + + // Local file header (30 bytes + name + data) + const localHeader = Buffer.alloc(30); + localHeader.writeUInt32LE(0x04034b50, 0); // signature + localHeader.writeUInt16LE(20, 4); // version needed + localHeader.writeUInt16LE(0, 6); // flags + localHeader.writeUInt16LE(0, 8); // compression (store) + localHeader.writeUInt16LE(0, 10); // mod time + localHeader.writeUInt16LE(0, 12); // mod date + localHeader.writeUInt32LE(crc32(data), 14); // crc-32 + localHeader.writeUInt32LE(data.length, 18); // compressed size + localHeader.writeUInt32LE(data.length, 22); // uncompressed size + localHeader.writeUInt16LE(name.length, 26); // filename length + localHeader.writeUInt16LE(0, 28); // extra field length + + entries.push({ name, data, offset }); + parts.push(localHeader, name, data); + offset += 30 + name.length + data.length; + } + + // Central directory + const cdStart = offset; + for (const entry of entries) { + const cdHeader = Buffer.alloc(46); + cdHeader.writeUInt32LE(0x02014b50, 0); // signature + cdHeader.writeUInt16LE(20, 4); // version made by + cdHeader.writeUInt16LE(20, 6); // version needed + cdHeader.writeUInt16LE(0, 8); // flags + cdHeader.writeUInt16LE(0, 10); // compression + cdHeader.writeUInt16LE(0, 12); // mod time + cdHeader.writeUInt16LE(0, 14); // mod date + cdHeader.writeUInt32LE(crc32(entry.data), 16); // crc-32 + cdHeader.writeUInt32LE(entry.data.length, 20); // compressed size + cdHeader.writeUInt32LE(entry.data.length, 24); // uncompressed size + cdHeader.writeUInt16LE(entry.name.length, 28); // filename length + cdHeader.writeUInt16LE(0, 30); // extra field length + cdHeader.writeUInt16LE(0, 32); // comment length + cdHeader.writeUInt16LE(0, 34); // disk number start + cdHeader.writeUInt16LE(0, 36); // internal attrs + cdHeader.writeUInt32LE(0, 38); // external attrs + cdHeader.writeUInt32LE(entry.offset, 42); // local header offset + + parts.push(cdHeader, entry.name); + offset += 46 + entry.name.length; + } + + const cdSize = offset - cdStart; + + // End of central directory + const eocd = Buffer.alloc(22); + eocd.writeUInt32LE(0x06054b50, 0); // signature + eocd.writeUInt16LE(0, 4); // disk number + eocd.writeUInt16LE(0, 6); // cd disk number + eocd.writeUInt16LE(entries.length, 8); // entries on disk + eocd.writeUInt16LE(entries.length, 10); // total entries + eocd.writeUInt32LE(cdSize, 12); // cd size + eocd.writeUInt32LE(cdStart, 16); // cd offset + eocd.writeUInt16LE(0, 20); // comment length + parts.push(eocd); + + return Buffer.concat(parts); +} + +/** CRC-32 lookup table */ +const crcTable: Uint32Array = (() => { + const table = new Uint32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) { + c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); + } + table[i] = c; + } + return table; +})(); + +function crc32(buf: Buffer): number { + let crc = 0xFFFFFFFF; + for (let i = 0; i < buf.length; i++) { + crc = crcTable[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8); + } + return (crc ^ 0xFFFFFFFF) >>> 0; +}