Compare commits
10 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9f37657fcf | |
|
|
406b6d417c | |
|
|
1884efa54e | |
|
|
993f038ae9 | |
|
|
9e9331f8bb | |
|
|
e18276ce07 | |
|
|
7ad389adc4 | |
|
|
88ddfd4ba7 | |
|
|
27b204b5fb | |
|
|
8b2351db21 |
49
SKILL.md
49
SKILL.md
|
|
@ -28,10 +28,53 @@ Save drafts as a JSON array file. Each draft must have:
|
||||||
- `subject` (string, under 60 chars)
|
- `subject` (string, under 60 chars)
|
||||||
- `body_html` (string, professional HTML with inline styles)
|
- `body_html` (string, professional HTML with inline styles)
|
||||||
- `body_text` (string, plain text fallback)
|
- `body_text` (string, plain text fallback)
|
||||||
|
- `personalization_context` (object, see below)
|
||||||
|
|
||||||
#### Email Composition Rules
|
#### a) Sender Profile
|
||||||
- Reference the business name and specific review insights
|
Before composing, read `sender-profile.json` in this skill directory. Use it to populate sender identity, product info, and signature in every email.
|
||||||
- Mention pain points or strengths identified from reviews
|
|
||||||
|
#### b) Language Selection
|
||||||
|
Auto-select email language based on the `country` field from fetch output:
|
||||||
|
- us/gb/au/ca → English
|
||||||
|
- cn → 中文
|
||||||
|
- jp → 日本語
|
||||||
|
- kr → 한국어
|
||||||
|
- de → Deutsch
|
||||||
|
- fr → Français
|
||||||
|
- es/mx → Español
|
||||||
|
- pt/br → Português
|
||||||
|
- Unlisted countries → default to English
|
||||||
|
|
||||||
|
Write the **entire** email (subject, body, signature) in the selected language.
|
||||||
|
|
||||||
|
#### c) Review Pain-Point Analysis
|
||||||
|
When `reviews_data` is non-empty for a business:
|
||||||
|
1. Parse JSON into a review array
|
||||||
|
2. Filter for negative reviews / complaints related to the sender's `product_category`
|
||||||
|
3. Extract 1–2 pain points that the sender's products can solve
|
||||||
|
4. Analyze at most 10 reviews, prioritizing low-score ones
|
||||||
|
5. If `reviews_data` is empty or null, fall back to the business's general info (category, rating) to craft the email
|
||||||
|
|
||||||
|
#### d) Email Template Structure
|
||||||
|
1. **Subject** — Under 60 chars, in the target language, referencing a specific pain point or business need
|
||||||
|
2. **Opening** — Address the business by name; demonstrate familiarity with their operations
|
||||||
|
3. **Pain Point Bridge** — Reference pain-point patterns from reviews (do NOT quote reviews verbatim); connect them to problems the sender's product solves
|
||||||
|
4. **Value Proposition** — Introduce sender company and products using `product_highlights` from sender-profile.json
|
||||||
|
5. **CTA** — Low-friction call-to-action: free samples, catalog, or a brief call
|
||||||
|
6. **Signature** — Use contact info from sender-profile.json (name, title, email, phone, website)
|
||||||
|
|
||||||
|
#### e) Personalization Tracking
|
||||||
|
Populate the `personalization_context` field on each draft:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"language": "selected language",
|
||||||
|
"pain_points_used": ["pain point 1", "pain point 2"],
|
||||||
|
"reviews_analyzed": true,
|
||||||
|
"sender_product_match": "brief note on how sender product connects to this business"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### General Rules
|
||||||
- Tone: professional, consultative, not salesy
|
- Tone: professional, consultative, not salesy
|
||||||
- Save the drafts array to a temp JSON file, e.g. `/tmp/drafts-<workflow-id>.json`
|
- Save the drafts array to a temp JSON file, e.g. `/tmp/drafts-<workflow-id>.json`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ interface:
|
||||||
This skill uses Bun+TypeScript CLI scripts. Do NOT create Python scripts or custom upload logic.
|
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=<WORKFLOW_ID>
|
Step 1: cd ~/clawd/skills/email-content-compose && bun run fetch -- --workflow-id=<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-<WORKFLOW_ID>.json
|
Step 2: Read sender-profile.json from the skill directory. Then read the fetch output. For each business with emails, compose a personalized EmailDraft following SKILL.md Section 2 rules (language selection by country, review pain-point analysis, structured email template, personalization_context tracking). Save all drafts as a JSON array to /tmp/drafts-<WORKFLOW_ID>.json
|
||||||
Step 3: bun run export -- --drafts=/tmp/drafts-<WORKFLOW_ID>.json --workflow-id=<WORKFLOW_ID>
|
Step 3: bun run export -- --drafts=/tmp/drafts-<WORKFLOW_ID>.json --workflow-id=<WORKFLOW_ID>
|
||||||
|
|
||||||
Return the export result JSON (file URLs + bundle URL). Read SKILL.md for composition rules.
|
Return the export result JSON (file URLs + bundle URL). Read SKILL.md for composition rules.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Auto-install auth-rt if not found
|
||||||
|
if ! command -v auth-rt &>/dev/null && [ ! -x "$HOME/.local/bin/auth-rt" ]; then
|
||||||
|
echo "auth-rt not found, installing..."
|
||||||
|
_FORGEJO="http://192.168.0.108:3030"
|
||||||
|
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
|
_ARCH="$(uname -m)"; case "$_ARCH" in x86_64) _ARCH="amd64";; aarch64) _ARCH="arm64";; esac
|
||||||
|
_URL="$_FORGEJO/agent-skills/auth-runtime/releases/download/latest/auth-rt-${_OS}-${_ARCH}"
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
if curl -fsSL "$_URL" -o "$HOME/.local/bin/auth-rt" 2>/dev/null; then
|
||||||
|
chmod +x "$HOME/.local/bin/auth-rt"
|
||||||
|
echo "auth-rt installed (downloaded)"
|
||||||
|
else
|
||||||
|
echo "Download failed, building from source..."
|
||||||
|
_SRC="$HOME/.local/share/auth-runtime"
|
||||||
|
if [ -d "$_SRC/.git" ]; then git -C "$_SRC" pull --ff-only
|
||||||
|
else git clone --depth 1 "$_FORGEJO/agent-skills/auth-runtime.git" "$_SRC"
|
||||||
|
fi
|
||||||
|
bash "$_SRC/install.sh"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.1004.0",
|
"@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"
|
"@clawd/r2-upload": "git+http://192.168.0.108:3030/agent-skills/r2-upload.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"company_name": "Your Company Name",
|
||||||
|
"website": "https://yourcompany.com",
|
||||||
|
"product_category": "e.g., industrial packaging supplies",
|
||||||
|
"product_highlights": ["Key benefit 1", "Key benefit 2"],
|
||||||
|
"value_proposition": "One sentence why businesses should use your products",
|
||||||
|
"contact_name": "Sales Rep Name",
|
||||||
|
"contact_title": "Business Development Manager",
|
||||||
|
"contact_email": "sales@yourcompany.com",
|
||||||
|
"contact_phone": "+1-555-0100"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* Thin CLI wrapper for auth-runtime.
|
||||||
|
*
|
||||||
|
* Copy this file into your skill's src/ directory. It calls the
|
||||||
|
* `auth-rt` binary (a standalone Go executable), so the skill has
|
||||||
|
* zero npm/runtime dependency on auth-runtime.
|
||||||
|
*
|
||||||
|
* Prerequisites:
|
||||||
|
* `auth-rt` must be in PATH or at ~/.local/bin/auth-rt
|
||||||
|
* (install.sh handles this automatically)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { createSkillClient } from './auth-cli.ts';
|
||||||
|
* const client = createSkillClient();
|
||||||
|
* const res = await client.post('/ecom/tasks/scrape', { url: '...' });
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
|
||||||
|
const home = process.env.HOME || os.homedir();
|
||||||
|
const AUTH_RT_BIN = process.env.AUTH_RT_BIN
|
||||||
|
|| (() => {
|
||||||
|
// Check if auth-rt is in PATH
|
||||||
|
const which = spawnSync('which', ['auth-rt'], { encoding: 'utf-8' });
|
||||||
|
if (which.status === 0 && which.stdout.trim()) {
|
||||||
|
return which.stdout.trim();
|
||||||
|
}
|
||||||
|
return path.join(home, '.local', 'bin', 'auth-rt');
|
||||||
|
})();
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: number;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionResponse {
|
||||||
|
accessToken: string;
|
||||||
|
expiresIn: number;
|
||||||
|
ownerSessionToken?: string;
|
||||||
|
hookUrl?: string;
|
||||||
|
hookToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillClientOptions {
|
||||||
|
apiBase?: string;
|
||||||
|
dryRun?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli(...args: string[]): string {
|
||||||
|
const result = spawnSync(AUTH_RT_BIN, args, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 60_000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(`auth-rt spawn failed: ${result.error.message}`);
|
||||||
|
}
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`auth-rt failed (exit ${result.status}): ${(result.stderr || '').trim()}`);
|
||||||
|
}
|
||||||
|
return (result.stdout || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SkillClient {
|
||||||
|
private readonly apiBase?: string;
|
||||||
|
private readonly dryRun: boolean;
|
||||||
|
|
||||||
|
constructor(options: SkillClientOptions = {}) {
|
||||||
|
this.apiBase = options.apiBase;
|
||||||
|
this.dryRun = options.dryRun ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async session(): Promise<SessionResponse> {
|
||||||
|
if (this.dryRun) {
|
||||||
|
return { accessToken: '<dry-run-token>', expiresIn: 900 };
|
||||||
|
}
|
||||||
|
return JSON.parse(runCli('session'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(urlPath: string): Promise<ApiResponse> {
|
||||||
|
return this.request('GET', urlPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async post(urlPath: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request('POST', urlPath, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(urlPath: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request('PUT', urlPath, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(urlPath: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request('PATCH', urlPath, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(urlPath: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
return this.request('DELETE', urlPath, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request(method: string, urlPath: string, body?: unknown): Promise<ApiResponse> {
|
||||||
|
if (this.dryRun) {
|
||||||
|
return { status: 200, body: JSON.stringify({ dryRun: true, method, path: urlPath }) };
|
||||||
|
}
|
||||||
|
const args = ['request', method, urlPath];
|
||||||
|
if (body != null) {
|
||||||
|
args.push('--body', JSON.stringify(body));
|
||||||
|
}
|
||||||
|
if (this.apiBase) {
|
||||||
|
args.push('--api-base', this.apiBase);
|
||||||
|
}
|
||||||
|
return JSON.parse(runCli(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSkillClient(options?: SkillClientOptions): SkillClient {
|
||||||
|
return new SkillClient(options);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { EmailDraft, ExportResult, ExportedFile, FetchResult } from './types.js';
|
import type { EmailDraft, ExportResult, ExportedFile, FetchResult } from './types.js';
|
||||||
import { createSkillClient } from '@clawd/auth-runtime';
|
import { createSkillClient } from './auth-cli.ts';
|
||||||
import { fetchLeadDataset } from './lead-dataset.js';
|
import { fetchLeadDataset } from './lead-dataset.js';
|
||||||
import { draftToEml, emlFilename } from './eml.js';
|
import { draftToEml, emlFilename } from './eml.js';
|
||||||
import { loadR2Config, uploadToR2 } from '@clawd/r2-upload';
|
import { loadR2Config, uploadToR2 } from '@clawd/r2-upload';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { LeadDatasetResponse } from './types.js';
|
import type { LeadDatasetResponse } from './types.js';
|
||||||
import type { SkillClient } from '@clawd/auth-runtime';
|
import type { SkillClient } from './auth-cli.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch lead-dataset from a completed cold-outreach workflow.
|
* Fetch lead-dataset from a completed cold-outreach workflow.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue