hanzi-browse 2.2.1 → 2.2.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.
- package/dist/cli/setup.js +10 -1
- package/dist/dashboard/assets/index-wVMUNuBA.js +61 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/index.js +40 -0
- package/dist/managed/api.d.ts +14 -0
- package/dist/managed/api.js +272 -0
- package/dist/managed/billing.js +2 -0
- package/dist/managed/deploy.js +18 -1
- package/dist/managed/notify.d.ts +7 -0
- package/dist/managed/notify.js +36 -0
- package/dist/managed/scheduler.d.ts +42 -0
- package/dist/managed/scheduler.js +282 -0
- package/dist/managed/store-pg.d.ts +116 -0
- package/dist/managed/store-pg.js +215 -0
- package/dist/managed/store.d.ts +13 -0
- package/dist/managed/store.js +15 -0
- package/dist/managed/telemetry.d.ts +8 -0
- package/dist/managed/telemetry.js +61 -0
- package/dist/telemetry.d.ts +14 -0
- package/dist/telemetry.js +141 -0
- package/package.json +4 -1
- package/dist/dashboard/assets/index-B6M8kZZo.js +0 -46
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler for automated browser tasks
|
|
3
|
+
*
|
|
4
|
+
* Checks every 60 seconds for automations whose next_run_at has passed.
|
|
5
|
+
* Runs scout tasks via the existing agent loop infrastructure.
|
|
6
|
+
*/
|
|
7
|
+
import { CronExpressionParser } from "cron-parser";
|
|
8
|
+
// These are injected via initScheduler() to avoid circular deps
|
|
9
|
+
let S;
|
|
10
|
+
let runTaskFn;
|
|
11
|
+
let isSessionConnectedFn;
|
|
12
|
+
let notifyFn = null;
|
|
13
|
+
let schedulerInterval = null;
|
|
14
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
15
|
+
// ── Init ──────────────────────────────────────────────────────────────
|
|
16
|
+
export function initScheduler(deps) {
|
|
17
|
+
S = deps.store;
|
|
18
|
+
runTaskFn = deps.runTask;
|
|
19
|
+
isSessionConnectedFn = deps.isSessionConnected;
|
|
20
|
+
notifyFn = deps.notify || null;
|
|
21
|
+
}
|
|
22
|
+
export function startScheduler() {
|
|
23
|
+
if (schedulerInterval)
|
|
24
|
+
return;
|
|
25
|
+
schedulerInterval = setInterval(tick, 60_000);
|
|
26
|
+
console.error("[Scheduler] Started — checking every 60s");
|
|
27
|
+
}
|
|
28
|
+
export function stopScheduler() {
|
|
29
|
+
if (schedulerInterval) {
|
|
30
|
+
clearInterval(schedulerInterval);
|
|
31
|
+
schedulerInterval = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// ── Tick ──────────────────────────────────────────────────────────────
|
|
35
|
+
async function tick() {
|
|
36
|
+
try {
|
|
37
|
+
const due = await S.getDueAutomations();
|
|
38
|
+
for (const auto of due) {
|
|
39
|
+
try {
|
|
40
|
+
await runScoutTask(auto);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
console.error(`[Scheduler] Error running automation ${auto.id}:`, err.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
console.error("[Scheduler] Tick error:", err.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ── Scout Task ───────────────────────────────────────────────────────
|
|
52
|
+
async function runScoutTask(auto) {
|
|
53
|
+
const { id, workspaceId, browserSessionId, config } = auto;
|
|
54
|
+
if (!browserSessionId) {
|
|
55
|
+
await S.updateAutomation(id, workspaceId, {
|
|
56
|
+
status: "error",
|
|
57
|
+
errorMessage: "No browser session configured",
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Check browser is connected
|
|
62
|
+
if (!isSessionConnectedFn(browserSessionId)) {
|
|
63
|
+
const failures = auto.consecutiveFailures + 1;
|
|
64
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
65
|
+
await S.updateAutomation(id, workspaceId, {
|
|
66
|
+
status: "error",
|
|
67
|
+
consecutiveFailures: failures,
|
|
68
|
+
errorMessage: "Browser offline at scheduled time",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await S.updateAutomation(id, workspaceId, {
|
|
73
|
+
consecutiveFailures: failures,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Build scout prompt
|
|
79
|
+
const engagedHandles = await S.getRecentlyEngagedHandles(workspaceId);
|
|
80
|
+
const prompt = buildScoutPrompt(config, engagedHandles);
|
|
81
|
+
// Compute next run time
|
|
82
|
+
const nextRunAt = computeNextRun(config.schedule_cron, config.timezone);
|
|
83
|
+
// Update automation state (mark as running)
|
|
84
|
+
await S.updateAutomation(id, workspaceId, {
|
|
85
|
+
lastRunAt: new Date(),
|
|
86
|
+
nextRunAt,
|
|
87
|
+
consecutiveFailures: 0,
|
|
88
|
+
errorMessage: null,
|
|
89
|
+
});
|
|
90
|
+
// Run the task
|
|
91
|
+
const result = await runTaskFn({
|
|
92
|
+
workspaceId,
|
|
93
|
+
browserSessionId,
|
|
94
|
+
task: prompt,
|
|
95
|
+
url: "https://x.com",
|
|
96
|
+
});
|
|
97
|
+
if (result.status !== "complete" || !result.answer) {
|
|
98
|
+
await S.updateAutomation(id, workspaceId, {
|
|
99
|
+
consecutiveFailures: auto.consecutiveFailures + 1,
|
|
100
|
+
errorMessage: `Scout task failed: ${result.status}`,
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Parse drafts from answer
|
|
105
|
+
const drafts = parseScoutAnswer(result.answer);
|
|
106
|
+
if (!drafts || drafts.length === 0) {
|
|
107
|
+
await S.updateAutomation(id, workspaceId, {
|
|
108
|
+
errorMessage: "Scout returned no usable drafts",
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Store drafts
|
|
113
|
+
const stored = await S.createDraftBatch({
|
|
114
|
+
automationId: id,
|
|
115
|
+
workspaceId,
|
|
116
|
+
scoutTaskId: result.taskId,
|
|
117
|
+
drafts,
|
|
118
|
+
});
|
|
119
|
+
// Notify
|
|
120
|
+
const email = config.notification_email;
|
|
121
|
+
if (email && notifyFn) {
|
|
122
|
+
try {
|
|
123
|
+
await notifyFn(email, stored.length);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.error(`[Scheduler] Notification failed:`, err.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Scout Prompt ─────────────────────────────────────────────────────
|
|
131
|
+
function buildScoutPrompt(config, engagedHandles) {
|
|
132
|
+
const keywords = (config.keywords || []).join('", "');
|
|
133
|
+
const maxDrafts = config.max_drafts || 8;
|
|
134
|
+
const replyMix = config.reply_mix || { a: 40, b: 40, c: 20 };
|
|
135
|
+
const voiceProfile = config.voice_profile
|
|
136
|
+
? JSON.stringify(config.voice_profile, null, 2)
|
|
137
|
+
: "No voice profile set. Use a casual, helpful developer tone.";
|
|
138
|
+
const skipList = engagedHandles.length > 0
|
|
139
|
+
? engagedHandles.join(", ")
|
|
140
|
+
: "None yet";
|
|
141
|
+
return `You are a professional X/Twitter marketing scout. Your job is to find high-value tweets and draft reply suggestions. You NEVER post anything — you only research and draft.
|
|
142
|
+
|
|
143
|
+
## Product
|
|
144
|
+
Name: ${config.product_name || ""}
|
|
145
|
+
URL: ${config.product_url || ""}
|
|
146
|
+
Description: ${config.product_description || ""}
|
|
147
|
+
|
|
148
|
+
## Voice Profile
|
|
149
|
+
${voiceProfile}
|
|
150
|
+
|
|
151
|
+
## Previously engaged handles (do NOT draft replies for these)
|
|
152
|
+
${skipList}
|
|
153
|
+
|
|
154
|
+
## Instructions
|
|
155
|
+
1. For each keyword below, navigate to the search URL and look at the Latest tab:
|
|
156
|
+
Keywords: "${keywords}"
|
|
157
|
+
Search URL pattern: https://x.com/search?q={keyword}&src=typed_query&f=live
|
|
158
|
+
|
|
159
|
+
2. Scroll through results. Collect 15-20 candidate tweets that are:
|
|
160
|
+
- Posted within the last 24 hours
|
|
161
|
+
- From real people (not bots, not brands with millions of followers)
|
|
162
|
+
- Related to the product's problem space
|
|
163
|
+
- Have some engagement but aren't viral (5-200 likes ideal)
|
|
164
|
+
|
|
165
|
+
3. For each promising tweet author, visit their profile briefly to understand who they are.
|
|
166
|
+
|
|
167
|
+
4. Score each tweet 1-10 based on: relevance, timing, author quality, reply visibility, conversation potential.
|
|
168
|
+
|
|
169
|
+
5. Select the top ${maxDrafts} tweets.
|
|
170
|
+
|
|
171
|
+
6. Draft a reply for each. Follow this mix:
|
|
172
|
+
- Type A (value-only, no product mention): ~${replyMix.a}%
|
|
173
|
+
- Type B (value + soft mention): ~${replyMix.b}%
|
|
174
|
+
- Type C (direct recommendation): ~${replyMix.c}%
|
|
175
|
+
|
|
176
|
+
7. Anti-AI rules for EVERY reply:
|
|
177
|
+
- Never use em dashes (—), semicolons, or words like "leverage", "harness", "streamline"
|
|
178
|
+
- Never start with "Hey!", "Great point!", "Love this!"
|
|
179
|
+
- Under 280 characters. Sound like a text message, not a press release.
|
|
180
|
+
- Use contractions (don't, can't, it's)
|
|
181
|
+
- Match the energy of the original poster
|
|
182
|
+
|
|
183
|
+
8. Return your results as a JSON block at the very end of your response.
|
|
184
|
+
|
|
185
|
+
## OUTPUT FORMAT (CRITICAL)
|
|
186
|
+
After completing your research, output ONLY this JSON block at the end:
|
|
187
|
+
|
|
188
|
+
\`\`\`json
|
|
189
|
+
{"drafts": [
|
|
190
|
+
{
|
|
191
|
+
"tweet_url": "https://x.com/user/status/123456",
|
|
192
|
+
"tweet_text": "the original tweet text...",
|
|
193
|
+
"tweet_author_handle": "@username",
|
|
194
|
+
"tweet_author_name": "Display Name",
|
|
195
|
+
"tweet_author_bio": "their bio...",
|
|
196
|
+
"tweet_author_followers": 2100,
|
|
197
|
+
"tweet_engagement": {"likes": 12, "replies": 3, "retweets": 1},
|
|
198
|
+
"tweet_age_hours": 2.5,
|
|
199
|
+
"reply_text": "your drafted reply text here",
|
|
200
|
+
"reply_type": "B",
|
|
201
|
+
"reply_reasoning": "Why this tweet and this reply approach",
|
|
202
|
+
"score": 8
|
|
203
|
+
}
|
|
204
|
+
]}
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
Output ONLY this JSON block at the end. No other text after it.`;
|
|
208
|
+
}
|
|
209
|
+
export function parseScoutAnswer(answer) {
|
|
210
|
+
let raw;
|
|
211
|
+
// Try: ```json ... ``` block
|
|
212
|
+
const fenced = answer.match(/```json\s*([\s\S]*?)```/);
|
|
213
|
+
if (fenced) {
|
|
214
|
+
try {
|
|
215
|
+
raw = JSON.parse(fenced[1]);
|
|
216
|
+
}
|
|
217
|
+
catch { }
|
|
218
|
+
}
|
|
219
|
+
// Try: raw JSON starting with {"drafts"
|
|
220
|
+
if (!raw) {
|
|
221
|
+
const jsonStart = answer.lastIndexOf('{"drafts"');
|
|
222
|
+
if (jsonStart !== -1) {
|
|
223
|
+
try {
|
|
224
|
+
raw = JSON.parse(answer.slice(jsonStart));
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Try: raw JSON array starting with [
|
|
230
|
+
if (!raw) {
|
|
231
|
+
const arrStart = answer.lastIndexOf('[{');
|
|
232
|
+
if (arrStart !== -1) {
|
|
233
|
+
try {
|
|
234
|
+
const parsed = JSON.parse(answer.slice(arrStart));
|
|
235
|
+
if (Array.isArray(parsed))
|
|
236
|
+
raw = { drafts: parsed };
|
|
237
|
+
}
|
|
238
|
+
catch { }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!raw?.drafts || !Array.isArray(raw.drafts))
|
|
242
|
+
return null;
|
|
243
|
+
// Normalize field names (LLM might use snake_case)
|
|
244
|
+
return raw.drafts
|
|
245
|
+
.filter((d) => d.tweet_url && d.reply_text)
|
|
246
|
+
.map((d) => ({
|
|
247
|
+
tweetUrl: d.tweet_url,
|
|
248
|
+
tweetText: d.tweet_text,
|
|
249
|
+
tweetAuthorHandle: d.tweet_author_handle,
|
|
250
|
+
tweetAuthorName: d.tweet_author_name,
|
|
251
|
+
tweetAuthorBio: d.tweet_author_bio,
|
|
252
|
+
tweetAuthorFollowers: d.tweet_author_followers,
|
|
253
|
+
tweetEngagement: d.tweet_engagement,
|
|
254
|
+
tweetAgeHours: d.tweet_age_hours,
|
|
255
|
+
replyText: d.reply_text,
|
|
256
|
+
replyType: d.reply_type,
|
|
257
|
+
replyReasoning: d.reply_reasoning,
|
|
258
|
+
score: d.score,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
// ── Cron Helpers ─────────────────────────────────────────────────────
|
|
262
|
+
export function computeNextRun(cronExpr, timezone) {
|
|
263
|
+
try {
|
|
264
|
+
const interval = CronExpressionParser.parse(cronExpr, {
|
|
265
|
+
tz: timezone || "UTC",
|
|
266
|
+
});
|
|
267
|
+
return interval.next().toDate();
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Post prompt for approved drafts
|
|
274
|
+
export function buildPostPrompt(tweetUrl, replyText) {
|
|
275
|
+
return `Go to ${tweetUrl}
|
|
276
|
+
|
|
277
|
+
Click the reply button. Type this exact text in the reply box:
|
|
278
|
+
|
|
279
|
+
${replyText}
|
|
280
|
+
|
|
281
|
+
Click the post/reply button to submit. Confirm the reply was posted successfully.`;
|
|
282
|
+
}
|
|
@@ -188,4 +188,120 @@ export declare function ensureDefaultWorkspace(): Promise<{
|
|
|
188
188
|
workspace: Workspace;
|
|
189
189
|
apiKey: ApiKey;
|
|
190
190
|
}>;
|
|
191
|
+
export interface Automation {
|
|
192
|
+
id: string;
|
|
193
|
+
workspaceId: string;
|
|
194
|
+
browserSessionId?: string;
|
|
195
|
+
type: string;
|
|
196
|
+
status: "active" | "paused" | "error";
|
|
197
|
+
config: Record<string, any>;
|
|
198
|
+
lastRunAt?: number;
|
|
199
|
+
nextRunAt?: number;
|
|
200
|
+
consecutiveFailures: number;
|
|
201
|
+
errorMessage?: string;
|
|
202
|
+
createdAt: number;
|
|
203
|
+
updatedAt: number;
|
|
204
|
+
}
|
|
205
|
+
export interface AutomationDraft {
|
|
206
|
+
id: string;
|
|
207
|
+
automationId: string;
|
|
208
|
+
workspaceId: string;
|
|
209
|
+
scoutTaskId?: string;
|
|
210
|
+
batchId: string;
|
|
211
|
+
status: "pending" | "approved" | "edited" | "skipped" | "posted" | "failed";
|
|
212
|
+
tweetUrl: string;
|
|
213
|
+
tweetText?: string;
|
|
214
|
+
tweetAuthorHandle?: string;
|
|
215
|
+
tweetAuthorName?: string;
|
|
216
|
+
tweetAuthorBio?: string;
|
|
217
|
+
tweetAuthorFollowers?: number;
|
|
218
|
+
tweetEngagement?: Record<string, any>;
|
|
219
|
+
tweetAgeHours?: number;
|
|
220
|
+
replyText: string;
|
|
221
|
+
replyType?: "A" | "B" | "C";
|
|
222
|
+
replyReasoning?: string;
|
|
223
|
+
score?: number;
|
|
224
|
+
postTaskId?: string;
|
|
225
|
+
postedAt?: number;
|
|
226
|
+
editedText?: string;
|
|
227
|
+
createdAt: number;
|
|
228
|
+
}
|
|
229
|
+
export interface EngagementEntry {
|
|
230
|
+
id: string;
|
|
231
|
+
workspaceId: string;
|
|
232
|
+
automationId?: string;
|
|
233
|
+
draftId?: string;
|
|
234
|
+
authorHandle: string;
|
|
235
|
+
replyType?: string;
|
|
236
|
+
keyword?: string;
|
|
237
|
+
tweetUrl?: string;
|
|
238
|
+
tweetSummary?: string;
|
|
239
|
+
replySummary?: string;
|
|
240
|
+
postedAt: number;
|
|
241
|
+
}
|
|
242
|
+
export declare function createAutomation(params: {
|
|
243
|
+
workspaceId: string;
|
|
244
|
+
browserSessionId: string;
|
|
245
|
+
type?: string;
|
|
246
|
+
config: Record<string, any>;
|
|
247
|
+
nextRunAt?: Date;
|
|
248
|
+
}): Promise<Automation>;
|
|
249
|
+
export declare function getAutomation(id: string): Promise<Automation | null>;
|
|
250
|
+
export declare function listAutomations(workspaceId: string): Promise<Automation[]>;
|
|
251
|
+
export declare function updateAutomation(id: string, workspaceId: string, fields: Partial<{
|
|
252
|
+
browserSessionId: string;
|
|
253
|
+
status: Automation["status"];
|
|
254
|
+
config: Record<string, any>;
|
|
255
|
+
lastRunAt: Date;
|
|
256
|
+
nextRunAt: Date | null;
|
|
257
|
+
consecutiveFailures: number;
|
|
258
|
+
errorMessage: string | null;
|
|
259
|
+
}>): Promise<Automation | null>;
|
|
260
|
+
export declare function deleteAutomation(id: string, workspaceId: string): Promise<boolean>;
|
|
261
|
+
export declare function getDueAutomations(): Promise<Automation[]>;
|
|
262
|
+
export declare function createDraftBatch(params: {
|
|
263
|
+
automationId: string;
|
|
264
|
+
workspaceId: string;
|
|
265
|
+
scoutTaskId: string;
|
|
266
|
+
drafts: Array<{
|
|
267
|
+
tweetUrl: string;
|
|
268
|
+
tweetText?: string;
|
|
269
|
+
tweetAuthorHandle?: string;
|
|
270
|
+
tweetAuthorName?: string;
|
|
271
|
+
tweetAuthorBio?: string;
|
|
272
|
+
tweetAuthorFollowers?: number;
|
|
273
|
+
tweetEngagement?: Record<string, any>;
|
|
274
|
+
tweetAgeHours?: number;
|
|
275
|
+
replyText: string;
|
|
276
|
+
replyType?: "A" | "B" | "C";
|
|
277
|
+
replyReasoning?: string;
|
|
278
|
+
score?: number;
|
|
279
|
+
}>;
|
|
280
|
+
}): Promise<AutomationDraft[]>;
|
|
281
|
+
export declare function listDrafts(workspaceId: string, filters?: {
|
|
282
|
+
status?: string;
|
|
283
|
+
automationId?: string;
|
|
284
|
+
batchId?: string;
|
|
285
|
+
limit?: number;
|
|
286
|
+
}): Promise<AutomationDraft[]>;
|
|
287
|
+
export declare function getDraft(id: string): Promise<AutomationDraft | null>;
|
|
288
|
+
export declare function updateDraft(id: string, workspaceId: string, fields: Partial<{
|
|
289
|
+
status: AutomationDraft["status"];
|
|
290
|
+
editedText: string;
|
|
291
|
+
postTaskId: string;
|
|
292
|
+
postedAt: Date;
|
|
293
|
+
}>): Promise<AutomationDraft | null>;
|
|
294
|
+
export declare function logEngagement(params: {
|
|
295
|
+
workspaceId: string;
|
|
296
|
+
automationId?: string;
|
|
297
|
+
draftId?: string;
|
|
298
|
+
authorHandle: string;
|
|
299
|
+
replyType?: string;
|
|
300
|
+
keyword?: string;
|
|
301
|
+
tweetUrl?: string;
|
|
302
|
+
tweetSummary?: string;
|
|
303
|
+
replySummary?: string;
|
|
304
|
+
}): Promise<void>;
|
|
305
|
+
export declare function getRecentlyEngagedHandles(workspaceId: string, daysBack?: number): Promise<string[]>;
|
|
306
|
+
export declare function listEngagements(workspaceId: string, limit?: number): Promise<EngagementEntry[]>;
|
|
191
307
|
export declare function startHeartbeatFlush(): void;
|
package/dist/managed/store-pg.js
CHANGED
|
@@ -473,6 +473,221 @@ export async function ensureDefaultWorkspace() {
|
|
|
473
473
|
const apiKey = await createApiKey(workspace.id, "default");
|
|
474
474
|
return { workspace, apiKey };
|
|
475
475
|
}
|
|
476
|
+
function rowToAutomation(r) {
|
|
477
|
+
return {
|
|
478
|
+
id: r.id,
|
|
479
|
+
workspaceId: r.workspace_id,
|
|
480
|
+
browserSessionId: r.browser_session_id || undefined,
|
|
481
|
+
type: r.type,
|
|
482
|
+
status: r.status,
|
|
483
|
+
config: r.config || {},
|
|
484
|
+
lastRunAt: r.last_run_at ? new Date(r.last_run_at).getTime() : undefined,
|
|
485
|
+
nextRunAt: r.next_run_at ? new Date(r.next_run_at).getTime() : undefined,
|
|
486
|
+
consecutiveFailures: r.consecutive_failures ?? 0,
|
|
487
|
+
errorMessage: r.error_message || undefined,
|
|
488
|
+
createdAt: new Date(r.created_at).getTime(),
|
|
489
|
+
updatedAt: new Date(r.updated_at).getTime(),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function rowToDraft(r) {
|
|
493
|
+
return {
|
|
494
|
+
id: r.id,
|
|
495
|
+
automationId: r.automation_id,
|
|
496
|
+
workspaceId: r.workspace_id,
|
|
497
|
+
scoutTaskId: r.scout_task_id || undefined,
|
|
498
|
+
batchId: r.batch_id,
|
|
499
|
+
status: r.status,
|
|
500
|
+
tweetUrl: r.tweet_url,
|
|
501
|
+
tweetText: r.tweet_text || undefined,
|
|
502
|
+
tweetAuthorHandle: r.tweet_author_handle || undefined,
|
|
503
|
+
tweetAuthorName: r.tweet_author_name || undefined,
|
|
504
|
+
tweetAuthorBio: r.tweet_author_bio || undefined,
|
|
505
|
+
tweetAuthorFollowers: r.tweet_author_followers ?? undefined,
|
|
506
|
+
tweetEngagement: r.tweet_engagement || undefined,
|
|
507
|
+
tweetAgeHours: r.tweet_age_hours != null ? parseFloat(r.tweet_age_hours) : undefined,
|
|
508
|
+
replyText: r.reply_text,
|
|
509
|
+
replyType: r.reply_type || undefined,
|
|
510
|
+
replyReasoning: r.reply_reasoning || undefined,
|
|
511
|
+
score: r.score ?? undefined,
|
|
512
|
+
postTaskId: r.post_task_id || undefined,
|
|
513
|
+
postedAt: r.posted_at ? new Date(r.posted_at).getTime() : undefined,
|
|
514
|
+
editedText: r.edited_text || undefined,
|
|
515
|
+
createdAt: new Date(r.created_at).getTime(),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
export async function createAutomation(params) {
|
|
519
|
+
const id = randomUUID();
|
|
520
|
+
const now = new Date();
|
|
521
|
+
const res = await db().query(`INSERT INTO automations (id, workspace_id, browser_session_id, type, config, next_run_at, created_at, updated_at)
|
|
522
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $7) RETURNING *`, [id, params.workspaceId, params.browserSessionId, params.type || "x-marketer", JSON.stringify(params.config), params.nextRunAt || null, now]);
|
|
523
|
+
return rowToAutomation(res.rows[0]);
|
|
524
|
+
}
|
|
525
|
+
export async function getAutomation(id) {
|
|
526
|
+
const res = await db().query("SELECT * FROM automations WHERE id = $1", [id]);
|
|
527
|
+
if (res.rows.length === 0)
|
|
528
|
+
return null;
|
|
529
|
+
return rowToAutomation(res.rows[0]);
|
|
530
|
+
}
|
|
531
|
+
export async function listAutomations(workspaceId) {
|
|
532
|
+
const res = await db().query("SELECT * FROM automations WHERE workspace_id = $1 ORDER BY created_at DESC", [workspaceId]);
|
|
533
|
+
return res.rows.map(rowToAutomation);
|
|
534
|
+
}
|
|
535
|
+
export async function updateAutomation(id, workspaceId, fields) {
|
|
536
|
+
const sets = [];
|
|
537
|
+
const vals = [];
|
|
538
|
+
let idx = 1;
|
|
539
|
+
if (fields.browserSessionId !== undefined) {
|
|
540
|
+
sets.push(`browser_session_id = $${idx++}`);
|
|
541
|
+
vals.push(fields.browserSessionId);
|
|
542
|
+
}
|
|
543
|
+
if (fields.status !== undefined) {
|
|
544
|
+
sets.push(`status = $${idx++}`);
|
|
545
|
+
vals.push(fields.status);
|
|
546
|
+
}
|
|
547
|
+
if (fields.config !== undefined) {
|
|
548
|
+
sets.push(`config = $${idx++}`);
|
|
549
|
+
vals.push(JSON.stringify(fields.config));
|
|
550
|
+
}
|
|
551
|
+
if (fields.lastRunAt !== undefined) {
|
|
552
|
+
sets.push(`last_run_at = $${idx++}`);
|
|
553
|
+
vals.push(fields.lastRunAt);
|
|
554
|
+
}
|
|
555
|
+
if (fields.nextRunAt !== undefined) {
|
|
556
|
+
sets.push(`next_run_at = $${idx++}`);
|
|
557
|
+
vals.push(fields.nextRunAt);
|
|
558
|
+
}
|
|
559
|
+
if (fields.consecutiveFailures !== undefined) {
|
|
560
|
+
sets.push(`consecutive_failures = $${idx++}`);
|
|
561
|
+
vals.push(fields.consecutiveFailures);
|
|
562
|
+
}
|
|
563
|
+
if (fields.errorMessage !== undefined) {
|
|
564
|
+
sets.push(`error_message = $${idx++}`);
|
|
565
|
+
vals.push(fields.errorMessage);
|
|
566
|
+
}
|
|
567
|
+
if (sets.length === 0)
|
|
568
|
+
return getAutomation(id);
|
|
569
|
+
sets.push(`updated_at = $${idx++}`);
|
|
570
|
+
vals.push(new Date());
|
|
571
|
+
vals.push(id);
|
|
572
|
+
vals.push(workspaceId);
|
|
573
|
+
const res = await db().query(`UPDATE automations SET ${sets.join(", ")} WHERE id = $${idx++} AND workspace_id = $${idx} RETURNING *`, vals);
|
|
574
|
+
if (res.rows.length === 0)
|
|
575
|
+
return null;
|
|
576
|
+
return rowToAutomation(res.rows[0]);
|
|
577
|
+
}
|
|
578
|
+
export async function deleteAutomation(id, workspaceId) {
|
|
579
|
+
const res = await db().query("DELETE FROM automations WHERE id = $1 AND workspace_id = $2", [id, workspaceId]);
|
|
580
|
+
return (res.rowCount ?? 0) > 0;
|
|
581
|
+
}
|
|
582
|
+
export async function getDueAutomations() {
|
|
583
|
+
const res = await db().query("SELECT * FROM automations WHERE status = 'active' AND next_run_at <= NOW()");
|
|
584
|
+
return res.rows.map(rowToAutomation);
|
|
585
|
+
}
|
|
586
|
+
// --- Automation Drafts ---
|
|
587
|
+
export async function createDraftBatch(params) {
|
|
588
|
+
const batchId = randomUUID();
|
|
589
|
+
const results = [];
|
|
590
|
+
for (const d of params.drafts) {
|
|
591
|
+
const id = randomUUID();
|
|
592
|
+
const res = await db().query(`INSERT INTO automation_drafts
|
|
593
|
+
(id, automation_id, workspace_id, scout_task_id, batch_id,
|
|
594
|
+
tweet_url, tweet_text, tweet_author_handle, tweet_author_name, tweet_author_bio,
|
|
595
|
+
tweet_author_followers, tweet_engagement, tweet_age_hours,
|
|
596
|
+
reply_text, reply_type, reply_reasoning, score)
|
|
597
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
|
|
598
|
+
RETURNING *`, [id, params.automationId, params.workspaceId, params.scoutTaskId, batchId,
|
|
599
|
+
d.tweetUrl, d.tweetText || null, d.tweetAuthorHandle || null, d.tweetAuthorName || null, d.tweetAuthorBio || null,
|
|
600
|
+
d.tweetAuthorFollowers ?? null, d.tweetEngagement ? JSON.stringify(d.tweetEngagement) : null, d.tweetAgeHours ?? null,
|
|
601
|
+
d.replyText, d.replyType || null, d.replyReasoning || null, d.score ?? null]);
|
|
602
|
+
results.push(rowToDraft(res.rows[0]));
|
|
603
|
+
}
|
|
604
|
+
return results;
|
|
605
|
+
}
|
|
606
|
+
export async function listDrafts(workspaceId, filters) {
|
|
607
|
+
const where = ["workspace_id = $1"];
|
|
608
|
+
const vals = [workspaceId];
|
|
609
|
+
let idx = 2;
|
|
610
|
+
if (filters?.status) {
|
|
611
|
+
where.push(`status = $${idx++}`);
|
|
612
|
+
vals.push(filters.status);
|
|
613
|
+
}
|
|
614
|
+
if (filters?.automationId) {
|
|
615
|
+
where.push(`automation_id = $${idx++}`);
|
|
616
|
+
vals.push(filters.automationId);
|
|
617
|
+
}
|
|
618
|
+
if (filters?.batchId) {
|
|
619
|
+
where.push(`batch_id = $${idx++}`);
|
|
620
|
+
vals.push(filters.batchId);
|
|
621
|
+
}
|
|
622
|
+
const limit = filters?.limit || 50;
|
|
623
|
+
const res = await db().query(`SELECT * FROM automation_drafts WHERE ${where.join(" AND ")} ORDER BY score DESC NULLS LAST, created_at DESC LIMIT ${limit}`, vals);
|
|
624
|
+
return res.rows.map(rowToDraft);
|
|
625
|
+
}
|
|
626
|
+
export async function getDraft(id) {
|
|
627
|
+
const res = await db().query("SELECT * FROM automation_drafts WHERE id = $1", [id]);
|
|
628
|
+
if (res.rows.length === 0)
|
|
629
|
+
return null;
|
|
630
|
+
return rowToDraft(res.rows[0]);
|
|
631
|
+
}
|
|
632
|
+
export async function updateDraft(id, workspaceId, fields) {
|
|
633
|
+
const sets = [];
|
|
634
|
+
const vals = [];
|
|
635
|
+
let idx = 1;
|
|
636
|
+
if (fields.status !== undefined) {
|
|
637
|
+
sets.push(`status = $${idx++}`);
|
|
638
|
+
vals.push(fields.status);
|
|
639
|
+
}
|
|
640
|
+
if (fields.editedText !== undefined) {
|
|
641
|
+
sets.push(`edited_text = $${idx++}`);
|
|
642
|
+
vals.push(fields.editedText);
|
|
643
|
+
}
|
|
644
|
+
if (fields.postTaskId !== undefined) {
|
|
645
|
+
sets.push(`post_task_id = $${idx++}`);
|
|
646
|
+
vals.push(fields.postTaskId);
|
|
647
|
+
}
|
|
648
|
+
if (fields.postedAt !== undefined) {
|
|
649
|
+
sets.push(`posted_at = $${idx++}`);
|
|
650
|
+
vals.push(fields.postedAt);
|
|
651
|
+
}
|
|
652
|
+
if (sets.length === 0)
|
|
653
|
+
return getDraft(id);
|
|
654
|
+
vals.push(id);
|
|
655
|
+
vals.push(workspaceId);
|
|
656
|
+
const res = await db().query(`UPDATE automation_drafts SET ${sets.join(", ")} WHERE id = $${idx++} AND workspace_id = $${idx} RETURNING *`, vals);
|
|
657
|
+
if (res.rows.length === 0)
|
|
658
|
+
return null;
|
|
659
|
+
return rowToDraft(res.rows[0]);
|
|
660
|
+
}
|
|
661
|
+
// --- Engagement Log ---
|
|
662
|
+
export async function logEngagement(params) {
|
|
663
|
+
await db().query(`INSERT INTO engagement_log
|
|
664
|
+
(id, workspace_id, automation_id, draft_id, author_handle, reply_type, keyword, tweet_url, tweet_summary, reply_summary)
|
|
665
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [randomUUID(), params.workspaceId, params.automationId || null, params.draftId || null,
|
|
666
|
+
params.authorHandle, params.replyType || null, params.keyword || null,
|
|
667
|
+
params.tweetUrl || null, params.tweetSummary || null, params.replySummary || null]);
|
|
668
|
+
}
|
|
669
|
+
export async function getRecentlyEngagedHandles(workspaceId, daysBack = 30) {
|
|
670
|
+
const res = await db().query(`SELECT DISTINCT author_handle FROM engagement_log
|
|
671
|
+
WHERE workspace_id = $1 AND posted_at > NOW() - INTERVAL '1 day' * $2
|
|
672
|
+
ORDER BY author_handle`, [workspaceId, daysBack]);
|
|
673
|
+
return res.rows.map((r) => r.author_handle);
|
|
674
|
+
}
|
|
675
|
+
export async function listEngagements(workspaceId, limit = 50) {
|
|
676
|
+
const res = await db().query(`SELECT * FROM engagement_log WHERE workspace_id = $1 ORDER BY posted_at DESC LIMIT $2`, [workspaceId, limit]);
|
|
677
|
+
return res.rows.map((r) => ({
|
|
678
|
+
id: r.id,
|
|
679
|
+
workspaceId: r.workspace_id,
|
|
680
|
+
automationId: r.automation_id || undefined,
|
|
681
|
+
draftId: r.draft_id || undefined,
|
|
682
|
+
authorHandle: r.author_handle,
|
|
683
|
+
replyType: r.reply_type || undefined,
|
|
684
|
+
keyword: r.keyword || undefined,
|
|
685
|
+
tweetUrl: r.tweet_url || undefined,
|
|
686
|
+
tweetSummary: r.tweet_summary || undefined,
|
|
687
|
+
replySummary: r.reply_summary || undefined,
|
|
688
|
+
postedAt: new Date(r.posted_at).getTime(),
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
476
691
|
// --- Heartbeat flush (no-op for Postgres, queries go to DB directly) ---
|
|
477
692
|
export function startHeartbeatFlush() {
|
|
478
693
|
// Not needed for Postgres — heartbeatSession writes to DB directly
|
package/dist/managed/store.d.ts
CHANGED
|
@@ -186,3 +186,16 @@ export declare function ensureDefaultWorkspace(): {
|
|
|
186
186
|
workspace: Workspace;
|
|
187
187
|
apiKey: ApiKey;
|
|
188
188
|
};
|
|
189
|
+
export declare function createAutomation(_p: any): Promise<any>;
|
|
190
|
+
export declare function getAutomation(_id: string): Promise<any>;
|
|
191
|
+
export declare function listAutomations(_wid: string): Promise<any[]>;
|
|
192
|
+
export declare function updateAutomation(_id: string, _wid: string, _f: any): Promise<any>;
|
|
193
|
+
export declare function deleteAutomation(_id: string, _wid: string): Promise<boolean>;
|
|
194
|
+
export declare function getDueAutomations(): Promise<any[]>;
|
|
195
|
+
export declare function createDraftBatch(_p: any): Promise<any[]>;
|
|
196
|
+
export declare function listDrafts(_wid: string, _f?: any): Promise<any[]>;
|
|
197
|
+
export declare function getDraft(_id: string): Promise<any>;
|
|
198
|
+
export declare function updateDraft(_id: string, _wid: string, _f: any): Promise<any>;
|
|
199
|
+
export declare function logEngagement(_p: any): Promise<void>;
|
|
200
|
+
export declare function getRecentlyEngagedHandles(_wid: string, _d?: number): Promise<string[]>;
|
|
201
|
+
export declare function listEngagements(_wid: string, _l?: number): Promise<any[]>;
|
package/dist/managed/store.js
CHANGED
|
@@ -377,3 +377,18 @@ export function ensureDefaultWorkspace() {
|
|
|
377
377
|
const apiKey = createApiKey(workspace.id, "default");
|
|
378
378
|
return { workspace, apiKey };
|
|
379
379
|
}
|
|
380
|
+
// --- Automation stubs (file store doesn't support automations — Postgres only) ---
|
|
381
|
+
const NOT_SUPPORTED = "Automations require Postgres (set DATABASE_URL)";
|
|
382
|
+
export async function createAutomation(_p) { throw new Error(NOT_SUPPORTED); }
|
|
383
|
+
export async function getAutomation(_id) { return null; }
|
|
384
|
+
export async function listAutomations(_wid) { return []; }
|
|
385
|
+
export async function updateAutomation(_id, _wid, _f) { return null; }
|
|
386
|
+
export async function deleteAutomation(_id, _wid) { return false; }
|
|
387
|
+
export async function getDueAutomations() { return []; }
|
|
388
|
+
export async function createDraftBatch(_p) { return []; }
|
|
389
|
+
export async function listDrafts(_wid, _f) { return []; }
|
|
390
|
+
export async function getDraft(_id) { return null; }
|
|
391
|
+
export async function updateDraft(_id, _wid, _f) { return null; }
|
|
392
|
+
export async function logEngagement(_p) { }
|
|
393
|
+
export async function getRecentlyEngagedHandles(_wid, _d) { return []; }
|
|
394
|
+
export async function listEngagements(_wid, _l) { return []; }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry for the managed backend (api.hanzilla.co).
|
|
3
|
+
* Gated by SENTRY_DSN and POSTHOG_API_KEY env vars — no-op in dev.
|
|
4
|
+
*/
|
|
5
|
+
export declare function initManagedTelemetry(): void;
|
|
6
|
+
export declare function trackManagedEvent(name: string, workspaceId: string, properties?: Record<string, any>): void;
|
|
7
|
+
export declare function captureManagedError(error: Error, context?: Record<string, string>): void;
|
|
8
|
+
export declare function shutdownManagedTelemetry(): Promise<void>;
|