@simonfestl/husky-cli 1.38.4 → 1.38.6

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.
@@ -250,7 +250,6 @@ supervisorCommand
250
250
  }
251
251
  console.log("\nšŸ• Husky Supervisor Workflow Status");
252
252
  console.log("=".repeat(50));
253
- // Task status
254
253
  console.log("\nšŸ“‹ Task Backlog:");
255
254
  const backlogTasks = tasks.tasks || [];
256
255
  if (backlogTasks.length === 0) {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { createRequire } from "module";
4
4
  import { taskCommand } from "./commands/task.js";
5
- import { configCommand } from "./commands/config.js";
5
+ import { configCommand, ensureValidSession, getAuthHeaders, getConfig } from "./commands/config.js";
6
6
  import { agentCommand } from "./commands/agent.js";
7
7
  import { roadmapCommand } from "./commands/roadmap.js";
8
8
  import { changelogCommand } from "./commands/changelog.js";
@@ -33,6 +33,7 @@ import { infraCommand } from "./commands/infra.js";
33
33
  import { e2eCommand } from "./commands/e2e.js";
34
34
  import { prCommand } from "./commands/pr.js";
35
35
  import { youtubeCommand } from "./commands/youtube.js";
36
+ import { researchCommand } from "./commands/research.js";
36
37
  import { imageCommand } from "./commands/image.js";
37
38
  import { authCommand } from "./commands/auth.js";
38
39
  import { businessCommand } from "./commands/business.js";
@@ -45,6 +46,49 @@ import { checkVersion } from "./lib/version-check.js";
45
46
  // Read version from package.json
46
47
  const require = createRequire(import.meta.url);
47
48
  const packageJson = require("../package.json");
49
+ const originalFetch = globalThis.fetch.bind(globalThis);
50
+ function isHuskyApiRequest(requestUrl) {
51
+ try {
52
+ const config = getConfig();
53
+ if (!config.apiUrl)
54
+ return false;
55
+ const apiBase = new URL(config.apiUrl);
56
+ const basePath = apiBase.pathname.replace(/\/$/, "");
57
+ const apiPrefix = `${basePath}/api/`.replace(/\/{2,}/g, "/");
58
+ return requestUrl.origin === apiBase.origin && requestUrl.pathname.startsWith(apiPrefix);
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ function isAuthBypassRequest(request) {
65
+ if (request.method.toUpperCase() !== "POST")
66
+ return false;
67
+ const pathname = new URL(request.url).pathname;
68
+ return pathname.endsWith("/api/auth/session") || pathname.endsWith("/api/auth/refresh");
69
+ }
70
+ globalThis.fetch = async (input, init) => {
71
+ const request = new Request(input, init);
72
+ const requestUrl = new URL(request.url);
73
+ if (!isHuskyApiRequest(requestUrl) || isAuthBypassRequest(request)) {
74
+ return originalFetch(request);
75
+ }
76
+ const headers = new Headers(request.headers);
77
+ headers.delete("x-api-key");
78
+ // Always ensure the stored session is valid (refresh if needed), and
79
+ // always use the current token for Husky API requests.
80
+ const ok = await ensureValidSession();
81
+ if (!ok) {
82
+ throw new Error("No active session. Run: husky auth login --agent <name>");
83
+ }
84
+ const authHeaders = getAuthHeaders();
85
+ const authToken = authHeaders.Authorization;
86
+ if (!authToken) {
87
+ throw new Error("No active session. Run: husky auth login --agent <name>");
88
+ }
89
+ headers.set("Authorization", authToken);
90
+ return originalFetch(new Request(request, { headers }));
91
+ };
48
92
  const program = new Command();
49
93
  program
50
94
  .name("husky")
@@ -82,6 +126,7 @@ program.addCommand(infraCommand);
82
126
  program.addCommand(e2eCommand);
83
127
  program.addCommand(prCommand);
84
128
  program.addCommand(youtubeCommand);
129
+ program.addCommand(researchCommand);
85
130
  program.addCommand(imageCommand);
86
131
  program.addCommand(authCommand);
87
132
  program.addCommand(businessCommand);
@@ -2,6 +2,7 @@ export interface ApiRequestOptions {
2
2
  method?: string;
3
3
  body?: unknown;
4
4
  skipAuth?: boolean;
5
+ signal?: AbortSignal;
5
6
  }
6
7
  export declare function apiRequest<T>(path: string, options?: ApiRequestOptions): Promise<T>;
7
8
  export declare function getApiClient(): {
@@ -1,56 +1,10 @@
1
- import { getConfig, getSessionConfig, setSessionConfig, clearSessionConfig } from "../commands/config.js";
2
- const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
3
- let refreshInProgress = null;
1
+ import { getConfig, getSessionConfig, clearSessionConfig, ensureValidSession } from "../commands/config.js";
4
2
  function isSessionExpired(expiresAt) {
5
3
  if (!expiresAt)
6
4
  return true;
7
5
  return new Date(expiresAt).getTime() < Date.now();
8
6
  }
9
- function isSessionExpiringSoon(expiresAt) {
10
- if (!expiresAt)
11
- return true;
12
- return new Date(expiresAt).getTime() - Date.now() < REFRESH_THRESHOLD_MS;
13
- }
14
- async function doRefresh(agentName) {
15
- const config = getConfig();
16
- if (!config.apiUrl || !config.apiKey)
17
- return null;
18
- try {
19
- const url = new URL("/api/auth/session", config.apiUrl);
20
- const res = await fetch(url.toString(), {
21
- method: "POST",
22
- headers: {
23
- "x-api-key": config.apiKey,
24
- "Content-Type": "application/json",
25
- },
26
- body: JSON.stringify({ agent: agentName }),
27
- });
28
- if (!res.ok)
29
- return null;
30
- const session = await res.json();
31
- // Extract agent id for storage (API returns object with id, name, emoji)
32
- setSessionConfig({
33
- token: session.token,
34
- expiresAt: session.expiresAt,
35
- role: session.role,
36
- agent: session.agent.id,
37
- });
38
- return session;
39
- }
40
- catch {
41
- return null;
42
- }
43
- }
44
- async function refreshSession(agentName) {
45
- if (refreshInProgress) {
46
- return refreshInProgress;
47
- }
48
- refreshInProgress = doRefresh(agentName).finally(() => {
49
- refreshInProgress = null;
50
- });
51
- return refreshInProgress;
52
- }
53
- async function doFetch(url, method, authHeader, body) {
7
+ async function doFetch(url, method, authHeader, body, signal) {
54
8
  return fetch(url.toString(), {
55
9
  method,
56
10
  headers: {
@@ -58,61 +12,40 @@ async function doFetch(url, method, authHeader, body) {
58
12
  "Content-Type": "application/json",
59
13
  },
60
14
  body: body ? JSON.stringify(body) : undefined,
15
+ signal,
61
16
  });
62
17
  }
63
- async function getAuthHeader(session, apiKey) {
64
- // DEBUG: console.log('DEBUG session:', session?.token ? 'exists' : 'missing', session?.expiresAt);
18
+ function getAuthHeader(session) {
65
19
  if (session?.token && session.expiresAt) {
66
- // DEBUG: console.log('DEBUG: Checking expiration...');
67
20
  if (isSessionExpired(session.expiresAt)) {
68
- // DEBUG: console.log('DEBUG: Session expired, refreshing...');
69
- if (session.agent) {
70
- const newSession = await refreshSession(session.agent);
71
- if (newSession) {
72
- // DEBUG: console.log('DEBUG: Refresh successful, using Bearer token');
73
- return { "Authorization": `Bearer ${newSession.token}` };
74
- }
75
- }
76
21
  clearSessionConfig();
77
- if (apiKey) {
78
- // DEBUG: console.log('DEBUG: Refresh failed, falling back to API key');
79
- return { "x-api-key": apiKey };
80
- }
81
- throw new Error("Session expired and no API key available for refresh");
22
+ throw new Error("Session expired. Run: husky auth login --agent <name>");
82
23
  }
83
- if (isSessionExpiringSoon(session.expiresAt) && session.agent) {
84
- refreshSession(session.agent).catch(() => { });
85
- }
86
- // DEBUG: console.log('DEBUG: Using existing Bearer token');
87
24
  return { "Authorization": `Bearer ${session.token}` };
88
25
  }
89
- if (apiKey) {
90
- // DEBUG: console.log('DEBUG: No session, using API key');
91
- return { "x-api-key": apiKey };
92
- }
93
- throw new Error("No authentication configured. Run: husky auth login --agent <name> or husky config set api-key <key>");
26
+ throw new Error("No active session. Run: husky auth login --agent <name>");
94
27
  }
95
28
  export async function apiRequest(path, options = {}) {
96
29
  const config = getConfig();
97
30
  if (!config.apiUrl) {
98
31
  throw new Error("API URL not configured. Run: husky config set api-url <url>");
99
32
  }
33
+ if (!options.skipAuth) {
34
+ const ok = await ensureValidSession();
35
+ if (!ok) {
36
+ throw new Error("No active session. Run: husky auth login --agent <name>");
37
+ }
38
+ }
100
39
  const session = getSessionConfig();
101
40
  const method = options.method || "GET";
102
41
  const url = new URL(path, config.apiUrl);
103
42
  const authHeader = options.skipAuth
104
43
  ? {}
105
- : await getAuthHeader(session, config.apiKey);
106
- const res = await doFetch(url, method, authHeader, options.body);
44
+ : getAuthHeader(session);
45
+ const res = await doFetch(url, method, authHeader, options.body, options.signal);
107
46
  if (!res.ok) {
108
- if (res.status === 401 && session?.token && session.agent) {
109
- const newSession = await refreshSession(session.agent);
110
- if (newSession) {
111
- const retryRes = await doFetch(url, method, { "Authorization": `Bearer ${newSession.token}` }, options.body);
112
- if (retryRes.ok) {
113
- return retryRes.json();
114
- }
115
- }
47
+ if (res.status === 401) {
48
+ clearSessionConfig();
116
49
  }
117
50
  const error = await res.json().catch(() => ({ error: res.statusText }));
118
51
  throw new Error(error.message || error.error || `HTTP ${res.status}`);
@@ -1,31 +1,18 @@
1
- import { getConfig } from "../../commands/config.js";
1
+ import { getAuthHeaders, getConfig } from "../../commands/config.js";
2
2
  import { canAccessKnowledgeBase as checkKbAccess } from "../permissions-cache.js";
3
3
  async function apiRequest(path, options = {}) {
4
4
  const config = getConfig();
5
5
  if (!config.apiUrl) {
6
6
  throw new Error("API not configured. Run: husky config set api-url <url>");
7
7
  }
8
- // Prefer session token (JWT), fall back to API key for backwards compatibility
8
+ const authHeaders = getAuthHeaders();
9
+ if (!authHeaders.Authorization) {
10
+ throw new Error("Not authenticated. Run: husky auth login --agent <name>");
11
+ }
9
12
  const headers = {
10
13
  "Content-Type": "application/json",
14
+ ...authHeaders,
11
15
  };
12
- if (config.sessionToken) {
13
- // Check if session is expired
14
- if (config.sessionExpiresAt) {
15
- const expiresAt = new Date(config.sessionExpiresAt);
16
- if (expiresAt < new Date()) {
17
- throw new Error("Session expired. Run: husky auth login --agent <name>");
18
- }
19
- }
20
- headers["Authorization"] = `Bearer ${config.sessionToken}`;
21
- }
22
- else if (config.apiKey) {
23
- // Legacy fallback - will be rejected by new API but kept for error message
24
- headers["x-api-key"] = config.apiKey;
25
- }
26
- else {
27
- throw new Error("Not authenticated. Run: husky auth login --agent <name>");
28
- }
29
16
  const url = new URL(`/api/brain${path}`, config.apiUrl);
30
17
  const res = await fetch(url.toString(), {
31
18
  method: options.method || "POST",
@@ -35,10 +22,6 @@ async function apiRequest(path, options = {}) {
35
22
  if (!res.ok) {
36
23
  const error = await res.json().catch(() => ({ error: res.statusText }));
37
24
  if (res.status === 401) {
38
- // Check if it's the new API rejecting API key auth
39
- if (error.upgrade) {
40
- throw new Error("Session required. Run: husky auth login --agent <name>");
41
- }
42
25
  throw new Error(error.message || "Authentication failed");
43
26
  }
44
27
  if (res.status === 403) {
@@ -173,7 +173,6 @@ export class EmovePlaywrightClient {
173
173
  catch (error) {
174
174
  // Customer details not available
175
175
  }
176
- // Extract line items
177
176
  const items = [];
178
177
  const itemRows = page.locator('table.woocommerce-table--order-details tbody tr.woocommerce-table__line-item');
179
178
  const itemCount = await itemRows.count();
@@ -18,3 +18,5 @@ export type { Order as ShopifyOrder, Customer as ShopifyCustomer, Product as Sho
18
18
  export { SupplierFeedService } from './supplier-feed.js';
19
19
  export type { SupplierId, SupplierProduct, SupplierProductSearchResult, SyncStats, } from './supplier-feed.js';
20
20
  export * from './supplier-feed-types.js';
21
+ export { RedditClient } from './reddit.js';
22
+ export { YouTubeMonitorClient } from './youtube-monitor.js';
@@ -14,3 +14,5 @@ export { GotessClient } from './gotess.js';
14
14
  export { ShopifyClient } from './shopify.js';
15
15
  export { SupplierFeedService } from './supplier-feed.js';
16
16
  export * from './supplier-feed-types.js';
17
+ export { RedditClient } from './reddit.js';
18
+ export { YouTubeMonitorClient } from './youtube-monitor.js';
@@ -0,0 +1,83 @@
1
+ export interface RedditConfig {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ userAgent: string;
5
+ }
6
+ export interface RedditApiPost {
7
+ id: string;
8
+ title: string;
9
+ selftext: string;
10
+ url: string;
11
+ author: string;
12
+ subreddit: string;
13
+ score: number;
14
+ upvote_ratio: number;
15
+ num_comments: number;
16
+ created_utc: number;
17
+ permalink: string;
18
+ is_self: boolean;
19
+ thumbnail?: string;
20
+ }
21
+ export interface RedditPost {
22
+ id: string;
23
+ title: string;
24
+ selftext: string;
25
+ url: string;
26
+ author: string;
27
+ subreddit: string;
28
+ score: number;
29
+ upvoteRatio: number;
30
+ numComments: number;
31
+ createdUtc: number;
32
+ permalink: string;
33
+ isSelf: boolean;
34
+ thumbnail?: string;
35
+ }
36
+ export interface RedditTokenResponse {
37
+ access_token: string;
38
+ token_type: string;
39
+ expires_in: number;
40
+ scope: string;
41
+ }
42
+ export interface RedditComment {
43
+ id: string;
44
+ author: string;
45
+ body: string;
46
+ score: number;
47
+ created_utc: number;
48
+ replies?: {
49
+ data: {
50
+ children: Array<{
51
+ data: RedditComment;
52
+ }>;
53
+ };
54
+ };
55
+ }
56
+ export declare class RedditClient {
57
+ private config;
58
+ private accessToken?;
59
+ private tokenExpiry?;
60
+ private baseUrl;
61
+ private static AUTH_URL;
62
+ private static TOKEN_REFRESH_BUFFER_MS;
63
+ constructor(config: RedditConfig);
64
+ static fromConfig(): RedditClient;
65
+ private ensureAuthenticated;
66
+ private makeRequest;
67
+ getSubredditPosts(subreddit: string, options?: {
68
+ sort?: 'hot' | 'new' | 'top' | 'rising';
69
+ time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all';
70
+ limit?: number;
71
+ after?: string;
72
+ }): Promise<{
73
+ posts: RedditPost[];
74
+ after: string | null;
75
+ }>;
76
+ searchSubreddit(subreddit: string, query: string, options?: {
77
+ sort?: 'relevance' | 'hot' | 'top' | 'new' | 'comments';
78
+ time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all';
79
+ limit?: number;
80
+ }): Promise<RedditPost[]>;
81
+ getPostComments(postId: string, subreddit: string): Promise<RedditComment[]>;
82
+ }
83
+ export declare function isRedditConfigured(): boolean;
@@ -0,0 +1,168 @@
1
+ import { getConfig } from "../../commands/config.js";
2
+ function mapRedditPost(raw) {
3
+ return {
4
+ id: raw.id,
5
+ title: raw.title,
6
+ selftext: raw.selftext,
7
+ url: raw.url,
8
+ author: raw.author,
9
+ subreddit: raw.subreddit,
10
+ score: raw.score,
11
+ upvoteRatio: raw.upvote_ratio,
12
+ numComments: raw.num_comments,
13
+ createdUtc: raw.created_utc,
14
+ permalink: raw.permalink,
15
+ isSelf: raw.is_self,
16
+ thumbnail: raw.thumbnail,
17
+ };
18
+ }
19
+ export class RedditClient {
20
+ config;
21
+ accessToken;
22
+ tokenExpiry;
23
+ baseUrl = 'https://oauth.reddit.com';
24
+ static AUTH_URL = 'https://www.reddit.com/api/v1/access_token';
25
+ static TOKEN_REFRESH_BUFFER_MS = 60_000;
26
+ constructor(config) {
27
+ this.config = config;
28
+ }
29
+ static fromConfig() {
30
+ const config = getConfig();
31
+ const env = process.env.HUSKY_ENV || 'PROD';
32
+ const clientId = process.env[`${env}_REDDIT_CLIENT_ID`] || process.env.REDDIT_CLIENT_ID || config.redditClientId;
33
+ const clientSecret = process.env[`${env}_REDDIT_CLIENT_SECRET`] || process.env.REDDIT_CLIENT_SECRET || config.redditClientSecret;
34
+ if (!clientId || !clientSecret) {
35
+ throw new Error("Reddit credentials not configured. Please set reddit-client-id and reddit-client-secret.");
36
+ }
37
+ const clientConfig = {
38
+ clientId,
39
+ clientSecret,
40
+ userAgent: process.env.REDDIT_USER_AGENT || 'HuskyResearchBot/1.0 (by /u/husky-bot)',
41
+ };
42
+ return new RedditClient(clientConfig);
43
+ }
44
+ async ensureAuthenticated() {
45
+ if (this.accessToken && this.tokenExpiry && Date.now() < this.tokenExpiry) {
46
+ return;
47
+ }
48
+ const authString = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
49
+ const timeout = 15000;
50
+ const controller = new AbortController();
51
+ const id = setTimeout(() => controller.abort(), timeout);
52
+ try {
53
+ const response = await fetch(RedditClient.AUTH_URL, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Authorization': `Basic ${authString}`,
57
+ 'Content-Type': 'application/x-www-form-urlencoded',
58
+ 'User-Agent': this.config.userAgent,
59
+ },
60
+ body: new URLSearchParams({
61
+ grant_type: 'client_credentials',
62
+ }),
63
+ signal: controller.signal,
64
+ });
65
+ clearTimeout(id);
66
+ if (!response.ok) {
67
+ const errorText = await response.text();
68
+ throw new Error(`Reddit auth failed: ${response.status} ${response.statusText} - ${errorText.substring(0, 500)}`);
69
+ }
70
+ const data = await response.json();
71
+ if (!data.access_token) {
72
+ throw new Error("Reddit auth response missing access_token");
73
+ }
74
+ this.accessToken = data.access_token;
75
+ this.tokenExpiry = Date.now() + (data.expires_in * 1000) - RedditClient.TOKEN_REFRESH_BUFFER_MS;
76
+ }
77
+ catch (err) {
78
+ clearTimeout(id);
79
+ throw err;
80
+ }
81
+ }
82
+ async makeRequest(endpoint, params, retryCount = 0) {
83
+ await this.ensureAuthenticated();
84
+ const url = new URL(`${this.baseUrl}${endpoint}`);
85
+ if (params) {
86
+ Object.entries(params).forEach(([key, value]) => {
87
+ url.searchParams.append(key, value);
88
+ });
89
+ }
90
+ const timeout = 15000;
91
+ const controller = new AbortController();
92
+ const id = setTimeout(() => controller.abort(), timeout);
93
+ try {
94
+ const response = await fetch(url.toString(), {
95
+ headers: {
96
+ 'Authorization': `Bearer ${this.accessToken}`,
97
+ 'User-Agent': this.config.userAgent,
98
+ },
99
+ signal: controller.signal,
100
+ });
101
+ clearTimeout(id);
102
+ if (response.status === 429 && retryCount < 2) {
103
+ const retryAfter = parseInt(response.headers.get('retry-after') || '5', 10);
104
+ await new Promise(resolve => setTimeout(resolve, (retryAfter + 1) * 1000));
105
+ return this.makeRequest(endpoint, params, retryCount + 1);
106
+ }
107
+ if (!response.ok) {
108
+ const errorText = await response.text();
109
+ throw new Error(`Reddit API error: ${response.status} ${response.statusText} - ${errorText.substring(0, 500)}`);
110
+ }
111
+ return response.json();
112
+ }
113
+ catch (err) {
114
+ clearTimeout(id);
115
+ throw err;
116
+ }
117
+ }
118
+ async getSubredditPosts(subreddit, options = {}) {
119
+ const { sort = 'hot', time = 'day', limit = 25, after } = options;
120
+ const params = {
121
+ limit: Math.min(limit, 100).toString(),
122
+ };
123
+ if (time && (sort === 'top' || sort === 'new')) { // Reddit also accepts 't' for some other sorts
124
+ params.t = time;
125
+ }
126
+ if (after) {
127
+ params.after = after;
128
+ }
129
+ const safeSubreddit = encodeURIComponent(subreddit);
130
+ const data = await this.makeRequest(`/r/${safeSubreddit}/${sort}`, params);
131
+ return {
132
+ posts: data.data.children.filter(child => child.kind === 't3').map(child => mapRedditPost(child.data)),
133
+ after: data.data.after,
134
+ };
135
+ }
136
+ async searchSubreddit(subreddit, query, options = {}) {
137
+ const { sort = 'relevance', time = 'week', limit = 25 } = options;
138
+ const params = {
139
+ q: query,
140
+ restrict_sr: 'true',
141
+ sort,
142
+ t: time,
143
+ limit: Math.min(limit, 100).toString(),
144
+ };
145
+ const safeSubreddit = encodeURIComponent(subreddit);
146
+ const data = await this.makeRequest(`/r/${safeSubreddit}/search`, params);
147
+ return data.data.children.filter(child => child.kind === 't3').map(child => mapRedditPost(child.data));
148
+ }
149
+ async getPostComments(postId, subreddit) {
150
+ const safeSubreddit = encodeURIComponent(subreddit);
151
+ const safePostId = encodeURIComponent(postId);
152
+ const data = await this.makeRequest(`/r/${safeSubreddit}/comments/${safePostId}`);
153
+ if (Array.isArray(data) && data.length > 1) {
154
+ const commentsData = data[1];
155
+ return commentsData.data.children
156
+ .filter(child => child.kind === 't1')
157
+ .map(child => child.data);
158
+ }
159
+ return [];
160
+ }
161
+ }
162
+ export function isRedditConfigured() {
163
+ const config = getConfig();
164
+ const env = process.env.HUSKY_ENV || 'PROD';
165
+ const clientId = process.env[`${env}_REDDIT_CLIENT_ID`] || process.env.REDDIT_CLIENT_ID || config.redditClientId;
166
+ const clientSecret = process.env[`${env}_REDDIT_CLIENT_SECRET`] || process.env.REDDIT_CLIENT_SECRET || config.redditClientSecret;
167
+ return !!(clientId && clientSecret);
168
+ }
@@ -153,7 +153,6 @@ export class SkuterzonePlaywrightClient {
153
153
  catch (error) {
154
154
  // Customer details not available
155
155
  }
156
- // Extract line items
157
156
  const items = [];
158
157
  const itemRows = page.locator('table.woocommerce-table--order-details tbody tr.woocommerce-table__line-item');
159
158
  const itemCount = await itemRows.count();
@@ -0,0 +1,27 @@
1
+ export interface XConfig {
2
+ bearerToken: string;
3
+ }
4
+ export interface XPost {
5
+ id: string;
6
+ text: string;
7
+ authorId: string;
8
+ username: string;
9
+ createdAt: string;
10
+ publicMetrics: {
11
+ retweetCount: number;
12
+ replyCount: number;
13
+ likeCount: number;
14
+ quoteCount: number;
15
+ };
16
+ }
17
+ export declare class XClient {
18
+ private config;
19
+ private baseUrl;
20
+ private userIdCache;
21
+ constructor(config: XConfig);
22
+ static fromConfig(): XClient | null;
23
+ private makeRequest;
24
+ searchRecentTweets(query: string, maxResults?: number): Promise<XPost[]>;
25
+ getUserTweets(username: string, maxResults?: number): Promise<XPost[]>;
26
+ }
27
+ export declare function isXConfigured(): boolean;