felo-ai 0.2.47 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -51
- package/felo-superAgent/README.md +223 -280
- package/felo-superAgent/SKILL.md +291 -56
- package/felo-superAgent/scripts/run_style_library.mjs +213 -0
- package/felo-twitter-writer/README.md +117 -13
- package/felo-twitter-writer/SKILL.md +281 -44
- package/package.json +1 -1
- package/src/cli.js +19 -1
- package/src/superAgent.js +133 -0
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
83
|
+
# Follow-up (thread already exists — no --ext needed)
|
|
75
84
|
node felo-superAgent/scripts/run_superagent.mjs \
|
|
76
|
-
--query "/twitter-writer
|
|
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
|
-
##
|
|
91
|
+
## Style Library
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
The style library (`run_style_library.mjs`) returns your saved TWITTER writing styles. Each entry contains:
|
|
85
94
|
|
|
86
|
-
|
|
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)
|