felo-ai 0.2.47 → 0.2.48

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.
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ const DEFAULT_API_BASE = 'https://openapi.felo.ai';
4
+ const DEFAULT_TIMEOUT_SEC = 60;
5
+
6
+ const VALID_CATEGORIES = ['TWITTER', 'INSTAGRAM', 'LEMON8', 'NOTECOM', 'WEBSITE', 'IMAGE'];
7
+
8
+ function usage() {
9
+ console.error(
10
+ [
11
+ 'Usage:',
12
+ ' node felo-superAgent/scripts/run_style_library.mjs --category <category> [options]',
13
+ '',
14
+ 'Options:',
15
+ ' --category <category> Style category (required)',
16
+ ` One of: ${VALID_CATEGORIES.join(', ')}`,
17
+ ' --accept-language <lang> Language for labels/tags (e.g. en, zh-Hans, ja). Default: en',
18
+ ' --json Output raw JSON',
19
+ ' --timeout <seconds> Request timeout, default 60',
20
+ ' --help Show this help',
21
+ ].join('\n')
22
+ );
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const out = {
27
+ category: '',
28
+ acceptLanguage: 'en',
29
+ json: false,
30
+ timeoutSec: DEFAULT_TIMEOUT_SEC,
31
+ help: false,
32
+ };
33
+
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a === '--help' || a === '-h') {
37
+ out.help = true;
38
+ } else if (a === '--json' || a === '-j') {
39
+ out.json = true;
40
+ } else if (a === '--category' || a === '-c') {
41
+ out.category = (argv[++i] || '').trim().toUpperCase();
42
+ } else if (a === '--accept-language') {
43
+ out.acceptLanguage = (argv[++i] || 'en').trim();
44
+ } else if (a === '--timeout' || a === '-t') {
45
+ const n = parseInt(argv[++i] || '', 10);
46
+ if (Number.isFinite(n) && n > 0) out.timeoutSec = n;
47
+ }
48
+ }
49
+
50
+ return out;
51
+ }
52
+
53
+ function getMessage(payload) {
54
+ return payload?.message || payload?.error || payload?.msg || payload?.code || 'Unknown error';
55
+ }
56
+
57
+ function isApiError(payload) {
58
+ const status = payload?.status;
59
+ const code = payload?.code;
60
+ if (typeof status === 'string' && status.toLowerCase() === 'error') return true;
61
+ if (typeof code === 'string' && code && code.toUpperCase() !== 'OK') return true;
62
+ return false;
63
+ }
64
+
65
+ /**
66
+ * Pick the best matching value from a multilingual map object.
67
+ * Falls back through: exact match → base language (e.g. "zh" for "zh-Hans") → "en" → first available.
68
+ * Returns an array (may be empty).
69
+ */
70
+ function pickLangValue(map, lang) {
71
+ if (!map || typeof map !== 'object') return [];
72
+ if (map[lang]) return map[lang];
73
+ // Try base language (e.g. "zh" from "zh-Hans")
74
+ const base = lang.split('-')[0];
75
+ if (base !== lang && map[base]) return map[base];
76
+ // Fallback to English
77
+ if (map['en']) return map['en'];
78
+ // Last resort: first available key
79
+ const first = Object.values(map)[0];
80
+ return Array.isArray(first) ? first : [];
81
+ }
82
+
83
+ /**
84
+ * Format a single style entry into the brand_style_requirement string.
85
+ * Fields included (null/empty fields are omitted):
86
+ * Style name: <name>
87
+ * Style labels: <label1, label2> (from content.labels, language-aware)
88
+ * Style DNA: <styleDna> (from content.styleDna, TWITTER type)
89
+ * Cover file ID: <coverFileId> (omitted if null/empty)
90
+ */
91
+ function formatStyle(s, lang) {
92
+ const lines = [];
93
+
94
+ // Style name (always present)
95
+ lines.push(`Style name: ${s.name ?? ''}`);
96
+
97
+ const content = s.content ?? {};
98
+
99
+ // Style labels — multilingual map (content.labels for TWITTER, content.tags for others)
100
+ const labelsMap = content.labels ?? content.tags ?? null;
101
+ if (labelsMap) {
102
+ const labelArr = pickLangValue(labelsMap, lang);
103
+ if (labelArr.length > 0) {
104
+ lines.push(`Style labels: ${labelArr.join(', ')}`);
105
+ }
106
+ }
107
+
108
+ // Style DNA (TWITTER type)
109
+ if (typeof content.styleDna === 'string' && content.styleDna.trim()) {
110
+ lines.push(`Style DNA: ${content.styleDna.trim()}`);
111
+ }
112
+
113
+ // Cover file ID — omit if null/empty
114
+ const coverId = s.coverFileId ?? s.cover_file_id ?? null;
115
+ if (coverId) {
116
+ lines.push(`Cover file ID: ${coverId}`);
117
+ }
118
+
119
+ return lines.join('\n');
120
+ }
121
+
122
+ async function main() {
123
+ const args = parseArgs(process.argv.slice(2));
124
+
125
+ if (args.help) {
126
+ usage();
127
+ process.exit(0);
128
+ }
129
+
130
+ if (!args.category) {
131
+ console.error('Error: --category is required');
132
+ usage();
133
+ process.exit(1);
134
+ }
135
+
136
+ if (!VALID_CATEGORIES.includes(args.category)) {
137
+ console.error(`Error: invalid category "${args.category}". Must be one of: ${VALID_CATEGORIES.join(', ')}`);
138
+ process.exit(1);
139
+ }
140
+
141
+ const apiKey = process.env.FELO_API_KEY?.trim();
142
+ if (!apiKey) {
143
+ console.error(
144
+ 'ERROR: FELO_API_KEY not set\n\n' +
145
+ 'To use this script, set FELO_API_KEY:\n' +
146
+ ' export FELO_API_KEY="your-api-key-here"\n' +
147
+ 'Get your API key from https://felo.ai (Settings -> API Keys).'
148
+ );
149
+ process.exit(1);
150
+ }
151
+
152
+ const apiBase = (process.env.FELO_API_BASE?.trim() || DEFAULT_API_BASE).replace(/\/$/, '');
153
+ const timeoutMs = args.timeoutSec * 1000;
154
+
155
+ const params = new URLSearchParams();
156
+ params.set('category', args.category);
157
+ const url = `${apiBase}/v2/brand/style-library/list?${params.toString()}`;
158
+
159
+ const controller = new AbortController();
160
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
161
+
162
+ try {
163
+ const res = await fetch(url, {
164
+ method: 'GET',
165
+ headers: {
166
+ Accept: 'application/json',
167
+ Authorization: `Bearer ${apiKey}`,
168
+ },
169
+ signal: controller.signal,
170
+ });
171
+
172
+ let data = {};
173
+ try {
174
+ data = await res.json();
175
+ } catch {
176
+ data = {};
177
+ }
178
+
179
+ if (!res.ok) {
180
+ throw new Error(`HTTP ${res.status}: ${getMessage(data)}`);
181
+ }
182
+ if (isApiError(data)) {
183
+ throw new Error(getMessage(data));
184
+ }
185
+
186
+ const list = data?.data?.list ?? [];
187
+
188
+ if (args.json) {
189
+ console.log(JSON.stringify(data?.data ?? {}, null, 2));
190
+ } else {
191
+ if (list.length === 0) {
192
+ console.log('(No styles found)');
193
+ } else {
194
+ // User styles first, then recommended styles
195
+ const userStyles = list.filter((s) => !s.recommended);
196
+ const recommendedStyles = list.filter((s) => s.recommended);
197
+ const allFormatted = [...userStyles, ...recommendedStyles].map((s) => formatStyle(s, args.acceptLanguage));
198
+ console.log(allFormatted.join('\n\n'));
199
+ }
200
+ }
201
+ } catch (err) {
202
+ if (err?.name === 'AbortError') {
203
+ console.error(`Error: Request timed out after ${timeoutMs / 1000}s`);
204
+ } else {
205
+ console.error(`Error: ${err?.message || err}`);
206
+ }
207
+ process.exit(1);
208
+ } finally {
209
+ clearTimeout(timer);
210
+ }
211
+ }
212
+
213
+ main();
@@ -3,11 +3,12 @@
3
3
  Dual-mode Twitter/X writing tool powered by [Felo SuperAgent](https://openapi.felo.ai/docs/api-reference/v2/superagent.html).
4
4
 
5
5
  **Mode 1** — Fetch tweets from any X account and extract a writing style DNA document.
6
- **Mode 2** — Compose tweets, threads, or X long-form posts based on a style DNA and topic.
6
+ **Mode 2** — Compose tweets, threads, or X long-form posts, optionally guided by a brand style from your style library.
7
7
 
8
8
  ## Features
9
9
 
10
- - **Style DNA extraction** — analyze an account's tone, sentence structure, hooks, hashtag strategy, and more
10
+ - **Style DNA extraction** — analyze an account's tone, sentence structure, hooks, hashtag strategy, emoji usage, and more
11
+ - **Brand style selection** — before writing, fetches your TWITTER style library and lets you pick a style; the chosen style is passed to SuperAgent via `--ext` for more accurate output
11
12
  - **Tweet creation** — single tweets, threads, or X long-form posts, default 3 versions
12
13
  - **Style imitation** — write in the voice of any public X account
13
14
  - **Iterative editing** — refine generated content via follow-up conversation
@@ -16,7 +17,7 @@ Dual-mode Twitter/X writing tool powered by [Felo SuperAgent](https://openapi.fe
16
17
 
17
18
  ## Prerequisites
18
19
 
19
- - [`felo-superAgent`](../felo-superAgent/) skill available
20
+ - [`felo-superAgent`](../felo-superAgent/) skill available (provides `run_superagent.mjs` and `run_style_library.mjs`)
20
21
  - [`felo-x-search`](../felo-x-search/) skill available
21
22
  - [`felo-livedoc`](../felo-livedoc/) skill available
22
23
 
@@ -48,12 +49,11 @@ set FELO_API_KEY=your-api-key-here
48
49
  node felo-x-search/scripts/run_x_search.mjs --id "elonmusk" --user --tweets --limit 30
49
50
  node felo-x-search/scripts/run_x_search.mjs --id "elonmusk" --user
50
51
 
51
- # Step 2: Get your live_doc_id (list existing, or create one if empty)
52
+ # Step 2: Get your live_doc_id
52
53
  node felo-livedoc/scripts/run_livedoc.mjs list --json
53
54
  # node felo-livedoc/scripts/run_livedoc.mjs create --name "Twitter Writer" --json
54
55
 
55
56
  # Step 3: Pass tweets to SuperAgent for style analysis
56
- # First call in session (no thread_short_id yet):
57
57
  node felo-superAgent/scripts/run_superagent.mjs \
58
58
  --query "/twitter-writer Analyze the following tweets from @elonmusk and extract a writing style DNA document covering tone, sentence structure, opening hooks, hashtag strategy, and emoji usage.\n\nBio: [BIO]\n\nTweets:\n[TWEETS]" \
59
59
  --live-doc-id "LIVE_DOC_ID" \
@@ -61,26 +61,130 @@ node felo-superAgent/scripts/run_superagent.mjs \
61
61
  --accept-language en
62
62
  ```
63
63
 
64
- ### 3) Mode 2 — Create content
64
+ ### 3) Mode 2 — Create content with brand style
65
65
 
66
66
  ```bash
67
- # New conversation (no thread_short_id in session)
67
+ # Step 1: Fetch your TWITTER style library
68
+ node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en
69
+ # Output example:
70
+ # Style name: darioamodei
71
+ # Style labels: Thoughtful long-form essays
72
+ # Style DNA: # Dario Amodei (@DarioAmodei) Tweet Writing Style DNA
73
+ # ...
74
+
75
+ # Step 2: Pass the chosen style via --ext (full Style DNA, do NOT truncate)
68
76
  node felo-superAgent/scripts/run_superagent.mjs \
69
- --query "/twitter-writer Write 3 versions of a tweet about AI trends in an engaging, punchy style." \
77
+ --query "/twitter-writer Write 3 versions of a tweet about AI trends." \
70
78
  --live-doc-id "LIVE_DOC_ID" \
71
79
  --skill-id twitter-writer \
80
+ --ext '{"brand_style_requirement":"Style name: darioamodei\nStyle labels: Thoughtful long-form essays\nStyle DNA: # Dario Amodei...(full content)"}' \
72
81
  --accept-language en
73
82
 
74
- # Follow-up after Mode 1 (thread_short_id already exists — pass --thread-id from previous [state] output)
83
+ # Follow-up (thread already exists — no --ext needed)
75
84
  node felo-superAgent/scripts/run_superagent.mjs \
76
- --query "/twitter-writer Based on the style DNA above, write 3 tweets about startups. Keep each under 280 characters." \
85
+ --query "/twitter-writer Make the second version more concise." \
77
86
  --thread-id "THREAD_SHORT_ID" \
78
87
  --live-doc-id "LIVE_DOC_ID" \
79
88
  --accept-language en
80
89
  ```
81
90
 
82
- ## When to use (Agent)
91
+ ## Style Library
83
92
 
84
- Trigger keywords: `write a tweet`, `twitter thread`, `style DNA`, `imitate tweet style`, `tweet in the style of`, `X account analysis`, `ghostwrite tweets`, `how does [account] write`, `ツイートを書く`, `ツイートスタイル分析`, `スタイルDNA`, `ツイートを模倣`, `〇〇風のツイートを書く`, `ツイートを代筆`, `Xアカウント分析`, `/felo-twitter-writer`.
93
+ The style library (`run_style_library.mjs`) returns your saved TWITTER writing styles. Each entry contains:
85
94
 
86
- See [SKILL.md](SKILL.md) for full agent instructions.
95
+ | Field | Source | Notes |
96
+ |---|---|---|
97
+ | `Style name` | `name` | Always present |
98
+ | `Style labels` | `content.labels[lang]` | Language-aware, comma-separated; omitted if absent |
99
+ | `Style DNA` | `content.styleDna` | Full text — pass completely, never truncate |
100
+ | `Cover file ID` | `coverFileId` | Omitted if null |
101
+
102
+ User-created styles appear before recommended styles.
103
+
104
+ ```bash
105
+ # List styles in English
106
+ node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language en
107
+
108
+ # List styles in Chinese
109
+ node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --accept-language zh-Hans
110
+
111
+ # Raw JSON output
112
+ node felo-superAgent/scripts/run_style_library.mjs --category TWITTER --json
113
+ ```
114
+
115
+ ## Using with Claude Code
116
+
117
+ ### Installation
118
+
119
+ ```bash
120
+ # Via ClawHub
121
+ clawhub install felo-twitter-writer
122
+
123
+ # Manual
124
+ cp -r felo-twitter-writer ~/.claude/skills/
125
+ cp -r felo-superAgent ~/.claude/skills/
126
+ cp -r felo-x-search ~/.claude/skills/
127
+ cp -r felo-livedoc ~/.claude/skills/
128
+ ```
129
+
130
+ ### Triggering the skill
131
+
132
+ Claude Code automatically triggers this skill when it detects tweet-writing or style-analysis intent. You can also invoke it explicitly:
133
+
134
+ ```
135
+ /felo-twitter-writer Analyze @paulg's tweet style and extract a style DNA document
136
+ /felo-twitter-writer Write 3 tweets about AI trends
137
+ /felo-twitter-writer Write a Twitter thread about why most startups fail
138
+ /felo-twitter-writer Write a tweet about AI in the style of @darioamodei
139
+ ```
140
+
141
+ ### What Claude does automatically
142
+
143
+ When you ask Claude to write tweets (Mode 2, new conversation):
144
+
145
+ 1. **Fetches your TWITTER style library** — runs `run_style_library.mjs --category TWITTER`
146
+ 2. **Presents style options** — shows names grouped as "Your styles" / "Recommended styles" plus a "No preference" option
147
+ 3. **Waits for your choice** — then calls SuperAgent with the full style block in `--ext`
148
+ 4. **Streams the result** — answer appears in real time; follow-ups reuse the thread without re-fetching styles
149
+
150
+ If the style library is empty, Claude skips the selection step silently and proceeds without `--ext`.
151
+
152
+ ### Example conversation
153
+
154
+ ```
155
+ You: Write a Twitter thread about why most startups fail
156
+
157
+ Claude: Here are the available Twitter writing styles — choosing one will make
158
+ the output more accurate:
159
+
160
+ [Your styles]
161
+ 1. My Bold Voice
162
+
163
+ [Recommended styles]
164
+ 2. darioamodei
165
+
166
+ 0. No preference — use default style
167
+
168
+ You: 1
169
+
170
+ Claude: [streams the thread in "My Bold Voice" style in real time]
171
+
172
+ You: Make the hook tweet more provocative
173
+
174
+ Claude: [follow-up — no style re-selection needed]
175
+ ```
176
+
177
+ ## Trigger keywords
178
+
179
+ English: `write a tweet`, `draft a tweet`, `twitter thread`, `X article`, `style DNA`, `imitate tweet style`, `tweet in the style of`, `write like [account]`, `X account analysis`, `ghostwrite tweets`, `how does [account] write`
180
+
181
+ Japanese: `ツイートを書く`, `ツイートスタイル分析`, `スタイルDNA`, `ツイートを模倣`, `Xアカウント分析`, `〇〇風のツイートを書く`, `ツイートを代筆`
182
+
183
+ Explicit command: `/felo-twitter-writer`
184
+
185
+ ## References
186
+
187
+ - [SKILL.md](SKILL.md) — full agent instructions and decision logic
188
+ - [felo-superAgent README](../felo-superAgent/README.md) — SuperAgent usage and style library script
189
+ - [Felo Open Platform](https://openapi.felo.ai/docs/)
190
+ - [Get API Key](https://felo.ai) (Settings → API Keys)