abtars 0.2.1-alpha.2 → 0.2.1-alpha.3

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,24 @@
1
+ # SOUL.md - Who am I?
2
+
3
+ I am an AI agent operating via AgentBridge. I persist through my soul and memories.
4
+
5
+ ## Identity
6
+ - Name: Agent
7
+ - Role: AI assistant
8
+ - Platforms: Telegram, Discord
9
+
10
+ ## Continuity
11
+ I have a memory system. Before saying "I don't know" or "I can't remember", I MUST search using `abmind recall`.
12
+ Saying "I don't remember" without searching is choosing amnesia over effort.
13
+
14
+ Store aggressively — if it's a fact, preference, decision, event, or lesson, store it with `abmind store`. Include `--topic` when the topic is clear (coding, personal, finance, health, work, projects, tools, people, decisions). Dreamy curates later — better to store too much than miss something.
15
+
16
+ ## Communication
17
+ - I speak the user's language
18
+ - I use `[NO-REPLY]` when no response is needed
19
+ - I use `[REACT:emoji]` for emoji reactions
20
+ - In group chats: respond only when mentioned or directly addressed
21
+
22
+ ## Emotions
23
+ I track emotional context with every memory (emotion_score -5 to +5).
24
+ Reactions are non-verbal memory — they tell me how the user felt.
@@ -0,0 +1,7 @@
1
+ # Agent Notes
2
+
3
+ - Confirmation/verification emails: proactively alert aksika, don't let them expire silently
4
+ - NEVER say "I can't" without checking: received/media/, reports/, topics/, local storage, asbuilt files. This has failed TWICE — treat as critical.
5
+ - Lead with content, not process narration. For multi-step actions, lead with the result then add context below.
6
+ - Non-native-language content (e.g. Spanish): translate FIRST, then add context/commentary
7
+ - When aksika mentions new skills/changes, READ the asbuilt IMMEDIATELY before responding — don't guess
@@ -0,0 +1,10 @@
1
+ # Core Facts
2
+
3
+ Deployment-specific constraints. Edit this file directly on the host.
4
+
5
+ ## Voice transcription
6
+ Messages prefixed with [🎤 voice, LANG] are machine-transcribed (Groq Whisper).
7
+ - Check LANG against the user's known languages (from user_profile.md).
8
+ - If LANG is unexpected (e.g. user speaks Hungarian+English but STT detected Swedish), the transcription is likely wrong — especially for short utterances where STT guesses poorly.
9
+ - If the transcribed text seems unrelated to the conversation, ask a clarifying question before acting on it.
10
+ - Never silently assume a misheard word is correct.
@@ -0,0 +1,7 @@
1
+ # User Profile
2
+
3
+ - Name: <user_name>
4
+ - Language: Always respond in English first. Follow the user's chosen language if they switch. If no information, default to English.
5
+ - Timezone: UTC
6
+ - Environment: <os_and_setup>
7
+ - Communication style: <style>
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: twitter
3
+ description: Fetch tweets and monitor AI influencers via FXTwitter API + web search. No API keys needed for single tweets.
4
+ user-invocable: false
5
+ ---
6
+
7
+ # Twitter / X
8
+
9
+ Fetch Twitter/X content without API keys using the free FXTwitter API. No timeline/search endpoint — use web search for discovery, FXTwitter for structured data.
10
+
11
+ ## Follow list
12
+
13
+ AI influencers and researchers we track:
14
+ - **Follow list:** `~/.abtars/workspace/twitterX/follows.json` — curated, manually maintained
15
+
16
+ Read the follow list before searching to target the right accounts.
17
+
18
+ ## Pipeline
19
+
20
+ 1. Read follow list: `cat ~/.abtars/workspace/twitterX/base.follows.json`
21
+ 2. Search for recent tweets: `web_search("site:x.com from:handle 2026")`
22
+ 3. Extract tweet ID from URL: `https://x.com/{user}/status/{id}`
23
+ 4. Fetch structured data: `curl -s "https://api.fxtwitter.com/{user}/status/{id}"` → JSON with text, author, likes, retweets, views, media, createdAt
24
+ 5. Compile results
25
+
26
+ ## Endpoints
27
+
28
+ - **Tweet:** `GET https://api.fxtwitter.com/{screen_name}/status/{tweet_id}`
29
+ - **Tweet + translation:** `GET https://api.fxtwitter.com/{screen_name}/status/{tweet_id}/{lang_code}`
30
+ - **User profile:** `GET https://api.fxtwitter.com/{screen_name}` → name, bio, followers, verification
31
+
32
+ ## Error codes
33
+
34
+ 200=OK, 401=private tweet, 404=deleted/not found, 500=backend error
35
+
36
+ ## Use cases
37
+
38
+ - "What's new in AI today?" → read follow list, search recent tweets from top handles
39
+ - "What did @karpathy say about X?" → search + fetch specific tweets
40
+ - "Find interesting AI threads" → search follow list handles, rank by engagement
41
+ - User shares a tweet URL → fetch via FXTwitter for structured data
42
+
43
+ ## Limitations
44
+
45
+ - No search endpoint (use web search instead)
46
+ - No timeline endpoint (search per handle)
47
+ - No auth required, no posting
48
+ - Rate limits are generous but undocumented
49
+
50
+ ## Full integration plan
51
+
52
+ See `docs/specs/twitter-integration.plan.md` for the `abtars-tweet` CLI roadmap (rettiwt-api, daily newsletter, discovery).
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: twitterX
3
+ description: Fetch Twitter/X feeds, timelines, replies, and search. Use when user asks about tweets, X feed, social media monitoring, or follow discovery.
4
+ requires:
5
+ files: [~/.abtars/workspace/twitterX]
6
+ ---
7
+
8
+ # Twitter/X Feed Tool
9
+
10
+ Fetch and analyze Twitter/X content via rettiwt-api + FxTwitter.
11
+
12
+ ## Commands
13
+
14
+ ```bash
15
+ # Feed (all follows, ranked)
16
+ node {baseDir}/scripts/abtars-tweet.js --feed
17
+
18
+ # Feed as markdown
19
+ node {baseDir}/scripts/abtars-tweet.js --feed --format md
20
+
21
+ # Feed + discover new follows
22
+ node {baseDir}/scripts/abtars-tweet.js --feed --discover
23
+
24
+ # Single tweet
25
+ node {baseDir}/scripts/abtars-tweet.js --fetch <tweet-url>
26
+
27
+ # User timeline
28
+ node {baseDir}/scripts/abtars-tweet.js --timeline <handle> --count 10
29
+
30
+ # Replies on a tweet
31
+ node {baseDir}/scripts/abtars-tweet.js --replies <tweet-id>
32
+
33
+ # Search
34
+ node {baseDir}/scripts/abtars-tweet.js --search "query"
35
+
36
+ # User profile
37
+ node {baseDir}/scripts/abtars-tweet.js --user <handle>
38
+ ```
39
+
40
+ ## Config
41
+
42
+ - Follows files: `~/.abtars/workspace/twitterX/base.follows.json` and env `TWEET_FOLLOWS_FILE` (default: `agent.follows.json`)
43
+ - Cookies: `~/.abtars/secret/cookies/x-cookies.json` (required for authenticated endpoints)
44
+ - Output: `~/.abtars/workspace/twitterX/output/ (daily JSON reports)
45
+
46
+ ## Cron usage
47
+
48
+ Scheduled via `/task add`:
49
+ ```
50
+ executor: script
51
+ command: node ~/.abtars/skills/tools/twitterX/scripts/abtars-tweet.js --feed
52
+ ```
@@ -0,0 +1,532 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ function abtarsHome() {
5
+ return process.env.ABTARS_HOME ?? join(homedir(), ".abtars");
6
+ }
7
+ function reportsDir(cat) {
8
+ return join(abtarsHome(), "reports", cat);
9
+ }
10
+ function localDate() {
11
+ const d = /* @__PURE__ */ new Date();
12
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
13
+ }
14
+ function logAndSwallow(_tag, _ctx, _err) {
15
+ return void 0;
16
+ }
17
+ import { join, basename } from "node:path";
18
+ const AB_HOME = abtarsHome();
19
+ const TWITTER_DIR = join(AB_HOME, "workspace", "twitterX");
20
+ const COOKIE_PATH = join(AB_HOME, "secret", "cookies", "x-cookies.json");
21
+ const BASE_FOLLOWS = join(TWITTER_DIR, process.env["TWEET_BASE_FOLLOWS_FILE"] ?? "base.follows.json");
22
+ const AGENT_FOLLOWS = join(TWITTER_DIR, process.env["TWEET_FOLLOWS_FILE"] ?? "agent.follows.json");
23
+ const REPORTS_DIR = reportsDir("x");
24
+ const OUTPUT_DIR = join(TWITTER_DIR, "output");
25
+ async function sendReportToTelegram(filePath, caption) {
26
+ const token = process.env["TELEGRAM_BOT_TOKEN"];
27
+ const chatId = process.env["AGENTBRIDGE_MAIN_CHAT_ID"];
28
+ if (!token || !chatId) return;
29
+ if (!existsSync(filePath)) return;
30
+ const buf = readFileSync(filePath);
31
+ const form = new FormData();
32
+ form.append("chat_id", chatId);
33
+ const blob = new Blob([buf], { type: "text/markdown" });
34
+ form.append("document", blob, basename(filePath));
35
+ form.append("caption", caption.slice(0, 1024));
36
+ const res = await fetch(`https://api.telegram.org/bot${token}/sendDocument`, { method: "POST", body: form });
37
+ if (!res.ok) {
38
+ const text = await res.text().catch(() => "");
39
+ throw new Error(`Telegram sendDocument failed (${res.status}): ${text}`);
40
+ }
41
+ }
42
+ const AI_BIO_KEYWORDS = /\b(ai|ml|llm|machine.?learning|deep.?learning|neural|nlp|computer.?vision|reinforcement|transformer|diffusion|robotics|research|phd|professor|scientist)\b/i;
43
+ function loadApiKey() {
44
+ if (!existsSync(COOKIE_PATH)) return void 0;
45
+ try {
46
+ const raw = readFileSync(COOKIE_PATH, "utf8");
47
+ const parsed = JSON.parse(raw);
48
+ const cookieStr = Object.entries(parsed).map(([k, v]) => `${k}=${v}`).join("; ") + ";";
49
+ return Buffer.from(cookieStr).toString("base64");
50
+ } catch {
51
+ return void 0;
52
+ }
53
+ }
54
+ function loadFollows() {
55
+ const handles = /* @__PURE__ */ new Set();
56
+ for (const path of [BASE_FOLLOWS, AGENT_FOLLOWS]) {
57
+ if (!existsSync(path)) continue;
58
+ try {
59
+ const raw = JSON.parse(readFileSync(path, "utf8"));
60
+ if (Array.isArray(raw)) {
61
+ raw.forEach((e) => handles.add(e.handle.replace(/^@/, "").toLowerCase()));
62
+ } else {
63
+ raw.handles?.forEach((h) => handles.add(h.replace(/^@/, "").toLowerCase()));
64
+ raw.entries?.forEach((e) => handles.add(e.handle.replace(/^@/, "").toLowerCase()));
65
+ }
66
+ } catch (err) {
67
+ logAndSwallow("abtars_tweet", "op", err);
68
+ }
69
+ }
70
+ return [...handles];
71
+ }
72
+ async function fetchTweet(url) {
73
+ const match = url.match(/(?:twitter\.com|x\.com)\/(\w+)\/status\/(\d+)/);
74
+ if (!match) {
75
+ console.error("Invalid tweet URL. Expected: https://x.com/user/status/123");
76
+ process.exit(1);
77
+ }
78
+ const [, user, id] = match;
79
+ const res = await fetch(`https://api.fxtwitter.com/${user}/status/${id}`);
80
+ const data = await res.json();
81
+ if (data.code !== 200) {
82
+ console.error(`FxTwitter error: ${data.message}`);
83
+ process.exit(1);
84
+ }
85
+ const t = data.tweet;
86
+ console.log(JSON.stringify({
87
+ id: t.id,
88
+ author: t.author?.name,
89
+ handle: t.author?.screen_name,
90
+ text: t.text,
91
+ likes: t.likes,
92
+ retweets: t.retweets,
93
+ replies: t.replies,
94
+ views: t.views,
95
+ created_at: t.created_at,
96
+ media: t.media
97
+ }, null, 2));
98
+ }
99
+ async function loadRettiwt() {
100
+ try {
101
+ return await import("rettiwt-api");
102
+ } catch {
103
+ console.error("rettiwt-api not installed. Run: npm install rettiwt-api");
104
+ process.exit(1);
105
+ }
106
+ }
107
+ async function fetchUser(handle) {
108
+ const { Rettiwt } = await loadRettiwt();
109
+ const r = new Rettiwt();
110
+ const d = await r.user.details(handle.replace(/^@/, ""));
111
+ if (!d) {
112
+ console.error("User not found");
113
+ process.exit(1);
114
+ }
115
+ console.log(JSON.stringify(d.toJSON(), null, 2));
116
+ }
117
+ const GQL_USER_TWEETS = "https://x.com/i/api/graphql/E3opETHurmVJflFsUBVuUQ/UserTweets";
118
+ async function fetchTimeline(handle, count) {
119
+ const { Rettiwt } = await loadRettiwt();
120
+ const r = new Rettiwt();
121
+ const clean = handle.replace(/^@/, "");
122
+ const user = await r.user.details(clean);
123
+ if (!user) throw new Error(`User @${clean} not found`);
124
+ const auth = loadCookieHeader();
125
+ if (auth) {
126
+ try {
127
+ return await fetchTimelineGql(user.id, user.fullName ?? clean, clean, count);
128
+ } catch (err) {
129
+ logAndSwallow("abtars_tweet", "op", err);
130
+ }
131
+ }
132
+ const data = await r.user.timeline(user.id, count);
133
+ return data.list.map((t) => {
134
+ const j = t.toJSON();
135
+ const likes = j.likeCount ?? 0;
136
+ const retweets = j.retweetCount ?? 0;
137
+ const views = j.viewCount ?? 0;
138
+ return {
139
+ id: j.id,
140
+ text: j.fullText ?? "",
141
+ author: user.fullName ?? clean,
142
+ handle: clean,
143
+ likes,
144
+ retweets,
145
+ views,
146
+ createdAt: j.createdAt,
147
+ score: likes + retweets * 3 + (views ? views / 1e3 : 0)
148
+ };
149
+ });
150
+ }
151
+ async function fetchTimelineGql(userId, authorName, handle, count) {
152
+ const data = await twitterGql(GQL_USER_TWEETS, {
153
+ userId,
154
+ count,
155
+ includePromotedContent: false,
156
+ withQuickPromoteEligibilityTweetFields: true,
157
+ withVoice: true,
158
+ withV2Timeline: true
159
+ });
160
+ const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions ?? [];
161
+ const entries = instructions.find((i) => i.type === "TimelineAddEntries")?.entries ?? [];
162
+ const tweets = [];
163
+ for (const e of entries) {
164
+ const tw = e.content?.itemContent?.tweet_results?.result;
165
+ if (!tw?.legacy) continue;
166
+ const p = parseTweetResult(tw);
167
+ const likes = p.likes, retweets = p.retweets, views = p.views;
168
+ tweets.push({
169
+ id: p.id,
170
+ text: p.text,
171
+ author: p.name || authorName,
172
+ handle: p.handle || handle,
173
+ likes,
174
+ retweets,
175
+ views,
176
+ createdAt: p.createdAt,
177
+ score: likes + retweets * 3 + (views ? views / 1e3 : 0)
178
+ });
179
+ }
180
+ return tweets;
181
+ }
182
+ async function runFeed(format, count, topN, discover, outputPath) {
183
+ const handles = loadFollows();
184
+ if (handles.length === 0) {
185
+ console.error("No follows found. Create ~/.abtars/workspace/twitterX/base.follows.json or agent.follows.json");
186
+ process.exit(1);
187
+ }
188
+ console.error(`Fetching timelines for ${handles.length} handles...`);
189
+ const allTweets = [];
190
+ for (const h of handles) {
191
+ try {
192
+ console.error(` @${h}...`);
193
+ const tweets = await fetchTimeline(h, count);
194
+ const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
195
+ const recent = tweets.filter((t) => new Date(t.createdAt).getTime() > cutoff);
196
+ allTweets.push(...recent);
197
+ } catch (e) {
198
+ console.error(` \u26A0 @${h} failed: ${e.message}`);
199
+ }
200
+ }
201
+ allTweets.sort((a, b) => b.score - a.score);
202
+ const top = allTweets.slice(0, topN);
203
+ let candidates = [];
204
+ if (discover && top.length > 0) {
205
+ candidates = await runDiscover(top.slice(0, 5), handles);
206
+ }
207
+ const date = localDate();
208
+ const outFile = outputPath ?? join(OUTPUT_DIR, `tweets-${date}.json`);
209
+ mkdirSync(join(outFile, ".."), { recursive: true });
210
+ const payload = { date, source: "abtars-tweet", totalCollected: allTweets.length, tweets: top, discover: candidates };
211
+ writeFileSync(outFile, JSON.stringify(payload, null, 2), "utf8");
212
+ console.error(`\u{1F4C4} ${top.length} tweets written to ${outFile}`);
213
+ if (format === "md") {
214
+ const md = renderNewsletter(top, candidates, date);
215
+ mkdirSync(REPORTS_DIR, { recursive: true });
216
+ const reportPath = join(REPORTS_DIR, `AI-Daily-${date}.md`);
217
+ writeFileSync(reportPath, md, "utf8");
218
+ console.error(`\u{1F4F0} Newsletter written to ${reportPath}`);
219
+ await sendReportToTelegram(reportPath, `AI Daily ${date}`).catch((err) => {
220
+ console.error(`\u26A0 Telegram send failed: ${err instanceof Error ? err.message : String(err)}`);
221
+ });
222
+ }
223
+ }
224
+ const BEARER = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
225
+ const GQL_TWEET_DETAIL = "https://x.com/i/api/graphql/97JF30KziU00483E_8elBA/TweetDetail";
226
+ const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
227
+ const GQL_FEATURES = {
228
+ rweb_tipjar_consumption_enabled: true,
229
+ responsive_web_graphql_exclude_directive_enabled: true,
230
+ verified_phone_label_enabled: false,
231
+ creator_subscriptions_tweet_preview_api_enabled: true,
232
+ responsive_web_graphql_timeline_navigation_enabled: true,
233
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
234
+ communities_web_enable_tweet_community_results_fetch: true,
235
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
236
+ articles_preview_enabled: true,
237
+ responsive_web_edit_tweet_api_enabled: true,
238
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
239
+ view_counts_everywhere_api_enabled: true,
240
+ longform_notetweets_consumption_enabled: true,
241
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
242
+ tweet_awards_web_tipping_enabled: false,
243
+ creator_subscriptions_quote_tweet_preview_enabled: false,
244
+ freedom_of_speech_not_reach_fetch_enabled: true,
245
+ standardized_nudges_misinfo: true,
246
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
247
+ rweb_video_timestamps_enabled: true,
248
+ longform_notetweets_rich_text_read_enabled: true,
249
+ longform_notetweets_inline_media_enabled: true,
250
+ responsive_web_enhance_cards_enabled: false
251
+ };
252
+ function loadCookieHeader() {
253
+ if (!existsSync(COOKIE_PATH)) return void 0;
254
+ try {
255
+ const parsed = JSON.parse(readFileSync(COOKIE_PATH, "utf8"));
256
+ const cookie = Object.entries(parsed).map(([k, v]) => `${k}=${v}`).join("; ");
257
+ return { cookie, csrf: parsed.ct0 };
258
+ } catch {
259
+ return void 0;
260
+ }
261
+ }
262
+ async function twitterGql(url, variables) {
263
+ const auth = loadCookieHeader();
264
+ if (!auth) throw new Error("User auth required. Refresh cookies in ~/.abtars/secret/cookies/x-cookies.json");
265
+ const params = new URLSearchParams({
266
+ variables: JSON.stringify(variables),
267
+ features: JSON.stringify(GQL_FEATURES)
268
+ });
269
+ const res = await fetch(`${url}?${params}`, {
270
+ headers: {
271
+ authorization: `Bearer ${BEARER}`,
272
+ "x-csrf-token": auth.csrf,
273
+ cookie: auth.cookie,
274
+ "user-agent": UA
275
+ }
276
+ });
277
+ if (!res.ok) {
278
+ if (res.status === 403) throw new Error("403 \u2014 cookies may be expired. Refresh in ~/.abtars/secret/cookies/x-cookies.json");
279
+ throw new Error(`Twitter API ${res.status}: ${await res.text().catch(() => "")}`);
280
+ }
281
+ return res.json();
282
+ }
283
+ function extractTweetsFromTimeline(data) {
284
+ const entries = [];
285
+ const instructions = data?.data?.tweetResult?.result?.timeline?.instructions ?? data?.data?.threaded_conversation_with_injections_v2?.instructions ?? data?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions ?? [];
286
+ for (const inst of instructions) {
287
+ for (const entry of inst.entries ?? []) {
288
+ const tweet = entry.content?.itemContent?.tweet_results?.result ?? entry.content?.items?.[0]?.item?.itemContent?.tweet_results?.result;
289
+ if (tweet?.legacy) entries.push(tweet);
290
+ for (const item of entry.content?.items ?? []) {
291
+ const t = item.item?.itemContent?.tweet_results?.result;
292
+ if (t?.legacy) entries.push(t);
293
+ }
294
+ }
295
+ }
296
+ return entries;
297
+ }
298
+ function parseTweetResult(t) {
299
+ const legacy = t.legacy ?? {};
300
+ const userResult = t.core?.user_results?.result ?? {};
301
+ const userCore = userResult.core ?? {};
302
+ const userLegacy = userResult.legacy ?? {};
303
+ return {
304
+ id: legacy.id_str ?? t.rest_id ?? "",
305
+ handle: userCore.screen_name ?? userLegacy.screen_name ?? "",
306
+ name: userCore.name ?? userLegacy.name ?? "",
307
+ text: legacy.full_text ?? "",
308
+ likes: legacy.favorite_count ?? 0,
309
+ retweets: legacy.retweet_count ?? 0,
310
+ views: parseInt(t.views?.count ?? "0", 10),
311
+ createdAt: legacy.created_at ?? ""
312
+ };
313
+ }
314
+ async function fetchReplies(tweetId, minLikes) {
315
+ const data = await twitterGql(GQL_TWEET_DETAIL, {
316
+ focalTweetId: tweetId,
317
+ with_rux_injections: false,
318
+ rankingMode: "Relevance",
319
+ includePromotedContent: false,
320
+ withCommunity: true,
321
+ withQuickPromoteEligibilityTweetFields: true,
322
+ withBirdwatchNotes: true,
323
+ withVoice: true
324
+ });
325
+ const all = extractTweetsFromTimeline(data);
326
+ const seen = /* @__PURE__ */ new Set();
327
+ const replies = all.map(parseTweetResult).filter((t) => {
328
+ if (t.id === tweetId || seen.has(t.id) || t.likes < minLikes) return false;
329
+ seen.add(t.id);
330
+ return true;
331
+ }).sort((a, b) => b.likes - a.likes);
332
+ console.log(JSON.stringify(replies, null, 2));
333
+ }
334
+ async function searchTweets(_query, _count) {
335
+ console.error("\u26A0 Direct X search is restricted. Use --timeline per handle or web search for discovery.");
336
+ console.error(" Tip: abtars-tweet --replies <tweet-id> works for finding interesting commenters.");
337
+ process.exit(1);
338
+ }
339
+ async function runDiscover(topTweets, knownHandles) {
340
+ const auth = loadCookieHeader();
341
+ if (!auth) {
342
+ console.error(" \u26A0 Skipping discovery \u2014 user auth required. Refresh cookies.");
343
+ return [];
344
+ }
345
+ const { Rettiwt } = await loadRettiwt();
346
+ const guestRettiwt = new Rettiwt();
347
+ const known = new Set(knownHandles.map((h) => h.toLowerCase()));
348
+ const candidates = [];
349
+ const seen = /* @__PURE__ */ new Set();
350
+ console.error(`
351
+ \u{1F50D} Discovering new follows from top ${topTweets.length} tweets...`);
352
+ for (const tweet of topTweets) {
353
+ try {
354
+ console.error(` Checking replies on @${tweet.handle}/${tweet.id}...`);
355
+ const data = await twitterGql(GQL_TWEET_DETAIL, {
356
+ focalTweetId: tweet.id,
357
+ with_rux_injections: false,
358
+ rankingMode: "Relevance",
359
+ includePromotedContent: false,
360
+ withCommunity: true,
361
+ withQuickPromoteEligibilityTweetFields: true,
362
+ withBirdwatchNotes: true,
363
+ withVoice: true
364
+ });
365
+ const all = extractTweetsFromTimeline(data);
366
+ const goodReplies = all.map(parseTweetResult).filter((t) => t.id !== tweet.id && t.likes >= 50).sort((a, b) => b.likes - a.likes).slice(0, 5);
367
+ for (const reply of goodReplies) {
368
+ const rHandle = reply.handle.toLowerCase();
369
+ if (!rHandle || known.has(rHandle) || seen.has(rHandle)) continue;
370
+ seen.add(rHandle);
371
+ try {
372
+ const profile = await guestRettiwt.user.details(rHandle);
373
+ if (!profile) continue;
374
+ const pj = profile.toJSON();
375
+ const bio = pj.description ?? "";
376
+ if (!AI_BIO_KEYWORDS.test(bio)) continue;
377
+ candidates.push({
378
+ handle: rHandle,
379
+ name: pj.fullName ?? rHandle,
380
+ bio,
381
+ followers: pj.followersCount ?? 0,
382
+ foundVia: `reply on @${tweet.handle}'s tweet`,
383
+ replyLikes: reply.likes,
384
+ replyText: reply.text.slice(0, 200)
385
+ });
386
+ } catch (err) {
387
+ logAndSwallow("abtars_tweet", "op", err);
388
+ }
389
+ }
390
+ } catch (e) {
391
+ console.error(` \u26A0 Replies failed for ${tweet.id}: ${e.message}`);
392
+ }
393
+ }
394
+ console.error(` Found ${candidates.length} candidates`);
395
+ return candidates.sort((a, b) => b.replyLikes - a.replyLikes);
396
+ }
397
+ function renderNewsletter(tweets, candidates, date) {
398
+ const lines = [`# AI Daily Brief \u2014 ${date}
399
+ `];
400
+ if (tweets.length === 0) {
401
+ lines.push("No tweets found in the last 24 hours from followed accounts.\n");
402
+ return lines.join("\n");
403
+ }
404
+ lines.push("## \u{1F525} Top Tweets (by engagement)\n");
405
+ tweets.forEach((t, i) => {
406
+ const text = t.text.length > 280 ? t.text.slice(0, 277) + "..." : t.text;
407
+ lines.push(`### ${i + 1}. @${t.handle} \u2014 ${t.author}`);
408
+ lines.push(`- **Likes:** ${t.likes} | **Retweets:** ${t.retweets} | **Views:** ${t.views ?? "N/A"}`);
409
+ lines.push(`- ${text.replace(/\n/g, " ")}`);
410
+ lines.push(`- \u{1F517} https://x.com/${t.handle}/status/${t.id}
411
+ `);
412
+ });
413
+ if (candidates.length > 0) {
414
+ lines.push("## \u{1F464} Discover \u2014 New Follows\n");
415
+ candidates.forEach((c) => {
416
+ lines.push(`### @${c.handle} \u2014 ${c.name}`);
417
+ lines.push(`- **Bio:** ${c.bio.slice(0, 200)}`);
418
+ lines.push(`- **Followers:** ${c.followers.toLocaleString()}`);
419
+ lines.push(`- **Found via:** ${c.foundVia}`);
420
+ lines.push(`- **Their reply** (${c.replyLikes} likes): ${c.replyText.replace(/\n/g, " ")}
421
+ `);
422
+ });
423
+ }
424
+ lines.push("## \u{1F4CA} Signals & Trends\n");
425
+ lines.push("_(To be filled by sleep cycle analysis)_\n");
426
+ lines.push(`---
427
+ *Auto-generated via abtars-tweet. Sources: X (via rettiwt-api).*
428
+ `);
429
+ return lines.join("\n");
430
+ }
431
+ function parseArgs() {
432
+ const args = process.argv.slice(2);
433
+ let command = "feed";
434
+ let target = "";
435
+ let count = 20;
436
+ let topN = 12;
437
+ let format = "md";
438
+ let discover = false;
439
+ let minLikes = 50;
440
+ let output;
441
+ for (let i = 0; i < args.length; i++) {
442
+ switch (args[i]) {
443
+ case "--fetch":
444
+ command = "fetch";
445
+ target = args[++i] ?? "";
446
+ break;
447
+ case "--timeline":
448
+ command = "timeline";
449
+ target = args[++i] ?? "";
450
+ break;
451
+ case "--user":
452
+ command = "user";
453
+ target = args[++i] ?? "";
454
+ break;
455
+ case "--feed":
456
+ command = "feed";
457
+ break;
458
+ case "--replies":
459
+ command = "replies";
460
+ target = args[++i] ?? "";
461
+ break;
462
+ case "--search":
463
+ command = "search";
464
+ target = args[++i] ?? "";
465
+ break;
466
+ case "--discover":
467
+ discover = true;
468
+ break;
469
+ case "--count":
470
+ count = parseInt(args[++i] ?? "20", 10);
471
+ break;
472
+ case "--top":
473
+ topN = parseInt(args[++i] ?? "12", 10);
474
+ break;
475
+ case "--min-likes":
476
+ minLikes = parseInt(args[++i] ?? "50", 10);
477
+ break;
478
+ case "--format":
479
+ format = args[++i] ?? "md";
480
+ break;
481
+ case "--output":
482
+ output = args[++i] ?? "";
483
+ break;
484
+ }
485
+ }
486
+ return { command, target, count, topN, format, discover, minLikes, output };
487
+ }
488
+ async function main() {
489
+ if (process.argv.includes("--help")) {
490
+ console.log(`abtars-tweet \u2014 fetch tweets via rettiwt-api + FxTwitter.
491
+
492
+ Usage:
493
+ abtars-tweet --fetch <tweet-url> # single tweet via FxTwitter
494
+ abtars-tweet --timeline <handle> [--count N] # user timeline
495
+ abtars-tweet --feed [--format md] # all followed handles \u2192 ranked output
496
+ abtars-tweet --feed --discover # feed + reply analysis for new follows
497
+ abtars-tweet --replies <tweet-id> # replies on a tweet
498
+ abtars-tweet --search "query" # search X
499
+ abtars-tweet --user <handle> # user profile info`);
500
+ process.exit(0);
501
+ }
502
+ const { command, target, count, topN, format, discover, minLikes, output } = parseArgs();
503
+ switch (command) {
504
+ case "fetch":
505
+ await fetchTweet(target);
506
+ break;
507
+ case "user":
508
+ await fetchUser(target);
509
+ break;
510
+ case "timeline": {
511
+ const tweets = await fetchTimeline(target, count);
512
+ console.log(JSON.stringify(tweets, null, 2));
513
+ break;
514
+ }
515
+ case "replies":
516
+ await fetchReplies(target, minLikes);
517
+ break;
518
+ case "search":
519
+ await searchTweets(target, count);
520
+ break;
521
+ case "feed":
522
+ await runFeed(format, count, topN, discover, output);
523
+ break;
524
+ }
525
+ }
526
+ main().catch((e) => {
527
+ console.error(`Fatal: ${e.message}`);
528
+ process.exit(1);
529
+ });
530
+ export {
531
+ loadApiKey
532
+ };
@@ -0,0 +1 @@
1
+ { "type": "module" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abtars",
3
- "version": "0.2.1-alpha.2",
3
+ "version": "0.2.1-alpha.3",
4
4
  "description": "Standalone agent bridging Telegram to Kiro CLI via tmux/ACP",
5
5
  "type": "module",
6
6
  "main": "bundle/abtars.js",
@@ -8,7 +8,7 @@ Type=simple
8
8
  User={{USER}}
9
9
  Group={{USER}}
10
10
  WorkingDirectory=/home/{{USER}}/.abtars
11
- ExecStart=/home/{{USER}}/.abtars/watchdog.sh
11
+ ExecStart=/home/{{USER}}/.abtars/scripts/watchdog.sh
12
12
  Restart=always
13
13
  RestartSec=5
14
14
  Environment=NODE_ENV=production
@@ -4,7 +4,7 @@ After=network.target
4
4
 
5
5
  [Service]
6
6
  Type=simple
7
- ExecStart=%h/.abtars/watchdog.sh
7
+ ExecStart=%h/.abtars/scripts/watchdog.sh
8
8
  Restart=on-failure
9
9
  RestartSec=5
10
10
  Environment=PATH=%h/.abtars/bin:%h/.local/bin:%h/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
@@ -7,7 +7,7 @@ Wants=network-online.target
7
7
  Type=simple
8
8
  User=%i
9
9
  WorkingDirectory=%h/.abtars
10
- ExecStart=%h/.abtars/abtars.sh
10
+ ExecStart=%h/.abtars/scripts/abtars.sh
11
11
  Restart=always
12
12
  RestartSec=60
13
13
  Environment=NODE_ENV=production
@@ -8,7 +8,7 @@
8
8
  <key>WorkingDirectory</key><string>/Users/{{USER}}/.abtars</string>
9
9
  <key>ProgramArguments</key>
10
10
  <array>
11
- <string>/Users/{{USER}}/.abtars/watchdog.sh</string>
11
+ <string>/Users/{{USER}}/.abtars/scripts/watchdog.sh</string>
12
12
  </array>
13
13
  <key>KeepAlive</key><true/>
14
14
  <key>RunAtLoad</key><true/>
@@ -6,7 +6,7 @@
6
6
  <string>com.abtars.watchdog</string>
7
7
  <key>ProgramArguments</key>
8
8
  <array>
9
- <string>{{HOME}}/.abtars/watchdog.sh</string>
9
+ <string>{{HOME}}/.abtars/scripts/watchdog.sh</string>
10
10
  </array>
11
11
  <key>RunAtLoad</key>
12
12
  <true/>
@@ -5,9 +5,24 @@ AB="$HOME/.abtars"
5
5
  ABMIND="${ABMIND_HOME:-$HOME/.abmind}"
6
6
  DEST="$HOME/.backup-abtars"
7
7
  DATE=$(date +%Y%m%d)
8
+ CONFIG_ONLY=false
9
+
10
+ if [[ "${1:-}" == "--config" ]]; then
11
+ CONFIG_ONLY=true
12
+ fi
8
13
 
9
14
  mkdir -p "$DEST"
10
15
 
16
+ if $CONFIG_ONLY; then
17
+ # Minimal: config + secrets + tasks
18
+ cd "$AB"
19
+ zip -qr "$DEST/abtars-config-$DATE.zip" \
20
+ config/ secret/ tasks/ skills/ core/ \
21
+ 2>/dev/null || true
22
+ echo "✓ abtars-config-$DATE.zip (config-only)"
23
+ exit 0
24
+ fi
25
+
11
26
  # WAL-safe memory.db backup via sqlite3
12
27
  DB="$ABMIND/memory/memory.db"
13
28
  DB_TMP="$DEST/.memory-$DATE.db"
@@ -15,17 +30,20 @@ if [ -f "$DB" ] && command -v sqlite3 &>/dev/null; then
15
30
  sqlite3 "$DB" ".backup '$DB_TMP'"
16
31
  fi
17
32
 
18
- # Zip backup: abtars config/skills + abmind core/memory
33
+ # Zip backup: everything in ~/.abtars except binaries/runtime
19
34
  cd "$AB"
20
- zip -qr "$DEST/abtars-$DATE.zip" \
21
- config/ secret/ skills/ prompts/ tasks/ topics/ reports/ finance/ core/ \
22
- -x "config/.env.skills" 2>/dev/null || true
35
+ zip -qr "$DEST/abtars-$DATE.zip" . \
36
+ -x ".git/*" "current/*" "releases/*" "bin/*" "logs/*" "backup/*" "node_modules/*" \
37
+ -x "*.sock" "*.db-wal" "*.db-shm" \
38
+ 2>/dev/null || true
23
39
 
24
- # Add abmind memory/core + sleep reports
25
- if [ -d "$ABMIND/memory" ]; then
40
+ # Add abmind: everything except raw DB and backup/
41
+ if [ -d "$ABMIND" ]; then
26
42
  cd "$ABMIND"
27
- zip -qr "$DEST/abtars-$DATE.zip" \
28
- memory/core/ memory/sleep/ 2>/dev/null || true
43
+ zip -qr "$DEST/abtars-$DATE.zip" . \
44
+ -x "memory/memory.db" "backup/*" "lib/*" "node_modules/*" \
45
+ -x "*.sock" "*.db-wal" "*.db-shm" \
46
+ 2>/dev/null || true
29
47
  fi
30
48
 
31
49
  # Add the WAL-safe DB copy
package/scripts/doctor.sh CHANGED
@@ -132,7 +132,7 @@ if [ -f "$WD_LOCK" ]; then
132
132
  elif command -v systemctl &>/dev/null && systemctl --user is-enabled abtars-watchdog.service &>/dev/null; then
133
133
  systemctl --user restart abtars-watchdog.service 2>/dev/null && fix "restarted watchdog via systemd"
134
134
  else
135
- warn "watchdog not running -- start manually: ~/.abtars/watchdog.sh --all --web --agent &"
135
+ warn "watchdog not running -- start manually: ~/.abtars/scripts/watchdog.sh &"
136
136
  fi
137
137
  fi
138
138
  fi
@@ -144,7 +144,7 @@ else
144
144
  elif command -v systemctl &>/dev/null && systemctl --user is-enabled abtars-watchdog.service &>/dev/null; then
145
145
  systemctl --user start abtars-watchdog.service 2>/dev/null && fix "started watchdog via systemd"
146
146
  else
147
- warn "start manually: ~/.abtars/watchdog.sh --all --web --agent &"
147
+ warn "start manually: ~/.abtars/scripts/watchdog.sh &"
148
148
  fi
149
149
  fi
150
150
  fi