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