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.
@@ -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;
@@ -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
@@ -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[]>;
@@ -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>;