@synap-core/workspace-templates 0.12.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26148 -23931
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +20851 -21496
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiVersion": "synap/v1",
|
|
3
|
+
"kind": "capability",
|
|
4
|
+
"meta": {
|
|
5
|
+
"slug": "unipile-linkedin",
|
|
6
|
+
"name": "Unipile — LinkedIn & Messaging",
|
|
7
|
+
"description": "Search LinkedIn, view profiles, send connection requests and first messages, and read or reply to conversations on LinkedIn, WhatsApp, Telegram and Instagram through Unipile. Needs a Unipile v2 API key and a connected account.",
|
|
8
|
+
"tags": [
|
|
9
|
+
"capability"
|
|
10
|
+
],
|
|
11
|
+
"isPublic": true,
|
|
12
|
+
"syncByDefault": false
|
|
13
|
+
},
|
|
14
|
+
"spec": {
|
|
15
|
+
"key": "unipile-linkedin",
|
|
16
|
+
"name": "Unipile — Messaging & LinkedIn",
|
|
17
|
+
"description": "Unified messaging + LinkedIn outreach via Unipile API v2 (API-key connector). Stores the Unipile v2 API key in the vault and registers one HTTP tool against the SINGLE global v2 host https://api.unipile.com/v2 (no per-tenant DSN — v2 removed it), authenticating with the X-API-KEY header. Skills: (a) provider-agnostic messaging — list accounts, list conversations, list messages, send into an existing thread, start a chat with one person on any account (unipile_start_chat) (works for LinkedIn/WhatsApp/Telegram/Instagram); (b) LinkedIn-specific — search people (read + persist variants), get a profile, send a connection request (v2 relation-request), start a NEW conversation (cold outreach via the CLASSIC_PRIMARY inbox), list relations + sent invitations, list the company pages an account administers (linkedin_list_company_pages), publish a post now (unipile_publish_post, the publish_post intent, one action per account from unipile_list_accounts). v2 notes baked in: account_id is a PATH segment (/{account_id}/...), send endpoints carry a /send suffix, LinkedIn cold-outreach uses /inboxes/CLASSIC_PRIMARY/chats/send with users_ids, invitations are /users/me/relation-requests, search is split per resource, and list envelopes are { data, total_count, next_cursor }. PRECONDITION: a Unipile v2 account (dashboardv2.unipile.com — v1 keys do NOT authenticate) with at least one connected provider account. Provider-agnostic skills take an explicit accountId (call unipile_list_accounts first). A pod app reaches unipile_send_message, unipile_start_chat, linkedin_send_first_message (send_message; the last one by the person's LinkedIn profile, 1st-degree connections only), linkedin_send_invite (request_connection, by their profile) and unipile_publish_post (publish_post) through their inputMaps: one action per account unipile_list_accounts lists, the recipient resolved by the pod ($to). LinkedIn skills take an accountId too: no skill code carries an install-time value, so the pod can update every install to a new version of this template. Skills call the tool by NAME via callProvider('unipile_linkedin', ...); the dispatcher injects the vaulted X-API-KEY server-side. key/tool name kept as unipile-linkedin/unipile_linkedin for backward compat with existing installs.",
|
|
18
|
+
"params": [
|
|
19
|
+
{
|
|
20
|
+
"name": "unipileApiKey",
|
|
21
|
+
"label": "Unipile v2 API key",
|
|
22
|
+
"type": "text",
|
|
23
|
+
"required": true,
|
|
24
|
+
"description": "The Unipile v2 X-API-KEY (from dashboardv2.unipile.com — a v1 key will NOT work against api.unipile.com/v2). Stored server-encrypted in the vault; never persisted in the template."
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"vault": [
|
|
28
|
+
{
|
|
29
|
+
"ref": "unipileApiKeySecret",
|
|
30
|
+
"name": "{{name}} Unipile API key",
|
|
31
|
+
"value": "{{unipileApiKey}}",
|
|
32
|
+
"type": "api_key",
|
|
33
|
+
"description": "Unipile v2 X-API-KEY for the connector."
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"tools": [
|
|
37
|
+
{
|
|
38
|
+
"name": "unipile_linkedin",
|
|
39
|
+
"kind": "api",
|
|
40
|
+
"description": "Authenticated HTTP access to the Unipile v2 API at the single global host https://api.unipile.com/v2 (no DSN). The applier injects the vaulted key into the X-API-KEY header; its credentialRef is remapped to the runtime vault://<id> at apply time.",
|
|
41
|
+
"credentialRef": "unipileApiKeySecret",
|
|
42
|
+
"executor": "is-agent",
|
|
43
|
+
"config": {
|
|
44
|
+
"baseUrl": "https://api.unipile.com/v2",
|
|
45
|
+
"auth": {
|
|
46
|
+
"in": "header",
|
|
47
|
+
"name": "X-API-KEY",
|
|
48
|
+
"prefix": ""
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"skills": [
|
|
54
|
+
{
|
|
55
|
+
"name": "unipile_create_hosted_auth",
|
|
56
|
+
"intent": "connect_account",
|
|
57
|
+
"kind": "code",
|
|
58
|
+
"scope": "pod",
|
|
59
|
+
"description": "Mints a Unipile hosted-auth link so a user can CONNECT a provider account (LinkedIn/Google/Telegram) — the account-onboarding primitive. Lets a product run its 'Connect' flow WITHOUT holding the Unipile key (it stays vaulted). Args: { providers: string[] (e.g. ['linkedin']), redirectUri: string, expiresOn?: string (ISO), state?: string, name?: string }. Returns { link }.",
|
|
60
|
+
"requires": [
|
|
61
|
+
"unipile_linkedin"
|
|
62
|
+
],
|
|
63
|
+
"executionMode": "sync",
|
|
64
|
+
"timeoutSeconds": 60,
|
|
65
|
+
"parameters": {
|
|
66
|
+
"providers": "array",
|
|
67
|
+
"redirectUri": "string",
|
|
68
|
+
"expiresOn": "string?",
|
|
69
|
+
"state": "string?",
|
|
70
|
+
"name": "string?"
|
|
71
|
+
},
|
|
72
|
+
"code": "// Mint a Unipile v2 hosted-auth link so a user can connect a provider account\n// (LinkedIn/Google/Telegram) WITHOUT the consuming product ever holding the\n// Unipile key — the key stays vaulted here and is injected server-side. This is\n// what lets the Arch's \"Connect LinkedIn\" flow run key-free. POST /v2/auth/link.\nconst providers = Array.isArray(args?.providers)\n ? args.providers\n : (args?.providers ? [String(args.providers)] : []);\nconst redirectUri = String(args?.redirectUri ?? '').trim();\nconst expiresOn = String(args?.expiresOn ?? '').trim();\nconst state = typeof args?.state === 'string' ? args.state : undefined;\nif (!providers.length) throw new Error('unipile_create_hosted_auth: providers is required');\nif (!redirectUri) throw new Error('unipile_create_hosted_auth: redirectUri is required');\n\nconst body = { providers, redirect_uri: redirectUri };\nif (expiresOn) body.expires_on = expiresOn;\nif (state) body.state = state;\nif (typeof args?.name === 'string' && args.name) body.name = args.name;\n\nconst res = await callProvider('unipile_linkedin', 'POST', '/auth/link', body);\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\nif (!ok) throw new Error(`unipile_create_hosted_auth: provider rejected (status ${res?.status ?? 'unknown'})`);\nconst payload = res?.body ?? res;\nreturn { link: payload?.link ?? payload?.url ?? null };\n",
|
|
73
|
+
"metadata": {
|
|
74
|
+
"verbType": "read"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "unipile_get_account",
|
|
79
|
+
"intent": "fetch_record",
|
|
80
|
+
"kind": "code",
|
|
81
|
+
"scope": "pod",
|
|
82
|
+
"description": "Fetches one connected account by id (v2 GET /accounts/{id}). Pure read — verify a just-connected account exists before persisting it. Args: { accountId: string }.",
|
|
83
|
+
"requires": [
|
|
84
|
+
"unipile_linkedin"
|
|
85
|
+
],
|
|
86
|
+
"executionMode": "sync",
|
|
87
|
+
"timeoutSeconds": 60,
|
|
88
|
+
"parameters": {
|
|
89
|
+
"accountId": "string"
|
|
90
|
+
},
|
|
91
|
+
"code": "// Fetch one connected account by id (v2 GET /accounts/{id}). Pure read — used to\n// verify a just-connected account materialized before the product persists it.\nconst accountId = String(args?.accountId ?? '').trim();\nif (!accountId) throw new Error('unipile_get_account: accountId is required');\nconst res = await callProvider('unipile_linkedin', 'GET', `/accounts/${encodeURIComponent(accountId)}`);\nreturn res?.body ?? res;\n"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "unipile_list_accounts",
|
|
95
|
+
"intent": "list_records",
|
|
96
|
+
"kind": "declarative",
|
|
97
|
+
"scope": "pod",
|
|
98
|
+
"description": "Lists every Unipile-connected account (LinkedIn, WhatsApp, Telegram, Instagram, Google, Outlook, IMAP) with its id, provider (lowercase: linkedin, instagram, whatsapp, telegram, google, outlook, imap), name and status. No args. Call this FIRST to discover which accountId to pass to the other skills. Read-only: the account source of the send verbs, so a pod app sees one action per account.",
|
|
99
|
+
"requires": [
|
|
100
|
+
"unipile_linkedin"
|
|
101
|
+
],
|
|
102
|
+
"executionMode": "sync",
|
|
103
|
+
"timeoutSeconds": 30,
|
|
104
|
+
"metadata": {
|
|
105
|
+
"readOnly": true
|
|
106
|
+
},
|
|
107
|
+
"parameters": {},
|
|
108
|
+
"providerSpec": {
|
|
109
|
+
"tool": "unipile_linkedin",
|
|
110
|
+
"method": "GET",
|
|
111
|
+
"pathTemplate": "/accounts/",
|
|
112
|
+
"responseShape": {
|
|
113
|
+
"collectionPath": "data",
|
|
114
|
+
"collectionAs": "accounts",
|
|
115
|
+
"item": {
|
|
116
|
+
"id": "id",
|
|
117
|
+
"name": "name",
|
|
118
|
+
"provider": "provider",
|
|
119
|
+
"status": "status"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "unipile_list_conversations",
|
|
126
|
+
"intent": "list_records",
|
|
127
|
+
"kind": "declarative",
|
|
128
|
+
"scope": "pod",
|
|
129
|
+
"description": "Lists conversations (chats) for one connected account, any provider. Args: { accountId: string (from unipile_list_accounts), cursor?: string }. v2: account_id is a path segment.",
|
|
130
|
+
"requires": [
|
|
131
|
+
"unipile_linkedin"
|
|
132
|
+
],
|
|
133
|
+
"executionMode": "sync",
|
|
134
|
+
"timeoutSeconds": 30,
|
|
135
|
+
"parameters": {
|
|
136
|
+
"accountId": "string",
|
|
137
|
+
"cursor": "string?"
|
|
138
|
+
},
|
|
139
|
+
"providerSpec": {
|
|
140
|
+
"tool": "unipile_linkedin",
|
|
141
|
+
"method": "GET",
|
|
142
|
+
"pathTemplate": "/{{accountId}}/inboxes/CLASSIC_PRIMARY/chats",
|
|
143
|
+
"responseShape": {
|
|
144
|
+
"collectionPath": "data",
|
|
145
|
+
"collectionAs": "conversations",
|
|
146
|
+
"scalar": {
|
|
147
|
+
"cursor": "payload.next_cursor"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"query": {
|
|
151
|
+
"limit": "25",
|
|
152
|
+
"cursor": "{{cursor}}"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "unipile_list_messages",
|
|
158
|
+
"intent": "list_records",
|
|
159
|
+
"kind": "declarative",
|
|
160
|
+
"scope": "pod",
|
|
161
|
+
"description": "Lists the messages in one conversation thread. Args: { threadId: string (from unipile_list_conversations), accountId: string }. v2: both ids are path segments.",
|
|
162
|
+
"requires": [
|
|
163
|
+
"unipile_linkedin"
|
|
164
|
+
],
|
|
165
|
+
"executionMode": "sync",
|
|
166
|
+
"timeoutSeconds": 30,
|
|
167
|
+
"parameters": {
|
|
168
|
+
"threadId": "string",
|
|
169
|
+
"accountId": "string"
|
|
170
|
+
},
|
|
171
|
+
"providerSpec": {
|
|
172
|
+
"tool": "unipile_linkedin",
|
|
173
|
+
"method": "GET",
|
|
174
|
+
"pathTemplate": "/{{accountId}}/chats/{{threadId}}/messages",
|
|
175
|
+
"responseShape": {
|
|
176
|
+
"collectionPath": "data",
|
|
177
|
+
"collectionAs": "messages",
|
|
178
|
+
"scalar": {
|
|
179
|
+
"cursor": "payload.next_cursor"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"query": {
|
|
183
|
+
"limit": "25"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "unipile_send_message",
|
|
189
|
+
"kind": "code",
|
|
190
|
+
"scope": "pod",
|
|
191
|
+
"description": "Sends a message into an EXISTING conversation thread on any connected account (LinkedIn, WhatsApp, Telegram), then notes it (proposal-gated). Args: { threadId: string, accountId: string, text: string }. Not the send_message intent: a message to a person goes through the pod's send door (messaging.send), which files it in their channel; this verb stays an agent tool call.",
|
|
192
|
+
"requires": [
|
|
193
|
+
"unipile_linkedin"
|
|
194
|
+
],
|
|
195
|
+
"executionMode": "async",
|
|
196
|
+
"timeoutSeconds": 60,
|
|
197
|
+
"parameters": {
|
|
198
|
+
"threadId": "string",
|
|
199
|
+
"accountId": "string",
|
|
200
|
+
"text": "string"
|
|
201
|
+
},
|
|
202
|
+
"code": "// Send a message into an EXISTING chat, then record a note. Executor wraps this\n// body as (async function(args, context){ <code> }); use args directly and\n// `return`. callProvider runs the tool by NAME — the dispatcher injects the\n// vaulted X-API-KEY; propose.entity routes the write through governance.\n// v2: account_id is a PATH segment and the endpoint gained a /send suffix.\nconst threadId = String(args?.threadId ?? '').trim();\nconst accountId = String(args?.accountId ?? '').trim();\nconst text = String(args?.text ?? '').trim();\nif (!threadId) throw new Error('unipile_send_message: threadId is required');\nif (!accountId) throw new Error('unipile_send_message: accountId is required');\nif (!text) throw new Error('unipile_send_message: text is required');\n\nconst res = await callProvider('unipile_linkedin', 'POST', `/${encodeURIComponent(accountId)}/chats/${encodeURIComponent(threadId)}/messages/send`, { text });\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, threadId };\n}\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\n// A non-2xx MUST throw. Returning { success:false } resolves the caller's\n// promise, so the Arch records the message as sent, consumes the account's daily\n// quota and marks the lead contacted — for a message the provider rejected.\nif (!ok) throw new Error(`unipile_send_message: provider rejected the send (status ${res?.status ?? 'unknown'}) for thread ${threadId}`);\nconst payload = res?.body ?? res;\nawait propose.entity({\n profileSlug: 'note',\n title: `Message sent in thread ${threadId}`,\n properties: { action: 'unipile_send_message', threadId, accountId, text, messageId: payload?.message_id ?? null, status: res?.status ?? null, success: ok, provenance: 'connector', provider: 'unipile' },\n reasoning: 'Recorded the outbound message for the activity timeline.'\n});\nreturn { success: ok, threadId, messageId: payload?.message_id ?? null, status: res?.status ?? null };\n"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "unipile_start_chat",
|
|
206
|
+
"kind": "code",
|
|
207
|
+
"scope": "pod",
|
|
208
|
+
"description": "Starts a 1to1 chat with one person on ANY connected account (LinkedIn, Instagram, WhatsApp, Telegram) and sends the first message, then notes it (proposal-gated). v2 POST /{account_id}/chats/send; LinkedIn goes through its CLASSIC_PRIMARY inbox (/inboxes/CLASSIC_PRIMARY/chats/send). On LinkedIn, a person you already talk to gets the message in that chat. Use unipile_send_message to answer in a chat you know. Args: { accountId: string (from unipile_list_accounts), attendeeId: string (the person's id on that provider: LinkedIn user id, Instagram messaging id, WhatsApp id), text: string, provider?: string (the account's provider; looked up when omitted) }. Returns { chatId, messageId }. Not the send_message intent: a message to a person goes through the pod's send door (messaging.send), which files it in their channel; this verb stays an agent tool call.",
|
|
209
|
+
"requires": [
|
|
210
|
+
"unipile_linkedin"
|
|
211
|
+
],
|
|
212
|
+
"executionMode": "async",
|
|
213
|
+
"timeoutSeconds": 60,
|
|
214
|
+
"parameters": {
|
|
215
|
+
"accountId": "string",
|
|
216
|
+
"attendeeId": "string",
|
|
217
|
+
"text": "string",
|
|
218
|
+
"provider": "string?"
|
|
219
|
+
},
|
|
220
|
+
"code": "// Start a 1to1 chat with one person on ANY connected account and send the\n// first message, then record a note. v2 (OpenAPI 2.62.0): POST\n// /{account_id}/chats/send, or for a provider that uses inboxes (LinkedIn\n// Classic: CLASSIC_PRIMARY, the only inbox allowed to initiate)\n// POST /{account_id}/inboxes/CLASSIC_PRIMARY/chats/send. users_ids is ONE\n// id as a string: an array, even of one id, asks for a group chat. Instagram\n// takes the person's messaging identifier. On LinkedIn, starting a chat with\n// someone you already talk to adds the message to that chat.\nconst accountId = String(args?.accountId ?? '').trim();\nconst attendeeId = String(args?.attendeeId ?? '').trim();\nconst text = String(args?.text ?? '').trim();\nlet provider = String(args?.provider ?? '').trim().toLowerCase();\nif (!accountId) throw new Error('unipile_start_chat: accountId is required — call unipile_list_accounts first to find it.');\nif (!attendeeId) throw new Error('unipile_start_chat: attendeeId (the person\\'s id on that provider) is required');\nif (!text) throw new Error('unipile_start_chat: text is required');\n\nconst account = encodeURIComponent(accountId);\nif (!provider) {\n const acc = await callProvider('unipile_linkedin', 'GET', `/accounts/${account}`);\n provider = String((acc?.body ?? acc)?.provider ?? '').toLowerCase();\n}\nconst path = provider === 'linkedin' ? `/${account}/inboxes/CLASSIC_PRIMARY/chats/send` : `/${account}/chats/send`;\nconst res = await callProvider('unipile_linkedin', 'POST', path, { text, users_ids: attendeeId });\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, attendeeId };\n}\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\nif (!ok) throw new Error(`unipile_start_chat: provider rejected the send (status ${res?.status ?? 'unknown'}) for ${attendeeId}`);\nconst payload = res?.body ?? res;\nawait propose.entity({\n profileSlug: 'note',\n title: `Message sent -> ${attendeeId}`,\n properties: { action: 'unipile_start_chat', attendeeId, accountId, provider: 'unipile', accountProvider: provider || null, text, chatId: payload?.chat_id ?? null, messageId: payload?.message_id ?? null, status: res?.status ?? null, success: ok, provenance: 'connector' },\n reasoning: 'Recorded the opening outbound message for the activity timeline.'\n});\nreturn { success: ok, attendeeId, chatId: payload?.chat_id ?? null, messageId: payload?.message_id ?? null, status: res?.status ?? null };\n"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"name": "linkedin_capture_profiles",
|
|
224
|
+
"intent": "capture_into_pod",
|
|
225
|
+
"kind": "code",
|
|
226
|
+
"scope": "pod",
|
|
227
|
+
"description": "Searches LinkedIn people via Unipile and PROPOSES each match as a person entity (proposal-gated). Use linkedin_search_people to read without persisting. Args: { keywords: string, accountId: string (from unipile_list_accounts), limit?: number, profileSlug?: string (default person) }.",
|
|
228
|
+
"requires": [
|
|
229
|
+
"unipile_linkedin"
|
|
230
|
+
],
|
|
231
|
+
"executionMode": "async",
|
|
232
|
+
"timeoutSeconds": 60,
|
|
233
|
+
"parameters": {
|
|
234
|
+
"keywords": "string",
|
|
235
|
+
"accountId": "string",
|
|
236
|
+
"limit": "number?",
|
|
237
|
+
"profileSlug": "string?"
|
|
238
|
+
},
|
|
239
|
+
"code": "// LinkedIn people search via Unipile, proposed as person entities. See\n// linkedin_search_people for the v2 endpoint shape; this variant PERSISTS each\n// match (so its name carries no read token -> classified `write` -> proposed).\nconst accountId = String(args?.accountId ?? '').trim();\nconst keywords = String(args?.keywords ?? '').trim();\nconst limit = Number(args?.limit ?? 10);\nif (!accountId) throw new Error('linkedin_capture_profiles: accountId is required — call unipile_list_accounts first to find yours, then pass it as accountId.');\nif (!keywords) throw new Error('linkedin_capture_profiles: keywords is required');\n\nconst qs = new URLSearchParams({ offset: '0', limit: String(limit) });\nconst res = await callProvider('unipile_linkedin', 'POST', `/${encodeURIComponent(accountId)}/linkedin/search/people?${qs.toString()}`, { keywords });\nconst payload = res?.body ?? res;\nconst items = Array.isArray(payload?.data) ? payload.data : (Array.isArray(payload) ? payload : []);\n\nconst profileSlug = String(args?.profileSlug ?? '').trim() || 'person';\nlet proposed = 0;\nfor (const p of items.slice(0, limit)) {\n // v2 PeopleSearchResult: display_name, id (provider-native), public_identifier,\n // headline, location, profile_url.\n const name = String(p?.display_name ?? [p?.first_name, p?.last_name].filter(Boolean).join(' ') ?? 'LinkedIn profile').trim() || 'LinkedIn profile';\n await propose.entity({\n profileSlug,\n title: name,\n properties: {\n headline: p?.headline ?? null,\n location: p?.location ?? null,\n linkedinProviderId: p?.id ?? null,\n publicIdentifier: p?.public_identifier ?? null,\n profileUrl: p?.profile_url ?? null,\n provenance: 'connector', provider: 'unipile'\n },\n reasoning: `LinkedIn profile matched search \"${keywords}\" via Unipile.`\n });\n proposed += 1;\n}\nreturn { proposed, keywords };\n"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"name": "linkedin_send_invite",
|
|
243
|
+
"intent": "request_connection",
|
|
244
|
+
"kind": "code",
|
|
245
|
+
"scope": "pod",
|
|
246
|
+
"description": "Sends a LinkedIn connection request (v2 relation-request) via Unipile, then notes it (proposal-gated). Takes their public identifier (profile URL slug) or member id and looks them up first; refuses someone already connected or with a request pending, and a note over 300 characters. Args: { providerId: string, accountId: string (from unipile_list_accounts), message?: string }.",
|
|
247
|
+
"requires": [
|
|
248
|
+
"unipile_linkedin"
|
|
249
|
+
],
|
|
250
|
+
"executionMode": "async",
|
|
251
|
+
"timeoutSeconds": 60,
|
|
252
|
+
"parameters": {
|
|
253
|
+
"providerId": "string",
|
|
254
|
+
"accountId": "string",
|
|
255
|
+
"message": "string?"
|
|
256
|
+
},
|
|
257
|
+
"inputMap": {
|
|
258
|
+
"params": {
|
|
259
|
+
"providerId": "$to.linkedin.publicIdentifier",
|
|
260
|
+
"accountId": "$account.id",
|
|
261
|
+
"message": "$.text"
|
|
262
|
+
},
|
|
263
|
+
"accounts": {
|
|
264
|
+
"verb": "unipile_list_accounts",
|
|
265
|
+
"id": "id",
|
|
266
|
+
"label": "name",
|
|
267
|
+
"provider": "provider"
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"code": "// Send a LinkedIn connection request, then record a note. v2: invitations are\n// \"relation requests\" — POST /{account_id}/users/me/relation-requests, and the\n// target field is user_id (v1 provider_id). The `providerId` PARAM name is kept\n// so callers/transport don't change.\n// The person may be given by their public identifier (the slug of their profile\n// URL, what a pod app's `to` resolves to) or their member id: either is looked\n// up first (GET /{account}/users/{id}; the visit is not notified) for the member\n// id the request needs. Someone already connected, or with a request already\n// pending, is refused before anything is sent. LinkedIn caps the note at 300\n// characters, so a longer one is refused here rather than by LinkedIn.\nconst accountId = String(args?.accountId ?? '').trim();\nconst target = String(args?.providerId ?? '').trim();\nconst message = typeof args?.message === 'string' && args.message.trim() ? args.message.trim() : undefined;\nif (!accountId) throw new Error('linkedin_send_invite: accountId is required — call unipile_list_accounts first to find yours.');\nif (!target) throw new Error('linkedin_send_invite: providerId (their LinkedIn public identifier or member id) is required');\nif (message && message.length > 300) throw new Error(`linkedin_send_invite: a connection note is at most 300 characters on LinkedIn (this one is ${message.length}).`);\n\nconst account = encodeURIComponent(accountId);\nconst found = await callProvider('unipile_linkedin', 'GET', `/${account}/users/${encodeURIComponent(target)}?variant=linkedin_classic`);\nif (found?.proposed === true) {\n return { success: false, proposed: true, proposalId: found?.proposalId ?? null, providerId: target };\n}\nif (found?.status && (found.status < 200 || found.status >= 300)) throw new Error(`linkedin_send_invite: LinkedIn has no profile for ${target} (status ${found.status}).`);\nconst profile = found?.body ?? found ?? {};\nconst userId = String(profile.provider_id ?? profile.id ?? '').trim();\nif (!userId) throw new Error(`linkedin_send_invite: LinkedIn gave no member id for ${target}.`);\nconst distance = String(profile.network_distance ?? profile.specifics?.linkedin?.network_distance ?? '').toUpperCase();\nconst name = [profile.first_name, profile.last_name].filter(Boolean).join(' ') || target;\nif (distance === 'FIRST_DEGREE' || distance === 'DISTANCE_1') throw new Error(`linkedin_send_invite: ${name} is already one of your LinkedIn connections: send them the message instead.`);\nconst invitation = profile.invitation ?? profile.specifics?.linkedin?.invitation ?? null;\nif (invitation && String(invitation.status ?? '').toUpperCase() === 'PENDING') throw new Error(`linkedin_send_invite: a connection request with ${name} is already pending.`);\n\nconst body = { user_id: userId };\nif (message) body.message = message;\nconst res = await callProvider('unipile_linkedin', 'POST', `/${account}/users/me/relation-requests`, body);\n// GOVERNANCE GUARD: a proposal-gated call returns { proposed:true } and has NOT\n// been sent — never record a not-sent invitation as sent.\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, providerId: userId };\n}\n// v2 returns 201 on success.\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\nif (!ok) throw new Error(`linkedin_send_invite: provider rejected the invitation (status ${res?.status ?? 'unknown'}) for ${name}`);\nconst payload = res?.body ?? res;\nawait propose.entity({\n profileSlug: 'note',\n title: `LinkedIn invitation sent -> ${name}`,\n properties: { action: 'linkedin_send_invite', providerId: userId, publicIdentifier: profile.public_identifier ?? null, networkDistance: distance || null, message: message ?? null, relationRequestId: payload?.id ?? null, status: res?.status ?? null, success: ok, provenance: 'connector', provider: 'unipile' },\n reasoning: 'Recorded the outbound LinkedIn connection request for the activity timeline.'\n});\nreturn { success: ok, providerId: userId, relationRequestId: payload?.id ?? null, status: res?.status ?? null };"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "linkedin_search_people",
|
|
274
|
+
"intent": "find_people",
|
|
275
|
+
"kind": "code",
|
|
276
|
+
"scope": "pod",
|
|
277
|
+
"description": "Searches LinkedIn people via Unipile and RETURNS the matches inline. Pure read -> runs INSTANTLY (no approval). Use to qualify/enrich a lead in-flow; use linkedin_capture_profiles to persist. Args: { keywords: string, accountId: string (from unipile_list_accounts), limit?: number }.",
|
|
278
|
+
"requires": [
|
|
279
|
+
"unipile_linkedin"
|
|
280
|
+
],
|
|
281
|
+
"executionMode": "sync",
|
|
282
|
+
"timeoutSeconds": 60,
|
|
283
|
+
"parameters": {
|
|
284
|
+
"keywords": "string",
|
|
285
|
+
"accountId": "string",
|
|
286
|
+
"limit": "number?"
|
|
287
|
+
},
|
|
288
|
+
"code": "// Pure-read LinkedIn people search — returns raw v2 rows, writes NOTHING (keeps\n// it classified `read` -> governance `auto`, so an agent enriches a lead inline).\n// v2: search is split per resource (/linkedin/search/people); account_id is a\n// path segment; offset+limit are query params; filters go in the JSON body.\nconst accountId = String(args?.accountId ?? '').trim();\nconst keywords = String(args?.keywords ?? '').trim();\nconst limit = Number(args?.limit ?? 10);\nif (!accountId) throw new Error('linkedin_search_people: accountId is required — call unipile_list_accounts first to find yours.');\nif (!keywords) throw new Error('linkedin_search_people: keywords is required');\n\nconst qs = new URLSearchParams({ offset: '0', limit: String(limit) });\nconst res = await callProvider('unipile_linkedin', 'POST', `/${encodeURIComponent(accountId)}/linkedin/search/people?${qs.toString()}`, { keywords });\nconst payload = res?.body ?? res;\n// v2 envelope: { data: [...], total_count, next_cursor }.\nconst items = Array.isArray(payload?.data) ? payload.data : (Array.isArray(payload) ? payload : []);\nconst results = items.slice(0, limit);\nreturn { count: results.length, keywords, results };\n"
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "linkedin_get_profile",
|
|
292
|
+
"intent": "enrich_entity",
|
|
293
|
+
"kind": "code",
|
|
294
|
+
"scope": "pod",
|
|
295
|
+
"description": "Fetches one full LinkedIn profile by user id or public identifier, sections expanded. Pure read -> runs instantly. The enrichment primitive. Args: { identifier: string, accountId: string (from unipile_list_accounts) }.",
|
|
296
|
+
"requires": [
|
|
297
|
+
"unipile_linkedin"
|
|
298
|
+
],
|
|
299
|
+
"executionMode": "sync",
|
|
300
|
+
"timeoutSeconds": 60,
|
|
301
|
+
"parameters": {
|
|
302
|
+
"identifier": "string",
|
|
303
|
+
"accountId": "string"
|
|
304
|
+
},
|
|
305
|
+
"code": "// Fetch a single LinkedIn profile. Pure read (GET, no propose.entity) -> the\n// catalog classifies it `read` -> governance `auto`, so enrichment runs inline.\n// v2: account_id is a path segment; sections use with_sections (was\n// linkedin_sections) with a linkedin_ prefix; variant selects the product API.\nconst accountId = String(args?.accountId ?? '').trim();\nconst identifier = String(args?.identifier ?? '').trim();\nif (!accountId) throw new Error('linkedin_get_profile: accountId is required — call unipile_list_accounts first to find yours.');\nif (!identifier) throw new Error('linkedin_get_profile: identifier (a LinkedIn provider_id or public identifier) is required');\n\nconst qs = new URLSearchParams({ variant: 'linkedin_classic' });\nqs.append('with_sections', 'linkedin_*');\nconst res = await callProvider('unipile_linkedin', 'GET', `/${encodeURIComponent(accountId)}/users/${encodeURIComponent(identifier)}?${qs.toString()}`);\n// Return the RAW v2 UserProfile. Note v2 renames: headline->description,\n// summary->bio, provider_id->id, and LinkedIn extras live under specifics.linkedin.\nreturn res?.body ?? res;\n"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "linkedin_send_first_message",
|
|
309
|
+
"kind": "code",
|
|
310
|
+
"scope": "pod",
|
|
311
|
+
"description": "Sends a LinkedIn message to one person: starts the conversation, or adds to the one you have. Takes their public identifier (profile URL slug) or member id, looks them up, and refuses anyone who is not a 1st-degree connection before sending. Proposal-gated. Args: { attendeeId: string, text: string, accountId: string (from unipile_list_accounts) }. Not the send_message intent: a message to a person goes through the pod's send door (messaging.send), which files it in their channel; this verb stays an agent tool call.",
|
|
312
|
+
"requires": [
|
|
313
|
+
"unipile_linkedin"
|
|
314
|
+
],
|
|
315
|
+
"executionMode": "async",
|
|
316
|
+
"timeoutSeconds": 60,
|
|
317
|
+
"parameters": {
|
|
318
|
+
"attendeeId": "string",
|
|
319
|
+
"text": "string",
|
|
320
|
+
"accountId": "string"
|
|
321
|
+
},
|
|
322
|
+
"code": "// Send a LinkedIn message to one person: starts the conversation, or adds to the\n// one you already have with them (v2: LinkedIn is inbox-based and CLASSIC_PRIMARY\n// is the inbox allowed to initiate; users_ids is ONE id as a string, an array\n// asks Unipile for a group chat).\n// The recipient may be their public identifier (the slug of their profile URL,\n// what a pod app's `to` resolves to) or their LinkedIn member id. Either is\n// looked up first (GET /{account}/users/{id}; the visit is not notified), which\n// gives the member id the send needs and the network distance: LinkedIn lets you\n// message only your connections (InMail aside), so anyone else is refused BEFORE\n// anything is sent, with what to do instead. An unknown distance never blocks.\nconst accountId = String(args?.accountId ?? '').trim();\nconst attendee = String(args?.attendeeId ?? '').trim();\nconst text = String(args?.text ?? '').trim();\nif (!accountId) throw new Error('linkedin_send_first_message: accountId is required — call unipile_list_accounts first to find yours.');\nif (!attendee) throw new Error('linkedin_send_first_message: attendeeId (their LinkedIn public identifier or member id) is required');\nif (!text) throw new Error('linkedin_send_first_message: text is required');\n\nconst account = encodeURIComponent(accountId);\nconst found = await callProvider('unipile_linkedin', 'GET', `/${account}/users/${encodeURIComponent(attendee)}?variant=linkedin_classic`);\nif (found?.proposed === true) {\n return { success: false, proposed: true, proposalId: found?.proposalId ?? null, attendeeId: attendee };\n}\nif (found?.status && (found.status < 200 || found.status >= 300)) throw new Error(`linkedin_send_first_message: LinkedIn has no profile for ${attendee} (status ${found.status}).`);\nconst profile = found?.body ?? found ?? {};\nconst memberId = String(profile.provider_id ?? profile.id ?? '').trim();\nif (!memberId) throw new Error(`linkedin_send_first_message: LinkedIn gave no member id for ${attendee}.`);\nconst distance = String(profile.network_distance ?? profile.specifics?.linkedin?.network_distance ?? '').toUpperCase();\nconst name = [profile.first_name, profile.last_name].filter(Boolean).join(' ') || attendee;\nif (['SECOND_DEGREE', 'THIRD_DEGREE', 'OUT_OF_NETWORK', 'DISTANCE_2', 'DISTANCE_3'].includes(distance)) {\n throw new Error(`linkedin_send_first_message: ${name} is not one of your LinkedIn connections yet (${distance}). Send a connection request first, then the message once they accept.`);\n}\n\nconst res = await callProvider('unipile_linkedin', 'POST', `/${account}/inboxes/CLASSIC_PRIMARY/chats/send`, { text, users_ids: memberId });\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null, attendeeId: memberId };\n}\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\nif (!ok) throw new Error(`linkedin_send_first_message: provider rejected the send (status ${res?.status ?? 'unknown'}) for ${name}`);\nconst payload = res?.body ?? res;\nawait propose.entity({\n profileSlug: 'note',\n title: `LinkedIn message sent -> ${name}`,\n properties: { action: 'linkedin_send_first_message', attendeeId: memberId, publicIdentifier: profile.public_identifier ?? null, networkDistance: distance || null, accountId, text, chatId: payload?.chat_id ?? null, status: res?.status ?? null, success: ok, provenance: 'connector', provider: 'unipile' },\n reasoning: 'Recorded the outbound LinkedIn message for the activity timeline.'\n});\nreturn { success: ok, attendeeId: memberId, networkDistance: distance || null, chatId: payload?.chat_id ?? null, status: res?.status ?? null };"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"name": "linkedin_list_relations",
|
|
326
|
+
"intent": "list_records",
|
|
327
|
+
"kind": "declarative",
|
|
328
|
+
"scope": "pod",
|
|
329
|
+
"description": "Lists the 1st-degree LinkedIn connections of a connected account, cursor-paginated. Read-only. Args: { accountId: string, cursor?: string }. v2: GET /{account_id}/users/me/relations.",
|
|
330
|
+
"requires": [
|
|
331
|
+
"unipile_linkedin"
|
|
332
|
+
],
|
|
333
|
+
"executionMode": "sync",
|
|
334
|
+
"timeoutSeconds": 30,
|
|
335
|
+
"parameters": {
|
|
336
|
+
"accountId": "string",
|
|
337
|
+
"cursor": "string?"
|
|
338
|
+
},
|
|
339
|
+
"providerSpec": {
|
|
340
|
+
"tool": "unipile_linkedin",
|
|
341
|
+
"method": "GET",
|
|
342
|
+
"pathTemplate": "/{{accountId}}/users/me/relations",
|
|
343
|
+
"responseShape": {
|
|
344
|
+
"collectionPath": "data",
|
|
345
|
+
"collectionAs": "relations",
|
|
346
|
+
"scalar": {
|
|
347
|
+
"cursor": "payload.next_cursor"
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
"query": {
|
|
351
|
+
"limit": "100",
|
|
352
|
+
"cursor": "{{cursor}}"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "linkedin_list_invitations",
|
|
358
|
+
"intent": "list_records",
|
|
359
|
+
"kind": "declarative",
|
|
360
|
+
"scope": "pod",
|
|
361
|
+
"description": "Lists SENT LinkedIn connection requests (v2 pending relation-requests). Diffing this over time is how you detect an accepted invitation. Read-only. Args: { accountId: string, cursor?: string }. v2 requires type=sent (hardcoded here).",
|
|
362
|
+
"requires": [
|
|
363
|
+
"unipile_linkedin"
|
|
364
|
+
],
|
|
365
|
+
"executionMode": "sync",
|
|
366
|
+
"timeoutSeconds": 30,
|
|
367
|
+
"parameters": {
|
|
368
|
+
"accountId": "string",
|
|
369
|
+
"cursor": "string?"
|
|
370
|
+
},
|
|
371
|
+
"providerSpec": {
|
|
372
|
+
"tool": "unipile_linkedin",
|
|
373
|
+
"method": "GET",
|
|
374
|
+
"pathTemplate": "/{{accountId}}/users/me/relation-requests",
|
|
375
|
+
"responseShape": {
|
|
376
|
+
"collectionPath": "data",
|
|
377
|
+
"collectionAs": "invitations",
|
|
378
|
+
"scalar": {
|
|
379
|
+
"cursor": "payload.next_cursor"
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
"query": {
|
|
383
|
+
"type": "sent",
|
|
384
|
+
"limit": "100",
|
|
385
|
+
"cursor": "{{cursor}}"
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"name": "linkedin_list_company_pages",
|
|
391
|
+
"intent": "list_records",
|
|
392
|
+
"kind": "declarative",
|
|
393
|
+
"scope": "pod",
|
|
394
|
+
"description": "Lists the LinkedIn company pages a connected LinkedIn account administers, with each page's id (what unipile_publish_post takes as postAs to post as that page) and name. Read-only. Args: { accountId: string }. v2: GET /{account_id}/linkedin/company/pages.",
|
|
395
|
+
"requires": [
|
|
396
|
+
"unipile_linkedin"
|
|
397
|
+
],
|
|
398
|
+
"executionMode": "sync",
|
|
399
|
+
"timeoutSeconds": 30,
|
|
400
|
+
"metadata": {
|
|
401
|
+
"readOnly": true
|
|
402
|
+
},
|
|
403
|
+
"parameters": {
|
|
404
|
+
"accountId": "string"
|
|
405
|
+
},
|
|
406
|
+
"providerSpec": {
|
|
407
|
+
"tool": "unipile_linkedin",
|
|
408
|
+
"method": "GET",
|
|
409
|
+
"pathTemplate": "/{{accountId}}/linkedin/company/pages",
|
|
410
|
+
"responseShape": {
|
|
411
|
+
"collectionPath": "data",
|
|
412
|
+
"collectionAs": "pages",
|
|
413
|
+
"item": {
|
|
414
|
+
"id": "id",
|
|
415
|
+
"name": "name"
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"name": "unipile_publish_post",
|
|
422
|
+
"intent": "publish_post",
|
|
423
|
+
"kind": "code",
|
|
424
|
+
"scope": "pod",
|
|
425
|
+
"description": "Publishes a post now from one connected account (v2 POST /{account_id}/posts). Text alone is sent as JSON; with media (images or videos, pod files sent as bytes) a multipart form with one attachments[i] part per file. Unipile has no scheduling: the post goes out when the run is approved. LinkedIn takes text with or without media, may post as a company page the account administers (postAs, Unipile's post_as: a page id from linkedin_list_company_pages) and may repost a post (quotedPostId, Unipile's quoted_post_id: with no text a plain repost, with text a quote); Instagram needs at least one image or video and has neither; WhatsApp, Telegram and mail accounts have no posts and are refused. Args: { accountId: string (from unipile_list_accounts), text: string (may be empty for a plain repost), provider?: string, media?: pod file ids, postAs?: string, quotedPostId?: string }. Returns { postId, shareUrl, attachments }.",
|
|
426
|
+
"requires": [
|
|
427
|
+
"unipile_linkedin"
|
|
428
|
+
],
|
|
429
|
+
"executionMode": "sync",
|
|
430
|
+
"timeoutSeconds": 120,
|
|
431
|
+
"metadata": {
|
|
432
|
+
"verbType": "write"
|
|
433
|
+
},
|
|
434
|
+
"parameters": {
|
|
435
|
+
"accountId": "string",
|
|
436
|
+
"text": "string",
|
|
437
|
+
"provider": "string?",
|
|
438
|
+
"media": "file[]:bytes?",
|
|
439
|
+
"postAs": "string?",
|
|
440
|
+
"quotedPostId": "string?"
|
|
441
|
+
},
|
|
442
|
+
"inputMap": {
|
|
443
|
+
"params": {
|
|
444
|
+
"text": "$.text",
|
|
445
|
+
"accountId": "$account.id",
|
|
446
|
+
"provider": "$account.provider",
|
|
447
|
+
"media": "$.media"
|
|
448
|
+
},
|
|
449
|
+
"accounts": {
|
|
450
|
+
"verb": "unipile_list_accounts",
|
|
451
|
+
"id": "id",
|
|
452
|
+
"label": "name",
|
|
453
|
+
"provider": "provider"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
"code": "// Publish a post now from one connected account (v2 POST /{account_id}/posts).\n// Text alone goes as JSON. With media, a multipart form: `media` is\n// `file[]:bytes`, so this code holds one handle per pod file and the pod\n// streams the bytes into each `attachments[i]` part (Unipile's field naming\n// for files). Unipile has no scheduling: the post goes out when the run is\n// approved. A non-2xx throws, so a refused post is never reported as sent.\n// LinkedIn only: `postAs` posts as a company page the account administers\n// (Unipile's post_as), `quotedPostId` reposts that post (quoted_post_id; with\n// no text a plain repost, with text a quote). Both are left out when empty.\nconst accountId = String(args?.accountId ?? '').trim();\nconst text = String(args?.text ?? '').trim();\nconst provider = String(args?.provider ?? '').trim().toLowerCase();\nconst media = Array.isArray(args?.media) ? args.media : (args?.media ? [args.media] : []);\nconst postAs = String(args?.postAs ?? '').trim();\nconst quotedPostId = String(args?.quotedPostId ?? '').trim();\nif (!accountId) throw new Error('unipile_publish_post: accountId is required (from unipile_list_accounts)');\nif (!text && !quotedPostId) throw new Error('unipile_publish_post: text is required');\nif (provider && provider !== 'linkedin' && provider !== 'instagram') {\n throw new Error(`unipile_publish_post: a ${provider} account has no posts; publish from a LinkedIn or Instagram account`);\n}\nif (provider === 'instagram' && (postAs || quotedPostId)) {\n throw new Error('unipile_publish_post: Instagram has no company page post or repost; send the post itself');\n}\nif (provider === 'instagram' && media.length === 0) {\n throw new Error('unipile_publish_post: Instagram needs at least one image or video in media');\n}\n\nconst fields = { text };\nif (postAs) fields.post_as = postAs;\nif (quotedPostId) fields.quoted_post_id = quotedPostId;\nconst path = `/${encodeURIComponent(accountId)}/posts`;\nlet res;\nif (media.length > 0) {\n const form = { ...fields };\n media.forEach((file, n) => { form[`attachments[${n}]`] = file; });\n res = await callProvider('unipile_linkedin', 'POST', path, form, { multipart: true });\n} else {\n res = await callProvider('unipile_linkedin', 'POST', path, fields);\n}\nif (res?.proposed === true) {\n return { success: false, proposed: true, proposalId: res?.proposalId ?? null };\n}\nconst ok = res?.status ? res.status >= 200 && res.status < 300 : true;\nif (!ok) throw new Error(`unipile_publish_post: provider rejected the post (status ${res?.status ?? 'unknown'})`);\nconst payload = res?.body ?? res;\nreturn { postId: payload?.id ?? null, shareUrl: payload?.share_url ?? null, attachments: media.length };\n"
|
|
457
|
+
}
|
|
458
|
+
],
|
|
459
|
+
"provides": [
|
|
460
|
+
"connect_account",
|
|
461
|
+
"fetch_record",
|
|
462
|
+
"list_records",
|
|
463
|
+
"capture_into_pod",
|
|
464
|
+
"request_connection",
|
|
465
|
+
"find_people",
|
|
466
|
+
"enrich_entity",
|
|
467
|
+
"publish_post"
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
"dependencies": []
|
|
471
|
+
}
|