@ever-works/activepieces-plugin 1.0.0 → 1.0.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.
- package/dist/index.cjs +3492 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +3465 -0
- package/package.json +82 -72
- package/LICENSE +0 -661
package/dist/index.js
ADDED
|
@@ -0,0 +1,3465 @@
|
|
|
1
|
+
// ../../../node_modules/.pnpm/@ever-co+connect-contracts@1.0.0-rc.2/node_modules/@ever-co/connect-contracts/dist/index.js
|
|
2
|
+
var CONSTANTS = {
|
|
3
|
+
"$comment": "Wire constants of the Ever Platform modules. tools/split-integrations.mjs refreshes the fields read from the contract (contracts_version, code patterns, install_sources, products, stats_headers, feed_event_types, integration_keys, root_keys); every other value is a default a product may override by its own configuration.",
|
|
4
|
+
"contracts_version": "1.0.0-rc.2",
|
|
5
|
+
"connect_code_pattern": "^EVC(-[0-9A-HJKMNP-TV-Za-hj-kmnp-tv-z]{4}){3}$",
|
|
6
|
+
"link_code_pattern": "^EVL(-[0-9A-HJKMNP-TV-Za-hj-kmnp-tv-z]{4}){3}$",
|
|
7
|
+
"install_sources": "^(cloud|self-hosted|ever\\.sh|works_app|desktop|partner:[a-z0-9-]{2,32})$",
|
|
8
|
+
"products": [
|
|
9
|
+
"gauzy",
|
|
10
|
+
"teams",
|
|
11
|
+
"works",
|
|
12
|
+
"rec",
|
|
13
|
+
"traduora",
|
|
14
|
+
"demand"
|
|
15
|
+
],
|
|
16
|
+
"instance_kinds": [
|
|
17
|
+
"cloud",
|
|
18
|
+
"self_hosted",
|
|
19
|
+
"works_app"
|
|
20
|
+
],
|
|
21
|
+
"instance_token_prefix": "evit_",
|
|
22
|
+
"entitlement_typ": "ever-entitlement+jwt",
|
|
23
|
+
"intent_typ": "ever-intent+jwt",
|
|
24
|
+
"assertion_typ": "ever-assertion+jwt",
|
|
25
|
+
"key_manifest_typ": "ever-key-manifest+jwt",
|
|
26
|
+
"stats_link_typ": "ever-stats-link+jwt",
|
|
27
|
+
"client_assertion_typ": "JWT",
|
|
28
|
+
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
|
|
29
|
+
"entitlement_aud": "ever-connect",
|
|
30
|
+
"stats_headers": {
|
|
31
|
+
"key": "Ever-Stats-Key",
|
|
32
|
+
"signature": "Ever-Stats-Signature",
|
|
33
|
+
"key_id": "Ever-Stats-Key-Id"
|
|
34
|
+
},
|
|
35
|
+
"stats_signature_prefix": "ed25519=",
|
|
36
|
+
"feed_event_types": [
|
|
37
|
+
"ever.consent.consent.granted",
|
|
38
|
+
"ever.consent.consent.revoked",
|
|
39
|
+
"ever.consent.integration.disabled",
|
|
40
|
+
"ever.consent.integration.enabled",
|
|
41
|
+
"ever.consent.integration.scope_bumped",
|
|
42
|
+
"ever.entitlements.entitlement.issued",
|
|
43
|
+
"ever.entitlements.entitlement.revoked",
|
|
44
|
+
"ever.entitlements.usage.limit_reached",
|
|
45
|
+
"ever.registry.instance.approved",
|
|
46
|
+
"ever.registry.instance.connected",
|
|
47
|
+
"ever.registry.instance.disconnected",
|
|
48
|
+
"ever.registry.instance.key_rotated",
|
|
49
|
+
"ever.registry.instance.revoked",
|
|
50
|
+
"ever.registry.instance.seen",
|
|
51
|
+
"ever.registry.managed_operation.requested",
|
|
52
|
+
"ever.registry.managed_operation.state_changed",
|
|
53
|
+
"ever.registry.membership.created",
|
|
54
|
+
"ever.registry.membership.removed",
|
|
55
|
+
"ever.registry.membership.role_changed",
|
|
56
|
+
"ever.registry.person.deletion_requested",
|
|
57
|
+
"ever.registry.person.export_requested",
|
|
58
|
+
"ever.registry.tenant_link.created",
|
|
59
|
+
"ever.registry.tenant_link.orphaned",
|
|
60
|
+
"ever.registry.tenant_link.resumed",
|
|
61
|
+
"ever.registry.tenant_link.suspended",
|
|
62
|
+
"ever.registry.tenant_link.unlinked"
|
|
63
|
+
],
|
|
64
|
+
"integration_keys": [
|
|
65
|
+
"instance_url",
|
|
66
|
+
"stats_link",
|
|
67
|
+
"ever_id_login",
|
|
68
|
+
"counterparty_lookup",
|
|
69
|
+
"counterparty_discoverable",
|
|
70
|
+
"profile_import",
|
|
71
|
+
"usage_reporting",
|
|
72
|
+
"app_sync",
|
|
73
|
+
"billing_link",
|
|
74
|
+
"webhooks",
|
|
75
|
+
"managed_operations",
|
|
76
|
+
"provider_access",
|
|
77
|
+
"marketplace_installs"
|
|
78
|
+
],
|
|
79
|
+
"key_manifest_path": "/.well-known/ever-keys.json",
|
|
80
|
+
"assertion_audience_path": "/v1/instances/token",
|
|
81
|
+
"assertion_max_ttl_s": 300,
|
|
82
|
+
"assertion_max_skew_s": 300,
|
|
83
|
+
"token_ttl_s": 3600,
|
|
84
|
+
"token_refresh_after_s": 3e3,
|
|
85
|
+
"key_rotation_overlap_s": 604800,
|
|
86
|
+
"entitlement": {
|
|
87
|
+
"validity_s": 604800,
|
|
88
|
+
"refresh_after_s": 21600,
|
|
89
|
+
"grace_s": 2592e3,
|
|
90
|
+
"max_reads_per_hour": 6
|
|
91
|
+
},
|
|
92
|
+
"key_manifest": {
|
|
93
|
+
"refresh_s": 86400,
|
|
94
|
+
"unknown_kid_refresh_min_s": 600,
|
|
95
|
+
"lifetime_s": 2592e3
|
|
96
|
+
},
|
|
97
|
+
"heartbeat_interval_s": 86400,
|
|
98
|
+
"heartbeat_after_boot_max_s": 300,
|
|
99
|
+
"heartbeat_min_interval_s": 60,
|
|
100
|
+
"feed": {
|
|
101
|
+
"wait_s": 25,
|
|
102
|
+
"interval_fallback_s": 900,
|
|
103
|
+
"page_limit": 50
|
|
104
|
+
},
|
|
105
|
+
"stats": {
|
|
106
|
+
"send_interval_s": 86400,
|
|
107
|
+
"min_interval_s": 3600,
|
|
108
|
+
"max_bytes": 16384,
|
|
109
|
+
"final_resend_days": [
|
|
110
|
+
1,
|
|
111
|
+
2,
|
|
112
|
+
3
|
|
113
|
+
],
|
|
114
|
+
"reports_per_day": 24
|
|
115
|
+
},
|
|
116
|
+
"lookup": {
|
|
117
|
+
"normalization_version": 1,
|
|
118
|
+
"max_hashes_per_query": 100,
|
|
119
|
+
"cache_positive_s": 86400,
|
|
120
|
+
"cache_negative_s": 3600,
|
|
121
|
+
"salt_cache_s": 86400
|
|
122
|
+
},
|
|
123
|
+
"mirror": {
|
|
124
|
+
"max_bytes": 4194304,
|
|
125
|
+
"interval_s": 30
|
|
126
|
+
},
|
|
127
|
+
"step_up_max_age_s": 300,
|
|
128
|
+
"stats_link_statement_max_age_s": 600,
|
|
129
|
+
"timeouts_ms": {
|
|
130
|
+
"read": 6e3,
|
|
131
|
+
"write": 1e4
|
|
132
|
+
},
|
|
133
|
+
"user_agent_pattern": "ever-connect-sdk/<sdk version> (<product>/<product version>)",
|
|
134
|
+
"root_keys": [
|
|
135
|
+
{
|
|
136
|
+
"kid": "ever-202610-0d77",
|
|
137
|
+
"iss": "https://api-dev.ever.co",
|
|
138
|
+
"kty": "OKP",
|
|
139
|
+
"crv": "Ed25519",
|
|
140
|
+
"x": "cgULEqOov0n4s9I8luZboIgJp9mIGusmsSdSL4oCjKk",
|
|
141
|
+
"use": "sig",
|
|
142
|
+
"alg": "EdDSA"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"kid": "ever-202610-8704",
|
|
146
|
+
"iss": "https://api-stage.ever.co",
|
|
147
|
+
"kty": "OKP",
|
|
148
|
+
"crv": "Ed25519",
|
|
149
|
+
"x": "Ync-MW21JL72GOiS-RVrlvS3gyFH5Krh5f6I66jw1Qc",
|
|
150
|
+
"use": "sig",
|
|
151
|
+
"alg": "EdDSA"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"root_keys_file_env": "EVER_PLATFORM_ROOT_KEYS_FILE",
|
|
155
|
+
"root_keys_file_hosts": [
|
|
156
|
+
"localhost",
|
|
157
|
+
"127.0.0.0/8",
|
|
158
|
+
"::1",
|
|
159
|
+
"10.0.0.0/8",
|
|
160
|
+
"172.16.0.0/12",
|
|
161
|
+
"192.168.0.0/16",
|
|
162
|
+
"*.localhost"
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
var FEED_EVENT_TYPES = CONSTANTS.feed_event_types;
|
|
166
|
+
var PROBLEM_CODES = [
|
|
167
|
+
"validation_failed",
|
|
168
|
+
"not_found",
|
|
169
|
+
"unauthorized",
|
|
170
|
+
"forbidden_role",
|
|
171
|
+
"forbidden_scope",
|
|
172
|
+
"ambiguous_credentials",
|
|
173
|
+
"credential_revoked",
|
|
174
|
+
"instance_disconnected",
|
|
175
|
+
"instance_pending_approval",
|
|
176
|
+
"not_connection_owner",
|
|
177
|
+
"integration_disabled",
|
|
178
|
+
"integration_revoked",
|
|
179
|
+
"denied_by_policy",
|
|
180
|
+
"integration_not_available",
|
|
181
|
+
"entitlement_required",
|
|
182
|
+
"tier_required",
|
|
183
|
+
"limit_exceeded",
|
|
184
|
+
"rate_limited",
|
|
185
|
+
"idempotency_mismatch",
|
|
186
|
+
"idempotency_in_progress",
|
|
187
|
+
"stale_version",
|
|
188
|
+
"module_disabled",
|
|
189
|
+
"handle_taken",
|
|
190
|
+
"handle_reserved",
|
|
191
|
+
"handle_retired",
|
|
192
|
+
"handle_invalid",
|
|
193
|
+
"handle_held",
|
|
194
|
+
"handle_held_by_you",
|
|
195
|
+
"code_invalid",
|
|
196
|
+
"code_expired",
|
|
197
|
+
"product_mismatch",
|
|
198
|
+
"product_not_supported",
|
|
199
|
+
"key_mismatch",
|
|
200
|
+
"signature_invalid",
|
|
201
|
+
"unlinked_org",
|
|
202
|
+
"schema_violation",
|
|
203
|
+
"mirror_owned_field",
|
|
204
|
+
"illegal_transition",
|
|
205
|
+
"already_exists",
|
|
206
|
+
"already_completed",
|
|
207
|
+
"billing_active",
|
|
208
|
+
"sso_required",
|
|
209
|
+
"reauth_required",
|
|
210
|
+
"session_required",
|
|
211
|
+
"step_up_required",
|
|
212
|
+
"session_bound",
|
|
213
|
+
"authorization_pending",
|
|
214
|
+
"slow_down",
|
|
215
|
+
"expired_token",
|
|
216
|
+
"already_connected",
|
|
217
|
+
"already_linked",
|
|
218
|
+
"already_claimed",
|
|
219
|
+
"invalid_client",
|
|
220
|
+
"public_jwk_invalid",
|
|
221
|
+
"identifier_claimed",
|
|
222
|
+
"salt_version_unknown",
|
|
223
|
+
"salt_version_retired",
|
|
224
|
+
"hashes_invalid",
|
|
225
|
+
"lookup_key_required",
|
|
226
|
+
"required_component",
|
|
227
|
+
"resync_required",
|
|
228
|
+
"last_owner",
|
|
229
|
+
"last_auth_method",
|
|
230
|
+
"instances_linked",
|
|
231
|
+
"install_in_progress",
|
|
232
|
+
"slug_reserved",
|
|
233
|
+
"checklist_incomplete",
|
|
234
|
+
"scope_version_outdated",
|
|
235
|
+
"method_not_allowed",
|
|
236
|
+
"payload_too_large",
|
|
237
|
+
"unsupported_media_type",
|
|
238
|
+
"gone",
|
|
239
|
+
"unavailable",
|
|
240
|
+
"upstream",
|
|
241
|
+
"database_absent",
|
|
242
|
+
"database_unreachable",
|
|
243
|
+
"topic_private",
|
|
244
|
+
"keys_unavailable",
|
|
245
|
+
"internal_error"
|
|
246
|
+
];
|
|
247
|
+
var CODES = new Set(PROBLEM_CODES);
|
|
248
|
+
|
|
249
|
+
// ../../contracts/dist/index.js
|
|
250
|
+
var USER_SELECTABLE_WORK_KINDS = [
|
|
251
|
+
"website",
|
|
252
|
+
"landing-page",
|
|
253
|
+
"blog",
|
|
254
|
+
"directory",
|
|
255
|
+
"awesome-repo",
|
|
256
|
+
"repo",
|
|
257
|
+
"app"
|
|
258
|
+
];
|
|
259
|
+
var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "project", "default"];
|
|
260
|
+
var WORK_KIND_SET = new Set(WORK_KINDS);
|
|
261
|
+
var GOAL_KINDS = ["metric", "delivery"];
|
|
262
|
+
var GOAL_KIND_SET = new Set(GOAL_KINDS);
|
|
263
|
+
var RECORD_VISIBILITIES = ["private", "team", "organization"];
|
|
264
|
+
var RECORD_VISIBILITY_SET = new Set(RECORD_VISIBILITIES);
|
|
265
|
+
var RECORD_LIST_VIEWS = ["mine", "shared", "organization"];
|
|
266
|
+
var RECORD_LIST_VIEW_SET = new Set(RECORD_LIST_VIEWS);
|
|
267
|
+
var APP_DEPENDENCY_BACKUP_OVERDUE_MS = 26 * 36e5;
|
|
268
|
+
var APP_DEPENDENCY_MANAGED = {
|
|
269
|
+
pgRoleConnectionLimit: 20,
|
|
270
|
+
pgDatabaseConnectionLimit: 25,
|
|
271
|
+
pgStatementTimeoutMs: 6e4,
|
|
272
|
+
pgIdleInTransactionTimeoutMs: 6e4,
|
|
273
|
+
bucketQuotaGiB: 10,
|
|
274
|
+
redisMaxMemoryMiB: 256,
|
|
275
|
+
backupMaxAgeMs: 864e5
|
|
276
|
+
};
|
|
277
|
+
var APP_ENV_VALIDATION_REFUSAL_CODES = [
|
|
278
|
+
"invalidName",
|
|
279
|
+
"reservedName",
|
|
280
|
+
"valueTooLarge",
|
|
281
|
+
"controlCharacter",
|
|
282
|
+
"lengthMismatch",
|
|
283
|
+
"tooShort",
|
|
284
|
+
"tooLong",
|
|
285
|
+
"patternMismatch"
|
|
286
|
+
];
|
|
287
|
+
var APP_ENV_API_ERROR_CODES = [
|
|
288
|
+
"generatedValueUseRotate",
|
|
289
|
+
"neverRotateNotAcknowledged",
|
|
290
|
+
"rotateConfirmationMismatch",
|
|
291
|
+
"secureStorageUnavailable",
|
|
292
|
+
"tooManyValues",
|
|
293
|
+
"valuesTooLarge",
|
|
294
|
+
"malformedLine",
|
|
295
|
+
"rotateRateLimited"
|
|
296
|
+
];
|
|
297
|
+
var APP_ENV_ERROR_CODES = [...APP_ENV_VALIDATION_REFUSAL_CODES, ...APP_ENV_API_ERROR_CODES];
|
|
298
|
+
var APP_LAUNCHER_CLIENT_CACHE_MS = 5 * 6e4;
|
|
299
|
+
var MANAGED_HOSTING_REASONS = [
|
|
300
|
+
"licenseNotGreen",
|
|
301
|
+
"upstreamAgreementMissing",
|
|
302
|
+
"entryDisallows",
|
|
303
|
+
"blueprintNotVerified",
|
|
304
|
+
"managedTierDisabled"
|
|
305
|
+
];
|
|
306
|
+
var MANAGED_HOSTING_AVAILABILITIES = ["available", ...MANAGED_HOSTING_REASONS];
|
|
307
|
+
var APP_PRIVATE_COPY_MAX_SIZE_KB = 512e3;
|
|
308
|
+
var APP_DEPLOY_MAX_DURATION_S = 7200;
|
|
309
|
+
var APP_DEPLOY_LOCK_STALE_S = APP_DEPLOY_MAX_DURATION_S + 60;
|
|
310
|
+
var APPS_CATALOG_LAST_GOOD_REGISTRY_MS = 7 * 864e5;
|
|
311
|
+
var APP_REPOSITORY_STAGE_LIMITS = {
|
|
312
|
+
"private-copy": {
|
|
313
|
+
maxSizeKb: APP_PRIVATE_COPY_MAX_SIZE_KB,
|
|
314
|
+
fetchesLfs: false,
|
|
315
|
+
refusalCodes: ["too_large_for_private_copy", "uses_lfs"]
|
|
316
|
+
},
|
|
317
|
+
provisioning: {
|
|
318
|
+
maxSizeKb: 3 * 1024 * 1024,
|
|
319
|
+
refusalCodes: ["repository-too-large"]
|
|
320
|
+
},
|
|
321
|
+
build: {
|
|
322
|
+
refusalCodes: []
|
|
323
|
+
},
|
|
324
|
+
sandbox: {
|
|
325
|
+
refusalCodes: []
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var TENANT_POSTGRES_ROLE_CONNECTION_LIMIT = APP_DEPENDENCY_MANAGED.pgRoleConnectionLimit;
|
|
329
|
+
var TENANT_POSTGRES_DATABASE_CONNECTION_LIMIT = APP_DEPENDENCY_MANAGED.pgDatabaseConnectionLimit;
|
|
330
|
+
var TENANT_POSTGRES_STATEMENT_TIMEOUT_MS = APP_DEPENDENCY_MANAGED.pgStatementTimeoutMs;
|
|
331
|
+
var TENANT_POSTGRES_IDLE_IN_TRANSACTION_TIMEOUT_MS = APP_DEPENDENCY_MANAGED.pgIdleInTransactionTimeoutMs;
|
|
332
|
+
var APP_PROVISION_LIMITS = {
|
|
333
|
+
attemptsDefault: 3,
|
|
334
|
+
attemptsMin: 1,
|
|
335
|
+
attemptsMax: 5,
|
|
336
|
+
attemptsPerAnswer: 2,
|
|
337
|
+
questionsMax: 3,
|
|
338
|
+
attemptsCeiling: 9,
|
|
339
|
+
optionsMax: 4,
|
|
340
|
+
tokenCapDefault: 3e6,
|
|
341
|
+
tokenCapMin: 5e5,
|
|
342
|
+
tokenCapMax: 1e7,
|
|
343
|
+
runnerMinuteCapDefault: 240,
|
|
344
|
+
runnerMinuteCapMin: 60,
|
|
345
|
+
runnerMinuteCapMax: 600,
|
|
346
|
+
/** Two starts of the same App Work inside this window yield one row (§4 "Start semantics"). */
|
|
347
|
+
startDedupeMs: 1e4,
|
|
348
|
+
analysisRunMs: 45 * 6e4,
|
|
349
|
+
iterateRunMs: 30 * 6e4,
|
|
350
|
+
forkWaitMs: 30 * 6e4,
|
|
351
|
+
activeDeadlineMs: 8 * 36e5,
|
|
352
|
+
/**
|
|
353
|
+
* Verification-Build reservation: `min(headSpec.build.resources.timeoutMinutes, buildMinutesMax)
|
|
354
|
+
* + runnerBootMinutesMax` — 90 minutes at the defaults, and the number §6.4 compares against the cap.
|
|
355
|
+
*/
|
|
356
|
+
buildMinutesMax: 60,
|
|
357
|
+
jobSecondsMax: 900,
|
|
358
|
+
startupSecondsMax: 900,
|
|
359
|
+
smokeRequestMs: 3e4,
|
|
360
|
+
smokeTotalMs: 3e5,
|
|
361
|
+
runnerBootMinutesMax: 30,
|
|
362
|
+
runnerBootMemoryGiB: 12,
|
|
363
|
+
buildMemoryGiBMax: 14,
|
|
364
|
+
namespaceTtlMinutes: 90,
|
|
365
|
+
repoMaxBytes: 3 * 1024 ** 3,
|
|
366
|
+
diffMaxFiles: 12,
|
|
367
|
+
diffMaxLines: 3e3,
|
|
368
|
+
fileMaxBytes: 128 * 1024,
|
|
369
|
+
outputMaxBytes: 512 * 1024,
|
|
370
|
+
reportMaxChars: 4e4,
|
|
371
|
+
instructionFileMaxBytes: 32 * 1024,
|
|
372
|
+
logTailLines: 200,
|
|
373
|
+
logTailBytes: 16 * 1024,
|
|
374
|
+
evidenceCommentMaxChars: 6e4,
|
|
375
|
+
chatMessagesMax: 12,
|
|
376
|
+
questionReminderMs: 72 * 36e5,
|
|
377
|
+
questionExpiryMs: 14 * 864e5,
|
|
378
|
+
activePerUser: 3,
|
|
379
|
+
activePerOrg: 10,
|
|
380
|
+
startsPerHour: 10,
|
|
381
|
+
pollMs: 5e3,
|
|
382
|
+
infraRetries: 3,
|
|
383
|
+
infraRetryWindowMs: 30 * 6e4,
|
|
384
|
+
suggestionsPer30Days: 5,
|
|
385
|
+
autoReprovisionPerSync: 1,
|
|
386
|
+
autoReprovisionPer7Days: 2,
|
|
387
|
+
clusterProbeMs: 1e4,
|
|
388
|
+
podPendingInfraMs: 10 * 6e4
|
|
389
|
+
};
|
|
390
|
+
var MAX_CANDIDATE_OPTIONS = APP_PROVISION_LIMITS.optionsMax - 1;
|
|
391
|
+
var KB_EXPORT_MAX_BYTES = 200 * 1024 * 1024;
|
|
392
|
+
var TERMINAL_MAX_FRAME_BYTES = 1024 * 1024;
|
|
393
|
+
var COMPUTER_MAX_FRAME_BYTES = 512 * 1024;
|
|
394
|
+
var COMPUTER_MAX_BATCH_BYTES = 512 * 1024;
|
|
395
|
+
var COMPUTER_MAX_WIRE_FRAME_BYTES = Math.ceil(COMPUTER_MAX_FRAME_BYTES * 4 / 3) + 4096;
|
|
396
|
+
var COMPUTER_QUALITY_PRESETS = Object.freeze({
|
|
397
|
+
sharp: Object.freeze({ width: 1280, maxFps: 8, keyframeMs: 5e3, q: 70 }),
|
|
398
|
+
smooth: Object.freeze({ width: 960, maxFps: 15, keyframeMs: 5e3, q: 55 }),
|
|
399
|
+
steady: Object.freeze({ width: 800, maxFps: 2, keyframeMs: 2e3, q: 45 })
|
|
400
|
+
});
|
|
401
|
+
var COMPUTER_CONTROL_IDLE_MS_DEFAULT = 10 * 6e4;
|
|
402
|
+
var COMPUTER_CONTROL_CEILING_MS_DEFAULT = 60 * 6e4;
|
|
403
|
+
var COMPUTER_KEY_MODIFIER_ALT = 1;
|
|
404
|
+
var COMPUTER_KEY_MODIFIER_CTRL = 2;
|
|
405
|
+
var COMPUTER_KEY_MODIFIER_META = 4;
|
|
406
|
+
var CTRL = COMPUTER_KEY_MODIFIER_CTRL;
|
|
407
|
+
var META = COMPUTER_KEY_MODIFIER_META;
|
|
408
|
+
var ALT = COMPUTER_KEY_MODIFIER_ALT;
|
|
409
|
+
function withCtrlOrMeta(code, key) {
|
|
410
|
+
return [
|
|
411
|
+
{ code, requires: CTRL, label: `Ctrl/\u2318+${key}` },
|
|
412
|
+
{ code, requires: META, label: `Ctrl/\u2318+${key}` }
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
var COMPUTER_BLOCKED_SHORTCUTS = Object.freeze([
|
|
416
|
+
...withCtrlOrMeta("KeyC", "C"),
|
|
417
|
+
...withCtrlOrMeta("KeyX", "X"),
|
|
418
|
+
...withCtrlOrMeta("KeyV", "V"),
|
|
419
|
+
...withCtrlOrMeta("KeyW", "W"),
|
|
420
|
+
...withCtrlOrMeta("KeyT", "T"),
|
|
421
|
+
...withCtrlOrMeta("KeyN", "N"),
|
|
422
|
+
...withCtrlOrMeta("KeyQ", "Q"),
|
|
423
|
+
...withCtrlOrMeta("Tab", "Tab"),
|
|
424
|
+
{ code: "Tab", requires: ALT, label: "Alt+Tab" },
|
|
425
|
+
{ code: "F4", requires: ALT, label: "Alt+F4" },
|
|
426
|
+
{ code: "Delete", requires: CTRL | ALT, label: "Ctrl+Alt+Delete" },
|
|
427
|
+
{ code: "MetaLeft", requires: 0, label: "\u2318/Win" },
|
|
428
|
+
{ code: "MetaRight", requires: 0, label: "\u2318/Win" }
|
|
429
|
+
]);
|
|
430
|
+
var TASK_BOARD_STATUSES = [
|
|
431
|
+
"backlog",
|
|
432
|
+
"todo",
|
|
433
|
+
"in_progress",
|
|
434
|
+
"in_review",
|
|
435
|
+
"blocked",
|
|
436
|
+
"done",
|
|
437
|
+
"cancelled"
|
|
438
|
+
];
|
|
439
|
+
var TASK_BOARD_TERMINAL_STATUSES = ["done", "cancelled"];
|
|
440
|
+
var TASK_BOARD_STATUS_COLUMNS = TASK_BOARD_STATUSES.map((status) => ({
|
|
441
|
+
key: status,
|
|
442
|
+
statuses: [status],
|
|
443
|
+
terminal: TASK_BOARD_TERMINAL_STATUSES.includes(status),
|
|
444
|
+
toggleOnly: false
|
|
445
|
+
}));
|
|
446
|
+
var PLATFORM_DEFAULT_MERGE_POLICY = Object.freeze({
|
|
447
|
+
allowAgentMerge: false,
|
|
448
|
+
requireGreenGate: true,
|
|
449
|
+
requireHumanApproval: true,
|
|
450
|
+
allowedMergeMethods: ["squash"],
|
|
451
|
+
protectedBranches: ["main", "master", "develop", "stage"]
|
|
452
|
+
});
|
|
453
|
+
var MERGE_APPROVAL_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
454
|
+
var TOOL_GRANT_SCOPE_PRECEDENCE = Object.freeze([
|
|
455
|
+
"tenant",
|
|
456
|
+
"organization",
|
|
457
|
+
"work",
|
|
458
|
+
"agent"
|
|
459
|
+
]);
|
|
460
|
+
var PLATFORM_DEFAULT_TOOL_GRANT = Object.freeze({
|
|
461
|
+
allow: Object.freeze(["*"]),
|
|
462
|
+
deny: Object.freeze([])
|
|
463
|
+
});
|
|
464
|
+
var AGENT_INIT_SCRIPT_MAX_BYTES = 16 * 1024;
|
|
465
|
+
var INGEST_EVENT_PAYLOAD_MAX_BYTES = 32 * 1024;
|
|
466
|
+
var VERSION = "1.0.0";
|
|
467
|
+
var GTM_SKILLS = [
|
|
468
|
+
// ── research ────────────────────────────────────────────────────────
|
|
469
|
+
{
|
|
470
|
+
slug: "lead-research",
|
|
471
|
+
title: "Lead research",
|
|
472
|
+
description: "Build a candidate lead list from seed criteria and public sources, recording a supporting source for every contact.",
|
|
473
|
+
stage: "research",
|
|
474
|
+
tags: ["gtm", "sales", "research"],
|
|
475
|
+
version: VERSION,
|
|
476
|
+
inputs: [
|
|
477
|
+
{ key: "icp", description: "Ideal-customer description: segment, size, geography, role titles." },
|
|
478
|
+
{ key: "seed_sources", description: "Seed lists, domains, or saved searches to start from." },
|
|
479
|
+
{ key: "max_leads", description: "Hard cap on how many candidates this run may collect." }
|
|
480
|
+
],
|
|
481
|
+
outputs: [
|
|
482
|
+
{ key: "contacts", description: "Candidate contacts with name, company, title, and a source reference." }
|
|
483
|
+
],
|
|
484
|
+
allowedTools: ["search", "content-extractor"],
|
|
485
|
+
body: [
|
|
486
|
+
"Build a candidate lead list from the supplied criteria and seed sources.",
|
|
487
|
+
"",
|
|
488
|
+
"## Rules",
|
|
489
|
+
"- NEVER invent a contact detail. An email address, phone number, or title is recorded only",
|
|
490
|
+
" when a source supports it; otherwise leave the field empty.",
|
|
491
|
+
"- Record `source` on every contact \u2014 the URL or seed list the entry came from. A contact",
|
|
492
|
+
" without a source is not a lead, it is a guess.",
|
|
493
|
+
"- Respect `max_leads` exactly. Stop collecting when the cap is reached and say how many",
|
|
494
|
+
" candidates were left unexamined.",
|
|
495
|
+
"- De-duplicate by email when present, otherwise by (company, name).",
|
|
496
|
+
"- Exclude anyone who has asked not to be contacted, and any source that forbids collection.",
|
|
497
|
+
"",
|
|
498
|
+
"## Output",
|
|
499
|
+
"Write `contacts`. Each entry carries `name`, and any of `email`, `company`, `title`,",
|
|
500
|
+
"`source`, `notes` that a source supports. Report the counts examined, kept, and dropped,",
|
|
501
|
+
"with the reason for each drop category."
|
|
502
|
+
].join("\n")
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
slug: "competitor-watch",
|
|
506
|
+
title: "Competitor watch",
|
|
507
|
+
description: "Monitor a watchlist of companies across public sources and emit dated, source-linked change signals.",
|
|
508
|
+
stage: "research",
|
|
509
|
+
tags: ["gtm", "marketing", "research"],
|
|
510
|
+
version: VERSION,
|
|
511
|
+
inputs: [
|
|
512
|
+
{ key: "watchlist", description: "Companies to monitor: name, website, public repositories." },
|
|
513
|
+
{ key: "focus_areas", description: "What to watch: pricing, features, positioning, hiring." },
|
|
514
|
+
{ key: "since", description: "Only report changes observed after this date." }
|
|
515
|
+
],
|
|
516
|
+
outputs: [{ key: "signals", description: "Dated observations with a source URL and the focus area they hit." }],
|
|
517
|
+
allowedTools: ["search", "content-extractor"],
|
|
518
|
+
body: [
|
|
519
|
+
"Monitor the watchlist across public sources and report what changed.",
|
|
520
|
+
"",
|
|
521
|
+
"## Rules",
|
|
522
|
+
"- Public sources only: published pages, public announcements, public repositories.",
|
|
523
|
+
"- Every signal carries a source URL and an observation date. No date or no URL means the",
|
|
524
|
+
" signal is not reportable.",
|
|
525
|
+
"- Separate FACT (what the source says) from ANALYSIS (what you infer). Never blend them in",
|
|
526
|
+
" the same sentence.",
|
|
527
|
+
"- Report only changes since `since`. If a focus area produced nothing, say so explicitly \u2014",
|
|
528
|
+
" an empty period is a finding, not a gap to fill.",
|
|
529
|
+
"- Do not speculate about unannounced plans, internal metrics, or private information.",
|
|
530
|
+
"",
|
|
531
|
+
"## Output",
|
|
532
|
+
"Write `signals`. Each entry carries `title`, `url`, `publishedDate`, the focus area, and a",
|
|
533
|
+
"one-line factual summary."
|
|
534
|
+
].join("\n")
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
slug: "news-signal-detection",
|
|
538
|
+
title: "News signal detection",
|
|
539
|
+
description: "Scan configured topics for newsworthy events and rank them by relevance to the campaign, discarding noise.",
|
|
540
|
+
stage: "research",
|
|
541
|
+
tags: ["gtm", "marketing", "research"],
|
|
542
|
+
version: VERSION,
|
|
543
|
+
inputs: [
|
|
544
|
+
{ key: "topics", description: "Topics, keywords, or accounts to scan." },
|
|
545
|
+
{ key: "lookback_days", description: "How far back the scan reaches." },
|
|
546
|
+
{ key: "relevance_threshold", description: "Minimum relevance (0-1) an item must clear to be kept." }
|
|
547
|
+
],
|
|
548
|
+
outputs: [{ key: "signals", description: "Relevance-ranked news items with source URL and publish date." }],
|
|
549
|
+
allowedTools: ["search", "content-extractor"],
|
|
550
|
+
body: [
|
|
551
|
+
"Scan the configured topics and keep only what is genuinely newsworthy for this campaign.",
|
|
552
|
+
"",
|
|
553
|
+
"## Rules",
|
|
554
|
+
"- Score each candidate 0-1 for relevance to the campaign brief and drop anything below",
|
|
555
|
+
" `relevance_threshold`. State the score and the one-line reason for every item you keep.",
|
|
556
|
+
"- Prefer primary sources over aggregators; when both exist, link the primary.",
|
|
557
|
+
"- Collapse duplicate coverage of the same event into one signal with the best source.",
|
|
558
|
+
"- Nothing older than `lookback_days`. Undated items are dropped, not guessed at.",
|
|
559
|
+
"- Never restate a headline as fact when the source hedges it \u2014 carry the hedge through.",
|
|
560
|
+
"",
|
|
561
|
+
"## Output",
|
|
562
|
+
"Write `signals` ordered by descending relevance, each with `title`, `url`, `publishedDate`,",
|
|
563
|
+
"the relevance score, and the reason it matters here."
|
|
564
|
+
].join("\n")
|
|
565
|
+
},
|
|
566
|
+
// ── qualify ─────────────────────────────────────────────────────────
|
|
567
|
+
{
|
|
568
|
+
slug: "lead-scoring",
|
|
569
|
+
title: "Lead scoring",
|
|
570
|
+
description: "Score contacts 0-100 against a declarative weight table and attach an explainable reason per point awarded.",
|
|
571
|
+
stage: "qualify",
|
|
572
|
+
tags: ["gtm", "sales", "scoring"],
|
|
573
|
+
version: VERSION,
|
|
574
|
+
inputs: [
|
|
575
|
+
{ key: "contacts", description: "Candidate contacts to score." },
|
|
576
|
+
{ key: "score_weights", description: "Weight table: signal name \u2192 points awarded." }
|
|
577
|
+
],
|
|
578
|
+
outputs: [
|
|
579
|
+
{ key: "scored_contacts", description: "Contacts with `score` (0-100) and `scoreReasons` per contact." }
|
|
580
|
+
],
|
|
581
|
+
allowedTools: [],
|
|
582
|
+
body: [
|
|
583
|
+
"Score every contact against the supplied weight table.",
|
|
584
|
+
"",
|
|
585
|
+
"## Rules",
|
|
586
|
+
"- The weight table is the whole model. Award points ONLY for signals it names; never invent",
|
|
587
|
+
" a bonus, and never award points for a signal the contact data does not actually support.",
|
|
588
|
+
"- Clamp the final score to 0-100. Record one `scoreReasons` line per point awarded, in the",
|
|
589
|
+
" form `<signal> +<points>`, so any score can be re-derived by hand.",
|
|
590
|
+
"- A missing field is worth zero points, never an assumed value.",
|
|
591
|
+
"- Scoring is deterministic: the same contact and the same weight table must always produce",
|
|
592
|
+
" the same score. Do not break ties with judgement calls \u2014 leave ties tied.",
|
|
593
|
+
"",
|
|
594
|
+
"## Output",
|
|
595
|
+
"Write `scored_contacts` sorted by descending score, then report the score distribution and",
|
|
596
|
+
"the three signals that moved the list most."
|
|
597
|
+
].join("\n")
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
slug: "risk-filter",
|
|
601
|
+
title: "Risk filter",
|
|
602
|
+
description: "Flag low-quality or unsafe contacts against a risk weight table and exclude those at or above the threshold.",
|
|
603
|
+
stage: "qualify",
|
|
604
|
+
tags: ["gtm", "sales", "safety"],
|
|
605
|
+
version: VERSION,
|
|
606
|
+
inputs: [
|
|
607
|
+
{ key: "scored_contacts", description: "Contacts already carrying a priority score." },
|
|
608
|
+
{ key: "risk_weights", description: "Risk weight table: risk signal \u2192 points." },
|
|
609
|
+
{ key: "risk_threshold", description: "Risk score at or above which a contact is excluded." }
|
|
610
|
+
],
|
|
611
|
+
outputs: [
|
|
612
|
+
{
|
|
613
|
+
key: "scored_contacts",
|
|
614
|
+
description: "The same contacts, now carrying `riskScore`, `riskReasons`, and an excluded flag."
|
|
615
|
+
}
|
|
616
|
+
],
|
|
617
|
+
allowedTools: [],
|
|
618
|
+
body: [
|
|
619
|
+
"Assess each scored contact for risk and mark \u2014 never silently delete \u2014 the excluded ones.",
|
|
620
|
+
"",
|
|
621
|
+
"## Rules",
|
|
622
|
+
"- Apply the risk weight table exactly as given; record one `riskReasons` line per point.",
|
|
623
|
+
"- Contacts at or above `risk_threshold` are EXCLUDED from outbound, but they stay in the",
|
|
624
|
+
" data set with their reasons attached so a human can overturn the call.",
|
|
625
|
+
'- Never drop a contact without a recorded reason. "Looked wrong" is not a reason.',
|
|
626
|
+
"- Risk is about deliverability and safety (unverifiable identity, throwaway domains,",
|
|
627
|
+
" do-not-contact signals) \u2014 it is not a second opinion on fit. Fit is `lead-scoring`.",
|
|
628
|
+
"",
|
|
629
|
+
"## Output",
|
|
630
|
+
"Write the enriched `scored_contacts` and report how many were excluded, grouped by the",
|
|
631
|
+
"dominant risk reason."
|
|
632
|
+
].join("\n")
|
|
633
|
+
},
|
|
634
|
+
// ── draft ───────────────────────────────────────────────────────────
|
|
635
|
+
{
|
|
636
|
+
slug: "outreach-personalization",
|
|
637
|
+
title: "Outreach personalization",
|
|
638
|
+
description: "Write one 80-120 word personalized outbound draft per qualified contact, grounded only in known facts.",
|
|
639
|
+
stage: "draft",
|
|
640
|
+
tags: ["gtm", "sales", "writing"],
|
|
641
|
+
version: VERSION,
|
|
642
|
+
inputs: [
|
|
643
|
+
{ key: "scored_contacts", description: "Qualified, non-excluded contacts to write for." },
|
|
644
|
+
{ key: "campaign_brief", description: "Offer, proof points, and the single ask." },
|
|
645
|
+
{ key: "tone", description: "Configured tone for the campaign." }
|
|
646
|
+
],
|
|
647
|
+
outputs: [{ key: "drafts", description: "One draft per contact with `ref`, `channel`, `subject`, `body`." }],
|
|
648
|
+
allowedTools: [],
|
|
649
|
+
body: [
|
|
650
|
+
"Write one outbound draft per qualified contact.",
|
|
651
|
+
"",
|
|
652
|
+
"## Rules",
|
|
653
|
+
"- HARD CONSTRAINT: drafts only. Nothing is sent, scheduled, or queued for delivery here.",
|
|
654
|
+
'- 80-120 words in the body. One ask. No multi-part asks, no "quick question" openers that',
|
|
655
|
+
" the rest of the message contradicts.",
|
|
656
|
+
"- Personalize ONLY from fields and signals actually present on the contact. If there is no",
|
|
657
|
+
" personalization material, write the generic version and say so \u2014 a fabricated detail is",
|
|
658
|
+
" worse than a generic opener.",
|
|
659
|
+
"- Write a subject line only for channels that carry one.",
|
|
660
|
+
"- No invented metrics, customer names, or mutual connections. Ever.",
|
|
661
|
+
"",
|
|
662
|
+
"## Output",
|
|
663
|
+
"Write `drafts`, ordered by descending contact score so the review gate sees the best",
|
|
664
|
+
"candidates first. Each draft carries a stable `ref` used later by review and the action log."
|
|
665
|
+
].join("\n")
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
slug: "newsletter-drafting",
|
|
669
|
+
title: "Newsletter drafting",
|
|
670
|
+
description: "Turn briefs, notes, and collected signals into a scannable newsletter issue with one clear call to action.",
|
|
671
|
+
stage: "draft",
|
|
672
|
+
tags: ["gtm", "marketing", "content", "writing"],
|
|
673
|
+
version: VERSION,
|
|
674
|
+
inputs: [
|
|
675
|
+
{ key: "campaign_brief", description: "Issue theme, audience, and the call to action." },
|
|
676
|
+
{ key: "signals", description: "Collected source material for the issue." },
|
|
677
|
+
{ key: "tone", description: "Configured editorial tone." }
|
|
678
|
+
],
|
|
679
|
+
outputs: [{ key: "drafts", description: "A single newsletter draft: subject line plus sectioned body." }],
|
|
680
|
+
allowedTools: ["content-extractor"],
|
|
681
|
+
body: [
|
|
682
|
+
"Draft one newsletter issue from the brief and the collected material.",
|
|
683
|
+
"",
|
|
684
|
+
"## Rules",
|
|
685
|
+
"- Draft-first: the issue is produced for human review. Never send or schedule it.",
|
|
686
|
+
"- Structure: subject line, one-sentence hook, 3-5 short sections, one call to action.",
|
|
687
|
+
" Scannable beats comprehensive.",
|
|
688
|
+
"- Every claim traces to the brief, the knowledge base, or a supplied signal. Link the",
|
|
689
|
+
" source when the section rests on one.",
|
|
690
|
+
"- Never invent metrics, quotes, or customer names.",
|
|
691
|
+
"- When the source material is thin, say so and list what extra input would raise quality",
|
|
692
|
+
" rather than padding the issue.",
|
|
693
|
+
"",
|
|
694
|
+
"## Output",
|
|
695
|
+
"Write `drafts` with a single entry: `subject` plus the sectioned `body`."
|
|
696
|
+
].join("\n")
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
slug: "social-scheduling",
|
|
700
|
+
title: "Social scheduling",
|
|
701
|
+
description: "Plan a channel-fit social calendar and stage each post for review, respecting cadence and angle variety.",
|
|
702
|
+
stage: "draft",
|
|
703
|
+
tags: ["gtm", "marketing", "social", "writing"],
|
|
704
|
+
version: VERSION,
|
|
705
|
+
inputs: [
|
|
706
|
+
{ key: "campaign_brief", description: "Campaign themes, channels, and cadence." },
|
|
707
|
+
{ key: "signals", description: "Source material and trends to build posts from." },
|
|
708
|
+
{ key: "cadence", description: "Posts per channel per period." }
|
|
709
|
+
],
|
|
710
|
+
outputs: [
|
|
711
|
+
{ key: "drafts", description: "Channel-fit post drafts, each with its planned slot in the calendar." }
|
|
712
|
+
],
|
|
713
|
+
allowedTools: [],
|
|
714
|
+
body: [
|
|
715
|
+
"Plan the social calendar for the cadence window and draft each post.",
|
|
716
|
+
"",
|
|
717
|
+
"## Rules",
|
|
718
|
+
"- Draft-first: posts are STAGED for review. Never publish, never schedule to a live channel.",
|
|
719
|
+
"- Fit each draft to its channel: length, hashtag conventions, and link placement differ and",
|
|
720
|
+
" a cross-posted body is a tell.",
|
|
721
|
+
"- Do not repeat the same angle inside one cadence window. Track the angle per slot and vary",
|
|
722
|
+
" it deliberately.",
|
|
723
|
+
"- Build only from the brief, the content Works, and supplied signals. Never invent product",
|
|
724
|
+
" claims or engagement numbers.",
|
|
725
|
+
"- Respect `cadence` exactly \u2014 proposing more slots than the operator configured is not",
|
|
726
|
+
" ambition, it is a spam risk.",
|
|
727
|
+
"",
|
|
728
|
+
"## Output",
|
|
729
|
+
"Write `drafts` with the planned slot (channel + relative day) on each entry, plus a compact",
|
|
730
|
+
"calendar view of the window."
|
|
731
|
+
].join("\n")
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
slug: "digest-compilation",
|
|
735
|
+
title: "Digest compilation",
|
|
736
|
+
description: "Compile collected signals into a recurring digest that separates facts from analysis and highlights change.",
|
|
737
|
+
stage: "draft",
|
|
738
|
+
tags: ["gtm", "marketing", "reporting", "writing"],
|
|
739
|
+
version: VERSION,
|
|
740
|
+
inputs: [
|
|
741
|
+
{ key: "signals", description: "Signals collected this period." },
|
|
742
|
+
{ key: "previous_digest", description: "The prior issue, used to compute what changed." },
|
|
743
|
+
{ key: "focus_areas", description: "Sections the digest must cover." }
|
|
744
|
+
],
|
|
745
|
+
outputs: [{ key: "drafts", description: "A digest draft: sections, change highlights, and a trend view." }],
|
|
746
|
+
allowedTools: [],
|
|
747
|
+
body: [
|
|
748
|
+
"Compile the period digest from the collected signals.",
|
|
749
|
+
"",
|
|
750
|
+
"## Rules",
|
|
751
|
+
"- One section per focus area, in the configured order, every issue. A stable shape is what",
|
|
752
|
+
" makes a recurring digest readable.",
|
|
753
|
+
"- Every section splits FACTS (sourced, dated, linked) from ANALYSIS (your interpretation),",
|
|
754
|
+
" under those labels.",
|
|
755
|
+
"- Lead with what CHANGED since `previous_digest`. Unchanged areas get one line, not a",
|
|
756
|
+
" restatement of last issue.",
|
|
757
|
+
"- No fabrication and no padding: if a focus area produced no signal this period, write",
|
|
758
|
+
" exactly that.",
|
|
759
|
+
"- Close with a short trend view over the recent periods \u2014 direction, not prophecy.",
|
|
760
|
+
"",
|
|
761
|
+
"## Output",
|
|
762
|
+
"Write `drafts` with a single digest entry for review."
|
|
763
|
+
].join("\n")
|
|
764
|
+
},
|
|
765
|
+
// ── act ─────────────────────────────────────────────────────────────
|
|
766
|
+
{
|
|
767
|
+
slug: "crm-sync-hygiene",
|
|
768
|
+
title: "CRM sync hygiene",
|
|
769
|
+
description: "Prepare reviewed records for a customer-record sync: normalized fields, no duplicates, no destructive overwrites.",
|
|
770
|
+
stage: "act",
|
|
771
|
+
tags: ["gtm", "sales", "operations"],
|
|
772
|
+
version: VERSION,
|
|
773
|
+
inputs: [
|
|
774
|
+
{ key: "approved_drafts", description: "Records cleared by the review gate." },
|
|
775
|
+
{ key: "field_map", description: "Local field \u2192 destination field mapping." }
|
|
776
|
+
],
|
|
777
|
+
outputs: [
|
|
778
|
+
{ key: "action_log", description: "Prepared record writes with the field diff and the reason for each." }
|
|
779
|
+
],
|
|
780
|
+
allowedTools: ["connector"],
|
|
781
|
+
body: [
|
|
782
|
+
"Prepare reviewed records for the destination system without losing data.",
|
|
783
|
+
"",
|
|
784
|
+
"## Rules",
|
|
785
|
+
"- Only records that passed the review gate are eligible. Nothing else, no exceptions.",
|
|
786
|
+
"- NEVER overwrite a populated destination field with an empty local value. Blank does not",
|
|
787
|
+
' mean "clear it", it means "we do not know".',
|
|
788
|
+
"- Match before you create: search by the destination key (email, then domain + name) and",
|
|
789
|
+
" update the existing record rather than minting a duplicate.",
|
|
790
|
+
"- Normalize on the way out \u2014 casing, phone format, domain form \u2014 per `field_map`.",
|
|
791
|
+
"- Record every intended write in `action_log` with its field-level diff BEFORE it is",
|
|
792
|
+
" applied, so the change is auditable and reversible.",
|
|
793
|
+
"",
|
|
794
|
+
"## Output",
|
|
795
|
+
"Write `action_log` with one entry per record, status `prepared` or `skipped`, and the",
|
|
796
|
+
"reason for every skip."
|
|
797
|
+
].join("\n")
|
|
798
|
+
},
|
|
799
|
+
// ── follow-up ───────────────────────────────────────────────────────
|
|
800
|
+
{
|
|
801
|
+
slug: "follow-up-cadence",
|
|
802
|
+
title: "Follow-up cadence",
|
|
803
|
+
description: "Schedule timed re-engagement for quiet threads, with strict caps and an unconditional stop on any reply.",
|
|
804
|
+
stage: "follow-up",
|
|
805
|
+
tags: ["gtm", "sales", "cadence"],
|
|
806
|
+
version: VERSION,
|
|
807
|
+
inputs: [
|
|
808
|
+
{ key: "action_log", description: "What was prepared, for whom, and when." },
|
|
809
|
+
{ key: "reply_state", description: "Which threads have received a reply." },
|
|
810
|
+
{ key: "cadence", description: "Follow-up spacing and the maximum number of touches." }
|
|
811
|
+
],
|
|
812
|
+
outputs: [
|
|
813
|
+
{ key: "follow_up_queue", description: "Queued follow-ups with a due offset and the rationale for each." }
|
|
814
|
+
],
|
|
815
|
+
allowedTools: [],
|
|
816
|
+
body: [
|
|
817
|
+
"Queue follow-ups for threads that have gone quiet.",
|
|
818
|
+
"",
|
|
819
|
+
"## Rules",
|
|
820
|
+
"- HARD STOP on any reply, bounce, or opt-out: that thread leaves the cadence immediately and",
|
|
821
|
+
" permanently. This overrides every other rule here.",
|
|
822
|
+
"- Never exceed the configured maximum touches per contact, and never shorten the configured",
|
|
823
|
+
" spacing to fit more in.",
|
|
824
|
+
"- Each follow-up must add something \u2014 a new angle, a new proof point, a genuine deadline.",
|
|
825
|
+
' "Bumping this to the top of your inbox" is not a follow-up.',
|
|
826
|
+
"- Follow-ups are drafts like any other outbound: they re-enter the review gate before",
|
|
827
|
+
" anything is prepared for delivery.",
|
|
828
|
+
"",
|
|
829
|
+
"## Output",
|
|
830
|
+
"Write `follow_up_queue` with `draftRef`, `channel`, `dueAfterDays`, and the rationale."
|
|
831
|
+
].join("\n")
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
slug: "reply-detection",
|
|
835
|
+
title: "Reply detection",
|
|
836
|
+
description: "Classify inbound replies into interested / not-now / decline / opt-out and route each to the right next step.",
|
|
837
|
+
stage: "follow-up",
|
|
838
|
+
tags: ["gtm", "sales", "classification"],
|
|
839
|
+
version: VERSION,
|
|
840
|
+
inputs: [
|
|
841
|
+
{ key: "inbound_messages", description: "Replies received against prepared outbound." },
|
|
842
|
+
{ key: "action_log", description: "The outbound each reply responds to." }
|
|
843
|
+
],
|
|
844
|
+
outputs: [
|
|
845
|
+
{ key: "reply_state", description: "Per-thread classification, confidence, and the routing decision." }
|
|
846
|
+
],
|
|
847
|
+
allowedTools: ["connector"],
|
|
848
|
+
body: [
|
|
849
|
+
"Classify each inbound reply and decide what happens to its thread.",
|
|
850
|
+
"",
|
|
851
|
+
"## Classes",
|
|
852
|
+
"`interested` \xB7 `not_now` \xB7 `decline` \xB7 `opt_out` \xB7 `auto_reply` \xB7 `unclear`",
|
|
853
|
+
"",
|
|
854
|
+
"## Rules",
|
|
855
|
+
"- `opt_out` and `decline` stop the cadence permanently. When in doubt between `opt_out` and",
|
|
856
|
+
" anything else, choose `opt_out` \u2014 the cost of a wrong stop is far below the cost of a",
|
|
857
|
+
" wrong send.",
|
|
858
|
+
"- `auto_reply` (out-of-office, autoresponder) does NOT count as a reply for cadence",
|
|
859
|
+
" purposes, but it does update the contact record if it names a new owner.",
|
|
860
|
+
"- Use `unclear` rather than guessing; unclear threads route to a human, they do not route",
|
|
861
|
+
" to the next touch.",
|
|
862
|
+
"- Record a confidence value and the phrase that drove the classification.",
|
|
863
|
+
"",
|
|
864
|
+
"## Output",
|
|
865
|
+
"Write `reply_state` with the class, confidence, evidence phrase, and routing decision per",
|
|
866
|
+
"thread."
|
|
867
|
+
].join("\n")
|
|
868
|
+
},
|
|
869
|
+
// ── enrich ──────────────────────────────────────────────────────────
|
|
870
|
+
{
|
|
871
|
+
slug: "contact-enrichment",
|
|
872
|
+
title: "Contact enrichment",
|
|
873
|
+
description: "Backfill missing contact and account fields from verifiable sources, recording the evidence for each fill.",
|
|
874
|
+
stage: "enrich",
|
|
875
|
+
tags: ["gtm", "sales", "data"],
|
|
876
|
+
version: VERSION,
|
|
877
|
+
inputs: [
|
|
878
|
+
{ key: "contacts", description: "Contacts with gaps to fill." },
|
|
879
|
+
{ key: "enrichment_fields", description: "Which fields this run is allowed to fill." }
|
|
880
|
+
],
|
|
881
|
+
outputs: [
|
|
882
|
+
{ key: "enriched_contacts", description: "Contacts with filled fields and a source note per filled field." }
|
|
883
|
+
],
|
|
884
|
+
allowedTools: ["search", "content-extractor", "connector"],
|
|
885
|
+
body: [
|
|
886
|
+
"Fill the requested gaps on each contact \u2014 and only those gaps.",
|
|
887
|
+
"",
|
|
888
|
+
"## Rules",
|
|
889
|
+
"- Evidence-bound: a field is filled ONLY when a source supports it, and the source goes in",
|
|
890
|
+
" the contact notes alongside the value. No source, no fill.",
|
|
891
|
+
"- NEVER guess or pattern-generate an email address. Constructing `first.last@domain` because",
|
|
892
|
+
" it usually works is fabrication, and it is the single most damaging thing this Skill can do.",
|
|
893
|
+
"- Never overwrite an existing non-empty value. Enrichment fills gaps; corrections are a",
|
|
894
|
+
" human decision.",
|
|
895
|
+
"- Only touch fields named in `enrichment_fields`.",
|
|
896
|
+
"- When two sources disagree, leave the field empty and flag the conflict.",
|
|
897
|
+
"",
|
|
898
|
+
"## Output",
|
|
899
|
+
"Write `enriched_contacts` and report fill rate per field plus every conflict flagged."
|
|
900
|
+
].join("\n")
|
|
901
|
+
},
|
|
902
|
+
// ── measure ─────────────────────────────────────────────────────────
|
|
903
|
+
{
|
|
904
|
+
slug: "seo-audit",
|
|
905
|
+
title: "Search-visibility audit",
|
|
906
|
+
description: "Audit a site or content Work for search visibility and return a prioritized, evidence-cited fix list.",
|
|
907
|
+
stage: "measure",
|
|
908
|
+
tags: ["gtm", "marketing", "seo", "audit"],
|
|
909
|
+
version: VERSION,
|
|
910
|
+
inputs: [
|
|
911
|
+
{ key: "pages", description: "Pages or posts in scope, with their current metadata and headings." },
|
|
912
|
+
{ key: "target_keywords", description: "Keywords or topics the Work is meant to rank for." },
|
|
913
|
+
{ key: "previous_audit", description: "The prior audit, used to report movement and regressions." }
|
|
914
|
+
],
|
|
915
|
+
outputs: [
|
|
916
|
+
{ key: "campaign_report", description: "Prioritized findings with page reference, impact, and fix." }
|
|
917
|
+
],
|
|
918
|
+
allowedTools: ["content-extractor", "search"],
|
|
919
|
+
body: [
|
|
920
|
+
"Audit the in-scope pages for search visibility and propose fixes.",
|
|
921
|
+
"",
|
|
922
|
+
"## What to check",
|
|
923
|
+
"Title and description metadata \xB7 heading structure and hierarchy \xB7 internal linking and",
|
|
924
|
+
"orphan pages \xB7 keyword coverage against `target_keywords` and the gaps between them \xB7",
|
|
925
|
+
"duplicate or thin content \xB7 canonical and indexability signals.",
|
|
926
|
+
"",
|
|
927
|
+
"## Rules",
|
|
928
|
+
"- Every finding cites the specific page or setting it applies to and states the expected",
|
|
929
|
+
" impact. A finding without a page reference is an opinion.",
|
|
930
|
+
"- Return a prioritized top 5-10, quick wins first \u2014 not an exhaustive dump.",
|
|
931
|
+
"- Propose changes as reviewable edits or tasks. Never modify published pages directly.",
|
|
932
|
+
"- When `previous_audit` is present, report movement honestly, INCLUDING regressions. A",
|
|
933
|
+
" clean report that hides a regression is worse than no report.",
|
|
934
|
+
"- No traffic or ranking projections. Estimate effort and impact qualitatively instead.",
|
|
935
|
+
"",
|
|
936
|
+
"## Output",
|
|
937
|
+
"Write `campaign_report` with the prioritized findings and the movement summary."
|
|
938
|
+
].join("\n")
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
slug: "campaign-reporting",
|
|
942
|
+
title: "Campaign reporting",
|
|
943
|
+
description: "Summarize a campaign period into counted totals, honest insights, and concrete hints for the next cycle.",
|
|
944
|
+
stage: "measure",
|
|
945
|
+
tags: ["gtm", "reporting"],
|
|
946
|
+
version: VERSION,
|
|
947
|
+
inputs: [
|
|
948
|
+
{ key: "action_log", description: "What the campaign prepared and skipped." },
|
|
949
|
+
{ key: "reply_state", description: "How recipients responded." },
|
|
950
|
+
{ key: "scored_contacts", description: "The qualified population the period worked from." }
|
|
951
|
+
],
|
|
952
|
+
outputs: [{ key: "campaign_report", description: "Totals, insights, and next-cycle variant hints." }],
|
|
953
|
+
allowedTools: [],
|
|
954
|
+
body: [
|
|
955
|
+
"Report the campaign period and close the loop into the next one.",
|
|
956
|
+
"",
|
|
957
|
+
"## Rules",
|
|
958
|
+
"- Totals are COUNTED from the supplied data, never estimated: contacts, qualified, excluded,",
|
|
959
|
+
" drafts, approved, prepared, follow-ups queued.",
|
|
960
|
+
"- State the denominator on every rate. A percentage without its base is not a metric.",
|
|
961
|
+
"- Below roughly 30 observations, report counts and refuse to draw a conclusion \u2014 small",
|
|
962
|
+
" samples produce confident nonsense.",
|
|
963
|
+
"- Insights must be falsifiable and tied to a number in the totals.",
|
|
964
|
+
"- End with `nextVariantHints`: concrete, testable changes for the next draft cycle, one per",
|
|
965
|
+
" line. This is the loop closing \u2014 a report that ends at description wasted the period.",
|
|
966
|
+
"",
|
|
967
|
+
"## Output",
|
|
968
|
+
"Write `campaign_report` with `summary`, `totals`, `insights`, and `nextVariantHints`."
|
|
969
|
+
].join("\n")
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
slug: "engagement-analysis",
|
|
973
|
+
title: "Engagement analysis",
|
|
974
|
+
description: "Compare engagement across variants, channels, and segments, and say which differences are real.",
|
|
975
|
+
stage: "measure",
|
|
976
|
+
tags: ["gtm", "marketing", "reporting"],
|
|
977
|
+
version: VERSION,
|
|
978
|
+
inputs: [
|
|
979
|
+
{ key: "engagement_events", description: "Per-item engagement observations for the period." },
|
|
980
|
+
{ key: "drafts", description: "The variants the events belong to." },
|
|
981
|
+
{ key: "comparison_window", description: "Prior period to compare against." }
|
|
982
|
+
],
|
|
983
|
+
outputs: [
|
|
984
|
+
{ key: "campaign_report", description: "Per-variant and per-segment engagement with a confidence call." }
|
|
985
|
+
],
|
|
986
|
+
allowedTools: ["metrics"],
|
|
987
|
+
body: [
|
|
988
|
+
"Analyze engagement and report what actually differs.",
|
|
989
|
+
"",
|
|
990
|
+
"## Rules",
|
|
991
|
+
"- Compare like with like: same channel, same audience segment, comparable send window.",
|
|
992
|
+
" Cross-channel comparisons need an explicit caveat.",
|
|
993
|
+
"- Report absolute counts alongside every rate.",
|
|
994
|
+
"- Say plainly when a difference is within noise. Declaring a winner on a handful of",
|
|
995
|
+
" observations is the most common way this analysis misleads a campaign for months.",
|
|
996
|
+
"- Separate what changed (observation) from why (hypothesis), and label hypotheses as such.",
|
|
997
|
+
"- Surface the losing variants too \u2014 a variant that consistently underperforms is the",
|
|
998
|
+
" cheapest thing to stop doing.",
|
|
999
|
+
"",
|
|
1000
|
+
"## Output",
|
|
1001
|
+
"Write `campaign_report` with the per-variant table, the segment breakdown, and an explicit",
|
|
1002
|
+
"confidence statement per comparison."
|
|
1003
|
+
].join("\n")
|
|
1004
|
+
}
|
|
1005
|
+
];
|
|
1006
|
+
var GTM_SKILL_SLUGS = GTM_SKILLS.map((skill) => skill.slug);
|
|
1007
|
+
var GTM_SKILL_BY_SLUG = new Map(
|
|
1008
|
+
GTM_SKILLS.map((skill) => [skill.slug, skill])
|
|
1009
|
+
);
|
|
1010
|
+
var SKILL_READINESS_STATES = [
|
|
1011
|
+
"ready",
|
|
1012
|
+
"needs_setup",
|
|
1013
|
+
"missing_requirements",
|
|
1014
|
+
"blocked_by_access",
|
|
1015
|
+
"unknown",
|
|
1016
|
+
"check_failed"
|
|
1017
|
+
];
|
|
1018
|
+
var SKILL_CARD_STATES = [...SKILL_READINESS_STATES, "disabled", "needs_review"];
|
|
1019
|
+
var SKILL_READINESS_FILTERS = [...SKILL_CARD_STATES, "attention"];
|
|
1020
|
+
var FLEET_AGENT_CREDENTIAL_FAMILIES = [
|
|
1021
|
+
{
|
|
1022
|
+
cli: "claude-code",
|
|
1023
|
+
// CLAUDE_CODE_OAUTH_TOKEN (a Claude plan) before ANTHROPIC_API_KEY
|
|
1024
|
+
// (per-token Console billing) — deliberately the REVERSE of the CLI's
|
|
1025
|
+
// own precedence, so the cheaper credential wins rather than the one
|
|
1026
|
+
// that silently spends money.
|
|
1027
|
+
envNames: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
cli: "codex",
|
|
1031
|
+
// CODEX_ACCESS_TOKEN draws the ChatGPT workspace entitlement;
|
|
1032
|
+
// OPENAI_API_KEY is per-token platform billing.
|
|
1033
|
+
envNames: ["CODEX_ACCESS_TOKEN", "OPENAI_API_KEY"]
|
|
1034
|
+
}
|
|
1035
|
+
];
|
|
1036
|
+
var FLEET_AGENT_CREDENTIAL_ENV_NAMES = FLEET_AGENT_CREDENTIAL_FAMILIES.flatMap(
|
|
1037
|
+
(family) => family.envNames
|
|
1038
|
+
);
|
|
1039
|
+
var FLEET_RUN_ENV_FILE_MAX_CONTENT_BYTES = 32 * 1024;
|
|
1040
|
+
var FLEET_RUN_ENV_FILES_MAX_TOTAL_BYTES = 256 * 1024;
|
|
1041
|
+
var FLEET_JOB_MAX_PAYLOAD_BYTES = 256 * 1024;
|
|
1042
|
+
var FLEET_JOB_MAX_RESULT_BYTES = 256 * 1024;
|
|
1043
|
+
var FLEET_JOB_DEFAULT_QUEUED_MAX_AGE_SEC = Object.freeze({
|
|
1044
|
+
"agent-task": 24 * 60 * 60,
|
|
1045
|
+
"acceptance-checks": 2 * 60 * 60,
|
|
1046
|
+
"browser-check": 2 * 60 * 60,
|
|
1047
|
+
// A live view nobody claimed is abandoned by the session service after
|
|
1048
|
+
// 40 seconds, which cancels its job there and then. The queue SLA is only
|
|
1049
|
+
// the backstop behind that, so it deliberately does not undercut the
|
|
1050
|
+
// shortest existing default (the fallback an unknown kind fails closed to).
|
|
1051
|
+
"computer-session": 2 * 60 * 60
|
|
1052
|
+
});
|
|
1053
|
+
var FLEET_JOB_MAX_QUEUED_MAX_AGE_SEC = 7 * 24 * 60 * 60;
|
|
1054
|
+
var FLEET_AGENT_EXECUTION_MAX_INSTRUCTIONS_BYTES = 160 * 1024;
|
|
1055
|
+
var FLEET_AGENT_TASK_TIMELINE_MAX_BYTES = 48 * 1024;
|
|
1056
|
+
var FLEET_AGENT_TASK_TRANSCRIPT_MAX_BYTES = 64 * 1024;
|
|
1057
|
+
var FLEET_AGENT_TASK_QUESTION_MAX_FILE_BYTES = 64 * 1024;
|
|
1058
|
+
var FLEET_AGENT_TASK_QUESTION_MAX_CONTEXT_BYTES = 6 * 1024;
|
|
1059
|
+
var FLEET_NODE_MAX_MEMORY_MB = 1024 * 1024;
|
|
1060
|
+
var FLEET_MAX_DAILY_COST_CEILING_CENTS = 1e5 * 100;
|
|
1061
|
+
var FLEET_MAX_DISK_FREE_BYTES = 2 ** 60;
|
|
1062
|
+
var FLEET_DEFAULT_ENROLLMENT_TOKEN_TTL_MS = 15 * 6e4;
|
|
1063
|
+
var FLEET_DEFAULT_NODE_OFFLINE_AFTER_MS = 5 * 6e4;
|
|
1064
|
+
var FLEET_DEFAULT_NODE_OFFLINE_NOTICE_AFTER_MS = 30 * 6e4;
|
|
1065
|
+
var FLEET_DEFAULT_CREDENTIAL_ROTATION_OVERLAP_MS = 15 * 6e4;
|
|
1066
|
+
var FLEET_MAX_CREDENTIAL_ROTATION_OVERLAP_MS = 24 * 60 * 6e4;
|
|
1067
|
+
var EMAIL_INBOX_DEFAULT_DAILY_CAP = 100;
|
|
1068
|
+
var EMAIL_INBOX_BURST_SENDS = 10;
|
|
1069
|
+
var EMAIL_INBOX_BURST_RECIPIENTS = 20;
|
|
1070
|
+
var EMAIL_WORKSPACE_DAILY_CAP = 500;
|
|
1071
|
+
var EMAIL_WORKSPACE_MONTHLY_CAP = 1e4;
|
|
1072
|
+
var EMAIL_MAX_RECIPIENTS_PER_MESSAGE = 50;
|
|
1073
|
+
var EMAIL_SEND_CAP_RECOMMENDED_DEFAULTS = Object.freeze({
|
|
1074
|
+
inboxDailySends: EMAIL_INBOX_DEFAULT_DAILY_CAP,
|
|
1075
|
+
inboxBurstSends: EMAIL_INBOX_BURST_SENDS,
|
|
1076
|
+
inboxBurstRecipients: EMAIL_INBOX_BURST_RECIPIENTS,
|
|
1077
|
+
recipientsPerMessage: EMAIL_MAX_RECIPIENTS_PER_MESSAGE,
|
|
1078
|
+
workspaceDailySends: EMAIL_WORKSPACE_DAILY_CAP,
|
|
1079
|
+
workspaceMonthlySends: EMAIL_WORKSPACE_MONTHLY_CAP
|
|
1080
|
+
});
|
|
1081
|
+
var EMAIL_DAY_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1082
|
+
var EMAIL_MONTH_WINDOW_MS = 30 * EMAIL_DAY_WINDOW_MS;
|
|
1083
|
+
var EMAIL_INBOX_CAP_FIELDS = [
|
|
1084
|
+
"inboxDailySends",
|
|
1085
|
+
"inboxBurstSends",
|
|
1086
|
+
"inboxBurstRecipients",
|
|
1087
|
+
"recipientsPerMessage"
|
|
1088
|
+
];
|
|
1089
|
+
var EMAIL_SEND_CAP_FIELDS = [
|
|
1090
|
+
...EMAIL_INBOX_CAP_FIELDS,
|
|
1091
|
+
"workspaceDailySends",
|
|
1092
|
+
"workspaceMonthlySends"
|
|
1093
|
+
];
|
|
1094
|
+
var INBOX_CAP_FIELD_SET = new Set(EMAIL_INBOX_CAP_FIELDS);
|
|
1095
|
+
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
1096
|
+
var PROMOTION_GATE_DECISION_GRACE_MS = 20 * 60 * 1e3;
|
|
1097
|
+
var RELEASE_VERIFY_ATTEMPT_INTERVAL_MS = 10 * 60 * 1e3;
|
|
1098
|
+
var RELEASE_VERIFY_BUDGET_MS = 8 * 60 * 60 * 1e3;
|
|
1099
|
+
var MAX_CONVERSATION_BODY_BYTES = 16 * 1024;
|
|
1100
|
+
var CONNECTION_CREDENTIAL_ERROR_CODES = Object.freeze([
|
|
1101
|
+
"credential_rejected",
|
|
1102
|
+
"credential_missing",
|
|
1103
|
+
"https_required"
|
|
1104
|
+
]);
|
|
1105
|
+
var CONNECTION_HEALTH_WARNING_CODES = Object.freeze([
|
|
1106
|
+
"insecure_transport"
|
|
1107
|
+
]);
|
|
1108
|
+
var BACKUP_TRIM_POLICIES = Object.freeze({
|
|
1109
|
+
activity: Object.freeze({ field: "createdAt", defaultDays: 365, fullHistoryDays: 1095 }),
|
|
1110
|
+
runLogs: Object.freeze({ field: "createdAt", defaultDays: 90, fullHistoryDays: 1095 }),
|
|
1111
|
+
terminalTranscripts: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
|
|
1112
|
+
notifications: Object.freeze({ field: "createdAt", defaultDays: 180, fullHistoryDays: 1095 }),
|
|
1113
|
+
// `occurredAt`, not `createdAt`: `PluginUsageEvent` names its
|
|
1114
|
+
// `@CreateDateColumn` `occurredAt` and has no `createdAt` at all. The trim
|
|
1115
|
+
// resolver fails OPEN on an unknown column, so the wrong name here did not
|
|
1116
|
+
// error — it silently exported every plugin usage event for the lifetime
|
|
1117
|
+
// of the workspace, from the archive's single largest history table, while
|
|
1118
|
+
// the docs page published a 180-day window.
|
|
1119
|
+
pluginUsageEvents: Object.freeze({ field: "occurredAt", defaultDays: 180, fullHistoryDays: 1095 }),
|
|
1120
|
+
deliveryLogs: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
|
|
1121
|
+
// `firedAt`, for the same reason: `InboundTriggerFire`'s creation
|
|
1122
|
+
// timestamp is `firedAt` and it has no `createdAt`, so the declared
|
|
1123
|
+
// 90-day window was never applied to `data/schedules/trigger-fires.jsonl`
|
|
1124
|
+
// and the `schedules` domain reported `complete` where the docs promise a
|
|
1125
|
+
// trim.
|
|
1126
|
+
triggerFires: Object.freeze({ field: "firedAt", defaultDays: 90, fullHistoryDays: 1095 }),
|
|
1127
|
+
retrievalTrail: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
|
|
1128
|
+
fleetJobs: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 })
|
|
1129
|
+
});
|
|
1130
|
+
var BACKUP_DOMAINS = Object.freeze([
|
|
1131
|
+
Object.freeze({
|
|
1132
|
+
key: "account",
|
|
1133
|
+
restorability: "restorable",
|
|
1134
|
+
collectorId: "account",
|
|
1135
|
+
dataDir: "account"
|
|
1136
|
+
}),
|
|
1137
|
+
Object.freeze({
|
|
1138
|
+
key: "organizations",
|
|
1139
|
+
restorability: "restorable",
|
|
1140
|
+
collectorId: "organizations",
|
|
1141
|
+
dataDir: "organizations"
|
|
1142
|
+
}),
|
|
1143
|
+
Object.freeze({
|
|
1144
|
+
key: "agents",
|
|
1145
|
+
restorability: "restorable",
|
|
1146
|
+
collectorId: "agents",
|
|
1147
|
+
dataDir: "agents"
|
|
1148
|
+
}),
|
|
1149
|
+
Object.freeze({
|
|
1150
|
+
key: "missions",
|
|
1151
|
+
restorability: "restorable",
|
|
1152
|
+
collectorId: "missions",
|
|
1153
|
+
dataDir: "missions"
|
|
1154
|
+
}),
|
|
1155
|
+
Object.freeze({
|
|
1156
|
+
key: "tasks",
|
|
1157
|
+
restorability: "restorable",
|
|
1158
|
+
collectorId: "tasks",
|
|
1159
|
+
dataDir: "tasks"
|
|
1160
|
+
}),
|
|
1161
|
+
Object.freeze({
|
|
1162
|
+
key: "works",
|
|
1163
|
+
restorability: "restorable",
|
|
1164
|
+
collectorId: "works",
|
|
1165
|
+
dataDir: "works"
|
|
1166
|
+
}),
|
|
1167
|
+
Object.freeze({
|
|
1168
|
+
key: "knowledge",
|
|
1169
|
+
restorability: "restorable",
|
|
1170
|
+
collectorId: "knowledge",
|
|
1171
|
+
dataDir: "knowledge"
|
|
1172
|
+
}),
|
|
1173
|
+
Object.freeze({
|
|
1174
|
+
key: "schedules",
|
|
1175
|
+
restorability: "restorable",
|
|
1176
|
+
collectorId: "schedules",
|
|
1177
|
+
dataDir: "schedules"
|
|
1178
|
+
}),
|
|
1179
|
+
Object.freeze({
|
|
1180
|
+
key: "runs",
|
|
1181
|
+
restorability: "record-only",
|
|
1182
|
+
collectorId: "runs",
|
|
1183
|
+
dataDir: "runs"
|
|
1184
|
+
}),
|
|
1185
|
+
Object.freeze({
|
|
1186
|
+
key: "decisions",
|
|
1187
|
+
restorability: "record-only",
|
|
1188
|
+
collectorId: "decisions",
|
|
1189
|
+
dataDir: "decisions"
|
|
1190
|
+
}),
|
|
1191
|
+
Object.freeze({
|
|
1192
|
+
key: "communication",
|
|
1193
|
+
restorability: "partly-restorable",
|
|
1194
|
+
collectorId: "communication",
|
|
1195
|
+
dataDir: "communication"
|
|
1196
|
+
}),
|
|
1197
|
+
Object.freeze({
|
|
1198
|
+
key: "connections",
|
|
1199
|
+
restorability: "restorable",
|
|
1200
|
+
collectorId: "connections",
|
|
1201
|
+
dataDir: "connections"
|
|
1202
|
+
}),
|
|
1203
|
+
Object.freeze({
|
|
1204
|
+
key: "fleet",
|
|
1205
|
+
restorability: "partly-restorable",
|
|
1206
|
+
collectorId: "fleet",
|
|
1207
|
+
dataDir: "fleet"
|
|
1208
|
+
}),
|
|
1209
|
+
Object.freeze({
|
|
1210
|
+
key: "billing",
|
|
1211
|
+
restorability: "record-only",
|
|
1212
|
+
collectorId: "billing",
|
|
1213
|
+
dataDir: "billing"
|
|
1214
|
+
}),
|
|
1215
|
+
Object.freeze({
|
|
1216
|
+
key: "activity",
|
|
1217
|
+
restorability: "record-only",
|
|
1218
|
+
collectorId: "activity",
|
|
1219
|
+
dataDir: "activity",
|
|
1220
|
+
trim: BACKUP_TRIM_POLICIES.activity
|
|
1221
|
+
})
|
|
1222
|
+
]);
|
|
1223
|
+
var BACKUP_DOMAIN_KEYS = Object.freeze(BACKUP_DOMAINS.map((domain) => domain.key));
|
|
1224
|
+
var BACKUP_DOMAIN_COUNT = BACKUP_DOMAINS.length;
|
|
1225
|
+
var BACKUP_EXCLUSIONS = Object.freeze([
|
|
1226
|
+
Object.freeze({
|
|
1227
|
+
code: "auth_tokens",
|
|
1228
|
+
summary: "Password hashes, password-reset tokens, magic-link tokens and email-verification tokens."
|
|
1229
|
+
}),
|
|
1230
|
+
Object.freeze({
|
|
1231
|
+
code: "sessions",
|
|
1232
|
+
summary: "Sessions, refresh tokens, and third-party auth provider access and refresh tokens."
|
|
1233
|
+
}),
|
|
1234
|
+
Object.freeze({
|
|
1235
|
+
code: "api_key_material",
|
|
1236
|
+
summary: "API key material. Keys appear as name, prefix, created date and active flag only."
|
|
1237
|
+
}),
|
|
1238
|
+
Object.freeze({
|
|
1239
|
+
code: "stored_credentials",
|
|
1240
|
+
summary: "Every stored credential, in plaintext or ciphertext \u2014 plugin secrets, connection headers, deployment secrets, webhook and trigger signing secrets, and encrypted runtime credentials. Only the names of the fields that were set are exported."
|
|
1241
|
+
}),
|
|
1242
|
+
Object.freeze({
|
|
1243
|
+
code: "node_enrolment_secrets",
|
|
1244
|
+
summary: "Node enrolment and heartbeat secrets."
|
|
1245
|
+
}),
|
|
1246
|
+
Object.freeze({
|
|
1247
|
+
code: "payment_identifiers",
|
|
1248
|
+
summary: "Payment-provider customer, subscription, payment-method and meter identifiers."
|
|
1249
|
+
}),
|
|
1250
|
+
Object.freeze({
|
|
1251
|
+
code: "platform_admin_flag",
|
|
1252
|
+
summary: "The platform-administrator flag."
|
|
1253
|
+
}),
|
|
1254
|
+
Object.freeze({
|
|
1255
|
+
code: "vector_embeddings",
|
|
1256
|
+
summary: "Vector embeddings and their coordinates, because they are derived and regenerate on demand."
|
|
1257
|
+
}),
|
|
1258
|
+
Object.freeze({
|
|
1259
|
+
code: "internal_caches",
|
|
1260
|
+
summary: "Internal caches and the delivery outbox."
|
|
1261
|
+
})
|
|
1262
|
+
]);
|
|
1263
|
+
var BACKUP_DEFAULT_LIMITS = Object.freeze({
|
|
1264
|
+
/** Ready archives kept for this long before their bytes are deleted. */
|
|
1265
|
+
retentionDays: 14,
|
|
1266
|
+
/** The record outlives its bytes by this long, so history stays legible. */
|
|
1267
|
+
recordRetentionDays: 90,
|
|
1268
|
+
/** Ready outcomes allowed per rolling 24 hours. Failures do not count. */
|
|
1269
|
+
dailyAllowance: 3,
|
|
1270
|
+
/** Rows the history list shows before "Show all". */
|
|
1271
|
+
historyPageSize: 20,
|
|
1272
|
+
/** Hard ceiling on a single history page. */
|
|
1273
|
+
historyPageLimit: 50,
|
|
1274
|
+
/** Bytes of one uploaded file that may be carried (200 MiB). */
|
|
1275
|
+
maxFileBytes: 200 * 1024 * 1024,
|
|
1276
|
+
/** Bytes of uploaded files in total (2 GiB). */
|
|
1277
|
+
maxAttachmentBytes: 2 * 1024 * 1024 * 1024,
|
|
1278
|
+
/** Bytes the finished archive may not exceed with a streaming backend (5 GiB). */
|
|
1279
|
+
maxArchiveBytes: 5 * 1024 * 1024 * 1024,
|
|
1280
|
+
/**
|
|
1281
|
+
* The ceiling when the active storage backend cannot accept a stream and
|
|
1282
|
+
* the archive has to be handed over as one buffer (512 MiB). A backend
|
|
1283
|
+
* without streaming is a smaller limit, never a wrong result.
|
|
1284
|
+
*/
|
|
1285
|
+
maxBufferedArchiveBytes: 512 * 1024 * 1024,
|
|
1286
|
+
/** A manifest larger than this is refused by the check without being read (8 MiB). */
|
|
1287
|
+
maxManifestBytes: 8 * 1024 * 1024,
|
|
1288
|
+
/** Minutes a download link stays valid before the client re-mints it. */
|
|
1289
|
+
downloadLinkTtlMinutes: 15,
|
|
1290
|
+
/** Minutes without a heartbeat before a running backup is called stalled. */
|
|
1291
|
+
stallMinutes: 10,
|
|
1292
|
+
/** Minutes a queued backup may wait before it is called stalled. */
|
|
1293
|
+
queuedStallMinutes: 15,
|
|
1294
|
+
/** Minutes a backup may run before it is stopped. */
|
|
1295
|
+
timeoutMinutes: 60,
|
|
1296
|
+
/** Retries a single domain gets before it is marked failed and the archive finishes without it. */
|
|
1297
|
+
domainRetries: 2,
|
|
1298
|
+
/** Retries a storage write gets before the backup fails as `storage_unavailable`. */
|
|
1299
|
+
storageRetries: 3
|
|
1300
|
+
});
|
|
1301
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
1302
|
+
var MINUTE_MS = 60 * 1e3;
|
|
1303
|
+
var ACTION_CATEGORIES = Object.freeze([
|
|
1304
|
+
"read.internal",
|
|
1305
|
+
"read.external",
|
|
1306
|
+
"write.internal",
|
|
1307
|
+
"write.destructive",
|
|
1308
|
+
"message.internal",
|
|
1309
|
+
"message.external",
|
|
1310
|
+
"publish.external",
|
|
1311
|
+
"spend.metered",
|
|
1312
|
+
"spend.commitment",
|
|
1313
|
+
"access.grant",
|
|
1314
|
+
"machine.run",
|
|
1315
|
+
"machine.admin",
|
|
1316
|
+
"agent.fanout"
|
|
1317
|
+
]);
|
|
1318
|
+
var LADDERED_CATEGORIES = Object.freeze(
|
|
1319
|
+
ACTION_CATEGORIES.filter((c) => c !== "read.internal")
|
|
1320
|
+
);
|
|
1321
|
+
var DRAFTABLE_CATEGORIES = Object.freeze([
|
|
1322
|
+
"write.internal",
|
|
1323
|
+
"write.destructive",
|
|
1324
|
+
"message.external",
|
|
1325
|
+
"publish.external",
|
|
1326
|
+
"machine.run",
|
|
1327
|
+
"machine.admin"
|
|
1328
|
+
]);
|
|
1329
|
+
var ACTION_CATEGORY_CEILING = Object.freeze({
|
|
1330
|
+
"read.external": "auto",
|
|
1331
|
+
"write.internal": "auto",
|
|
1332
|
+
"write.destructive": "ask",
|
|
1333
|
+
"message.internal": "auto",
|
|
1334
|
+
"message.external": "ask",
|
|
1335
|
+
"publish.external": "ask",
|
|
1336
|
+
"spend.metered": "auto",
|
|
1337
|
+
"spend.commitment": "off",
|
|
1338
|
+
"access.grant": "ask",
|
|
1339
|
+
"machine.run": "auto",
|
|
1340
|
+
"machine.admin": "ask",
|
|
1341
|
+
"agent.fanout": "auto"
|
|
1342
|
+
});
|
|
1343
|
+
var ACTION_CATEGORY_DEFAULT = Object.freeze({
|
|
1344
|
+
"read.external": "auto",
|
|
1345
|
+
"write.internal": "auto",
|
|
1346
|
+
"write.destructive": "ask",
|
|
1347
|
+
"message.internal": "auto",
|
|
1348
|
+
"message.external": "draft",
|
|
1349
|
+
"publish.external": "ask",
|
|
1350
|
+
"spend.metered": "auto",
|
|
1351
|
+
"spend.commitment": "off",
|
|
1352
|
+
"access.grant": "ask",
|
|
1353
|
+
"machine.run": "ask",
|
|
1354
|
+
"machine.admin": "ask",
|
|
1355
|
+
"agent.fanout": "ask"
|
|
1356
|
+
});
|
|
1357
|
+
var ACTION_CATEGORY_RESTRICTIVENESS = Object.freeze([
|
|
1358
|
+
"spend.commitment",
|
|
1359
|
+
"access.grant",
|
|
1360
|
+
"machine.admin",
|
|
1361
|
+
"publish.external",
|
|
1362
|
+
"message.external",
|
|
1363
|
+
"write.destructive",
|
|
1364
|
+
"machine.run",
|
|
1365
|
+
"agent.fanout",
|
|
1366
|
+
"spend.metered",
|
|
1367
|
+
"write.internal",
|
|
1368
|
+
"message.internal",
|
|
1369
|
+
"read.external",
|
|
1370
|
+
"read.internal"
|
|
1371
|
+
]);
|
|
1372
|
+
var ACTION_CATEGORY_I18N_KEY = Object.freeze({
|
|
1373
|
+
"read.internal": "readInternal",
|
|
1374
|
+
"read.external": "readExternal",
|
|
1375
|
+
"write.internal": "writeInternal",
|
|
1376
|
+
"write.destructive": "writeDestructive",
|
|
1377
|
+
"message.internal": "messageInternal",
|
|
1378
|
+
"message.external": "messageExternal",
|
|
1379
|
+
"publish.external": "publishExternal",
|
|
1380
|
+
"spend.metered": "spendMetered",
|
|
1381
|
+
"spend.commitment": "spendCommitment",
|
|
1382
|
+
"access.grant": "accessGrant",
|
|
1383
|
+
"machine.run": "machineRun",
|
|
1384
|
+
"machine.admin": "machineAdmin",
|
|
1385
|
+
"agent.fanout": "agentFanout"
|
|
1386
|
+
});
|
|
1387
|
+
var TRUST_RUNG_ORDER = Object.freeze([
|
|
1388
|
+
"off",
|
|
1389
|
+
"draft",
|
|
1390
|
+
"ask",
|
|
1391
|
+
"auto"
|
|
1392
|
+
]);
|
|
1393
|
+
var SAFETY_RAIL_ORDER = Object.freeze([
|
|
1394
|
+
"platform-stop",
|
|
1395
|
+
"workspace-pause",
|
|
1396
|
+
"scope-pause",
|
|
1397
|
+
"grants",
|
|
1398
|
+
"ladder",
|
|
1399
|
+
"caps",
|
|
1400
|
+
"rules"
|
|
1401
|
+
]);
|
|
1402
|
+
var SAFETY_REASON_CODES = Object.freeze([
|
|
1403
|
+
"platform-stopped",
|
|
1404
|
+
"workspace-paused",
|
|
1405
|
+
"scope-paused",
|
|
1406
|
+
"rung-off",
|
|
1407
|
+
"rung-held",
|
|
1408
|
+
"ceiling-refused",
|
|
1409
|
+
"cap-reached",
|
|
1410
|
+
"grant-denied",
|
|
1411
|
+
"rule-blocked",
|
|
1412
|
+
"policy-refused",
|
|
1413
|
+
"unclassified-action",
|
|
1414
|
+
"instruction-widening-attempt",
|
|
1415
|
+
"non-human-actor",
|
|
1416
|
+
"safe-mode"
|
|
1417
|
+
]);
|
|
1418
|
+
var SAFETY_ALLOW = Object.freeze({
|
|
1419
|
+
decision: "allow",
|
|
1420
|
+
railId: null,
|
|
1421
|
+
category: null,
|
|
1422
|
+
rung: null,
|
|
1423
|
+
reasonCode: null,
|
|
1424
|
+
summary: null
|
|
1425
|
+
});
|
|
1426
|
+
var AUTONOMY_GRANT_SCOPE_PRECEDENCE = Object.freeze([
|
|
1427
|
+
"workspace",
|
|
1428
|
+
"agent"
|
|
1429
|
+
]);
|
|
1430
|
+
var RAIL_REFUSAL_SUBJECT_TYPES = Object.freeze([
|
|
1431
|
+
"run",
|
|
1432
|
+
"agent",
|
|
1433
|
+
"mission",
|
|
1434
|
+
"task",
|
|
1435
|
+
"schedule",
|
|
1436
|
+
"trigger"
|
|
1437
|
+
]);
|
|
1438
|
+
var WORKSPACE_RUNNING = Object.freeze({
|
|
1439
|
+
paused: false,
|
|
1440
|
+
unverified: false,
|
|
1441
|
+
reason: null,
|
|
1442
|
+
pausedByUserId: null,
|
|
1443
|
+
pausedAt: null,
|
|
1444
|
+
refusedStarts: 0,
|
|
1445
|
+
cleanlyStopped: 0
|
|
1446
|
+
});
|
|
1447
|
+
var WORKSPACE_PAUSED_UNVERIFIED = Object.freeze({
|
|
1448
|
+
paused: true,
|
|
1449
|
+
unverified: true,
|
|
1450
|
+
reason: null,
|
|
1451
|
+
pausedByUserId: null,
|
|
1452
|
+
pausedAt: null,
|
|
1453
|
+
refusedStarts: 0,
|
|
1454
|
+
cleanlyStopped: 0
|
|
1455
|
+
});
|
|
1456
|
+
var READINESS_BLOCKERS = Object.freeze([
|
|
1457
|
+
"at-ceiling",
|
|
1458
|
+
"too-few-decisions",
|
|
1459
|
+
"approval-rate",
|
|
1460
|
+
"withdrawn",
|
|
1461
|
+
"other-refusals"
|
|
1462
|
+
]);
|
|
1463
|
+
var EVER_STATS_KEY_HEADER = CONSTANTS.stats_headers.key;
|
|
1464
|
+
var EVER_STATS_SIGNATURE_HEADER = CONSTANTS.stats_headers.signature;
|
|
1465
|
+
var EVER_STATS_KEY_ID_HEADER = CONSTANTS.stats_headers.key_id;
|
|
1466
|
+
var EVER_STATS_SIGNATURE_PREFIX = CONSTANTS.stats_signature_prefix;
|
|
1467
|
+
var EVER_STATS_MAX_BODY_BYTES = CONSTANTS.stats.max_bytes;
|
|
1468
|
+
var DOMAIN_EVENT_CATALOG = [
|
|
1469
|
+
// EW-P2-6 — `org:write` (and the app) create Organizations.
|
|
1470
|
+
{ type: "organization.created", domain: "organization", aggregateType: "organization", phase: "P2" },
|
|
1471
|
+
{ type: "organization.updated", domain: "organization", aggregateType: "organization", phase: "P1" },
|
|
1472
|
+
{
|
|
1473
|
+
type: "organization.member.added",
|
|
1474
|
+
domain: "organization",
|
|
1475
|
+
aggregateType: "organization",
|
|
1476
|
+
phase: "P1",
|
|
1477
|
+
child: "member"
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
type: "organization.member.updated",
|
|
1481
|
+
domain: "organization",
|
|
1482
|
+
aggregateType: "organization",
|
|
1483
|
+
phase: "P1",
|
|
1484
|
+
child: "member"
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
type: "organization.member.removed",
|
|
1488
|
+
domain: "organization",
|
|
1489
|
+
aggregateType: "organization",
|
|
1490
|
+
phase: "P1",
|
|
1491
|
+
child: "member"
|
|
1492
|
+
},
|
|
1493
|
+
{ type: "agent.created", domain: "agents", aggregateType: "agent", phase: "P1" },
|
|
1494
|
+
{ type: "agent.updated", domain: "agents", aggregateType: "agent", phase: "P1" },
|
|
1495
|
+
{ type: "agent.status_changed", domain: "agents", aggregateType: "agent", phase: "P1" },
|
|
1496
|
+
{ type: "agent.archived", domain: "agents", aggregateType: "agent", phase: "P1" },
|
|
1497
|
+
{ type: "agent.deleted", domain: "agents", aggregateType: "agent", phase: "P1" },
|
|
1498
|
+
{ type: "team.created", domain: "teams", aggregateType: "team", phase: "P1" },
|
|
1499
|
+
{ type: "team.updated", domain: "teams", aggregateType: "team", phase: "P1" },
|
|
1500
|
+
{ type: "team.deleted", domain: "teams", aggregateType: "team", phase: "P1" },
|
|
1501
|
+
{ type: "team.member.added", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
|
|
1502
|
+
{ type: "team.member.updated", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
|
|
1503
|
+
{ type: "team.member.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "member" },
|
|
1504
|
+
{ type: "team.resource.added", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
|
|
1505
|
+
{ type: "team.resource.removed", domain: "teams", aggregateType: "team", phase: "P1", child: "resource" },
|
|
1506
|
+
{ type: "work.created", domain: "works", aggregateType: "work", phase: "P1" },
|
|
1507
|
+
{ type: "work.updated", domain: "works", aggregateType: "work", phase: "P1" },
|
|
1508
|
+
{ type: "work.archived", domain: "works", aggregateType: "work", phase: "P1" },
|
|
1509
|
+
{ type: "work.deleted", domain: "works", aggregateType: "work", phase: "P1" },
|
|
1510
|
+
{ type: "work.member.added", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
|
|
1511
|
+
{ type: "work.member.updated", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
|
|
1512
|
+
{ type: "work.member.removed", domain: "works", aggregateType: "work", phase: "P1", child: "member" },
|
|
1513
|
+
{ type: "mission.created", domain: "missions", aggregateType: "mission", phase: "P2" },
|
|
1514
|
+
{ type: "mission.updated", domain: "missions", aggregateType: "mission", phase: "P2" },
|
|
1515
|
+
{ type: "mission.status_changed", domain: "missions", aggregateType: "mission", phase: "P2" },
|
|
1516
|
+
{ type: "mission.deleted", domain: "missions", aggregateType: "mission", phase: "P2" },
|
|
1517
|
+
{ type: "mission.goal.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
|
|
1518
|
+
{ type: "mission.goal.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "goal" },
|
|
1519
|
+
{ type: "mission.work.linked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
|
|
1520
|
+
{ type: "mission.work.unlinked", domain: "missions", aggregateType: "mission", phase: "P2", child: "work" },
|
|
1521
|
+
{ type: "goal.created", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1522
|
+
{ type: "goal.updated", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1523
|
+
{ type: "goal.status_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1524
|
+
{ type: "goal.archived", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1525
|
+
{ type: "goal.unarchived", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1526
|
+
{ type: "goal.deleted", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1527
|
+
{ type: "goal.progress_changed", domain: "goals", aggregateType: "goal", phase: "P2" },
|
|
1528
|
+
{ type: "idea.created", domain: "ideas", aggregateType: "idea", phase: "P2" },
|
|
1529
|
+
{ type: "idea.updated", domain: "ideas", aggregateType: "idea", phase: "P2" },
|
|
1530
|
+
{ type: "idea.status_changed", domain: "ideas", aggregateType: "idea", phase: "P2" },
|
|
1531
|
+
{ type: "idea.deleted", domain: "ideas", aggregateType: "idea", phase: "P2" },
|
|
1532
|
+
{ type: "task.created", domain: "tasks", aggregateType: "task", phase: "P1" },
|
|
1533
|
+
{ type: "task.updated", domain: "tasks", aggregateType: "task", phase: "P1" },
|
|
1534
|
+
{ type: "task.transitioned", domain: "tasks", aggregateType: "task", phase: "P1" },
|
|
1535
|
+
{ type: "task.deleted", domain: "tasks", aggregateType: "task", phase: "P1" },
|
|
1536
|
+
{ type: "task.assignee.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
|
|
1537
|
+
{ type: "task.assignee.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "assignee" },
|
|
1538
|
+
{ type: "task.watcher.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
|
|
1539
|
+
{ type: "task.watcher.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "watcher" },
|
|
1540
|
+
{ type: "task.block.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
|
|
1541
|
+
{ type: "task.block.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "block" },
|
|
1542
|
+
{ type: "task.relation.added", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
|
|
1543
|
+
{ type: "task.relation.removed", domain: "tasks", aggregateType: "task", phase: "P1", child: "relation" },
|
|
1544
|
+
{ type: "task.comment.created", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
|
|
1545
|
+
{ type: "task.comment.updated", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
|
|
1546
|
+
{ type: "task.comment.deleted", domain: "tasks", aggregateType: "task", phase: "P1", child: "comment" },
|
|
1547
|
+
{ type: "task.attachment.added", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
|
|
1548
|
+
{ type: "task.attachment.removed", domain: "tasks", aggregateType: "task", phase: "P3", child: "attachment" },
|
|
1549
|
+
{ type: "task.runtime_updated", domain: "tasks", aggregateType: "task", phase: "P1", optIn: true },
|
|
1550
|
+
{ type: "inbox.item.created", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
|
|
1551
|
+
{ type: "inbox.item.answered", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
|
|
1552
|
+
{ type: "inbox.item.state_changed", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
|
|
1553
|
+
{ type: "inbox.item.deleted", domain: "inbox", aggregateType: "inbox_item", phase: "P3" },
|
|
1554
|
+
{ type: "kb.document.created", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1555
|
+
{ type: "kb.document.updated", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1556
|
+
{ type: "kb.document.moved", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1557
|
+
{ type: "kb.document.archived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1558
|
+
{ type: "kb.document.unarchived", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1559
|
+
{ type: "kb.document.deleted", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1560
|
+
{ type: "kb.document.review_changed", domain: "kb", aggregateType: "kb_document", phase: "P3" },
|
|
1561
|
+
{ type: "kb.folder.created", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
|
|
1562
|
+
{ type: "kb.folder.updated", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
|
|
1563
|
+
{ type: "kb.folder.deleted", domain: "kb", aggregateType: "kb_folder", phase: "P3" },
|
|
1564
|
+
{ type: "email.message.received", domain: "email", aggregateType: "email_message", phase: "P4" },
|
|
1565
|
+
{ type: "email.message.sent", domain: "email", aggregateType: "email_message", phase: "P4" },
|
|
1566
|
+
{ type: "email.message.status_changed", domain: "email", aggregateType: "email_message", phase: "P4" },
|
|
1567
|
+
{ type: "task.visibility_changed", domain: "visibility", aggregateType: "task", phase: "P1" },
|
|
1568
|
+
{ type: "agent.visibility_changed", domain: "visibility", aggregateType: "agent", phase: "P1" },
|
|
1569
|
+
{ type: "goal.visibility_changed", domain: "visibility", aggregateType: "goal", phase: "P2" },
|
|
1570
|
+
{ type: "mission.visibility_changed", domain: "visibility", aggregateType: "mission", phase: "P2" },
|
|
1571
|
+
{ type: "idea.visibility_changed", domain: "visibility", aggregateType: "idea", phase: "P2" }
|
|
1572
|
+
];
|
|
1573
|
+
var DOMAIN_EVENT_TYPES = DOMAIN_EVENT_CATALOG.map((entry) => entry.type);
|
|
1574
|
+
var CATALOG_BY_TYPE = new Map(
|
|
1575
|
+
DOMAIN_EVENT_CATALOG.map((entry) => [entry.type, entry])
|
|
1576
|
+
);
|
|
1577
|
+
var RECIPIENT_SCOPED_EVENT_DOMAINS = {
|
|
1578
|
+
inbox: "inbox:read",
|
|
1579
|
+
email: "email:read"
|
|
1580
|
+
};
|
|
1581
|
+
var RECIPIENT_EVENT_SCOPES = Object.values(RECIPIENT_SCOPED_EVENT_DOMAINS);
|
|
1582
|
+
var INTEGRATION_SCOPES = [
|
|
1583
|
+
"org:read",
|
|
1584
|
+
"org:write",
|
|
1585
|
+
"members:read",
|
|
1586
|
+
"members:write",
|
|
1587
|
+
"agents:read",
|
|
1588
|
+
"teams:read",
|
|
1589
|
+
"teams:write",
|
|
1590
|
+
"works:read",
|
|
1591
|
+
"works:write",
|
|
1592
|
+
"missions:read",
|
|
1593
|
+
"missions:write",
|
|
1594
|
+
"goals:read",
|
|
1595
|
+
"goals:write",
|
|
1596
|
+
"ideas:read",
|
|
1597
|
+
"ideas:write",
|
|
1598
|
+
"tasks:read",
|
|
1599
|
+
"tasks:write",
|
|
1600
|
+
"inbox:read",
|
|
1601
|
+
"inbox:write",
|
|
1602
|
+
"kb:read",
|
|
1603
|
+
"kb:write",
|
|
1604
|
+
"email:read",
|
|
1605
|
+
"email:write",
|
|
1606
|
+
"events:read",
|
|
1607
|
+
"webhooks:manage",
|
|
1608
|
+
"links:write"
|
|
1609
|
+
];
|
|
1610
|
+
var INTEGRATION_OPT_IN_SCOPES = ["org:write", "email:write"];
|
|
1611
|
+
var EVER_GAUZY_PRESET_SCOPES = INTEGRATION_SCOPES.filter(
|
|
1612
|
+
(scope) => !INTEGRATION_OPT_IN_SCOPES.includes(scope)
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
// ../../plugin/dist/index.js
|
|
1616
|
+
var DEFAULT_DIFF_MAX_BYTES = 256 * 1024;
|
|
1617
|
+
var HARD_DIFF_MAX_BYTES = 1024 * 1024;
|
|
1618
|
+
var PLUGIN_CAPABILITIES = {
|
|
1619
|
+
AI_PROVIDER: "ai-provider",
|
|
1620
|
+
SEARCH: "search",
|
|
1621
|
+
SCREENSHOT: "screenshot",
|
|
1622
|
+
CONTENT_EXTRACTOR: "content-extractor",
|
|
1623
|
+
DATA_SOURCE: "data-source",
|
|
1624
|
+
PIPELINE: "pipeline",
|
|
1625
|
+
PIPELINE_MODIFIER: "pipeline-modifier",
|
|
1626
|
+
CODE_EDIT: "code-edit",
|
|
1627
|
+
FORM_SCHEMA_PROVIDER: "form-schema-provider",
|
|
1628
|
+
DEPLOYMENT: "deployment",
|
|
1629
|
+
GIT_PROVIDER: "git-provider",
|
|
1630
|
+
OAUTH: "oauth",
|
|
1631
|
+
DEVICE_AUTH: "device-auth",
|
|
1632
|
+
PROMPT_PROVIDER: "prompt-provider",
|
|
1633
|
+
// EW-637 — pluggable object storage. `put-object` + `get-object` are the
|
|
1634
|
+
// floor; `presigned-put` is opt-in for backends that can hand the
|
|
1635
|
+
// browser a direct-upload URL (S3, MinIO).
|
|
1636
|
+
STORAGE: "storage",
|
|
1637
|
+
PUT_OBJECT: "put-object",
|
|
1638
|
+
GET_OBJECT: "get-object",
|
|
1639
|
+
PRESIGNED_PUT: "presigned-put",
|
|
1640
|
+
// Agents/Skills/Tasks PR #1017 — Phase 8 (ADR-012). Plugin
|
|
1641
|
+
// category for Skill catalog providers. "Ever Works Skills" is
|
|
1642
|
+
// the first-party default; community plugins implement the same
|
|
1643
|
+
// `ISkillsProviderPlugin` contract to surface other catalogs.
|
|
1644
|
+
SKILLS_PROVIDER: "skills-provider",
|
|
1645
|
+
// Agents/Skills/Tasks PR #1017 — Phase 11 (ADR-013). Plugin
|
|
1646
|
+
// category for external task trackers. "Ever Works Task Tracker"
|
|
1647
|
+
// is the first-party default. Community plugins (Linear / Jira /
|
|
1648
|
+
// GitHub Issues) implement the same contract.
|
|
1649
|
+
TASK_TRACKER: "task-tracker",
|
|
1650
|
+
// Notifications v2 (EW-650) — Email Providers. Plugins MAY declare
|
|
1651
|
+
// `EMAIL_OUTBOUND`, `EMAIL_INBOUND`, or both. See
|
|
1652
|
+
// `capabilities/email-provider.interface.ts` for the contract.
|
|
1653
|
+
EMAIL_OUTBOUND: "email-outbound",
|
|
1654
|
+
EMAIL_INBOUND: "email-inbound",
|
|
1655
|
+
// Notifications v2 (sibling of EW-650) — Notification Channels.
|
|
1656
|
+
// Plugins declare `NOTIFICATION_CHANNEL` (umbrella) plus the
|
|
1657
|
+
// channel-specific constant for plugin discovery + UI grouping.
|
|
1658
|
+
// See `capabilities/notification-channel.interface.ts`.
|
|
1659
|
+
NOTIFICATION_CHANNEL: "notification-channel",
|
|
1660
|
+
NOTIFICATION_CHANNEL_DISCORD: "notification-channel-discord",
|
|
1661
|
+
NOTIFICATION_CHANNEL_SLACK: "notification-channel-slack",
|
|
1662
|
+
NOTIFICATION_CHANNEL_TELEGRAM: "notification-channel-telegram",
|
|
1663
|
+
NOTIFICATION_CHANNEL_WHATSAPP: "notification-channel-whatsapp",
|
|
1664
|
+
NOTIFICATION_CHANNEL_NOVU: "notification-channel-novu",
|
|
1665
|
+
// Connectors ("Connector fabric") — first-party BIDIRECTIONAL
|
|
1666
|
+
// communication-channel plugins. Each connector declares `CONNECTOR`
|
|
1667
|
+
// (umbrella, for discovery/grouping) plus its `CONNECTOR_<provider>`
|
|
1668
|
+
// constant. Superset of the outbound-only notification channels; see
|
|
1669
|
+
// `capabilities/connector.interface.ts`.
|
|
1670
|
+
CONNECTOR: "connector",
|
|
1671
|
+
CONNECTOR_SLACK: "connector-slack",
|
|
1672
|
+
CONNECTOR_DISCORD: "connector-discord",
|
|
1673
|
+
CONNECTOR_WHATSAPP: "connector-whatsapp",
|
|
1674
|
+
CONNECTOR_LINEAR: "connector-linear",
|
|
1675
|
+
CONNECTOR_NOTION: "connector-notion",
|
|
1676
|
+
CONNECTOR_MICROSOFT_365: "connector-microsoft-365",
|
|
1677
|
+
// CRM / enrichment connectors — first-party native connectors over
|
|
1678
|
+
// the vendors' own Node SDKs. Outbound writes CRM records/notes,
|
|
1679
|
+
// the event-source leg streams record changes into the ingest spine.
|
|
1680
|
+
CONNECTOR_HUBSPOT: "connector-hubspot",
|
|
1681
|
+
CONNECTOR_PIPEDRIVE: "connector-pipedrive",
|
|
1682
|
+
// Social connectors — public-timeline surfaces. Outbound publishes a
|
|
1683
|
+
// post; the event-source leg streams mentions/replies + the account's
|
|
1684
|
+
// own timeline into the ingest spine.
|
|
1685
|
+
CONNECTOR_BLUESKY: "connector-bluesky",
|
|
1686
|
+
CONNECTOR_MASTODON: "connector-mastodon",
|
|
1687
|
+
// Pluggable persistent memory for AI coding / generation agents.
|
|
1688
|
+
// First-party implementation: `@ever-works/agentmemory-plugin`
|
|
1689
|
+
// (talks to the `agentmemory` standalone Node server on :3111 —
|
|
1690
|
+
// runs locally OR hosted via a configurable `baseUrl` + bearer
|
|
1691
|
+
// token). Community plugins (mem0, zep, langmem) implement the
|
|
1692
|
+
// same `IAgentMemoryPlugin` contract.
|
|
1693
|
+
AGENT_MEMORY: "agent-memory",
|
|
1694
|
+
// Goals feature PR-7 — read-only metrics collectors. First-party
|
|
1695
|
+
// providers: `custom-http` (GET-only, SSRF-guarded) and `stripe`
|
|
1696
|
+
// (official SDK; balance + income windows). See
|
|
1697
|
+
// `capabilities/metrics-provider.interface.ts` for the contract.
|
|
1698
|
+
METRICS_PROVIDER: "metrics-provider",
|
|
1699
|
+
// Streaming-terminal session hosts (Wave 1 M5). First-party:
|
|
1700
|
+
// pty-local (node-pty in the executing job-runtime worker, with a
|
|
1701
|
+
// child_process pipe floor). Future: pty-ssh (user's own box),
|
|
1702
|
+
// k8s-exec. See capabilities/terminal-stream.interface.ts.
|
|
1703
|
+
TERMINAL_STREAM: "terminal-stream",
|
|
1704
|
+
// Isolated git working contexts for agent Tasks (Wave 2).
|
|
1705
|
+
WORKSPACE: "workspace",
|
|
1706
|
+
// Headless browser drivers (audit item G22). navigate / extract /
|
|
1707
|
+
// screenshot / act, headless by default, behind a default-deny
|
|
1708
|
+
// navigation allowlist re-checked on every redirect hop. First-party:
|
|
1709
|
+
// `browser-automation` (Playwright). See
|
|
1710
|
+
// capabilities/browser-automation.interface.ts.
|
|
1711
|
+
BROWSER_AUTOMATION: "browser-automation",
|
|
1712
|
+
// Event-ingest spine (Wave 6) — plugins that pull/push normalized
|
|
1713
|
+
// external events into the platform ingest pipeline. See
|
|
1714
|
+
// capabilities/event-source.interface.ts.
|
|
1715
|
+
EVENT_SOURCE: "event-source",
|
|
1716
|
+
// Capability & playbook catalogue (AW-21) — plugins that supply Playbook
|
|
1717
|
+
// catalogue entries (packaged outcomes: trigger, steps, required
|
|
1718
|
+
// capabilities, artefacts, escalation points). Consumed only through
|
|
1719
|
+
// PlaybookCatalogFacadeService. See
|
|
1720
|
+
// capabilities/playbook-provider.interface.ts.
|
|
1721
|
+
PLAYBOOK_PROVIDER: "playbook-provider",
|
|
1722
|
+
// AW-15 — optional access-level declaration ("Read only" / "Read and
|
|
1723
|
+
// write") mapped onto the tool-grant lattice. Declared alongside a
|
|
1724
|
+
// provider's main capability; see
|
|
1725
|
+
// capabilities/connection-scopes.interface.ts.
|
|
1726
|
+
CONNECTION_SCOPES: "connection-scopes",
|
|
1727
|
+
// App Works (APW-07 T3) — App dependency providers: the `k8s` plugin serves
|
|
1728
|
+
// the three in-cluster ids, `app-dependencies-external` the external ones,
|
|
1729
|
+
// and P2's `apps-tier-dependencies` the managed ones. Consumed only through
|
|
1730
|
+
// `AppDependencyFacadeService`; see
|
|
1731
|
+
// capabilities/app-dependency.interface.ts.
|
|
1732
|
+
APP_DEPENDENCY: "app-dependency",
|
|
1733
|
+
// App Works (APW-05 T2) — producing a container image from a commit. The
|
|
1734
|
+
// `github-actions-build` plugin declares it (`IBuildPlugin`, `buildKind
|
|
1735
|
+
// 'github-actions'`), and P3's `apps-builder` declares the same capability
|
|
1736
|
+
// with the other `buildKind`. Consumed only through `BuildFacadeService`
|
|
1737
|
+
// (APW-05 T16); see `capabilities/build.interface.ts`. Resolution R-13 keeps
|
|
1738
|
+
// `build` a CAPABILITY while the strategy (`dockerfile` / `image` / `auto` /
|
|
1739
|
+
// `none`) stays the App spec's own choice.
|
|
1740
|
+
BUILD: "build",
|
|
1741
|
+
// App Works (APW-12 T4) — the Ever ID relying-party capability. The
|
|
1742
|
+
// `oidc-identity` plugin declares it (APW-12 T5/T6), and it is consumed only
|
|
1743
|
+
// through `IdentityProviderFacadeService` (APW-12 plan §4.4); see
|
|
1744
|
+
// `capabilities/identity-provider.interface.ts`. Appended after `build`, and
|
|
1745
|
+
// landed in the same change as the `identity` category in
|
|
1746
|
+
// `plugin-manifest.types.ts` — a plugin contract with no category to be
|
|
1747
|
+
// discovered under is a manifest the loader rejects.
|
|
1748
|
+
IDENTITY_PROVIDER: "identity-provider",
|
|
1749
|
+
// App Works (APW-10 T2) — the managed hosting tier's zone capability. The
|
|
1750
|
+
// `ever-works-apps` plugin declares it together with `deployment`
|
|
1751
|
+
// (plan §5.2:602) and is consumed only through `AppsTierFacadeService`
|
|
1752
|
+
// (APW-10 plan §5.3:655); see `capabilities/apps-tier.interface.ts`.
|
|
1753
|
+
// Appended after `identity-provider`, and appended **last** on purpose:
|
|
1754
|
+
// every existing constant keeps its value and its position, because the
|
|
1755
|
+
// capability list is an append-only surface — a member that moves silently
|
|
1756
|
+
// changes what a persisted manifest means. Deliberately NOT paired with a
|
|
1757
|
+
// new category: the tier's plugin is a `deployment` plugin, so
|
|
1758
|
+
// `PLUGIN_CATEGORIES` is untouched and every exhaustive map over it in
|
|
1759
|
+
// `apps/web` stays total.
|
|
1760
|
+
APPS_TIER: "apps-tier",
|
|
1761
|
+
// Domain events (EW-P1-1) — plugins that follow the durable domain event
|
|
1762
|
+
// outbox (`task.updated`, `team.member.added`, …) through the relay, at
|
|
1763
|
+
// least once, with a per-plugin cursor. See
|
|
1764
|
+
// `capabilities/domain-event-subscriber.interface.ts`. Appended last, so
|
|
1765
|
+
// every existing constant keeps its value and its position; no category is
|
|
1766
|
+
// added (a subscriber plugin keeps the category of what it otherwise is).
|
|
1767
|
+
DOMAIN_EVENT_SUBSCRIBER: "domain-event-subscriber"
|
|
1768
|
+
};
|
|
1769
|
+
var ALL_PLUGIN_CAPABILITIES = Object.values(PLUGIN_CAPABILITIES);
|
|
1770
|
+
function initializePipelineState(stepDefinitions) {
|
|
1771
|
+
const steps = /* @__PURE__ */ new Map();
|
|
1772
|
+
for (const def of stepDefinitions) {
|
|
1773
|
+
steps.set(def.id, { definition: def, status: "pending" });
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
steps,
|
|
1777
|
+
completedSteps: [],
|
|
1778
|
+
failedSteps: [],
|
|
1779
|
+
isRunning: true,
|
|
1780
|
+
isCancelled: false,
|
|
1781
|
+
startedAt: Date.now()
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
function updatePipelineStepState(state, stepId, status, error) {
|
|
1785
|
+
const existing = state.steps.get(stepId);
|
|
1786
|
+
if (!existing) return state;
|
|
1787
|
+
const now = Date.now();
|
|
1788
|
+
const isTerminal = status === "completed" || status === "failed" || status === "skipped";
|
|
1789
|
+
const updated = {
|
|
1790
|
+
...existing,
|
|
1791
|
+
status,
|
|
1792
|
+
startedAt: status === "running" ? now : existing.startedAt,
|
|
1793
|
+
completedAt: isTerminal ? now : existing.completedAt,
|
|
1794
|
+
error: error ?? existing.error
|
|
1795
|
+
};
|
|
1796
|
+
const steps = new Map(state.steps);
|
|
1797
|
+
steps.set(stepId, updated);
|
|
1798
|
+
const completedSteps = status === "completed" || status === "skipped" ? [...state.completedSteps, stepId] : state.completedSteps;
|
|
1799
|
+
const failedSteps = status === "failed" ? [...state.failedSteps, stepId] : state.failedSteps;
|
|
1800
|
+
return {
|
|
1801
|
+
...state,
|
|
1802
|
+
steps,
|
|
1803
|
+
currentStep: status === "running" ? stepId : state.currentStep === stepId ? void 0 : state.currentStep,
|
|
1804
|
+
completedSteps,
|
|
1805
|
+
failedSteps
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function reportPipelineProgress(onProgress, totalSteps, stepIndex, percent, stepName, message, itemsProcessed) {
|
|
1809
|
+
onProgress?.({
|
|
1810
|
+
percent,
|
|
1811
|
+
currentStepIndex: stepIndex,
|
|
1812
|
+
totalSteps,
|
|
1813
|
+
currentStepName: stepName,
|
|
1814
|
+
message,
|
|
1815
|
+
itemsProcessed
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
function reportPipelineItemProgress(onProgress, totalSteps, newItemCount, targetItems, stepIndex, stepName = "Generate Items") {
|
|
1819
|
+
const ratio = newItemCount / Math.max(targetItems, 1);
|
|
1820
|
+
const percent = Math.min(30 + Math.round(ratio * 53), 83);
|
|
1821
|
+
onProgress?.({
|
|
1822
|
+
percent,
|
|
1823
|
+
currentStepIndex: stepIndex,
|
|
1824
|
+
totalSteps,
|
|
1825
|
+
currentStepName: stepName,
|
|
1826
|
+
message: `${newItemCount} items generated`,
|
|
1827
|
+
itemsProcessed: newItemCount
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
async function resolveScopedSettings(context, userId, workId) {
|
|
1831
|
+
if (!context) return {};
|
|
1832
|
+
try {
|
|
1833
|
+
const [userSettings, workSettings] = await Promise.all([
|
|
1834
|
+
context.getSettings("user", userId),
|
|
1835
|
+
context.getSettings("work", workId)
|
|
1836
|
+
]);
|
|
1837
|
+
const merged = { ...userSettings };
|
|
1838
|
+
for (const key in workSettings) {
|
|
1839
|
+
if (workSettings[key] !== void 0 && workSettings[key] !== null) {
|
|
1840
|
+
merged[key] = workSettings[key];
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return merged;
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
const isAuthError = err instanceof Error && (/unauthorized|forbidden|access denied/i.test(err.message) || "status" in err && err.status === 401 || "status" in err && err.status === 403 || "statusCode" in err && err.statusCode === 401 || "statusCode" in err && err.statusCode === 403);
|
|
1846
|
+
if (isAuthError) {
|
|
1847
|
+
throw err;
|
|
1848
|
+
}
|
|
1849
|
+
context.logger?.warn(
|
|
1850
|
+
`resolveScopedSettings: failed to load settings for user=${userId} work=${workId}: ${err instanceof Error ? err.message : String(err)}`
|
|
1851
|
+
);
|
|
1852
|
+
return {};
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function buildPipelineMetrics(startTime, duration, itemCount, custom) {
|
|
1856
|
+
return {
|
|
1857
|
+
startTime,
|
|
1858
|
+
duration,
|
|
1859
|
+
itemsProcessed: itemCount,
|
|
1860
|
+
steps: {},
|
|
1861
|
+
...custom ? { custom } : {}
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function finalizePipelineState(state) {
|
|
1865
|
+
return {
|
|
1866
|
+
...state,
|
|
1867
|
+
isRunning: false,
|
|
1868
|
+
isCancelled: false,
|
|
1869
|
+
currentStep: void 0,
|
|
1870
|
+
completedAt: Date.now()
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function buildPipelineErrorResult(state, error, startTime, totalSteps, initializeState2) {
|
|
1874
|
+
let currentState = state ?? initializeState2();
|
|
1875
|
+
for (const [stepId, stepState] of currentState.steps) {
|
|
1876
|
+
if (stepState.status === "running") {
|
|
1877
|
+
currentState = updatePipelineStepState(currentState, stepId, "failed", error.message);
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
currentState = {
|
|
1882
|
+
...currentState,
|
|
1883
|
+
isRunning: false,
|
|
1884
|
+
isCancelled: false,
|
|
1885
|
+
currentStep: void 0,
|
|
1886
|
+
completedAt: Date.now()
|
|
1887
|
+
};
|
|
1888
|
+
return {
|
|
1889
|
+
state: currentState,
|
|
1890
|
+
result: buildErrorPipelineResult(error, {
|
|
1891
|
+
outputs: createEmptyPipelineOutputs(),
|
|
1892
|
+
duration: Date.now() - startTime,
|
|
1893
|
+
stepsCompleted: currentState.completedSteps.length,
|
|
1894
|
+
totalSteps,
|
|
1895
|
+
failedStep: currentState.failedSteps[currentState.failedSteps.length - 1],
|
|
1896
|
+
state: currentState
|
|
1897
|
+
})
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function buildPipelineCancelledResult(state, startTime, totalSteps, initializeState2) {
|
|
1901
|
+
const currentState = {
|
|
1902
|
+
...state ?? initializeState2(),
|
|
1903
|
+
isRunning: false,
|
|
1904
|
+
isCancelled: true,
|
|
1905
|
+
currentStep: void 0,
|
|
1906
|
+
completedAt: Date.now()
|
|
1907
|
+
};
|
|
1908
|
+
return {
|
|
1909
|
+
state: currentState,
|
|
1910
|
+
result: buildCancelledPipelineResult({
|
|
1911
|
+
outputs: createEmptyPipelineOutputs(),
|
|
1912
|
+
duration: Date.now() - startTime,
|
|
1913
|
+
stepsCompleted: currentState.completedSteps.length,
|
|
1914
|
+
totalSteps,
|
|
1915
|
+
state: currentState
|
|
1916
|
+
})
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function delay(ms) {
|
|
1920
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1921
|
+
}
|
|
1922
|
+
function createPipelineRuntimeHelpers(options) {
|
|
1923
|
+
const initializeState2 = () => initializePipelineState(options.stepDefinitions);
|
|
1924
|
+
const updateStepState2 = (state, stepId, status, error) => updatePipelineStepState(state, stepId, status, error);
|
|
1925
|
+
const reportProgress2 = (onProgress, stepIndex, percent, stepName, message, itemsProcessed) => {
|
|
1926
|
+
reportPipelineProgress(onProgress, options.totalSteps, stepIndex, percent, stepName, message, itemsProcessed);
|
|
1927
|
+
};
|
|
1928
|
+
const reportItemProgress = (onProgress, newItemCount, targetItems, stepIndex, stepName = options.itemProgressStepName ?? "Generate Items") => {
|
|
1929
|
+
reportPipelineItemProgress(onProgress, options.totalSteps, newItemCount, targetItems, stepIndex, stepName);
|
|
1930
|
+
};
|
|
1931
|
+
const buildMetrics2 = (startTime, duration, itemCount, custom) => buildPipelineMetrics(startTime, duration, itemCount, custom);
|
|
1932
|
+
const finalizeCompletedState2 = (state) => finalizePipelineState(state);
|
|
1933
|
+
const buildErrorResult2 = (state, error, startTime) => buildPipelineErrorResult(state, error, startTime, options.totalSteps, initializeState2);
|
|
1934
|
+
const buildCancelledResult2 = (state, startTime) => buildPipelineCancelledResult(state, startTime, options.totalSteps, initializeState2);
|
|
1935
|
+
return {
|
|
1936
|
+
initializeState: initializeState2,
|
|
1937
|
+
updateStepState: updateStepState2,
|
|
1938
|
+
reportProgress: reportProgress2,
|
|
1939
|
+
reportItemProgress,
|
|
1940
|
+
resolveSettings: resolveScopedSettings,
|
|
1941
|
+
buildMetrics: buildMetrics2,
|
|
1942
|
+
finalizeCompletedState: finalizeCompletedState2,
|
|
1943
|
+
buildErrorResult: buildErrorResult2,
|
|
1944
|
+
buildCancelledResult: buildCancelledResult2,
|
|
1945
|
+
delay
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
var ITEM_SCHEMA_PROMPT_TEXT = `
|
|
1949
|
+
Each item must be a JSON file with the following structure:
|
|
1950
|
+
|
|
1951
|
+
{
|
|
1952
|
+
"name": "string (required) - The item's canonical display name",
|
|
1953
|
+
"description": "string (required) - A concise, informative summary of the item and its relevance to the work topic (2-4 sentences). Generate from page content if not directly available.",
|
|
1954
|
+
"source_url": "string (required) - The most direct, canonical URL for the item itself (homepage or official page). Must be real and working. Never use blog posts or articles *about* the item.",
|
|
1955
|
+
"category": "string (required) - ONE primary category based on the item's core function (e.g., 'Monitoring', 'CI/CD', 'Data Visualization')",
|
|
1956
|
+
"tags": ["string"] - 1-3 specific, descriptive tags (e.g., "open-source", "real-time", "cloud-native"),
|
|
1957
|
+
"featured": boolean - Whether this is a notable/featured item (optional, default false),
|
|
1958
|
+
"slug": "string - URL-friendly identifier (optional, auto-generated from name if omitted)",
|
|
1959
|
+
"brand": "string - At most one brand/company name per item (optional)",
|
|
1960
|
+
"brand_logo_url": "string - Canonical logo URL from official domain, prefer SVG/PNG (optional)",
|
|
1961
|
+
"images": ["string"] - Multiple high-quality image URLs (screenshots, product imagery) from official sources (optional),
|
|
1962
|
+
"markdown": "string - Detailed product/service information in markdown: features, pricing, use cases (see Markdown Rules below). Do not repeat category, tags, or other metadata fields."
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
Required fields: name, description, source_url, category
|
|
1966
|
+
|
|
1967
|
+
### Example
|
|
1968
|
+
|
|
1969
|
+
\`\`\`json
|
|
1970
|
+
{
|
|
1971
|
+
"name": "Prometheus",
|
|
1972
|
+
"description": "Open-source monitoring and alerting toolkit designed for reliability and scalability, widely adopted in cloud-native environments.",
|
|
1973
|
+
"source_url": "https://prometheus.io",
|
|
1974
|
+
"category": "Monitoring",
|
|
1975
|
+
"tags": ["open-source", "metrics", "cloud-native"],
|
|
1976
|
+
"featured": false,
|
|
1977
|
+
"brand": "Cloud Native Computing Foundation",
|
|
1978
|
+
"brand_logo_url": "https://www.cncf.io/wp-content/uploads/2022/07/cncf-color-bg.svg",
|
|
1979
|
+
"images": ["https://prometheus.io/assets/architecture.png"],
|
|
1980
|
+
"markdown": "## Overview\\n\\nPrometheus is an open-source systems monitoring and alerting toolkit originally built at SoundCloud.\\n\\n## Features\\n\\n- Multi-dimensional data model with time series data identified by metric name and key/value pairs\\n- PromQL, a flexible query language\\n- No reliance on distributed storage; single server nodes are autonomous\\n- Time series collection via a pull model over HTTP\\n- Targets discovered via service discovery or static configuration\\n\\n## Pricing\\n\\nFree and open-source under the Apache 2.0 license."
|
|
1981
|
+
}
|
|
1982
|
+
\`\`\`
|
|
1983
|
+
`.trim();
|
|
1984
|
+
function createEmptyPipelineOutputs() {
|
|
1985
|
+
return {
|
|
1986
|
+
items: [],
|
|
1987
|
+
categories: [],
|
|
1988
|
+
tags: [],
|
|
1989
|
+
collections: [],
|
|
1990
|
+
brands: []
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function buildSuccessPipelineResult(outputs, base) {
|
|
1994
|
+
return {
|
|
1995
|
+
success: true,
|
|
1996
|
+
outputs,
|
|
1997
|
+
duration: base.duration,
|
|
1998
|
+
stepsCompleted: base.stepsCompleted,
|
|
1999
|
+
totalSteps: base.totalSteps,
|
|
2000
|
+
state: base.state,
|
|
2001
|
+
metrics: base.metrics,
|
|
2002
|
+
warnings: base.warnings
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
function buildErrorPipelineResult(error, base) {
|
|
2006
|
+
return {
|
|
2007
|
+
success: false,
|
|
2008
|
+
outputs: base.outputs ?? createEmptyPipelineOutputs(),
|
|
2009
|
+
duration: base.duration,
|
|
2010
|
+
stepsCompleted: base.stepsCompleted,
|
|
2011
|
+
totalSteps: base.totalSteps,
|
|
2012
|
+
state: base.state,
|
|
2013
|
+
metrics: base.metrics,
|
|
2014
|
+
warnings: base.warnings,
|
|
2015
|
+
error,
|
|
2016
|
+
failedStep: base.failedStep
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function buildCancelledPipelineResult(base) {
|
|
2020
|
+
return buildErrorPipelineResult("Pipeline cancelled", base);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// src/types.ts
|
|
2024
|
+
var ACTIVEPIECES_STEP_IDS = [
|
|
2025
|
+
"validate-activepieces",
|
|
2026
|
+
"prepare-payload",
|
|
2027
|
+
"execute-flow",
|
|
2028
|
+
"collect-results",
|
|
2029
|
+
"capture-screenshots",
|
|
2030
|
+
"cleanup"
|
|
2031
|
+
];
|
|
2032
|
+
var DEFAULT_BASE_URL = "https://cloud.activepieces.com/api/v1";
|
|
2033
|
+
var DEFAULT_TARGET_ITEMS = 50;
|
|
2034
|
+
var TERMINAL_FLOW_RUN_STATUSES = [
|
|
2035
|
+
"SUCCEEDED",
|
|
2036
|
+
"FAILED",
|
|
2037
|
+
"TIMED_OUT",
|
|
2038
|
+
"INTERNAL_ERROR",
|
|
2039
|
+
"QUOTA_EXCEEDED",
|
|
2040
|
+
"STOPPED",
|
|
2041
|
+
"MEMORY_LIMIT_EXCEEDED"
|
|
2042
|
+
];
|
|
2043
|
+
|
|
2044
|
+
// src/steps.ts
|
|
2045
|
+
var STEP_DEFINITIONS = [
|
|
2046
|
+
{
|
|
2047
|
+
id: "validate-activepieces",
|
|
2048
|
+
name: "Validate Activepieces Connection",
|
|
2049
|
+
description: "Verify API key and check that the Activepieces flow is enabled",
|
|
2050
|
+
position: { type: "first" },
|
|
2051
|
+
dependencies: [],
|
|
2052
|
+
optional: false,
|
|
2053
|
+
parallelizable: false,
|
|
2054
|
+
estimatedDuration: 3
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
id: "prepare-payload",
|
|
2058
|
+
name: "Prepare Flow Payload",
|
|
2059
|
+
description: "Build input payload from work context, existing items, and config",
|
|
2060
|
+
position: { type: "after", stepId: "validate-activepieces" },
|
|
2061
|
+
dependencies: [{ stepId: "validate-activepieces", required: true }],
|
|
2062
|
+
optional: false,
|
|
2063
|
+
parallelizable: false,
|
|
2064
|
+
estimatedDuration: 2
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
id: "execute-flow",
|
|
2068
|
+
name: "Execute Activepieces Flow",
|
|
2069
|
+
description: "Trigger the Activepieces flow webhook (sync or async with polling)",
|
|
2070
|
+
position: { type: "after", stepId: "prepare-payload" },
|
|
2071
|
+
dependencies: [{ stepId: "prepare-payload", required: true }],
|
|
2072
|
+
optional: false,
|
|
2073
|
+
parallelizable: false,
|
|
2074
|
+
estimatedDuration: 120
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
id: "collect-results",
|
|
2078
|
+
name: "Collect & Validate Results",
|
|
2079
|
+
description: "Parse Activepieces flow output, validate items, and deduplicate",
|
|
2080
|
+
position: { type: "after", stepId: "execute-flow" },
|
|
2081
|
+
dependencies: [{ stepId: "execute-flow", required: true }],
|
|
2082
|
+
optional: false,
|
|
2083
|
+
parallelizable: false,
|
|
2084
|
+
estimatedDuration: 5
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
id: "capture-screenshots",
|
|
2088
|
+
name: "Capture Screenshots",
|
|
2089
|
+
description: "Capture screenshots for generated items",
|
|
2090
|
+
position: { type: "after", stepId: "collect-results" },
|
|
2091
|
+
dependencies: [{ stepId: "collect-results", required: true }],
|
|
2092
|
+
optional: true,
|
|
2093
|
+
parallelizable: false,
|
|
2094
|
+
estimatedDuration: 30
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
id: "cleanup",
|
|
2098
|
+
name: "Cleanup",
|
|
2099
|
+
description: "Release resources and clear temporary data",
|
|
2100
|
+
position: { type: "last" },
|
|
2101
|
+
dependencies: [{ stepId: "capture-screenshots", required: false }],
|
|
2102
|
+
optional: true,
|
|
2103
|
+
parallelizable: false,
|
|
2104
|
+
estimatedDuration: 1
|
|
2105
|
+
}
|
|
2106
|
+
];
|
|
2107
|
+
|
|
2108
|
+
// src/utils/activepieces-client.ts
|
|
2109
|
+
var ASYNC_POLL_INTERVAL_MS = 2e3;
|
|
2110
|
+
var ActivepiecesClient = class {
|
|
2111
|
+
constructor(options) {
|
|
2112
|
+
this.apiKey = options.apiKey;
|
|
2113
|
+
this.baseUrl = stripTrailingSlash(options.baseUrl);
|
|
2114
|
+
this.logger = options.logger;
|
|
2115
|
+
}
|
|
2116
|
+
/** Lightweight ping that hits the flows list to verify auth + reachability. */
|
|
2117
|
+
async ping(projectId) {
|
|
2118
|
+
const query = { limit: 1 };
|
|
2119
|
+
if (projectId) query.projectId = projectId;
|
|
2120
|
+
await this.request("/flows", { method: "GET", query });
|
|
2121
|
+
}
|
|
2122
|
+
/** Fetches a flow by id and ensures it is enabled. */
|
|
2123
|
+
async validateFlow(flowId) {
|
|
2124
|
+
const flow = await this.request(`/flows/${encodeURIComponent(flowId)}`, {
|
|
2125
|
+
method: "GET"
|
|
2126
|
+
});
|
|
2127
|
+
if (!flow || typeof flow !== "object") {
|
|
2128
|
+
throw new Error(`Activepieces flow "${flowId}" returned an unexpected response`);
|
|
2129
|
+
}
|
|
2130
|
+
if (flow.status && flow.status !== "ENABLED") {
|
|
2131
|
+
throw new Error(
|
|
2132
|
+
`Activepieces flow "${flowId}" is not enabled (status: ${flow.status}). Enable the flow from the Activepieces dashboard before triggering it.`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
if (!flow.publishedVersionId) {
|
|
2136
|
+
this.logger.warn(
|
|
2137
|
+
`Activepieces flow "${flowId}" has no published version. Publish the flow before invoking it; the webhook trigger will fail otherwise.`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
return flow;
|
|
2141
|
+
}
|
|
2142
|
+
/** Fetches a single flow run by id via `GET /flow-runs/{id}`. */
|
|
2143
|
+
async getFlowRun(runId, signal) {
|
|
2144
|
+
return this.request(`/flow-runs/${encodeURIComponent(runId)}`, {
|
|
2145
|
+
method: "GET",
|
|
2146
|
+
signal
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Returns the most recent flow run for a flow (used for async-mode polling).
|
|
2151
|
+
* Activepieces requires `projectId` on the list endpoint.
|
|
2152
|
+
*/
|
|
2153
|
+
async findLatestRunForFlow(flowId, projectId, options = {}) {
|
|
2154
|
+
const query = {
|
|
2155
|
+
projectId,
|
|
2156
|
+
flowId,
|
|
2157
|
+
limit: 1
|
|
2158
|
+
};
|
|
2159
|
+
if (options.createdAfter) query.createdAfter = options.createdAfter;
|
|
2160
|
+
const response = await this.request("/flow-runs", {
|
|
2161
|
+
method: "GET",
|
|
2162
|
+
query,
|
|
2163
|
+
signal: options.signal
|
|
2164
|
+
});
|
|
2165
|
+
return Array.isArray(response?.data) && response.data.length > 0 ? response.data[0] : void 0;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Triggers an Activepieces flow webhook with the prepared payload.
|
|
2169
|
+
*
|
|
2170
|
+
* **Sync mode** (`/sync`): Activepieces holds the request open until the flow's
|
|
2171
|
+
* Response action fires; the response body is returned as the flow output.
|
|
2172
|
+
*
|
|
2173
|
+
* **Async mode**: the webhook returns immediately. The client then polls
|
|
2174
|
+
* `GET /flow-runs?flowId=...&projectId=...` until the run reaches a terminal
|
|
2175
|
+
* status, and finally fetches the full run via `GET /flow-runs/{id}`.
|
|
2176
|
+
* Async mode requires `settings.projectId` to be configured.
|
|
2177
|
+
*/
|
|
2178
|
+
async executeFlow(flowId, input, settings, onProgress, signal) {
|
|
2179
|
+
if (signal?.aborted) throw new Error("Pipeline execution was cancelled");
|
|
2180
|
+
if (settings.webhookMode === "async" && !settings.projectId) {
|
|
2181
|
+
throw new Error(
|
|
2182
|
+
"Async mode requires a Default Project ID in plugin settings so the latest flow run can be polled."
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
const startTime = Date.now();
|
|
2186
|
+
const path = webhookPath(flowId, settings.webhookMode);
|
|
2187
|
+
this.logger.log(`Triggering Activepieces webhook ${path} (mode=${settings.webhookMode})`);
|
|
2188
|
+
onProgress?.(1, "running");
|
|
2189
|
+
const triggerStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2190
|
+
const response = await this.request(path, {
|
|
2191
|
+
method: "POST",
|
|
2192
|
+
body: input,
|
|
2193
|
+
signal,
|
|
2194
|
+
timeoutMs: settings.timeoutMs,
|
|
2195
|
+
// Webhook endpoints are public per-flow URLs and do not need the platform token.
|
|
2196
|
+
// Sending the bearer is harmless for cloud, but self-hosted gateways may reject it,
|
|
2197
|
+
// so we only forward the API key on platform endpoints.
|
|
2198
|
+
includeAuth: false
|
|
2199
|
+
});
|
|
2200
|
+
if (settings.webhookMode === "async") {
|
|
2201
|
+
const run2 = await this.pollUntilTerminal(
|
|
2202
|
+
flowId,
|
|
2203
|
+
settings.projectId,
|
|
2204
|
+
triggerStartedAt,
|
|
2205
|
+
settings.timeoutMs,
|
|
2206
|
+
onProgress,
|
|
2207
|
+
signal
|
|
2208
|
+
);
|
|
2209
|
+
onProgress?.(1, "completed");
|
|
2210
|
+
return {
|
|
2211
|
+
output: run2.steps ?? {},
|
|
2212
|
+
flowRunId: run2.id,
|
|
2213
|
+
flowDuration: Date.now() - startTime,
|
|
2214
|
+
run: run2
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
onProgress?.(1, "completed");
|
|
2218
|
+
const flowDuration = Date.now() - startTime;
|
|
2219
|
+
const flowRunId = extractFlowRunId(response);
|
|
2220
|
+
let run;
|
|
2221
|
+
if (flowRunId) {
|
|
2222
|
+
try {
|
|
2223
|
+
run = await this.getFlowRun(flowRunId, signal);
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
const reason = error instanceof Error ? error.message : "unknown error";
|
|
2226
|
+
this.logger.warn(`Could not fetch run record for "${flowRunId}": ${reason}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return {
|
|
2230
|
+
output: response,
|
|
2231
|
+
flowRunId,
|
|
2232
|
+
flowDuration,
|
|
2233
|
+
run
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
async pollUntilTerminal(flowId, projectId, createdAfter, timeoutMs, onProgress, signal) {
|
|
2237
|
+
const deadline = Date.now() + timeoutMs;
|
|
2238
|
+
let attempt = 0;
|
|
2239
|
+
let lastSeenRunId;
|
|
2240
|
+
while (Date.now() < deadline) {
|
|
2241
|
+
if (signal?.aborted) throw new Error("Pipeline execution was cancelled");
|
|
2242
|
+
attempt += 1;
|
|
2243
|
+
const run = await this.findLatestRunForFlow(flowId, projectId, { createdAfter, signal });
|
|
2244
|
+
if (run) {
|
|
2245
|
+
lastSeenRunId = run.id;
|
|
2246
|
+
onProgress?.(attempt, run.status);
|
|
2247
|
+
if (TERMINAL_FLOW_RUN_STATUSES.includes(run.status)) {
|
|
2248
|
+
return await this.getFlowRun(run.id, signal);
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
onProgress?.(attempt, "queued");
|
|
2252
|
+
}
|
|
2253
|
+
await sleep(ASYNC_POLL_INTERVAL_MS, signal);
|
|
2254
|
+
}
|
|
2255
|
+
throw new Error(
|
|
2256
|
+
`Activepieces flow "${flowId}" did not finish within ${Math.round(timeoutMs / 1e3)}s` + (lastSeenRunId ? ` (last run: ${lastSeenRunId})` : "")
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
async request(path, options = {}) {
|
|
2260
|
+
const url = buildUrl(this.baseUrl, path, options.query);
|
|
2261
|
+
const headers = {
|
|
2262
|
+
Accept: "application/json"
|
|
2263
|
+
};
|
|
2264
|
+
if (options.includeAuth !== false) {
|
|
2265
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
2266
|
+
}
|
|
2267
|
+
let body;
|
|
2268
|
+
if (options.body !== void 0) {
|
|
2269
|
+
headers["Content-Type"] = "application/json";
|
|
2270
|
+
body = JSON.stringify(options.body);
|
|
2271
|
+
}
|
|
2272
|
+
const controller = new AbortController();
|
|
2273
|
+
const timeoutHandle = options.timeoutMs ? setTimeout(() => controller.abort(new Error("Request timed out")), options.timeoutMs) : void 0;
|
|
2274
|
+
const onAbort = () => controller.abort(options.signal?.reason ?? new Error("Aborted"));
|
|
2275
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2276
|
+
try {
|
|
2277
|
+
const response = await fetch(url, {
|
|
2278
|
+
method: options.method ?? "GET",
|
|
2279
|
+
headers,
|
|
2280
|
+
body,
|
|
2281
|
+
signal: controller.signal
|
|
2282
|
+
});
|
|
2283
|
+
if (!response.ok) {
|
|
2284
|
+
throw await buildHttpError(response, url);
|
|
2285
|
+
}
|
|
2286
|
+
const text = await response.text();
|
|
2287
|
+
if (!text) return void 0;
|
|
2288
|
+
try {
|
|
2289
|
+
return JSON.parse(text);
|
|
2290
|
+
} catch {
|
|
2291
|
+
return text;
|
|
2292
|
+
}
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
if (isAbortError(error)) {
|
|
2295
|
+
if (options.signal?.aborted) {
|
|
2296
|
+
throw new Error("Pipeline execution was cancelled");
|
|
2297
|
+
}
|
|
2298
|
+
throw new Error(`Activepieces request timed out: ${path}`);
|
|
2299
|
+
}
|
|
2300
|
+
throw error;
|
|
2301
|
+
} finally {
|
|
2302
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
2303
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
function stripTrailingSlash(url) {
|
|
2308
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
2309
|
+
}
|
|
2310
|
+
function buildUrl(baseUrl, path, query) {
|
|
2311
|
+
const url = new URL(`${baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
|
|
2312
|
+
if (query) {
|
|
2313
|
+
for (const [key, value] of Object.entries(query)) {
|
|
2314
|
+
if (value === void 0) continue;
|
|
2315
|
+
url.searchParams.set(key, String(value));
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return url.toString();
|
|
2319
|
+
}
|
|
2320
|
+
function webhookPath(flowId, mode) {
|
|
2321
|
+
const encoded = encodeURIComponent(flowId);
|
|
2322
|
+
return mode === "sync" ? `/webhooks/${encoded}/sync` : `/webhooks/${encoded}`;
|
|
2323
|
+
}
|
|
2324
|
+
function extractFlowRunId(response) {
|
|
2325
|
+
if (!response || typeof response !== "object") return void 0;
|
|
2326
|
+
const obj = response;
|
|
2327
|
+
for (const key of ["runId", "flowRunId"]) {
|
|
2328
|
+
const value = obj[key];
|
|
2329
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2330
|
+
}
|
|
2331
|
+
return void 0;
|
|
2332
|
+
}
|
|
2333
|
+
async function buildHttpError(response, url) {
|
|
2334
|
+
let detail = "";
|
|
2335
|
+
try {
|
|
2336
|
+
const text = await response.text();
|
|
2337
|
+
if (text) detail = text.length > 500 ? text.slice(0, 500) + "..." : text;
|
|
2338
|
+
} catch {
|
|
2339
|
+
}
|
|
2340
|
+
const baseMessage = `Activepieces request failed (${response.status} ${response.statusText}) for ${url}`;
|
|
2341
|
+
if (response.status === 401) {
|
|
2342
|
+
return new Error("Invalid Activepieces API key. Please update your API key in plugin settings.");
|
|
2343
|
+
}
|
|
2344
|
+
if (response.status === 403) {
|
|
2345
|
+
return new Error(
|
|
2346
|
+
"Activepieces API key does not have permission to access this resource. Check that the key belongs to the correct project."
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
if (response.status === 404) {
|
|
2350
|
+
return new Error(
|
|
2351
|
+
"Activepieces resource not found. Verify the flow ID, project ID, and that the flow is published."
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
if (response.status === 429) {
|
|
2355
|
+
return new Error("Activepieces rate limit exceeded. Please wait and try again.");
|
|
2356
|
+
}
|
|
2357
|
+
if (response.status >= 500) {
|
|
2358
|
+
return new Error(
|
|
2359
|
+
`${baseMessage}. The Activepieces server returned an error.${detail ? ` Detail: ${detail}` : ""}`
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
return new Error(`${baseMessage}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
2363
|
+
}
|
|
2364
|
+
function isAbortError(error) {
|
|
2365
|
+
if (!error || typeof error !== "object") return false;
|
|
2366
|
+
const name = error.name;
|
|
2367
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
2368
|
+
}
|
|
2369
|
+
function sleep(ms, signal) {
|
|
2370
|
+
return new Promise((resolve, reject) => {
|
|
2371
|
+
if (signal?.aborted) {
|
|
2372
|
+
reject(new Error("Pipeline execution was cancelled"));
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const handle = setTimeout(() => {
|
|
2376
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2377
|
+
resolve();
|
|
2378
|
+
}, ms);
|
|
2379
|
+
const onAbort = () => {
|
|
2380
|
+
clearTimeout(handle);
|
|
2381
|
+
reject(new Error("Pipeline execution was cancelled"));
|
|
2382
|
+
};
|
|
2383
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// ../../plugin/dist/helpers/ssrf-guard.js
|
|
2388
|
+
import { isIP } from "net";
|
|
2389
|
+
function isSafeWebhookUrl(rawUrl) {
|
|
2390
|
+
let url;
|
|
2391
|
+
try {
|
|
2392
|
+
url = new URL(rawUrl);
|
|
2393
|
+
} catch {
|
|
2394
|
+
return false;
|
|
2395
|
+
}
|
|
2396
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
let host = url.hostname.toLowerCase();
|
|
2400
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
2401
|
+
host = host.slice(1, -1);
|
|
2402
|
+
}
|
|
2403
|
+
if (CLOUD_METADATA_HOSTNAMES.has(host)) {
|
|
2404
|
+
return false;
|
|
2405
|
+
}
|
|
2406
|
+
const ipKind = isIP(host);
|
|
2407
|
+
if (ipKind === 4 && isPrivateIPv4(host)) return false;
|
|
2408
|
+
if (ipKind === 6 && isPrivateIPv6(host)) return false;
|
|
2409
|
+
return true;
|
|
2410
|
+
}
|
|
2411
|
+
var CLOUD_METADATA_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2412
|
+
// GCP
|
|
2413
|
+
"metadata.google.internal",
|
|
2414
|
+
"metadata.goog",
|
|
2415
|
+
// AWS
|
|
2416
|
+
"instance-data",
|
|
2417
|
+
"instance-data.ec2.internal",
|
|
2418
|
+
// Azure
|
|
2419
|
+
"metadata.azure.internal"
|
|
2420
|
+
]);
|
|
2421
|
+
function isPrivateIPv4(ip) {
|
|
2422
|
+
const parts = ip.split(".").map((p) => Number(p));
|
|
2423
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return false;
|
|
2424
|
+
const [a, b] = parts;
|
|
2425
|
+
if (a === 10) return true;
|
|
2426
|
+
if (a === 127) return true;
|
|
2427
|
+
if (a === 0) return true;
|
|
2428
|
+
if (a === 169 && b === 254) return true;
|
|
2429
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
2430
|
+
if (a === 192 && b === 168) return true;
|
|
2431
|
+
if (a === 192 && b === 0 && parts[2] === 0) return true;
|
|
2432
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
2433
|
+
if (a >= 224) return true;
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
function isPrivateIPv6(ip) {
|
|
2437
|
+
const lower = ip.toLowerCase();
|
|
2438
|
+
if (lower === "::1" || lower === "::") return true;
|
|
2439
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
2440
|
+
const firstHextet = lower.split(":", 1)[0];
|
|
2441
|
+
if (/^[0-9a-f]{1,4}$/.test(firstHextet)) {
|
|
2442
|
+
const group0 = parseInt(firstHextet, 16);
|
|
2443
|
+
if ((group0 & 65472) === 65152) return true;
|
|
2444
|
+
}
|
|
2445
|
+
if (lower.startsWith("::ffff:")) {
|
|
2446
|
+
const v4 = lower.slice("::ffff:".length);
|
|
2447
|
+
if (isIP(v4) === 4) return isPrivateIPv4(v4);
|
|
2448
|
+
}
|
|
2449
|
+
return false;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/utils/payload-builder.ts
|
|
2453
|
+
function assertSafeGithubRepoUrl(rawUrl) {
|
|
2454
|
+
const trimmed = rawUrl.trim();
|
|
2455
|
+
let parsed;
|
|
2456
|
+
try {
|
|
2457
|
+
parsed = new URL(trimmed);
|
|
2458
|
+
} catch {
|
|
2459
|
+
throw new Error("Data repository URL is not a valid URL.");
|
|
2460
|
+
}
|
|
2461
|
+
if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "github.com") {
|
|
2462
|
+
throw new Error("Data repository URL must be an https://github.com/... repository URL.");
|
|
2463
|
+
}
|
|
2464
|
+
if (!isSafeWebhookUrl(trimmed)) {
|
|
2465
|
+
throw new Error("Data repository URL is not safe to forward (SSRF guard blocked the destination host).");
|
|
2466
|
+
}
|
|
2467
|
+
return trimmed;
|
|
2468
|
+
}
|
|
2469
|
+
function buildFlowPayload(options) {
|
|
2470
|
+
const { work, request, existing, config } = options;
|
|
2471
|
+
const payload = {
|
|
2472
|
+
metadata: {
|
|
2473
|
+
workId: work.id,
|
|
2474
|
+
workName: work.name,
|
|
2475
|
+
workSlug: work.slug,
|
|
2476
|
+
workDescription: work.description,
|
|
2477
|
+
prompt: request.prompt,
|
|
2478
|
+
generationMethod: request.generationMethod,
|
|
2479
|
+
targetItems: config.target_items ?? DEFAULT_TARGET_ITEMS
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2482
|
+
if (config.pass_existing_items !== false && existing.items.length > 0) {
|
|
2483
|
+
payload.existingSummary = {
|
|
2484
|
+
totalItems: existing.items.length,
|
|
2485
|
+
categories: existing.categories.map((c) => c.name),
|
|
2486
|
+
tags: existing.tags.map((t) => t.name),
|
|
2487
|
+
sampleItems: existing.items.slice(0, 20).map((item) => ({
|
|
2488
|
+
name: item.name,
|
|
2489
|
+
url: item.source_url
|
|
2490
|
+
}))
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
if (config.pass_repo_access && config.repo_url) {
|
|
2494
|
+
const repoUrl = assertSafeGithubRepoUrl(String(config.repo_url));
|
|
2495
|
+
payload.dataSource = {
|
|
2496
|
+
type: "github-repo",
|
|
2497
|
+
repoUrl,
|
|
2498
|
+
accessToken: config.repo_access_token,
|
|
2499
|
+
branch: config.repo_branch || "data",
|
|
2500
|
+
path: "items/"
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
if (config.flow_params && typeof config.flow_params === "object") {
|
|
2504
|
+
payload.flowParams = config.flow_params;
|
|
2505
|
+
}
|
|
2506
|
+
return payload;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/utils/result-parser.ts
|
|
2510
|
+
function parseActivepiecesOutput(raw) {
|
|
2511
|
+
if (!raw || typeof raw !== "object" && typeof raw !== "string") {
|
|
2512
|
+
throw new Error("Activepieces flow returned empty or non-object output");
|
|
2513
|
+
}
|
|
2514
|
+
const output = normalizeOutput(raw);
|
|
2515
|
+
const items = parseItems(output.items ?? []);
|
|
2516
|
+
if (items.length === 0) {
|
|
2517
|
+
throw new Error(
|
|
2518
|
+
"Activepieces flow returned a valid response but with no usable items. Ensure the flow ends with a Return Response action that emits { items: [...] }. Inspect the flow run logs in the Activepieces dashboard."
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
const categories = parseCategories(output.categories, items);
|
|
2522
|
+
const tags = parseTags(output.tags, items);
|
|
2523
|
+
const brands = parseBrands(output.brands, items);
|
|
2524
|
+
return { items, categories, tags, brands };
|
|
2525
|
+
}
|
|
2526
|
+
function normalizeOutput(raw) {
|
|
2527
|
+
if (typeof raw === "string") {
|
|
2528
|
+
const parsed = tryParse(raw);
|
|
2529
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
2530
|
+
return normalizeOutput(parsed);
|
|
2531
|
+
}
|
|
2532
|
+
const preview = raw.length > 200 ? raw.slice(0, 200) + "..." : raw;
|
|
2533
|
+
throw new Error(
|
|
2534
|
+
`Activepieces flow returned a string that is not valid JSON. Ensure the Return Response action emits a JSON object with { items: [...] }. Received: ${preview}`
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
if (Array.isArray(raw)) {
|
|
2538
|
+
return { items: raw };
|
|
2539
|
+
}
|
|
2540
|
+
const obj = raw;
|
|
2541
|
+
if ("items" in obj) {
|
|
2542
|
+
const items = Array.isArray(obj.items) ? obj.items : [];
|
|
2543
|
+
return {
|
|
2544
|
+
items,
|
|
2545
|
+
categories: obj.categories,
|
|
2546
|
+
tags: obj.tags,
|
|
2547
|
+
brands: obj.brands
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
for (const key of ["body", "output", "result", "data", "response", "value"]) {
|
|
2551
|
+
if (obj[key] != null && obj[key] !== "") {
|
|
2552
|
+
return normalizeOutput(obj[key]);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof obj.message === "string") {
|
|
2556
|
+
const parsed = tryParse(obj.message);
|
|
2557
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
2558
|
+
return normalizeOutput(parsed);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
throw new Error(
|
|
2562
|
+
`Activepieces flow output does not contain an "items" array. Expected format: { items: [...] } or a direct array of items. Received keys: ${Object.keys(obj).join(", ")}`
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
function tryParse(str) {
|
|
2566
|
+
try {
|
|
2567
|
+
return JSON.parse(str);
|
|
2568
|
+
} catch {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
function sanitizeItemUrl(rawUrl) {
|
|
2573
|
+
if (typeof rawUrl !== "string" || rawUrl.trim() === "") return "";
|
|
2574
|
+
const trimmed = rawUrl.trim();
|
|
2575
|
+
return isSafeWebhookUrl(trimmed) ? trimmed : "";
|
|
2576
|
+
}
|
|
2577
|
+
function parseItems(rawItems) {
|
|
2578
|
+
const items = [];
|
|
2579
|
+
for (const raw of rawItems) {
|
|
2580
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2581
|
+
if (!raw.name || typeof raw.name !== "string") continue;
|
|
2582
|
+
const rawSourceUrl = typeof raw.url === "string" ? raw.url : typeof raw.source_url === "string" ? raw.source_url : "";
|
|
2583
|
+
const item = {
|
|
2584
|
+
name: raw.name.trim(),
|
|
2585
|
+
description: typeof raw.description === "string" ? raw.description.trim() : "",
|
|
2586
|
+
source_url: sanitizeItemUrl(rawSourceUrl),
|
|
2587
|
+
markdown: typeof raw.content === "string" ? raw.content : void 0,
|
|
2588
|
+
category: typeof raw.category === "string" ? raw.category.trim() : "",
|
|
2589
|
+
tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : [],
|
|
2590
|
+
brand: typeof raw.brand === "string" ? raw.brand.trim() : void 0,
|
|
2591
|
+
images: Array.isArray(raw.images) ? raw.images.filter((i) => typeof i === "string").map((i) => sanitizeItemUrl(i)).filter((i) => i !== "") : void 0
|
|
2592
|
+
};
|
|
2593
|
+
items.push(item);
|
|
2594
|
+
}
|
|
2595
|
+
return items;
|
|
2596
|
+
}
|
|
2597
|
+
function parseCategories(rawCategories, items) {
|
|
2598
|
+
const categoryNames = /* @__PURE__ */ new Set();
|
|
2599
|
+
if (Array.isArray(rawCategories)) {
|
|
2600
|
+
for (const cat of rawCategories) {
|
|
2601
|
+
if (cat && typeof cat.name === "string" && cat.name.trim()) {
|
|
2602
|
+
categoryNames.add(cat.name.trim());
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
for (const item of items) {
|
|
2607
|
+
if (typeof item.category === "string" && item.category) {
|
|
2608
|
+
categoryNames.add(item.category);
|
|
2609
|
+
} else if (Array.isArray(item.category)) {
|
|
2610
|
+
for (const c of item.category) {
|
|
2611
|
+
if (typeof c === "string" && c.trim()) categoryNames.add(c.trim());
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return Array.from(categoryNames).map((name) => {
|
|
2616
|
+
const explicit = rawCategories?.find((c) => c.name === name);
|
|
2617
|
+
return {
|
|
2618
|
+
id: name.toLowerCase().replace(/\s+/g, "-"),
|
|
2619
|
+
name,
|
|
2620
|
+
description: explicit?.description
|
|
2621
|
+
};
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
function parseTags(rawTags, items) {
|
|
2625
|
+
const tagNames = /* @__PURE__ */ new Set();
|
|
2626
|
+
if (Array.isArray(rawTags)) {
|
|
2627
|
+
for (const tag of rawTags) {
|
|
2628
|
+
if (tag && typeof tag.name === "string" && tag.name.trim()) {
|
|
2629
|
+
tagNames.add(tag.name.trim());
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
for (const item of items) {
|
|
2634
|
+
if (Array.isArray(item.tags)) {
|
|
2635
|
+
for (const t of item.tags) {
|
|
2636
|
+
if (typeof t === "string") tagNames.add(t);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return Array.from(tagNames).map((name) => ({
|
|
2641
|
+
id: name.toLowerCase().replace(/\s+/g, "-"),
|
|
2642
|
+
name
|
|
2643
|
+
}));
|
|
2644
|
+
}
|
|
2645
|
+
function parseBrands(rawBrands, items) {
|
|
2646
|
+
const brandNames = /* @__PURE__ */ new Set();
|
|
2647
|
+
if (Array.isArray(rawBrands)) {
|
|
2648
|
+
for (const brand of rawBrands) {
|
|
2649
|
+
if (brand && typeof brand.name === "string" && brand.name.trim()) {
|
|
2650
|
+
brandNames.add(brand.name.trim());
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
for (const item of items) {
|
|
2655
|
+
if (item.brand) {
|
|
2656
|
+
if (typeof item.brand === "string") {
|
|
2657
|
+
brandNames.add(item.brand);
|
|
2658
|
+
} else if (typeof item.brand === "object" && item.brand.name) {
|
|
2659
|
+
brandNames.add(item.brand.name);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return Array.from(brandNames).map((name) => {
|
|
2664
|
+
const explicit = rawBrands?.find((b) => b.name === name);
|
|
2665
|
+
return {
|
|
2666
|
+
id: name.toLowerCase().replace(/\s+/g, "-"),
|
|
2667
|
+
name,
|
|
2668
|
+
website: explicit?.url
|
|
2669
|
+
};
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
function deduplicateItems(newItems, existingItemNames) {
|
|
2673
|
+
const existingSet = new Set(existingItemNames.map((n) => n.toLowerCase().trim()));
|
|
2674
|
+
return newItems.filter((item) => !existingSet.has(item.name.toLowerCase().trim()));
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/utils/pipeline-helpers.ts
|
|
2678
|
+
var runtime = createPipelineRuntimeHelpers({
|
|
2679
|
+
stepDefinitions: STEP_DEFINITIONS,
|
|
2680
|
+
totalSteps: ACTIVEPIECES_STEP_IDS.length
|
|
2681
|
+
});
|
|
2682
|
+
var initializeState = runtime.initializeState;
|
|
2683
|
+
var updateStepState = runtime.updateStepState;
|
|
2684
|
+
var reportProgress = runtime.reportProgress;
|
|
2685
|
+
var resolveSettings = runtime.resolveSettings;
|
|
2686
|
+
var finalizeCompletedState = runtime.finalizeCompletedState;
|
|
2687
|
+
var buildErrorResult = runtime.buildErrorResult;
|
|
2688
|
+
var buildCancelledResult = runtime.buildCancelledResult;
|
|
2689
|
+
function buildMetrics(startTime, duration, itemCount, flowMetrics) {
|
|
2690
|
+
return runtime.buildMetrics(startTime, duration, itemCount, flowMetrics);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// src/form-schema.ts
|
|
2694
|
+
var REPO_ACCESS_CONDITION = { field: "pass_repo_access", operator: "eq", value: true };
|
|
2695
|
+
function getFormFields() {
|
|
2696
|
+
return [
|
|
2697
|
+
{
|
|
2698
|
+
name: "flow_id",
|
|
2699
|
+
type: "text",
|
|
2700
|
+
label: "Activepieces Flow ID",
|
|
2701
|
+
description: "Override the default flow for this generation (optional if set in plugin settings)",
|
|
2702
|
+
placeholder: "e.g., 8h7g6f5e4d3c2b1a",
|
|
2703
|
+
group: "flow"
|
|
2704
|
+
},
|
|
2705
|
+
{
|
|
2706
|
+
name: "webhook_mode",
|
|
2707
|
+
type: "select",
|
|
2708
|
+
label: "Webhook Execution Mode",
|
|
2709
|
+
description: "Sync waits for the flow to finish; async returns immediately and is not supported by this pipeline",
|
|
2710
|
+
defaultValue: "sync",
|
|
2711
|
+
options: [
|
|
2712
|
+
{ label: "Synchronous (recommended)", value: "sync" },
|
|
2713
|
+
{ label: "Asynchronous", value: "async" }
|
|
2714
|
+
],
|
|
2715
|
+
group: "flow"
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
name: "target_items",
|
|
2719
|
+
type: "number",
|
|
2720
|
+
label: "Target Items",
|
|
2721
|
+
description: "Target number of new items to generate",
|
|
2722
|
+
defaultValue: DEFAULT_TARGET_ITEMS,
|
|
2723
|
+
validation: { min: 1, max: 500 },
|
|
2724
|
+
group: "flow"
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
name: "flow_timeout",
|
|
2728
|
+
type: "number",
|
|
2729
|
+
label: "Flow Timeout (minutes)",
|
|
2730
|
+
description: "Maximum time to wait for the Activepieces flow to complete",
|
|
2731
|
+
defaultValue: 60,
|
|
2732
|
+
validation: { min: 1, max: 120 },
|
|
2733
|
+
group: "flow"
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
name: "pass_existing_items",
|
|
2737
|
+
type: "boolean",
|
|
2738
|
+
label: "Pass Existing Items Summary",
|
|
2739
|
+
description: "Include a summary of existing work items in flow input for deduplication",
|
|
2740
|
+
defaultValue: true,
|
|
2741
|
+
group: "data"
|
|
2742
|
+
},
|
|
2743
|
+
{
|
|
2744
|
+
name: "pass_repo_access",
|
|
2745
|
+
type: "boolean",
|
|
2746
|
+
label: "Pass Data Repository Access",
|
|
2747
|
+
description: "Grant the Activepieces flow read access to the work data repository. \u26A0 The repository URL and access token are sent in the webhook payload and become visible to every step of your flow and to the Activepieces run logs.",
|
|
2748
|
+
defaultValue: false,
|
|
2749
|
+
group: "data"
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
name: "repo_url",
|
|
2753
|
+
type: "text",
|
|
2754
|
+
label: "Data Repository URL",
|
|
2755
|
+
description: "GitHub repository URL containing work data",
|
|
2756
|
+
placeholder: "e.g., https://github.com/org/repo",
|
|
2757
|
+
group: "data",
|
|
2758
|
+
showIf: REPO_ACCESS_CONDITION,
|
|
2759
|
+
requiredIf: REPO_ACCESS_CONDITION
|
|
2760
|
+
},
|
|
2761
|
+
{
|
|
2762
|
+
name: "repo_access_token",
|
|
2763
|
+
type: "password",
|
|
2764
|
+
label: "Repository Access Token",
|
|
2765
|
+
description: "Read-only token for the data repository. \u26A0 This token is forwarded to Activepieces in the webhook body and persists in the flow run logs and dashboard. Use a fine-grained, short-lived, read-only token scoped to this repository only \u2014 never a classic PAT with broad scopes.",
|
|
2766
|
+
placeholder: "ghp_... (fine-grained, read-only)",
|
|
2767
|
+
group: "data",
|
|
2768
|
+
showIf: REPO_ACCESS_CONDITION,
|
|
2769
|
+
requiredIf: REPO_ACCESS_CONDITION
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
name: "repo_branch",
|
|
2773
|
+
type: "text",
|
|
2774
|
+
label: "Repository Branch",
|
|
2775
|
+
description: "Branch to read from",
|
|
2776
|
+
defaultValue: "data",
|
|
2777
|
+
group: "data",
|
|
2778
|
+
showIf: REPO_ACCESS_CONDITION
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
name: "capture_screenshots",
|
|
2782
|
+
type: "boolean",
|
|
2783
|
+
label: "Capture Screenshots",
|
|
2784
|
+
description: "Take screenshots for generated items (requires a screenshot plugin)",
|
|
2785
|
+
defaultValue: false,
|
|
2786
|
+
group: "features"
|
|
2787
|
+
},
|
|
2788
|
+
{
|
|
2789
|
+
name: "flow_params",
|
|
2790
|
+
type: "json",
|
|
2791
|
+
label: "Custom Flow Parameters",
|
|
2792
|
+
description: "Additional key-value parameters to pass to the Activepieces flow",
|
|
2793
|
+
defaultValue: {},
|
|
2794
|
+
group: "advanced"
|
|
2795
|
+
}
|
|
2796
|
+
];
|
|
2797
|
+
}
|
|
2798
|
+
function getFormGroups() {
|
|
2799
|
+
return [
|
|
2800
|
+
{
|
|
2801
|
+
name: "flow",
|
|
2802
|
+
title: "Flow Configuration",
|
|
2803
|
+
description: "Configure which Activepieces flow to execute and how many items to generate",
|
|
2804
|
+
order: 0
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
name: "data",
|
|
2808
|
+
title: "Data Passing",
|
|
2809
|
+
description: "Configure how data is passed to the Activepieces flow",
|
|
2810
|
+
order: 1,
|
|
2811
|
+
collapsible: true,
|
|
2812
|
+
collapsed: false
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
name: "features",
|
|
2816
|
+
title: "Features",
|
|
2817
|
+
description: "Optional generation features",
|
|
2818
|
+
order: 2,
|
|
2819
|
+
collapsible: true,
|
|
2820
|
+
collapsed: true
|
|
2821
|
+
},
|
|
2822
|
+
{
|
|
2823
|
+
name: "advanced",
|
|
2824
|
+
title: "Advanced",
|
|
2825
|
+
description: "Advanced flow parameters",
|
|
2826
|
+
order: 3,
|
|
2827
|
+
collapsible: true,
|
|
2828
|
+
collapsed: true
|
|
2829
|
+
}
|
|
2830
|
+
];
|
|
2831
|
+
}
|
|
2832
|
+
function validateFormInput(values) {
|
|
2833
|
+
if (values.pass_repo_access) {
|
|
2834
|
+
if (!values.repo_url || typeof values.repo_url !== "string" || values.repo_url.trim() === "") {
|
|
2835
|
+
return {
|
|
2836
|
+
valid: false,
|
|
2837
|
+
errors: [{ path: "repo_url", message: "Repository URL is required when repository access is enabled" }]
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
let parsedRepoUrl = null;
|
|
2841
|
+
try {
|
|
2842
|
+
parsedRepoUrl = new URL(values.repo_url.trim());
|
|
2843
|
+
} catch {
|
|
2844
|
+
parsedRepoUrl = null;
|
|
2845
|
+
}
|
|
2846
|
+
if (!parsedRepoUrl || parsedRepoUrl.protocol !== "https:" || parsedRepoUrl.hostname.toLowerCase() !== "github.com") {
|
|
2847
|
+
return {
|
|
2848
|
+
valid: false,
|
|
2849
|
+
errors: [{ path: "repo_url", message: "Repository URL must be an https://github.com/... URL" }]
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (!values.repo_access_token || typeof values.repo_access_token !== "string" || values.repo_access_token.trim() === "") {
|
|
2853
|
+
return {
|
|
2854
|
+
valid: false,
|
|
2855
|
+
errors: [
|
|
2856
|
+
{
|
|
2857
|
+
path: "repo_access_token",
|
|
2858
|
+
message: "Repository access token is required when repository access is enabled"
|
|
2859
|
+
}
|
|
2860
|
+
]
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
return { valid: true };
|
|
2865
|
+
}
|
|
2866
|
+
function getDefaultValues(fields) {
|
|
2867
|
+
const defaults = {};
|
|
2868
|
+
for (const field of fields) {
|
|
2869
|
+
if (field.defaultValue !== void 0) {
|
|
2870
|
+
defaults[field.name] = field.defaultValue;
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
return defaults;
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// src/readme.ts
|
|
2877
|
+
var README = [
|
|
2878
|
+
"## What is the Activepieces Automation plugin?",
|
|
2879
|
+
"",
|
|
2880
|
+
"This plugin delegates work generation steps to an [Activepieces](https://www.activepieces.com) flow.",
|
|
2881
|
+
"Activepieces is the AI-first, open-source automation platform for every team.",
|
|
2882
|
+
"Instead of running the built-in generation pipeline, you build your own automated flow in Activepieces and this plugin triggers it during the appropriate generation stage.",
|
|
2883
|
+
"",
|
|
2884
|
+
"## Why use it?",
|
|
2885
|
+
"",
|
|
2886
|
+
"- **Full control** \u2014 design your own generation logic visually in Activepieces",
|
|
2887
|
+
"- **Custom data sources** \u2014 connect to any of Activepieces 280+ pieces (APIs, databases, scrapers)",
|
|
2888
|
+
"- **Flexible AI models** \u2014 call any AI provider via Activepieces actions",
|
|
2889
|
+
"- **Sync execution** \u2014 webhook trigger waits synchronously for flow output",
|
|
2890
|
+
"- **Deduplication** \u2014 existing work items are summarized and passed to the flow to avoid duplicates",
|
|
2891
|
+
"",
|
|
2892
|
+
"## How it works",
|
|
2893
|
+
"",
|
|
2894
|
+
"1. **Validate** \u2014 checks your API key against the Activepieces API and verifies the target flow is enabled",
|
|
2895
|
+
"2. **Prepare Payload** \u2014 builds the input with work metadata, existing items summary, and custom parameters",
|
|
2896
|
+
"3. **Execute Flow** \u2014 calls the Activepieces flow webhook (synchronously by default) with the payload",
|
|
2897
|
+
"4. **Collect Results** \u2014 parses the flow output into structured work items with categories, tags, and brands",
|
|
2898
|
+
"5. **Capture Screenshots** \u2014 optionally captures screenshots for generated items using the configured screenshot provider",
|
|
2899
|
+
"6. **Cleanup** \u2014 releases resources",
|
|
2900
|
+
"",
|
|
2901
|
+
"## API integration",
|
|
2902
|
+
"",
|
|
2903
|
+
"The plugin talks to the Activepieces REST API directly (no SDK):",
|
|
2904
|
+
"",
|
|
2905
|
+
"- **Auth**: `Authorization: Bearer <API_KEY>` (generated from the Platform Dashboard)",
|
|
2906
|
+
"- **Base URL**: `https://cloud.activepieces.com/api/v1` (configurable for self-hosted)",
|
|
2907
|
+
"- **Flows**: `GET /flows/{id}` to validate the flow",
|
|
2908
|
+
"- **Webhook trigger**: `POST /webhooks/{flowId}/sync` (sync mode) or `POST /webhooks/{flowId}` (async)",
|
|
2909
|
+
"- **Flow runs**: `GET /flow-runs` to inspect recent executions",
|
|
2910
|
+
"",
|
|
2911
|
+
"## Data passing strategies",
|
|
2912
|
+
"",
|
|
2913
|
+
"- **Inline summary** (default) \u2014 work metadata and a sample of existing items are sent in the webhook payload",
|
|
2914
|
+
"- **GitHub repository** \u2014 pass a repo URL and access token so the flow can read the full dataset directly",
|
|
2915
|
+
"",
|
|
2916
|
+
"## Security",
|
|
2917
|
+
"",
|
|
2918
|
+
"\u26A0 **Repository access tokens are forwarded to Activepieces in the webhook body**, not in the platform Authorization header.",
|
|
2919
|
+
"That means anything you put under `dataSource` becomes visible to:",
|
|
2920
|
+
"",
|
|
2921
|
+
"- every action and branch downstream of the Webhook trigger in your flow (via `{{trigger.body.dataSource.accessToken}}`)",
|
|
2922
|
+
"- the Activepieces flow run inspector and dashboard",
|
|
2923
|
+
"- any logging, AI prompt, or HTTP request your flow performs that interpolates the trigger body",
|
|
2924
|
+
"",
|
|
2925
|
+
"If you enable **Pass Data Repository Access**:",
|
|
2926
|
+
"",
|
|
2927
|
+
"- Use a [GitHub fine-grained personal access token](https://github.com/settings/tokens?type=beta) scoped to *only* the data repository, with **Contents: Read-only** and the shortest expiration you can tolerate.",
|
|
2928
|
+
"- Do **not** use a classic PAT, an org-admin token, or any token with `repo` write scope.",
|
|
2929
|
+
"- Rotate the token after a generation run if your threat model requires it.",
|
|
2930
|
+
"- Audit your Activepieces flow to ensure no step logs the trigger body verbatim or sends it to a third-party.",
|
|
2931
|
+
"",
|
|
2932
|
+
"When this option is enabled, the plugin emits a warning to the pipeline logs at the prepare-payload step so the operator has an audit trail.",
|
|
2933
|
+
"",
|
|
2934
|
+
"## Flow input & output",
|
|
2935
|
+
"",
|
|
2936
|
+
"Your Activepieces flow must start with a **Webhook trigger** so it can be invoked by this plugin.",
|
|
2937
|
+
"The webhook trigger receives the JSON payload as the trigger output, accessible in subsequent steps.",
|
|
2938
|
+
"",
|
|
2939
|
+
"<details>",
|
|
2940
|
+
"<summary><strong>Input your flow receives</strong></summary>",
|
|
2941
|
+
"",
|
|
2942
|
+
"```json",
|
|
2943
|
+
"{",
|
|
2944
|
+
' "metadata": {',
|
|
2945
|
+
' "workId": "dir-123",',
|
|
2946
|
+
' "workName": "Best AI Tools",',
|
|
2947
|
+
' "workSlug": "best-ai-tools",',
|
|
2948
|
+
' "workDescription": "A curated list of the best AI tools",',
|
|
2949
|
+
' "prompt": "Find the best AI writing tools",',
|
|
2950
|
+
' "generationMethod": "create-update",',
|
|
2951
|
+
' "targetItems": 50',
|
|
2952
|
+
" },",
|
|
2953
|
+
' "existingSummary": {',
|
|
2954
|
+
' "totalItems": 10,',
|
|
2955
|
+
' "categories": ["Writing", "Image Generation"],',
|
|
2956
|
+
' "tags": ["free", "open-source"],',
|
|
2957
|
+
' "sampleItems": [',
|
|
2958
|
+
' { "name": "ChatGPT", "url": "https://chat.openai.com" }',
|
|
2959
|
+
" ]",
|
|
2960
|
+
" },",
|
|
2961
|
+
' "flowParams": {}',
|
|
2962
|
+
"}",
|
|
2963
|
+
"```",
|
|
2964
|
+
"",
|
|
2965
|
+
"- `existingSummary` is optional \u2014 absent if the work is empty or the user disabled it",
|
|
2966
|
+
"- `flowParams` contains any custom key-value pairs passed from the Advanced form section",
|
|
2967
|
+
"- `metadata.targetItems` is the number of new items the user wants",
|
|
2968
|
+
"",
|
|
2969
|
+
"</details>",
|
|
2970
|
+
"",
|
|
2971
|
+
"<details>",
|
|
2972
|
+
"<summary><strong>Output your flow must return</strong></summary>",
|
|
2973
|
+
"",
|
|
2974
|
+
"End your flow with a **Return Response** action that returns a JSON object with an `items` array:",
|
|
2975
|
+
"",
|
|
2976
|
+
"```json",
|
|
2977
|
+
"{",
|
|
2978
|
+
' "items": [',
|
|
2979
|
+
" {",
|
|
2980
|
+
' "name": "Tool Name",',
|
|
2981
|
+
' "description": "A concise description (1-3 sentences)",',
|
|
2982
|
+
' "url": "https://example.com",',
|
|
2983
|
+
' "category": "Category Name",',
|
|
2984
|
+
' "tags": ["tag1", "tag2"],',
|
|
2985
|
+
' "content": "Optional longer markdown content",',
|
|
2986
|
+
' "brand": "Optional brand name",',
|
|
2987
|
+
' "images": ["https://example.com/screenshot.png"]',
|
|
2988
|
+
" }",
|
|
2989
|
+
" ]",
|
|
2990
|
+
"}",
|
|
2991
|
+
"```",
|
|
2992
|
+
"",
|
|
2993
|
+
"**Required fields:** `name`, `description`, `url`, `category`, `tags`",
|
|
2994
|
+
"",
|
|
2995
|
+
"**Optional fields:** `content` (stored as markdown), `brand`, `images`",
|
|
2996
|
+
"",
|
|
2997
|
+
"</details>",
|
|
2998
|
+
"",
|
|
2999
|
+
"## Getting started",
|
|
3000
|
+
"",
|
|
3001
|
+
"1. Build a flow in [Activepieces](https://www.activepieces.com) starting with a **Webhook trigger**",
|
|
3002
|
+
"2. Add steps to fetch and shape data, then end with a **Return Response** action",
|
|
3003
|
+
"3. Generate an API key from the Activepieces Platform Dashboard",
|
|
3004
|
+
"4. Enable this plugin and enter your API key, base URL, and project ID in plugin settings",
|
|
3005
|
+
"5. When generating a work, select Activepieces as the pipeline and provide the flow ID"
|
|
3006
|
+
].join("\n");
|
|
3007
|
+
|
|
3008
|
+
// src/activepieces.plugin.ts
|
|
3009
|
+
var ActivepiecesPlugin = class {
|
|
3010
|
+
constructor() {
|
|
3011
|
+
this.id = "activepieces";
|
|
3012
|
+
this.name = "Activepieces Automation";
|
|
3013
|
+
this.version = "1.0.0";
|
|
3014
|
+
this.category = "pipeline";
|
|
3015
|
+
this.capabilities = ["pipeline", "form-schema-provider"];
|
|
3016
|
+
this.configurationMode = "user-required";
|
|
3017
|
+
this.handledConfigFields = ["*"];
|
|
3018
|
+
this.settingsSchema = {
|
|
3019
|
+
type: "object",
|
|
3020
|
+
properties: {
|
|
3021
|
+
apiKey: {
|
|
3022
|
+
type: "string",
|
|
3023
|
+
title: "Activepieces API Key",
|
|
3024
|
+
description: "API key generated from your Activepieces Platform Dashboard (Platform or Enterprise edition required)",
|
|
3025
|
+
"x-secret": true,
|
|
3026
|
+
"x-scope": "user",
|
|
3027
|
+
"x-envVar": "ACTIVEPIECES_API_KEY"
|
|
3028
|
+
},
|
|
3029
|
+
baseUrl: {
|
|
3030
|
+
type: "string",
|
|
3031
|
+
title: "Activepieces API Base URL",
|
|
3032
|
+
description: "Custom Activepieces instance API base URL (leave empty for Activepieces Cloud)",
|
|
3033
|
+
default: DEFAULT_BASE_URL,
|
|
3034
|
+
"x-scope": "user"
|
|
3035
|
+
},
|
|
3036
|
+
projectId: {
|
|
3037
|
+
type: "string",
|
|
3038
|
+
title: "Default Project ID",
|
|
3039
|
+
description: "Activepieces project ID \u2014 required to list flows and inspect runs",
|
|
3040
|
+
"x-scope": "user"
|
|
3041
|
+
},
|
|
3042
|
+
defaultFlowId: {
|
|
3043
|
+
type: "string",
|
|
3044
|
+
title: "Default Flow ID",
|
|
3045
|
+
description: "Default Activepieces flow to invoke when not specified in the generator form",
|
|
3046
|
+
"x-scope": "user"
|
|
3047
|
+
}
|
|
3048
|
+
},
|
|
3049
|
+
required: ["apiKey"]
|
|
3050
|
+
};
|
|
3051
|
+
this.context = null;
|
|
3052
|
+
this._lastState = null;
|
|
3053
|
+
this._lastAbortController = null;
|
|
3054
|
+
}
|
|
3055
|
+
// ── IPlugin lifecycle ──────────────────────────────────────────────
|
|
3056
|
+
async onLoad(context) {
|
|
3057
|
+
this.context = context;
|
|
3058
|
+
context.logger.log("Activepieces Automation plugin loaded");
|
|
3059
|
+
}
|
|
3060
|
+
async onUnload() {
|
|
3061
|
+
await this.cancel();
|
|
3062
|
+
this.context = null;
|
|
3063
|
+
}
|
|
3064
|
+
async healthCheck() {
|
|
3065
|
+
return {
|
|
3066
|
+
status: "healthy",
|
|
3067
|
+
message: "Activepieces Automation plugin is ready",
|
|
3068
|
+
checkedAt: Date.now()
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
async isAvailable(settings) {
|
|
3072
|
+
const resolved = settings || {};
|
|
3073
|
+
const apiKey = resolved.apiKey;
|
|
3074
|
+
return !!apiKey && apiKey.trim().length > 0;
|
|
3075
|
+
}
|
|
3076
|
+
async validateSettings(settings) {
|
|
3077
|
+
const apiKey = settings.apiKey;
|
|
3078
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
3079
|
+
return { valid: false, errors: [{ path: "apiKey", message: "Activepieces API key is required" }] };
|
|
3080
|
+
}
|
|
3081
|
+
return { valid: true };
|
|
3082
|
+
}
|
|
3083
|
+
async validateConnection(rawSettings) {
|
|
3084
|
+
const settings = this.flattenSettings(rawSettings);
|
|
3085
|
+
const apiKey = settings.apiKey;
|
|
3086
|
+
if (!apiKey) {
|
|
3087
|
+
return { success: false, message: "Activepieces API key is required" };
|
|
3088
|
+
}
|
|
3089
|
+
try {
|
|
3090
|
+
const baseUrl = resolveSafeBaseUrl(settings.baseUrl);
|
|
3091
|
+
const client = new ActivepiecesClient({
|
|
3092
|
+
apiKey,
|
|
3093
|
+
baseUrl,
|
|
3094
|
+
logger: this.context?.logger ?? console
|
|
3095
|
+
});
|
|
3096
|
+
const projectId = settings.projectId;
|
|
3097
|
+
const flowId = settings.defaultFlowId;
|
|
3098
|
+
if (flowId) {
|
|
3099
|
+
const flow = await client.validateFlow(flowId);
|
|
3100
|
+
return {
|
|
3101
|
+
success: true,
|
|
3102
|
+
message: `Connected to Activepieces. Flow "${flow.id}" is ${flow.status ?? "available"}.`
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
await client.ping(projectId);
|
|
3106
|
+
return { success: true, message: "Connected to Activepieces successfully." };
|
|
3107
|
+
} catch (error) {
|
|
3108
|
+
const message = error instanceof Error ? error.message : "Connection validation failed";
|
|
3109
|
+
return { success: false, message };
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
getManifest() {
|
|
3113
|
+
return {
|
|
3114
|
+
id: this.id,
|
|
3115
|
+
name: this.name,
|
|
3116
|
+
version: this.version,
|
|
3117
|
+
description: "Pipeline plugin that delegates work generation steps to Activepieces flows",
|
|
3118
|
+
category: this.category,
|
|
3119
|
+
capabilities: [...this.capabilities],
|
|
3120
|
+
author: { name: "Ever Works Team" },
|
|
3121
|
+
license: "AGPL-3.0",
|
|
3122
|
+
builtIn: true,
|
|
3123
|
+
autoEnable: false,
|
|
3124
|
+
visibility: "public",
|
|
3125
|
+
selectableProviderCategories: ["screenshot"],
|
|
3126
|
+
uiHints: {
|
|
3127
|
+
includeInOnboarding: true,
|
|
3128
|
+
onboardingPriority: 3,
|
|
3129
|
+
completionFields: ["apiKey", "defaultFlowId"],
|
|
3130
|
+
onboardingDescription: "Connect Activepieces to delegate work generation steps to AI-first automation flows."
|
|
3131
|
+
},
|
|
3132
|
+
readme: README,
|
|
3133
|
+
homepage: "https://www.activepieces.com/docs/endpoints/overview",
|
|
3134
|
+
icon: {
|
|
3135
|
+
type: "url",
|
|
3136
|
+
value: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/webp/activepieces.webp"
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
// ── IFormSchemaProvider ─────────────────────────────────────────────
|
|
3141
|
+
getFormFields() {
|
|
3142
|
+
return getFormFields();
|
|
3143
|
+
}
|
|
3144
|
+
getFormGroups() {
|
|
3145
|
+
return getFormGroups();
|
|
3146
|
+
}
|
|
3147
|
+
validateFormInput(values) {
|
|
3148
|
+
return validateFormInput(values);
|
|
3149
|
+
}
|
|
3150
|
+
getDefaultValues() {
|
|
3151
|
+
return getDefaultValues(this.getFormFields());
|
|
3152
|
+
}
|
|
3153
|
+
// ── IPipelinePlugin ─────────────────────────────────────────────────
|
|
3154
|
+
getStepDefinitions() {
|
|
3155
|
+
return STEP_DEFINITIONS;
|
|
3156
|
+
}
|
|
3157
|
+
async execute(work, request, existing, options, onProgress) {
|
|
3158
|
+
const startTime = Date.now();
|
|
3159
|
+
const abortController = new AbortController();
|
|
3160
|
+
if (options?.signal) {
|
|
3161
|
+
if (options.signal.aborted) {
|
|
3162
|
+
abortController.abort();
|
|
3163
|
+
} else {
|
|
3164
|
+
options.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const signal = abortController.signal;
|
|
3168
|
+
let state = initializeState();
|
|
3169
|
+
this._lastState = state;
|
|
3170
|
+
this._lastAbortController = abortController;
|
|
3171
|
+
const setState = (stepId, status, error) => {
|
|
3172
|
+
state = updateStepState(state, stepId, status, error);
|
|
3173
|
+
this._lastState = state;
|
|
3174
|
+
};
|
|
3175
|
+
const handleError = (error) => {
|
|
3176
|
+
const { result, state: s } = buildErrorResult(state, error, startTime);
|
|
3177
|
+
state = s;
|
|
3178
|
+
this._lastState = state;
|
|
3179
|
+
return result;
|
|
3180
|
+
};
|
|
3181
|
+
const handleCancel = () => {
|
|
3182
|
+
const { result, state: s } = buildCancelledResult(state, startTime);
|
|
3183
|
+
state = s;
|
|
3184
|
+
this._lastState = state;
|
|
3185
|
+
return result;
|
|
3186
|
+
};
|
|
3187
|
+
const logger = this.context?.logger ?? console;
|
|
3188
|
+
const userId = work.user?.id;
|
|
3189
|
+
if (!userId) {
|
|
3190
|
+
return handleError(new Error("User ID is required"));
|
|
3191
|
+
}
|
|
3192
|
+
try {
|
|
3193
|
+
const pluginSettings = await resolveSettings(this.context, userId, work.id);
|
|
3194
|
+
const config = request.config || {};
|
|
3195
|
+
const apSettings = this.resolveApSettings(pluginSettings, config);
|
|
3196
|
+
const flowId = this.resolveFlowId(config, apSettings);
|
|
3197
|
+
if (!flowId) {
|
|
3198
|
+
return handleError(
|
|
3199
|
+
new Error(
|
|
3200
|
+
"No Activepieces flow ID provided. Set it in the generator form or in plugin settings (defaultFlowId)."
|
|
3201
|
+
)
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
const apClient = new ActivepiecesClient({
|
|
3205
|
+
apiKey: apSettings.apiKey,
|
|
3206
|
+
baseUrl: apSettings.baseUrl,
|
|
3207
|
+
logger
|
|
3208
|
+
});
|
|
3209
|
+
setState("validate-activepieces", "running");
|
|
3210
|
+
reportProgress(onProgress, 0, 5, "Validate Activepieces Connection");
|
|
3211
|
+
await apClient.validateFlow(flowId);
|
|
3212
|
+
setState("validate-activepieces", "completed");
|
|
3213
|
+
if (signal.aborted) return handleCancel();
|
|
3214
|
+
setState("prepare-payload", "running");
|
|
3215
|
+
reportProgress(onProgress, 1, 10, "Prepare Flow Payload");
|
|
3216
|
+
const payload = buildFlowPayload({ work, request, existing, config });
|
|
3217
|
+
logger.log(
|
|
3218
|
+
`Payload prepared: ${payload.metadata.targetItems} target items, ${payload.existingSummary?.totalItems ?? 0} existing items, dataSource=${payload.dataSource?.type ?? "none"}`
|
|
3219
|
+
);
|
|
3220
|
+
if (payload.dataSource?.accessToken) {
|
|
3221
|
+
logger.warn(
|
|
3222
|
+
"Activepieces payload contains a repository access token. This token will be visible in the Activepieces flow run logs and dashboard, and to every downstream step of your flow. Use a fine-grained, short-lived, read-only token scoped to this repository only."
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
setState("prepare-payload", "completed");
|
|
3226
|
+
if (signal.aborted) return handleCancel();
|
|
3227
|
+
setState("execute-flow", "running");
|
|
3228
|
+
reportProgress(onProgress, 2, 15, "Execute Activepieces Flow", `Triggering flow "${flowId}"...`);
|
|
3229
|
+
const execResult = await apClient.executeFlow(
|
|
3230
|
+
flowId,
|
|
3231
|
+
payload,
|
|
3232
|
+
apSettings,
|
|
3233
|
+
(attempt, status) => {
|
|
3234
|
+
const percent = Math.min(15 + Math.round(attempt / 60 * 55), 70);
|
|
3235
|
+
reportProgress(
|
|
3236
|
+
onProgress,
|
|
3237
|
+
2,
|
|
3238
|
+
percent,
|
|
3239
|
+
"Execute Activepieces Flow",
|
|
3240
|
+
`Flow ${status} (poll #${attempt})...`
|
|
3241
|
+
);
|
|
3242
|
+
},
|
|
3243
|
+
signal
|
|
3244
|
+
);
|
|
3245
|
+
logger.log(
|
|
3246
|
+
`Activepieces flow completed. duration: ${execResult.flowDuration ?? "unknown"}ms` + (execResult.flowRunId ? `, runId: ${execResult.flowRunId}` : "") + (execResult.run?.status ? `, status: ${execResult.run.status}` : "")
|
|
3247
|
+
);
|
|
3248
|
+
if (execResult.run?.status && execResult.run.status !== "SUCCEEDED") {
|
|
3249
|
+
const failedStep = execResult.run.failedStep?.displayName ?? execResult.run.failedStep?.name;
|
|
3250
|
+
const reason = failedStep ? `failed at step "${failedStep}" (status: ${execResult.run.status})` : `did not succeed (status: ${execResult.run.status})`;
|
|
3251
|
+
if (apSettings.webhookMode === "async") {
|
|
3252
|
+
throw new Error(`Activepieces flow run ${reason}. Inspect the run in the Activepieces dashboard.`);
|
|
3253
|
+
}
|
|
3254
|
+
logger.warn(
|
|
3255
|
+
`Activepieces sync flow returned a body but the run record ${reason}. Proceeding with the response body \u2014 verify the items below are correct.`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
setState("execute-flow", "completed");
|
|
3259
|
+
if (signal.aborted) return handleCancel();
|
|
3260
|
+
setState("collect-results", "running");
|
|
3261
|
+
reportProgress(onProgress, 3, 75, "Collect & Validate Results");
|
|
3262
|
+
logger.log(
|
|
3263
|
+
`Raw Activepieces output type: ${typeof execResult.output}, value: ${redactedPreview(execResult.output)}`
|
|
3264
|
+
);
|
|
3265
|
+
const parsed = parseActivepiecesOutput(execResult.output);
|
|
3266
|
+
const existingNames = existing.items.map((i) => i.name);
|
|
3267
|
+
const items = deduplicateItems(parsed.items, existingNames);
|
|
3268
|
+
logger.log(
|
|
3269
|
+
`Collected ${parsed.items.length} items from Activepieces, ${parsed.items.length - items.length} duplicates removed, ${items.length} new items`
|
|
3270
|
+
);
|
|
3271
|
+
setState("collect-results", "completed");
|
|
3272
|
+
const screenshotWarnings = await this.captureScreenshots(
|
|
3273
|
+
setState,
|
|
3274
|
+
request,
|
|
3275
|
+
options?.execContext,
|
|
3276
|
+
items,
|
|
3277
|
+
{ userId, workId: work.id },
|
|
3278
|
+
signal,
|
|
3279
|
+
onProgress,
|
|
3280
|
+
logger
|
|
3281
|
+
);
|
|
3282
|
+
setState("cleanup", "running");
|
|
3283
|
+
reportProgress(onProgress, 5, 95, "Cleanup");
|
|
3284
|
+
setState("cleanup", "completed");
|
|
3285
|
+
reportProgress(onProgress, 6, 100, "Complete");
|
|
3286
|
+
const duration = Date.now() - startTime;
|
|
3287
|
+
const flowMetrics = {
|
|
3288
|
+
flowId,
|
|
3289
|
+
flowRunId: execResult.flowRunId,
|
|
3290
|
+
flowDuration: execResult.flowDuration,
|
|
3291
|
+
webhookMode: apSettings.webhookMode,
|
|
3292
|
+
runStatus: execResult.run?.status,
|
|
3293
|
+
failedStepName: execResult.run?.failedStep?.name,
|
|
3294
|
+
finishTime: execResult.run?.finishTime
|
|
3295
|
+
};
|
|
3296
|
+
state = finalizeCompletedState(state);
|
|
3297
|
+
this._lastState = state;
|
|
3298
|
+
return buildSuccessPipelineResult(
|
|
3299
|
+
{
|
|
3300
|
+
items,
|
|
3301
|
+
categories: parsed.categories,
|
|
3302
|
+
tags: parsed.tags,
|
|
3303
|
+
brands: parsed.brands,
|
|
3304
|
+
collections: []
|
|
3305
|
+
},
|
|
3306
|
+
{
|
|
3307
|
+
metrics: buildMetrics(startTime, duration, items.length, flowMetrics),
|
|
3308
|
+
duration,
|
|
3309
|
+
stepsCompleted: state.completedSteps.length,
|
|
3310
|
+
totalSteps: ACTIVEPIECES_STEP_IDS.length,
|
|
3311
|
+
state,
|
|
3312
|
+
warnings: screenshotWarnings.length > 0 ? screenshotWarnings : void 0
|
|
3313
|
+
}
|
|
3314
|
+
);
|
|
3315
|
+
} catch (error) {
|
|
3316
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
3317
|
+
logger.error(`Activepieces pipeline failed: ${err.message}`);
|
|
3318
|
+
return handleError(err);
|
|
3319
|
+
} finally {
|
|
3320
|
+
if (this._lastAbortController === abortController) {
|
|
3321
|
+
this._lastAbortController = null;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
async cancel() {
|
|
3326
|
+
this._lastAbortController?.abort();
|
|
3327
|
+
}
|
|
3328
|
+
getState() {
|
|
3329
|
+
return this._lastState;
|
|
3330
|
+
}
|
|
3331
|
+
// ── Private helpers ────────────────────────────────────────────────
|
|
3332
|
+
resolveApSettings(pluginSettings, config) {
|
|
3333
|
+
const apiKey = pluginSettings.apiKey;
|
|
3334
|
+
if (!apiKey) {
|
|
3335
|
+
throw new Error("Activepieces API key is not configured. Please set it in plugin settings.");
|
|
3336
|
+
}
|
|
3337
|
+
const timeoutMinutes = Math.min(Math.max(1, Number(config.flow_timeout) || 60), 120);
|
|
3338
|
+
const webhookMode = normalizeWebhookMode(config.webhook_mode);
|
|
3339
|
+
return {
|
|
3340
|
+
apiKey,
|
|
3341
|
+
// Security (SSRF): see resolveSafeBaseUrl — reject internal/metadata hosts before
|
|
3342
|
+
// the tenant-supplied baseUrl reaches the API-key-bearing client.
|
|
3343
|
+
baseUrl: resolveSafeBaseUrl(pluginSettings.baseUrl),
|
|
3344
|
+
projectId: pluginSettings.projectId,
|
|
3345
|
+
defaultFlowId: pluginSettings.defaultFlowId,
|
|
3346
|
+
webhookMode,
|
|
3347
|
+
timeoutMs: timeoutMinutes * 60 * 1e3
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
resolveFlowId(config, settings) {
|
|
3351
|
+
const fromConfig = config.flow_id;
|
|
3352
|
+
if (fromConfig && fromConfig.trim()) return fromConfig.trim();
|
|
3353
|
+
return settings.defaultFlowId;
|
|
3354
|
+
}
|
|
3355
|
+
async captureScreenshots(setState, request, execContext, items, facadeOptions, signal, onProgress, logger) {
|
|
3356
|
+
const shouldCapture = (request.config || {}).capture_screenshots === true;
|
|
3357
|
+
const screenshotFacade = execContext?.screenshotFacade;
|
|
3358
|
+
if (!shouldCapture || items.length === 0 || signal.aborted || !screenshotFacade) {
|
|
3359
|
+
setState("capture-screenshots", "skipped");
|
|
3360
|
+
return [];
|
|
3361
|
+
}
|
|
3362
|
+
if (!screenshotFacade.isAvailable()) {
|
|
3363
|
+
setState("capture-screenshots", "skipped");
|
|
3364
|
+
return ["Screenshot provider is not configured. Enable a screenshot plugin to capture item images."];
|
|
3365
|
+
}
|
|
3366
|
+
setState("capture-screenshots", "running");
|
|
3367
|
+
reportProgress(onProgress, 4, 80, "Capture Screenshots");
|
|
3368
|
+
const errors = [];
|
|
3369
|
+
const itemsNeedingImages = items.filter(
|
|
3370
|
+
(item) => item.source_url && (!item.images || item.images.length === 0)
|
|
3371
|
+
);
|
|
3372
|
+
for (const item of itemsNeedingImages) {
|
|
3373
|
+
if (signal.aborted) break;
|
|
3374
|
+
if (typeof item.source_url !== "string" || !isSafeWebhookUrl(item.source_url)) {
|
|
3375
|
+
logger.warn(`Skipping screenshot for "${item.name}": source_url failed SSRF guard`);
|
|
3376
|
+
errors.push(`source_url for "${item.name}" is not a safe URL`);
|
|
3377
|
+
continue;
|
|
3378
|
+
}
|
|
3379
|
+
try {
|
|
3380
|
+
const result = await screenshotFacade.getSmartImage(
|
|
3381
|
+
{ url: item.source_url, itemName: item.name },
|
|
3382
|
+
facadeOptions
|
|
3383
|
+
);
|
|
3384
|
+
if (result.primaryImage) {
|
|
3385
|
+
item.images = [result.primaryImage, ...item.images || []];
|
|
3386
|
+
}
|
|
3387
|
+
} catch (error) {
|
|
3388
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
3389
|
+
logger.warn(`Failed to capture image for ${item.name}: ${reason}`);
|
|
3390
|
+
errors.push(reason);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
setState(
|
|
3394
|
+
"capture-screenshots",
|
|
3395
|
+
errors.length > 0 && errors.length === itemsNeedingImages.length ? "failed" : "completed"
|
|
3396
|
+
);
|
|
3397
|
+
if (errors.length > 0) {
|
|
3398
|
+
const providerName = await screenshotFacade.getActiveProviderName?.(facadeOptions);
|
|
3399
|
+
const label = providerName ? `Screenshot capture (${providerName})` : "Screenshot capture";
|
|
3400
|
+
const unique = [...new Set(errors)];
|
|
3401
|
+
return [`${label} failed for ${errors.length} item(s): ${unique.join("; ")}`];
|
|
3402
|
+
}
|
|
3403
|
+
return [];
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Flatten a ResolvedSettings map into plain key->value pairs.
|
|
3407
|
+
*/
|
|
3408
|
+
flattenSettings(settings) {
|
|
3409
|
+
const flat = {};
|
|
3410
|
+
for (const [key, entry] of Object.entries(settings)) {
|
|
3411
|
+
if (entry && typeof entry === "object" && "value" in entry) {
|
|
3412
|
+
flat[key] = entry.value;
|
|
3413
|
+
} else {
|
|
3414
|
+
flat[key] = entry;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
return flat;
|
|
3418
|
+
}
|
|
3419
|
+
};
|
|
3420
|
+
function resolveSafeBaseUrl(rawBaseUrl) {
|
|
3421
|
+
const baseUrl = typeof rawBaseUrl === "string" && rawBaseUrl.trim() !== "" ? rawBaseUrl.trim() : DEFAULT_BASE_URL;
|
|
3422
|
+
if (!isSafeWebhookUrl(baseUrl)) {
|
|
3423
|
+
throw new Error("Activepieces API base URL is not safe to call (SSRF guard blocked the destination host).");
|
|
3424
|
+
}
|
|
3425
|
+
return baseUrl;
|
|
3426
|
+
}
|
|
3427
|
+
var WEBHOOK_MODES = ["sync", "async"];
|
|
3428
|
+
function normalizeWebhookMode(value) {
|
|
3429
|
+
return typeof value === "string" && WEBHOOK_MODES.includes(value) ? value : "sync";
|
|
3430
|
+
}
|
|
3431
|
+
var SECRET_KEY_PATTERN = /(access[_-]?token|^token$|secret|api[_-]?key|password|authorization|bearer)/i;
|
|
3432
|
+
function redactSecrets(value, depth = 0) {
|
|
3433
|
+
if (depth > 6 || value === null || typeof value !== "object") return value;
|
|
3434
|
+
if (Array.isArray(value)) return value.map((entry) => redactSecrets(entry, depth + 1));
|
|
3435
|
+
const out = {};
|
|
3436
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
3437
|
+
out[key] = SECRET_KEY_PATTERN.test(key) ? "[redacted]" : redactSecrets(entry, depth + 1);
|
|
3438
|
+
}
|
|
3439
|
+
return out;
|
|
3440
|
+
}
|
|
3441
|
+
function redactedPreview(value, maxLength = 500) {
|
|
3442
|
+
try {
|
|
3443
|
+
const s = JSON.stringify(redactSecrets(value));
|
|
3444
|
+
if (s === void 0) return typeof value;
|
|
3445
|
+
return s.length > maxLength ? `${s.slice(0, maxLength)}...` : s;
|
|
3446
|
+
} catch {
|
|
3447
|
+
return typeof value;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
var activepieces_plugin_default = ActivepiecesPlugin;
|
|
3451
|
+
export {
|
|
3452
|
+
ActivepiecesPlugin,
|
|
3453
|
+
activepieces_plugin_default as default
|
|
3454
|
+
};
|
|
3455
|
+
/**
|
|
3456
|
+
* One header finding of the licence scan (plan §2.6:324-325, FR-54): the
|
|
3457
|
+
* `SPDX-License-Identifier:` / `@license` tag a file declares in its first
|
|
3458
|
+
* `LICENSE_HEADER_SCAN_BYTES` bytes.
|
|
3459
|
+
*
|
|
3460
|
+
* **Gap, named rather than guessed:** plan §3.1:437 writes the column's third
|
|
3461
|
+
* field as `headerFindings: …` without a shape. `path` is certain — it is the
|
|
3462
|
+
* first field of the scan's input — and the detected identifier is the second
|
|
3463
|
+
* fact the scan produces, so both are declared; anything further an
|
|
3464
|
+
* implementation adds is additive.
|
|
3465
|
+
*/
|