@lobu/cli 6.0.0 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -27
- package/dist/bundled-skills/lobu/SKILL.md +12 -12
- package/dist/commands/_lib/apply/apply-cmd.d.ts +2 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +26 -0
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +1 -1
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +6 -6
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/agent.d.ts +7 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +65 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts +12 -9
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +117 -56
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/dev.d.ts +15 -7
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +79 -44
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +136 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts +8 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +56 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +20 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +332 -183
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +28 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +14 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +4 -4
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/install-targets.d.ts.map +1 -1
- package/dist/commands/memory/_lib/install-targets.js +1 -5
- package/dist/commands/memory/_lib/install-targets.js.map +1 -1
- package/dist/commands/memory/_lib/mcp.d.ts +2 -2
- package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
- package/dist/commands/memory/_lib/mcp.js +24 -12
- package/dist/commands/memory/_lib/mcp.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
- package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
- package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +2 -2
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +3 -3
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +5 -6
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/memory/run.d.ts.map +1 -1
- package/dist/commands/memory/run.js +2 -2
- package/dist/commands/memory/run.js.map +1 -1
- package/dist/commands/platforms/platform-prompts.d.ts +0 -1
- package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
- package/dist/commands/platforms/platform-prompts.js +54 -8
- package/dist/commands/platforms/platform-prompts.js.map +1 -1
- package/dist/commands/telemetry.d.ts +10 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +68 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connectors/README.md +534 -0
- package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
- package/dist/connectors/browser-scraper-utils.ts +214 -0
- package/dist/connectors/capterra.ts +273 -0
- package/dist/connectors/g2.ts +286 -0
- package/dist/connectors/github.ts +1553 -0
- package/dist/connectors/glassdoor.ts +291 -0
- package/dist/connectors/gmaps.ts +197 -0
- package/dist/connectors/google_calendar.ts +631 -0
- package/dist/connectors/google_gmail.ts +751 -0
- package/dist/connectors/google_photos.ts +776 -0
- package/dist/connectors/google_play.ts +342 -0
- package/dist/connectors/hackernews.ts +471 -0
- package/dist/connectors/index.ts +23 -0
- package/dist/connectors/ios_appstore.ts +226 -0
- package/dist/connectors/linkedin.ts +471 -0
- package/dist/connectors/microsoft_outlook.ts +410 -0
- package/dist/connectors/producthunt.ts +471 -0
- package/dist/connectors/reddit.ts +600 -0
- package/dist/connectors/rss.ts +448 -0
- package/dist/connectors/spotify.ts +590 -0
- package/dist/connectors/trustpilot.ts +199 -0
- package/dist/connectors/website.ts +629 -0
- package/dist/connectors/whatsapp.ts +1073 -0
- package/dist/connectors/x.ts +526 -0
- package/dist/connectors/youtube.ts +666 -0
- package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
- package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
- package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
- package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
- package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
- package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
- package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
- package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
- package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
- package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
- package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
- package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
- package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
- package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
- package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
- package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
- package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
- package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
- package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
- package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
- package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
- package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
- package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
- package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
- package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
- package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
- package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
- package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
- package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
- package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
- package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
- package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
- package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
- package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
- package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
- package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
- package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
- package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
- package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
- package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
- package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
- package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
- package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
- package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
- package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
- package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -23
- package/dist/index.js.map +1 -1
- package/dist/internal/api-client.d.ts +4 -8
- package/dist/internal/api-client.d.ts.map +1 -1
- package/dist/internal/api-client.js +1 -1
- package/dist/internal/api-client.js.map +1 -1
- package/dist/internal/context.js +2 -2
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +6 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/index.d.ts +2 -3
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +2 -2
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/oauth.d.ts +7 -6
- package/dist/internal/oauth.d.ts.map +1 -1
- package/dist/internal/oauth.js +3 -3
- package/dist/internal/project-link.d.ts +10 -0
- package/dist/internal/project-link.d.ts.map +1 -0
- package/dist/internal/project-link.js +48 -0
- package/dist/internal/project-link.js.map +1 -0
- package/dist/providers.json +2 -2
- package/dist/server.bundle.mjs +3173 -4404
- package/dist/start-local.bundle.mjs +71481 -0
- package/dist/templates/README.md.tmpl +10 -11
- package/package.json +14 -12
- package/dist/__tests__/chat.integration.test.d.ts +0 -2
- package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
- package/dist/__tests__/chat.integration.test.js +0 -337
- package/dist/__tests__/chat.integration.test.js.map +0 -1
- package/dist/__tests__/dev.test.d.ts +0 -2
- package/dist/__tests__/dev.test.d.ts.map +0 -1
- package/dist/__tests__/dev.test.js +0 -25
- package/dist/__tests__/dev.test.js.map +0 -1
- package/dist/__tests__/init-memory.test.d.ts +0 -2
- package/dist/__tests__/init-memory.test.d.ts.map +0 -1
- package/dist/__tests__/init-memory.test.js +0 -45
- package/dist/__tests__/init-memory.test.js.map +0 -1
- package/dist/__tests__/token.test.d.ts +0 -2
- package/dist/__tests__/token.test.d.ts.map +0 -1
- package/dist/__tests__/token.test.js +0 -52
- package/dist/__tests__/token.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
- package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
- package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
- package/dist/commands/apply.d.ts +0 -3
- package/dist/commands/apply.d.ts.map +0 -1
- package/dist/commands/apply.js +0 -5
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
- package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
- package/dist/internal/__tests__/api-client.test.d.ts +0 -2
- package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
- package/dist/internal/__tests__/api-client.test.js +0 -95
- package/dist/internal/__tests__/api-client.test.js.map +0 -1
- package/dist/internal/__tests__/context.test.d.ts +0 -2
- package/dist/internal/__tests__/context.test.d.ts.map +0 -1
- package/dist/internal/__tests__/context.test.js +0 -77
- package/dist/internal/__tests__/context.test.js.map +0 -1
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Hunt Connector (V1 runtime)
|
|
3
|
+
*
|
|
4
|
+
* Searches Product Hunt posts and comments via the GraphQL API.
|
|
5
|
+
* Supports both authenticated (Developer Token) and unauthenticated modes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ActionContext,
|
|
10
|
+
type ActionResult,
|
|
11
|
+
type ConnectorDefinition,
|
|
12
|
+
ConnectorRuntime,
|
|
13
|
+
calculateEngagementScore,
|
|
14
|
+
type EventEnvelope,
|
|
15
|
+
type SyncContext,
|
|
16
|
+
type SyncResult,
|
|
17
|
+
} from '@lobu/connector-sdk';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Product Hunt GraphQL API types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface ProductHuntMaker {
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProductHuntTopicEdge {
|
|
28
|
+
node: {
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ProductHuntComment {
|
|
34
|
+
id: string;
|
|
35
|
+
body: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
votesCount: number;
|
|
38
|
+
user: {
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ProductHuntCommentEdge {
|
|
44
|
+
node: ProductHuntComment;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ProductHuntPost {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
tagline: string;
|
|
51
|
+
description: string;
|
|
52
|
+
url: string;
|
|
53
|
+
votesCount: number;
|
|
54
|
+
commentsCount: number;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
makers: ProductHuntMaker[];
|
|
57
|
+
topics: {
|
|
58
|
+
edges: ProductHuntTopicEdge[];
|
|
59
|
+
};
|
|
60
|
+
comments: {
|
|
61
|
+
edges: ProductHuntCommentEdge[];
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ProductHuntPostEdge {
|
|
66
|
+
node: ProductHuntPost;
|
|
67
|
+
cursor: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ProductHuntPageInfo {
|
|
71
|
+
hasNextPage: boolean;
|
|
72
|
+
endCursor: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ProductHuntPostsResponse {
|
|
76
|
+
data: {
|
|
77
|
+
posts: {
|
|
78
|
+
edges: ProductHuntPostEdge[];
|
|
79
|
+
pageInfo: ProductHuntPageInfo;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
errors?: Array<{ message: string }>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ProductHuntCheckpoint {
|
|
86
|
+
last_cursor?: string;
|
|
87
|
+
last_sync_at?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// GraphQL queries
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
const LIST_POSTS_QUERY = `
|
|
95
|
+
query ListPosts($topic: String, $postedAfter: DateTime, $after: String) {
|
|
96
|
+
posts(topic: $topic, postedAfter: $postedAfter, after: $after, first: 10, order: NEWEST) {
|
|
97
|
+
edges {
|
|
98
|
+
node {
|
|
99
|
+
id
|
|
100
|
+
name
|
|
101
|
+
tagline
|
|
102
|
+
description
|
|
103
|
+
url
|
|
104
|
+
votesCount
|
|
105
|
+
commentsCount
|
|
106
|
+
createdAt
|
|
107
|
+
makers { name }
|
|
108
|
+
topics { edges { node { name } } }
|
|
109
|
+
comments(first: 10) {
|
|
110
|
+
edges {
|
|
111
|
+
node {
|
|
112
|
+
id
|
|
113
|
+
body
|
|
114
|
+
createdAt
|
|
115
|
+
votesCount
|
|
116
|
+
user { name }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
cursor
|
|
122
|
+
}
|
|
123
|
+
pageInfo {
|
|
124
|
+
hasNextPage
|
|
125
|
+
endCursor
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Connector
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
export default class ProductHuntConnector extends ConnectorRuntime {
|
|
136
|
+
readonly definition: ConnectorDefinition = {
|
|
137
|
+
key: 'producthunt',
|
|
138
|
+
name: 'Product Hunt',
|
|
139
|
+
description: 'Searches Product Hunt posts and comments for a given query.',
|
|
140
|
+
version: '1.0.0',
|
|
141
|
+
faviconDomain: 'producthunt.com',
|
|
142
|
+
authSchema: {
|
|
143
|
+
methods: [
|
|
144
|
+
{
|
|
145
|
+
type: 'env_keys',
|
|
146
|
+
required: false,
|
|
147
|
+
fields: [
|
|
148
|
+
{
|
|
149
|
+
key: 'PRODUCTHUNT_TOKEN',
|
|
150
|
+
label: 'Product Hunt Developer Token',
|
|
151
|
+
description:
|
|
152
|
+
'Create at producthunt.com/v2/oauth/applications — add an app, then copy the Developer Token.',
|
|
153
|
+
secret: true,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
feeds: {
|
|
160
|
+
posts: {
|
|
161
|
+
key: 'posts',
|
|
162
|
+
name: 'Posts & Comments',
|
|
163
|
+
description: 'Search Product Hunt for posts and their comments.',
|
|
164
|
+
configSchema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
required: ['search_query'],
|
|
167
|
+
properties: {
|
|
168
|
+
search_query: {
|
|
169
|
+
type: 'string',
|
|
170
|
+
minLength: 1,
|
|
171
|
+
description: 'Search term to find posts on Product Hunt.',
|
|
172
|
+
},
|
|
173
|
+
lookback_days: {
|
|
174
|
+
type: 'integer',
|
|
175
|
+
minimum: 1,
|
|
176
|
+
maximum: 730,
|
|
177
|
+
default: 365,
|
|
178
|
+
description: 'Number of days to look back for historical data.',
|
|
179
|
+
},
|
|
180
|
+
max_pages: {
|
|
181
|
+
type: 'integer',
|
|
182
|
+
minimum: 1,
|
|
183
|
+
maximum: 50,
|
|
184
|
+
default: 10,
|
|
185
|
+
description: 'Maximum number of pages to fetch.',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
eventKinds: {
|
|
190
|
+
post: {
|
|
191
|
+
description: 'A Product Hunt launch/post',
|
|
192
|
+
metadataSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
tagline: { type: 'string', description: 'Short tagline for the product' },
|
|
196
|
+
votes_count: { type: 'number', description: 'Number of upvotes' },
|
|
197
|
+
comments_count: { type: 'number', description: 'Number of comments' },
|
|
198
|
+
makers: {
|
|
199
|
+
type: 'array',
|
|
200
|
+
items: { type: 'string' },
|
|
201
|
+
description: 'Names of the product makers',
|
|
202
|
+
},
|
|
203
|
+
topics: {
|
|
204
|
+
type: 'array',
|
|
205
|
+
items: { type: 'string' },
|
|
206
|
+
description: 'Topic tags',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
comment: {
|
|
212
|
+
description: 'A comment on a Product Hunt post',
|
|
213
|
+
metadataSchema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
votes_count: { type: 'number', description: 'Number of upvotes on the comment' },
|
|
217
|
+
post_id: { type: 'string', description: 'ID of the parent post' },
|
|
218
|
+
post_name: { type: 'string', description: 'Name of the parent post' },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
optionsSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
required: ['search_query'],
|
|
228
|
+
properties: {
|
|
229
|
+
search_query: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
minLength: 1,
|
|
232
|
+
description: 'Search term to find posts on Product Hunt.',
|
|
233
|
+
},
|
|
234
|
+
lookback_days: {
|
|
235
|
+
type: 'integer',
|
|
236
|
+
minimum: 1,
|
|
237
|
+
maximum: 730,
|
|
238
|
+
default: 365,
|
|
239
|
+
description: 'Number of days to look back for historical data.',
|
|
240
|
+
},
|
|
241
|
+
max_pages: {
|
|
242
|
+
type: 'integer',
|
|
243
|
+
minimum: 1,
|
|
244
|
+
maximum: 50,
|
|
245
|
+
default: 10,
|
|
246
|
+
description: 'Maximum number of pages to fetch.',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
private readonly API_URL = 'https://api.producthunt.com/v2/api/graphql';
|
|
253
|
+
private readonly RATE_LIMIT_MS = 1000;
|
|
254
|
+
|
|
255
|
+
// -------------------------------------------------------------------------
|
|
256
|
+
// sync
|
|
257
|
+
// -------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
async sync(ctx: SyncContext): Promise<SyncResult> {
|
|
260
|
+
const searchQuery = ctx.config.search_query as string;
|
|
261
|
+
const lookbackDays = (ctx.config.lookback_days as number) ?? 365;
|
|
262
|
+
const maxPages = (ctx.config.max_pages as number) ?? 10;
|
|
263
|
+
const token = ctx.config.PRODUCTHUNT_TOKEN as string | undefined;
|
|
264
|
+
|
|
265
|
+
const cutoffDate = new Date();
|
|
266
|
+
cutoffDate.setDate(cutoffDate.getDate() - lookbackDays);
|
|
267
|
+
|
|
268
|
+
const previousCheckpoint = ctx.checkpoint as ProductHuntCheckpoint | null;
|
|
269
|
+
|
|
270
|
+
const events: EventEnvelope[] = [];
|
|
271
|
+
const seenIds = new Set<string>();
|
|
272
|
+
let cursor: string | null = previousCheckpoint?.last_cursor ?? null;
|
|
273
|
+
let page = 0;
|
|
274
|
+
let reachedCutoff = false;
|
|
275
|
+
|
|
276
|
+
while (page < maxPages && !reachedCutoff) {
|
|
277
|
+
const variables: Record<string, string | null> = {
|
|
278
|
+
postedAfter: cutoffDate.toISOString(),
|
|
279
|
+
after: cursor,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const headers: Record<string, string> = {
|
|
283
|
+
'Content-Type': 'application/json',
|
|
284
|
+
Accept: 'application/json',
|
|
285
|
+
};
|
|
286
|
+
if (token) {
|
|
287
|
+
headers.Authorization = `Bearer ${token}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const response = await fetch(this.API_URL, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers,
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
query: LIST_POSTS_QUERY,
|
|
295
|
+
variables,
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const status = response.status;
|
|
301
|
+
if (status === 429) {
|
|
302
|
+
throw new Error('Product Hunt rate limit exceeded. Please wait before retrying.');
|
|
303
|
+
}
|
|
304
|
+
if (status === 401) {
|
|
305
|
+
if (!token) {
|
|
306
|
+
// PH v2 API requires auth; return empty results when no token configured
|
|
307
|
+
console.warn(
|
|
308
|
+
'Product Hunt API requires a Developer Token. Configure PRODUCTHUNT_TOKEN for results.'
|
|
309
|
+
);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
throw new Error('Product Hunt authentication failed. Check your Developer Token.');
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`Product Hunt API error (${status}): ${await response.text()}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const result = (await response.json()) as ProductHuntPostsResponse;
|
|
318
|
+
|
|
319
|
+
if (result.errors && result.errors.length > 0) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Product Hunt GraphQL error: ${result.errors.map((e) => e.message).join(', ')}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const edges = result.data.posts.edges;
|
|
326
|
+
if (edges.length === 0) break;
|
|
327
|
+
|
|
328
|
+
const queryLower = searchQuery.toLowerCase();
|
|
329
|
+
|
|
330
|
+
for (const edge of edges) {
|
|
331
|
+
const post = edge.node;
|
|
332
|
+
const postDate = new Date(post.createdAt);
|
|
333
|
+
|
|
334
|
+
if (postDate < cutoffDate) {
|
|
335
|
+
reachedCutoff = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Client-side filter: match search query in name, tagline, or description
|
|
340
|
+
const matchesQuery =
|
|
341
|
+
post.name.toLowerCase().includes(queryLower) ||
|
|
342
|
+
post.tagline.toLowerCase().includes(queryLower) ||
|
|
343
|
+
(post.description ?? '').toLowerCase().includes(queryLower) ||
|
|
344
|
+
post.topics?.edges?.some((t) => t.node.name.toLowerCase().includes(queryLower));
|
|
345
|
+
|
|
346
|
+
if (!matchesQuery) continue;
|
|
347
|
+
|
|
348
|
+
// Add post event
|
|
349
|
+
const postExternalId = `producthunt_post_${post.id}`;
|
|
350
|
+
if (!seenIds.has(postExternalId)) {
|
|
351
|
+
seenIds.add(postExternalId);
|
|
352
|
+
events.push(this.transformPost(post));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add comment events
|
|
356
|
+
if (post.comments?.edges) {
|
|
357
|
+
for (const commentEdge of post.comments.edges) {
|
|
358
|
+
const comment = commentEdge.node;
|
|
359
|
+
const commentExternalId = `producthunt_comment_${comment.id}`;
|
|
360
|
+
if (!seenIds.has(commentExternalId)) {
|
|
361
|
+
seenIds.add(commentExternalId);
|
|
362
|
+
events.push(this.transformComment(comment, post));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const pageInfo = result.data.posts.pageInfo;
|
|
369
|
+
if (!pageInfo.hasNextPage) break;
|
|
370
|
+
|
|
371
|
+
cursor = pageInfo.endCursor;
|
|
372
|
+
page++;
|
|
373
|
+
|
|
374
|
+
if (page < maxPages && !reachedCutoff) {
|
|
375
|
+
await this.sleep(this.RATE_LIMIT_MS);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Sort events by occurred_at descending
|
|
380
|
+
events.sort((a, b) => b.occurred_at.getTime() - a.occurred_at.getTime());
|
|
381
|
+
|
|
382
|
+
const checkpoint: ProductHuntCheckpoint = {
|
|
383
|
+
last_cursor: cursor ?? undefined,
|
|
384
|
+
last_sync_at: new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
events,
|
|
389
|
+
checkpoint: checkpoint as Record<string, unknown>,
|
|
390
|
+
metadata: {
|
|
391
|
+
items_found: events.length,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// -------------------------------------------------------------------------
|
|
397
|
+
// execute
|
|
398
|
+
// -------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
async execute(_ctx: ActionContext): Promise<ActionResult> {
|
|
401
|
+
return { success: false, error: 'Actions not supported' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// -------------------------------------------------------------------------
|
|
405
|
+
// Transform helpers
|
|
406
|
+
// -------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
private transformPost(post: ProductHuntPost): EventEnvelope {
|
|
409
|
+
const makers = post.makers.map((m) => m.name);
|
|
410
|
+
const topics = post.topics.edges.map((e) => e.node.name);
|
|
411
|
+
|
|
412
|
+
const engagementScore = calculateEngagementScore('producthunt', {
|
|
413
|
+
upvotes: post.votesCount,
|
|
414
|
+
reply_count: post.commentsCount,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const description = post.description ?? '';
|
|
418
|
+
const content = description.trim() || post.tagline;
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
origin_id: `producthunt_post_${post.id}`,
|
|
422
|
+
title: post.name,
|
|
423
|
+
payload_text: content,
|
|
424
|
+
author_name: makers.join(', ') || undefined,
|
|
425
|
+
source_url: post.url,
|
|
426
|
+
occurred_at: new Date(post.createdAt),
|
|
427
|
+
origin_type: 'post',
|
|
428
|
+
score: engagementScore,
|
|
429
|
+
metadata: {
|
|
430
|
+
tagline: post.tagline,
|
|
431
|
+
votes_count: post.votesCount,
|
|
432
|
+
comments_count: post.commentsCount,
|
|
433
|
+
makers,
|
|
434
|
+
topics,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private transformComment(
|
|
440
|
+
comment: ProductHuntComment,
|
|
441
|
+
parentPost: ProductHuntPost
|
|
442
|
+
): EventEnvelope {
|
|
443
|
+
const engagementScore = calculateEngagementScore('producthunt', {
|
|
444
|
+
upvotes: comment.votesCount,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
origin_id: `producthunt_comment_${comment.id}`,
|
|
449
|
+
payload_text: comment.body ?? '',
|
|
450
|
+
author_name: comment.user.name,
|
|
451
|
+
source_url: parentPost.url,
|
|
452
|
+
occurred_at: new Date(comment.createdAt),
|
|
453
|
+
origin_type: 'comment',
|
|
454
|
+
score: engagementScore,
|
|
455
|
+
origin_parent_id: `producthunt_post_${parentPost.id}`,
|
|
456
|
+
metadata: {
|
|
457
|
+
votes_count: comment.votesCount,
|
|
458
|
+
post_id: parentPost.id,
|
|
459
|
+
post_name: parentPost.name,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// -------------------------------------------------------------------------
|
|
465
|
+
// Utilities
|
|
466
|
+
// -------------------------------------------------------------------------
|
|
467
|
+
|
|
468
|
+
private sleep(ms: number): Promise<void> {
|
|
469
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
470
|
+
}
|
|
471
|
+
}
|