@audienti/cli 0.1.145 → 0.2.0-exora.2

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.
Files changed (66) hide show
  1. package/README.md +47 -895
  2. package/bin/audienti-mcp.mjs +2 -0
  3. package/cli/account-intake-operations.mjs +28 -0
  4. package/cli/account-intake.mjs +33 -0
  5. package/cli/account-replies.mjs +35 -0
  6. package/cli/account-reply-operations.mjs +30 -0
  7. package/cli/agents.mjs +107 -0
  8. package/cli/approach-operations.mjs +22 -0
  9. package/cli/assignment-operations.mjs +218 -0
  10. package/cli/assignment.mjs +116 -0
  11. package/cli/audience-operations.mjs +16 -0
  12. package/cli/audiences.mjs +26 -0
  13. package/cli/audienti.mjs +124 -0
  14. package/cli/connection-registry-operations.mjs +7 -0
  15. package/cli/content.mjs +29 -0
  16. package/cli/discovery-operations.mjs +27 -0
  17. package/cli/discovery.mjs +14 -0
  18. package/cli/experiment-operations.mjs +25 -0
  19. package/cli/experiments.mjs +26 -0
  20. package/cli/json-file.mjs +13 -0
  21. package/cli/mcp.mjs +47 -0
  22. package/cli/offer-operations.mjs +12 -0
  23. package/cli/offers.mjs +24 -0
  24. package/cli/owner-account-client.mjs +18 -0
  25. package/cli/profiles.mjs +29 -0
  26. package/cli/progress.mjs +12 -0
  27. package/cli/prospecting-settings-operations.mjs +16 -0
  28. package/cli/prospecting-settings.mjs +15 -0
  29. package/cli/recipient-operations.mjs +13 -0
  30. package/cli/recipients.mjs +30 -0
  31. package/cli/sender-account-operations.mjs +24 -0
  32. package/cli/sender-accounts.mjs +23 -0
  33. package/cli/skills-mcp.mjs +24 -0
  34. package/cli/skills.mjs +56 -0
  35. package/cli/suppression-operations.mjs +8 -0
  36. package/cli/suppression.mjs +12 -0
  37. package/cli/tools-operations.mjs +70 -0
  38. package/cli/tools.mjs +72 -0
  39. package/cli/work-execution-operations.mjs +35 -0
  40. package/cli/work-execution.mjs +29 -0
  41. package/cli/work-feedback-operations.mjs +17 -0
  42. package/cli/work-feedback.mjs +18 -0
  43. package/cli/work-operations.mjs +28 -0
  44. package/cli/work.mjs +27 -0
  45. package/cli/workspaces.mjs +158 -0
  46. package/package.json +15 -26
  47. package/skills/audienti/SKILL.md +61 -323
  48. package/skills/audienti/references/helpers.md +149 -0
  49. package/skills/audienti/references/profile-workflows.md +212 -0
  50. package/skills/audienti/references/workflow-receipt.md +49 -0
  51. package/skills/audienti/scripts/contracts.mjs +72 -0
  52. package/skills/audienti/scripts/profile-helpers.mjs +138 -0
  53. package/skills/audienti/scripts/run.mjs +40 -0
  54. package/source-manifest.json +60 -0
  55. package/.mcp.json +0 -13
  56. package/CHANGELOG.md +0 -1018
  57. package/bin/audienti-mcp.js +0 -5
  58. package/bin/audienti.js +0 -6
  59. package/install +0 -83
  60. package/skills/audienti/references/methodology.md +0 -111
  61. package/skills/audienti-gift-research/SKILL.md +0 -47
  62. package/src/agent-skills.js +0 -60
  63. package/src/api-client.js +0 -1639
  64. package/src/cli.js +0 -16992
  65. package/src/config.js +0 -130
  66. package/src/mcp.js +0 -103
@@ -0,0 +1,149 @@
1
+ # Helpers and installation
2
+
3
+ Read the installed package version with `skills show audienti --json`. Node 24.22.1 is the repository's supported runtime. The helper
4
+ scripts use only Node built-ins and need no network, login or provider credentials.
5
+ The CLI/MCP adapter uses the dependencies shipped with the CLI distribution.
6
+
7
+ Use the complete versioned skills/audienti directory included in the CLI archive. For Codex, place this
8
+ directory under your configured `.agents/skills/audienti` directory. For Claude
9
+ Code, use its `.claude/skills/audienti` location. Installation is an explicit local
10
+ action; no command here installs a plugin, grants access or modifies credentials.
11
+ For another Node-capable agent, retain the directory structure and invoke
12
+ `scripts/run.mjs`. To use local MCP, run `node cli/audienti.mjs mcp` from the extracted CLI directory
13
+ through the host's stdio configuration. The server exposes `skills_list`,
14
+ `skills_show`, `skills_profile_queries`, `skills_profile_candidates`,
15
+ `skills_profile_completeness` and `skills_profile_evidence` without
16
+ account authentication; existing CRM/tools still check their own credentials.
17
+
18
+ `skills show audienti --json` publishes each helper's input/output JSON schema and
19
+ a digest of this package. Inputs have a 1 MiB cap. Unknown fields and helper names
20
+ are rejected. Use `--file -` to read bounded JSON from standard input. JSON errors
21
+ never echo supplied values. Outputs go to stdout; choose private output storage
22
+ if retaining personal data. No automatic transcript or credential store is created.
23
+
24
+ ## profile-queries
25
+
26
+ Input: `{ "kind": "social", "profile": { "displayName": "Avery Example",
27
+ "companyName": "Example Studio", "companyDomain": "example.org",
28
+ "title": "Founder", "handles": ["avery.example"] } }`.
29
+
30
+ `kind` is `social`, `email` or `website`; optional `mode` is `broad` (default) or
31
+ `attachable`, reflecting V10's query network gates, not a promise that every
32
+ network has a supported enrichment adapter. Facts are supplied by the agent;
33
+ the helper never fetches a company or canonicalizes an identity.
34
+
35
+ Output has `version`, `queries` and `missing`. An empty query list means there is
36
+ not enough usable context; it is not a research result. Optional name fallback
37
+ uses `prospectName` for social queries only. Email/website queries and candidate
38
+ name scoring require `displayName`. Social handle permutations are search terms, not profile IDs.
39
+
40
+ ## profile-candidates
41
+
42
+ Input: `{ "profile": { "displayName": "Avery Example" }, "candidates": [
43
+ { "url": "https://example.org/team", "title": "Avery Example", "snippet": "Founder",
44
+ "domain": "example.org" } ], "limit": 12 }`.
45
+
46
+ At most 80 results, at most 12 returned candidates. Supply optional `profileId`
47
+ only from an actual CRM response for that exact candidate URL; never reuse the
48
+ anchor Profile ID for unconfirmed candidates. Canonical-reference dedup uses that ID; otherwise
49
+ dedup is exact URL string equality. The helper does not infer identity from URLs.
50
+ `saved_reference` means the input supplied an ID, not independently verified access.
51
+
52
+ V10 advisory weights are name .4, company-domain .3, known-source .2, social-domain
53
+ .1, text overlap .15. Its legacy substring domain feature is preserved for ranking;
54
+ it is not a trust or matching check. Scores below .4 suggest skipping, above .7
55
+ suggest inspecting the page; otherwise inspect available snippets. You choose and
56
+ invoke any subsequent tool yourself. Dedup keeps the first supplied item, ranking
57
+ ties preserve input order. `duplicatesRemoved` and `omitted` explain truncation.
58
+
59
+ Port reference: V10 commit 915cae248a4e8698e5ef773f9624249481f1a0ce,
60
+ `profiles/expansion/query_builder.rb`, `result_scorer.rb`,
61
+ `profiles/social_query_planner.rb` and `config/web_search_queries.yml`.
62
+ Database selections in V10 are supplied input facts here. No V10 production data,
63
+ Rails callbacks, paid-provider orchestration or final identity matcher is bundled.
64
+
65
+ ## Supplying facts
66
+
67
+ Map the saved Prospect's projection name to displayName, title to title, and bio
68
+ or description to the matching helper field. Profiles expose canonicalUrl and
69
+ providerId; a known LinkedIn URL can be supplied as linkedinReference, its known
70
+ vanity segment as linkedinSlug. Supply handles only when observed; do not derive
71
+ identity from guessed handles. Choose companyName/companyDomain from the CRM's
72
+ selected employment relationship and its company projection. companyDomain must
73
+ be a bare hostname such as example.org, not a URL. Missing facts stay omitted.
74
+
75
+ The ranking helper uses each candidate's supplied domain for all domain signals;
76
+ it does not extract a hostname from url. Supply the observed hostname explicitly
77
+ when available. These signals only prioritize inspection and confer no trust.
78
+
79
+ HTTP clients can read the public catalog at GET /api/v1/skills and the complete
80
+ artifact at GET /api/v1/skills/audienti. Check the version and digest before saving
81
+ the returned files locally. Reading the artifact does not execute a helper.
82
+ Agents need a local Node environment or the local stdio MCP server to execute
83
+ these helpers; an HTTP-only client cannot execute them through this API.
84
+
85
+ To check the file digest, iterate artifact.files in its supplied order. Feed SHA-256
86
+ UTF-8 bytes for path, a NUL byte, the decimal UTF-8 content byte length, another
87
+ NUL byte, then the content, for each file. Compare the lowercase hex result with
88
+ digest. This detects changed contents; it is not an authenticity signature. Get
89
+ artifacts from your trusted installation or the expected HTTPS API origin.
90
+
91
+ ## profile-completeness
92
+
93
+ Input: `{ "requiredFields": ["name", "websites"], "fields": [
94
+ { "name": "name", "state": "available" },
95
+ { "name": "websites", "state": "absent" } ] }`.
96
+
97
+ `fields` is required; use `[]` when the saved read did not report states.
98
+ Map `profiles.enrich.operation.result.fieldStates` (or the legacy
99
+ `profiles.fetch.result.fieldStates`) to the helper's `fields` array of
100
+ `{name,state}` entries using the actual provider field names (`about`,
101
+ `websites`, not projection names `bio` or a made-up `website`); do not manufacture an
102
+ `available` state because a name or URL seems plausible. Required field names are
103
+ selected by the agent for the task. The output preserves `available` and known
104
+ `absent` separately. `needsEvidence` reports unavailable, truncated, unreported
105
+ or conflicting supplied states, in requested order with duplicate names removed.
106
+ Contradictory reports never get resolved by input order.
107
+
108
+ `allFieldsKnown` means every required field has one supplied available/absent
109
+ state. It does not establish freshness, fit, identity, cache eligibility or cost.
110
+ An absent field is not a reason to automatically purchase another lookup. The
111
+ server alone owns the 30-day cache and paid admission. A truncated requested field
112
+ can cause `profile_fields_truncated`: omit that requirement or explicitly choose
113
+ a paid refresh; never refresh automatically. Saved Profile list/get results do
114
+ not report these states. Missing reports remain `not_reported`. This helper applies the
115
+ accepted Exora field-state contract to V10's required-field planning pattern; it
116
+ does not port CompanyCitationEnricher's hidden company calls or employer selection.
117
+
118
+ ## profile-evidence
119
+
120
+ Input: `{ "profileId": "<optional saved Profile UUID>",
121
+ "observationId": "<optional observation UUID>", "platform": "twitter",
122
+ "bio": "Founder. See https://example.org and @colleague",
123
+ "websiteUrl": "https://example.org" }`.
124
+
125
+ Returns exact observed URL candidates and lowercase mention handles with their
126
+ input fields. Exact duplicate URLs combine their field references. The source IDs
127
+ are merely supplied references: they do not prove access or trusted provenance.
128
+ The explicit website link is considered first so a long bio cannot crowd it out.
129
+ Every output is `caller_supplied`, even when the caller claims a verified source.
130
+ A mention is not a resolved Profile URL. No links are fetched or canonicalized,
131
+ no provider IDs guessed, and no citations or identity attachments are written.
132
+
133
+ V10 source: `profiles/writer.rb#extract_citations_from_bio` and
134
+ `#extract_citations_from_website_url`, and `utils/text_scanner.rb` at the pinned
135
+ commit above. Portable tests reuse its URL and Twitter/Reddit/Instagram fixtures.
136
+ This package stops before V10's URL parser/citation writer: use explicit CRM
137
+ normalization and evidence operations after inspecting the actual target. It
138
+ accepts absolute HTTP(S) links only; other website values remain unresolved.
139
+ Email addresses and URL text are excluded from mention extraction so they do not
140
+ become invented social identities. URLs with userinfo or recognized credential parameter names (including OAuth
141
+ fragments and signed-URL keys) are omitted, with `rejectedLinks` reported. This
142
+ is not a guarantee that arbitrary secrets hidden in URLs can be detected; never
143
+ put secrets in any helper input.
144
+
145
+ Each result caps links and mentions at 50, with omitted counts. Bio input is at
146
+ most 10,000 characters; URLs at most 2,048. Oversize mention handles are counted
147
+ as omitted. Empty text returns empty candidates, not a research success. Relevant
148
+ source text and its observation time stay in the original evidence record; these
149
+ candidates are preparation for the agent's next explicit step.
@@ -0,0 +1,212 @@
1
+ # Agent-owned profile workflows
2
+
3
+ The agent runs these recipes. Each search, retrieval, AI judgment and contact
4
+ purchase is an explicit agent action. The helper scripts do not chain tools and
5
+ the CRM does not run a hidden research job. Read the installed schemas and selected
6
+ workspace permissions; do not invent flags from prose. Keep an
7
+ [agent research receipt](workflow-receipt.md), including incomplete work.
8
+
9
+ First read saved records with `prospect_list`/`prospect_get` and
10
+ `profile_list`/`profile_get` (CLI `prospects list|get`, `profiles list|get`).
11
+ These search the selected workspace's saved records. External search is a separate
12
+ explicit call to `profiles.discover`; legacy `profiles.search` has those same paid
13
+ external semantics. Query builders and candidate scores are planning aids.
14
+
15
+ ## Enrich a known Profile
16
+
17
+ 1. Resolve a supplied URL or saved Profile ID against saved records. Use the
18
+ current Prospect ID returned by the server; do not infer identity from a name.
19
+ 2. Decide which fields the task needs. Pass prior supplier `fieldStates` to
20
+ `profile-completeness`; saved list/get projections do not report them, so use
21
+ `fields:[]` when no such report exists. Use actual field names such as `about`
22
+ and `websites`. Known absence is not an automatic refresh request.
23
+ 3. For supported LinkedIn person/company URLs or authorized saved Profile IDs,
24
+ call `profiles.enrich` explicitly with stable request ID and required fields.
25
+ This finds/creates the source Profile, saves its facts, resolves existing
26
+ canonical identity and returns its Prospect and visible Profile page. The
27
+ server reuses complete workspace data for 30 days; `refresh:true` deliberately
28
+ buys a new lookup and is subject to current paid admission.
29
+ 4. Inspect `operation` for status and billing. `prospect`, `profilesPage`,
30
+ `projectionStatus` and `projectionReadAt` are a separate current authorized CRM
31
+ read. The `prospect` value is a detail object: its canonical ID is
32
+ `prospect.prospect.id`; `matchedProfileId` identifies the enriched source
33
+ Profile. Page with `prospect_get` if more profiles are needed. Do not treat a
34
+ historical paid result's Prospect ID as current ownership after correction.
35
+ 5. On interrupted/running work, inspect the operation and retry with the exact
36
+ original ID/input/principal. If projection is unavailable, retry free saved
37
+ reads. Never buy another lookup to recover a response. Expired evidence,
38
+ truncated fields and unsupported networks remain explicit gaps.
39
+
40
+ `profiles.fetch` remains the legacy flat paid-operation result. Its admission is
41
+ shared with `profiles.enrich`, which wraps that operation with the current CRM
42
+ read; changing names never buys a second attempt. Likewise `profiles.discover`
43
+ and legacy `profiles.search` share admission. A new completed-cache call and a
44
+ same-request replay are different events even when both are free.
45
+
46
+ ## Find people through an audience or question
47
+
48
+ 1. Read the caller's scope: selected workspace, audience/role criteria, company
49
+ filters, questions, time window, requested count and tool/budget limits. Use
50
+ saved audience definitions when supplied. Missing evidence remains unknown;
51
+ do not pool different people or companies to satisfy a conjunction.
52
+ 2. Search saved records first. Choose an available research mechanism explicitly:
53
+ compatible agent search/browsing, `web.search`, or `profiles.discover` using its
54
+ actual supported input schema. Unsupported audience filters must be evaluated
55
+ separately from retained evidence, not invented as provider arguments.
56
+ 3. Inspect primary sources and dates. A current official source may establish a
57
+ present state; a retrieved historical event is not automatically current. A
58
+ failed retrieval, search miss or JavaScript shell is not evidence of absence.
59
+ 4. Select relevant identities, then use the enrichment recipe or submit attributed
60
+ externally acquired Profile facts through `prospect_record_observation`.
61
+ Preserve producing skill version, source URL/time and actual response IDs.
62
+ Contact lookup, optional `decide`, classification and further retrieval require
63
+ their own deliberate agent calls. No automatic email/mobile finding or send.
64
+ 5. Compare sourced company/role/question judgments to the brief, preserving
65
+ counterevidence. Record observations/interpretations with the shared evidence
66
+ owner. The server's exact qualification operation, when requested, owns
67
+ admission to an experiment; a helper score or this receipt is not admission.
68
+ 6. Continue until requested scope/count, agreed budget/time limit or documented
69
+ source exhaustion. Return supported findings plus pending/rejected/unknown
70
+ records and next actions; one completed search batch is not completion.
71
+
72
+ This adapts the existing signal-prospect-research method's separation of scope,
73
+ primary-source evidence, current state, unknown answers and unfinished work. It
74
+ adds no parallel CRM rule engine or automatic research executor.
75
+
76
+ ## Find people through a topic, post and its participants
77
+
78
+ 1. Reuse a saved topic with `topics_list`/`topics_get`, or deliberately create one
79
+ with `topics_create`. A topic is saved context, not proof of a person's intent.
80
+ 2. Explicitly search/fetch a relevant post with `posts.search`/`posts.fetch`,
81
+ supplying the selected topic ID when supported. Keep the returned topic/post
82
+ IDs and immutable evidence references; local report references are not CRM IDs.
83
+ 3. Explicitly retrieve `posts.comments` or `posts.reactions` and follow requested
84
+ pages. These time-scoped pages retrieve fresh data; they do not use the
85
+ 30-day Profile cache. Same-request replay remains safe. A valid empty page is
86
+ distinct from malformed data and can still have verified supplier expense and
87
+ a customer charge. Report partial pages and unknown observation times.
88
+ 4. Read the observed author/participant references and evidence. Choose whom to
89
+ inspect as the agent; a reaction alone does not prove pain, fit, buying intent
90
+ or consent. Incoming interactions do not establish outgoing engagement history.
91
+ 5. Enrich the selected participant with an explicit call, or record compatible
92
+ external research as attributed evidence. Preserve post/topic/interaction
93
+ provenance using supported citation/evidence operations and actual stored IDs.
94
+ 6. Read back the current Prospect and visible Profiles before reporting saved
95
+ results. Keep observation and interpretation separate. Do not silently buy
96
+ contacts, invoke AI, send outreach or install another research tool.
97
+
98
+ ## Expand an identity into additional Profiles
99
+
100
+ 1. Read the anchor and visible Profiles. Take selected employer/title from the
101
+ CRM relationship projection; do not silently choose an employer from scraped
102
+ text. Read required field states rather than assuming a saved name is complete.
103
+ 2. Run `profile-queries` on supplied context, then explicitly execute selected
104
+ searches within scope. `social` uses V10 network gates; `website` and `email`
105
+ prepare query text only. The queries themselves do not buy contact data.
106
+ 3. Pass supplied results to `profile-candidates` to prioritize inspection. Scores,
107
+ inferred domains, handles and legacy `trusted_source` weights are advisory.
108
+ Inspect promising sources explicitly. `profile-evidence` extracts supplied
109
+ links/mentions without fetching, canonicalizing or certifying them.
110
+ 4. Search saved workspace Profiles for a candidate's actual URL/ID. Keep an
111
+ unconfirmed candidate separate from the anchor. Record an independently
112
+ observed candidate only with its own actual source identity; omit the anchor
113
+ `prospectId` from observation intake. That intake is not a speculative
114
+ candidate inbox and must not be used to force an attachment.
115
+ 5. Explicitly enrich if needed and supported. Hosted profile retrieval supports
116
+ LinkedIn people and companies. Other networks can have attributed externally
117
+ obtained facts; a query mentioning a network does not imply a hosted adapter.
118
+ 6. If the server exposes `profile_match_evaluate`, evaluate the saved anchor and
119
+ candidate IDs. CLI: `profiles match evaluate SOURCE_ID CANDIDATE_ID`. This is a
120
+ free deterministic check, not a provider call. The first supported cross-network
121
+ rule requires a forward link from SOURCE to CANDIDATE. SOURCE must be a
122
+ supported LinkedIn Profile with a provider ID and a succeeded supplier
123
+ snapshot in this workspace. Its payload must be unexpired, its retrieval
124
+ under 30 days old, and its `websites` state `available` with the candidate's
125
+ current canonical URL listed. The newest snapshot for that source identity
126
+ controls the result; an older positive link cannot override newer evidence.
127
+ Reversing the two arguments does not reverse the evidence. A GitHub-to-LinkedIn
128
+ link or a caller-reported bio/image link cannot satisfy this release's rule.
129
+ `insufficient_evidence` / `no_current_verified_direct_link` means the proof is
130
+ missing, stale or unsupported; it does not mean different people. If current
131
+ retrieval is justified, explicitly choose a supported paid enrichment/refresh
132
+ within the user's budget, then reevaluate. Otherwise keep the candidate
133
+ unconfirmed and record the missing evidence. Never refresh automatically.
134
+ `identity_kind_conflict`, `contradictory_provider_id` and `staff_separation`
135
+ are conflicts, not a reason to force an attachment or bypass the guard.
136
+ Caller citations,
137
+ text, images, scores and a shared name do not make that evidence trusted.
138
+ 7. On `match`, explicitly call `profile_match_apply` with the exact evaluation
139
+ digest and new stable request ID. CLI: `profiles match apply SOURCE_ID
140
+ CANDIDATE_ID --evaluation-digest HASH --request-id UUID`. The server rechecks
141
+ current evidence/authority, chooses the compatible canonical owner and records
142
+ lineage. `already_associated` needs no new merge. Keep returned IDs.
143
+ 8. On changed state, reread and reevaluate; a new reviewed input needs a new
144
+ request ID. On uncertain apply completion, retry the exact original ID/input
145
+ first, never silently change its digest. `insufficient_evidence` and `conflict`
146
+ are not attachments. Decide whether further explicit research can resolve them.
147
+ Active staff separation cannot be overridden by a workspace agent. Mistaken
148
+ global associations require a verified global administrator's reviewed browser
149
+ resolution; this skill grants no such authority and cannot call that path.
150
+ 9. Read the Prospect and its visible Profiles. `profile_identity_history` (CLI
151
+ `profiles history RESOLUTION_ID`) returns only this workspace's movement
152
+ history, oldest first, at most 100 rows with an explicit truncation flag.
153
+ When truncated, newer movements may be unavailable in that response. Use the
154
+ apply response's `resolutionId`; `already_associated` may return null because
155
+ no new movement was recorded. It does not reveal
156
+ global staff notes or other workspaces. A historical apply receipt is distinct
157
+ from current identity state after a later correction.
158
+
159
+ Inspect installed client descriptors and server capability before using match
160
+ operations. An older server/client may not implement them; report the capability
161
+ gap and retain the research rather than forcing an observation association.
162
+ A person and company remain separate typed Prospects linked by citations.
163
+
164
+ ## Evidence, cost and safe resumption
165
+
166
+ Use shared observation/citation/evidence/qualification owners and their live
167
+ schemas. Other research tools are welcome; their submissions remain attributed
168
+ to the caller, never forged supplier receipts or verified identity flags.
169
+ Preserve unknown fields, contrary sources, producing version and coverage limits.
170
+ Every CRM identifier comes from a server result, not a URL or local helper ref.
171
+
172
+ For `prospect_record_evidence` with `kind:interpretation`, `data` accepts
173
+ `conclusion`, optional `reason`, and optional `evidenceIds` only. Use actual saved
174
+ observation IDs; a citation ID or local receipt reference is not an observation.
175
+ Keep topic/post context in the existing saved graph, the reason and the local
176
+ receipt, rather than inventing extra `data` keys. For example, replace every
177
+ bracketed value with the selected workspace or a returned server ID:
178
+
179
+ ```json
180
+ {
181
+ "workspaceId": "<selected workspace UUID>",
182
+ "requestId": "<new stable request UUID>",
183
+ "subject": { "kind": "person", "prospectId": "<current canonical Prospect UUID>" },
184
+ "kind": "interpretation",
185
+ "data": {
186
+ "conclusion": "Investigate; engagement alone does not establish fit.",
187
+ "reason": "State the retained source context and any missing page coverage.",
188
+ "evidenceIds": ["<retained observation UUID>"]
189
+ },
190
+ "sourceUrl": "https://example.org/observed-source",
191
+ "observedAt": null,
192
+ "skill": { "id": "audienti", "version": "1.3.0" }
193
+ }
194
+ ```
195
+
196
+ `observedAt:null` means unknown, not now. Signal evidence has its own live
197
+ `signalId`/`result`/`details` contract; an interpretation is not a qualification
198
+ or experiment admission.
199
+
200
+ For operation recovery, use CLI `tools runs get OPERATION_ID` / `tools runs list`
201
+ or MCP `tools_runs_get` / `tools_runs_list`. Paid primitive MCP names use `tools_`
202
+ plus dots replaced by underscores, for example `tools_profiles_enrich` and
203
+ `tools_posts_comments`; saved-record and identity operation names are the explicit
204
+ names shown above, not this prefix rule.
205
+
206
+ Keep exact paid request IDs, inputs, operation IDs, status, usage and reported
207
+ charge/retrieval provenance. Read current balance through the existing tools.
208
+ New paid tasks stop at zero or below; admitted small work can overdraw. The agent
209
+ must not recreate tariffs or reserve balances. Core reads/evidence remain free
210
+ under normal workspace authorization, even when using compatible external tools
211
+ at zero hosted credits. No request, credential or billing information belongs in
212
+ an unprotected public report. Never include bearer/proxy/provider secrets.
@@ -0,0 +1,49 @@
1
+ # Agent research receipt
2
+
3
+ Keep a local research receipt as the agent performs explicit steps. This is a
4
+ reporting convention, not a new CRM object or permission. Never include bearer
5
+ tokens, proxy credentials or raw provider secrets. Use the installed tool schemas
6
+ for requests; this receipt does not define replacement API payloads.
7
+
8
+ Record the producing skill ID/version, selected workspace ID and requested scope.
9
+ For each finding keep:
10
+
11
+ - Its local reference, distinct from server-issued Prospect, Profile, observation,
12
+ topic and post IDs. Do not manufacture a CRM ID from a URL or local reference.
13
+ - The observed fact, source URL and observation time (or explicitly unknown time).
14
+ Keep the agent's interpretation separate. A reaction is not evidence of a buyer
15
+ problem, intent or consent to contact.
16
+ - Acquisition provenance: which explicit tool returned it, its operation/request
17
+ ID when available, and the evidence reference actually stored in the CRM.
18
+ An alternative research tool's result remains caller-attributed evidence.
19
+ - Current identity status comes from a fresh authorized `profile_get` /
20
+ `prospect_get`: saved independently, currently associated, or unconfirmed.
21
+ Record that read's time and returned canonical ID. A match/apply receipt is a
22
+ historical mutation event; replay can return it after a staff correction and
23
+ cannot confirm present ownership. A helper score or mention never confirms
24
+ identity. If the installed server lacks the guarded operations, preserve the
25
+ candidate and report the capability gap.
26
+ - Coverage limits, missing fields, pagination not retrieved, truncated content and
27
+ time uncertainty. Incoming comments/reactions on a post do not establish an
28
+ author's outgoing engagement history.
29
+
30
+ For paid calls retain the exact request ID and normalized input you sent, plus the
31
+ returned operation ID, terminal status and billing result. If the response is lost,
32
+ read that operation or replay the same request; do not invent a new ID. Do not count
33
+ an alias or request replay as an additional purchase. A cached result is distinct
34
+ from replaying an operation. Current CRM projection is distinct from paid history.
35
+
36
+ When resuming with a new skill version, retain the original producing version on
37
+ existing evidence and assessments. New interpretations get new evidence and their
38
+ actual producing version. Report an unavailable tool or exhausted scope as a gap;
39
+ do not silently install software, buy contact data, invoke AI or send outreach.
40
+
41
+ Read back the selected workspace's Prospect, visible profiles and saved evidence
42
+ before reporting persistence. Unconfirmed candidate URLs remain in this report;
43
+ never use observation intake with the anchor Prospect ID to force an association.
44
+
45
+ This convention follows the distinction between local references, CRM identities,
46
+ interaction evidence and incomplete coverage in the separately maintained
47
+ Audienti profile-audience-targeting and profile-engagement-targeting marketplace
48
+ skills. Those packages have separate publication ownership and are not installed
49
+ or executed by this package.
@@ -0,0 +1,72 @@
1
+ export const SKILL_VERSION = '1.3.0';
2
+ export const MAX_INPUT_BYTES = 1024 * 1024;
3
+ const text = maxLength => ({ type: 'string', maxLength });
4
+ const object = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
5
+ const array = (items, maxItems) => ({ type: 'array', items, maxItems });
6
+ const uuid = { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' };
7
+ const profile = object({
8
+ displayName: text(300), prospectName: text(300), companyName: text(300),
9
+ companyDomain: text(253), title: text(500), bio: text(10000), headline: text(1000),
10
+ description: text(10000), linkedinReference: text(1024), linkedinSlug: text(200),
11
+ handles: array(text(200), 12),
12
+ });
13
+ const candidate = object({
14
+ url: { ...text(2048), minLength: 1 }, title: text(1000), snippet: text(10000),
15
+ domain: text(253), profileId: uuid, prospectId: uuid,
16
+ }, ['url']);
17
+ const query = object({ query: text(16000), kind: { enum: ['email', 'website', 'social'] } }, ['query', 'kind']);
18
+ const ranked = object({
19
+ ...candidate.properties, score: { type: 'number' }, signals: array(text(40), 5),
20
+ identityStatus: { enum: ['saved_reference', 'unresolved'] },
21
+ suggestedAction: { enum: ['inspect_page', 'inspect_snippet', 'skip_low_score'] },
22
+ }, ['url', 'score', 'signals', 'identityStatus', 'suggestedAction']);
23
+ export const helperContracts = {
24
+ 'profile-completeness': {
25
+ description: 'Summarize supplied field states for research planning. Known absence stays distinct from missing evidence. Does not determine cache eligibility, billing or authority.',
26
+ inputSchema: object({ requiredFields: array({ ...text(80), minLength: 1 }, 40), fields: array(object({ name: { ...text(80), minLength: 1 }, state: { enum: ['available', 'absent', 'unavailable', 'truncated'] } }, ['name', 'state']), 80) }, ['requiredFields', 'fields']),
27
+ outputSchema: object({ version: { const: SKILL_VERSION }, authority: { const: 'planning_only' }, allFieldsKnown: { type: 'boolean' }, available: array(text(80), 40), absent: array(text(80), 40), needsEvidence: array(object({ field: text(80), state: { enum: ['unavailable', 'truncated', 'not_reported', 'conflicting'] } }, ['field', 'state']), 40) }, ['version', 'authority', 'allFieldsKnown', 'available', 'absent', 'needsEvidence']),
28
+ },
29
+ 'profile-evidence': {
30
+ description: 'Extract bounded observed URL and mention candidates from supplied profile text using V10 text rules. Caller-attributed evidence only; no fetch, canonicalization, attachment or identity decision.',
31
+ inputSchema: object({ profileId: uuid, observationId: uuid, platform: { enum: ['twitter', 'x', 'reddit', 'instagram', 'facebook', 'linkedin', 'tiktok', 'youtube', 'threads', 'github', 'medium'] }, bio: text(10000), websiteUrl: text(2048) }),
32
+ outputSchema: object({ version: { const: SKILL_VERSION }, authority: { const: 'caller_supplied' }, profileId: uuid, observationId: uuid, links: array(object({ url: text(2048), fields: array({ enum: ['bio', 'websiteUrl'] }, 2) }, ['url', 'fields']), 50), mentions: array(object({ platform: text(40), handle: text(200), field: { const: 'bio' } }, ['platform', 'handle', 'field']), 50), rejectedLinks: { type: 'integer', minimum: 0 }, omittedLinks: { type: 'integer', minimum: 0 }, omittedMentions: { type: 'integer', minimum: 0 } }, ['version', 'authority', 'links', 'mentions', 'rejectedLinks', 'omittedLinks', 'omittedMentions']),
33
+ },
34
+ 'profile-queries': {
35
+ description: 'Prepare bounded V10-derived profile search queries from supplied facts. Does not search or call the CRM.',
36
+ inputSchema: object({ profile, kind: { enum: ['social', 'email', 'website'] }, mode: { enum: ['broad', 'attachable'] } }, ['profile', 'kind']),
37
+ outputSchema: object({ version: { const: SKILL_VERSION }, queries: array(query, 5), missing: array(text(80), 2) }, ['version', 'queries', 'missing']),
38
+ },
39
+ 'profile-candidates': {
40
+ description: 'Rank at most 80 supplied results using V10 advisory weights; deduplicate references without claiming an identity match.',
41
+ inputSchema: object({ profile, candidates: array(candidate, 80), limit: { type: 'integer', minimum: 1, maximum: 12 } }, ['profile', 'candidates']),
42
+ outputSchema: object({ version: { const: SKILL_VERSION }, candidates: array(ranked, 12), duplicatesRemoved: { type: 'integer', minimum: 0 }, omitted: { type: 'integer', minimum: 0 } }, ['version', 'candidates', 'duplicatesRemoved', 'omitted']),
43
+ },
44
+ };
45
+
46
+ // This validator serves only the closed, bundled schemas above. It accepts no
47
+ // caller-provided schema, references, regular expressions or executable code.
48
+ export function validateInput(name, value) {
49
+ const contract = Object.hasOwn(helperContracts, name) && helperContracts[name];
50
+ if (!contract) throw new Error('Unknown bundled helper.');
51
+ function visit(schema, item) {
52
+ const invalid = () => { throw new Error('Invalid helper input; inspect the bundled input schema.'); };
53
+ if (schema.enum && !schema.enum.includes(item)) invalid();
54
+ if (schema.type === 'object') {
55
+ if (!item || typeof item !== 'object' || Array.isArray(item)) invalid();
56
+ if (Object.keys(item).some(key => !Object.hasOwn(schema.properties, key))) invalid();
57
+ if (schema.required.some(key => !Object.hasOwn(item, key))) invalid();
58
+ for (const [key, child] of Object.entries(item)) visit(schema.properties[key], child);
59
+ } else if (schema.type === 'array') {
60
+ if (!Array.isArray(item) || item.length > schema.maxItems) invalid();
61
+ for (const child of item) visit(schema.items, child);
62
+ } else if (schema.type === 'string') {
63
+ if (typeof item !== 'string' || item.length > (schema.maxLength ?? Infinity) || item.length < (schema.minLength ?? 0)) invalid();
64
+ if (schema.pattern && !new RegExp(schema.pattern).test(item)) invalid();
65
+ } else if (schema.type === 'integer') {
66
+ if (!Number.isInteger(item) || item < schema.minimum || item > schema.maximum) invalid();
67
+ }
68
+ }
69
+ visit(contract.inputSchema, value);
70
+ if (Buffer.byteLength(JSON.stringify(value), 'utf8') > MAX_INPUT_BYTES) throw new Error('Helper input exceeds 1 MiB.');
71
+ return value;
72
+ }
@@ -0,0 +1,138 @@
1
+ import { SKILL_VERSION, validateInput } from './contracts.mjs';
2
+
3
+ const broadNetworks = ['x.com', 'twitter.com', 'linkedin.com/in', 'instagram.com', 'facebook.com', 'github.com', 'medium.com', 'youtube.com', 'tiktok.com', 'threads.net', 'reddit.com'];
4
+ const attachableNetworks = ['x.com', 'twitter.com', 'linkedin.com/in', 'instagram.com', 'facebook.com', 'tiktok.com', 'reddit.com', 'youtube.com'];
5
+ const stopwords = new Set('a an and are as at be but by for if in into is it no not of on or such that the their then there these they this to was will with from via over under'.split(' '));
6
+ const trustedSources = ['rocketreach.co', 'apollo.io', 'hunter.io', 'clearbit.com', 'zoominfo.com', 'lusha.com'];
7
+ const socialSources = ['twitter.com', 'instagram.com', 'facebook.com', 'linkedin.com', 'github.com', 'medium.com', 'youtube.com', 'tiktok.com', 'threads.net', 'reddit.com'];
8
+ const term = value => (value ?? '').replaceAll('"', ' ').replace(/\s+/g, ' ').trim();
9
+ const unique = values => [...new Set(values.filter(Boolean))];
10
+ const quoted = value => `"${term(value)}"`;
11
+ const personName = value => {
12
+ const name = term(value), tokens = name.split(' ');
13
+ return /^[\p{L}\s'.-]+$/u.test(name) && tokens.length >= 2 && tokens.every(token => token.length >= 2) ? name : '';
14
+ };
15
+
16
+ // Inputs are already selected facts. Employer selection, canonical URL parsing,
17
+ // identity validation and database reads belong to the CRM and the calling agent.
18
+ function queries({ profile, kind, mode = 'broad' }) {
19
+ const displayName = term(profile.displayName), company = term(profile.companyName), domain = term(profile.companyDomain);
20
+ let values = [];
21
+ if (kind === 'social') {
22
+ const name = personName(displayName) || personName(profile.prospectName);
23
+ const evidence = unique([company, domain, company && profile.title ? `${term(profile.title)} at ${company}` : '', term(profile.linkedinReference), term(profile.linkedinSlug)]);
24
+ const gate = (mode === 'broad' ? broadNetworks : attachableNetworks).map(host => `site:${host}`).join(' OR ');
25
+ if (name) values.push(`${quoted(name)}${evidence.length ? ` (${evidence.map(quoted).join(' OR ')})` : ''} (${gate})`);
26
+ const handles = unique([...(profile.handles ?? []), profile.linkedinSlug].map(value => term(value).replace(/^@/, '')).filter(value => value.length >= 3 && !/\s|@|:\/\//.test(value) && !/^(seed-|unknown$|profile$)/i.test(value)));
27
+ const single = [], groups = [];
28
+ for (const handle of handles) {
29
+ // V10 keeps LinkedIn opaque IDs case-sensitive; it does not manufacture
30
+ // permutations of them. Other observed handles supply query variants only.
31
+ if (/^ACo[A-Za-z0-9_-]+$/.test(handle)) { single.push(handle); continue; }
32
+ const tokens = handle.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
33
+ if (tokens.length === 1) single.push(tokens[0]);
34
+ else if (tokens.length) groups.push(tokens);
35
+ }
36
+ const variants = unique([...single.sort(), ...['', '-', '_'].flatMap(separator => [...groups].sort((a, b) => a.join(separator) < b.join(separator) ? -1 : a.join(separator) > b.join(separator) ? 1 : 0).map(tokens => tokens.join(separator)))]);
37
+ const fallback = unique([company, domain, name]);
38
+ if (variants.length >= 2 && fallback.length) values.push(`(${variants.map(quoted).join(' OR ')}) (${fallback.map(quoted).join(' OR ')}) (${gate})`);
39
+ } else if (displayName) {
40
+ if (kind === 'email') {
41
+ // V10 templates requiring missing values are skipped, not filled with
42
+ // guessed company identity or fabricated contact details.
43
+ if (company) values.push(`${displayName} ${company} email`);
44
+ if (domain) values.push(`${displayName} "@${domain}" email`);
45
+ } else {
46
+ values = [`${displayName} website`, `${displayName} about`];
47
+ if (company) values.push(`${displayName} ${company} contact`);
48
+ }
49
+ }
50
+ values = unique(values).slice(0, 5);
51
+ return { version: SKILL_VERSION, queries: values.map(query => ({ query, kind })), missing: values.length ? [] : ['usable_profile_search_context'] };
52
+ }
53
+
54
+ const normalizeName = value => value.toLowerCase().replace(/[-_.]/g, ' ').replace(/ +/g, ' ').trim();
55
+ const tokens = value => new Set(value.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(token => token.length >= 3 && !stopwords.has(token)));
56
+ function score(profile, candidate) {
57
+ const name = normalizeName(profile.displayName ?? ''), parts = name.split(/\s+/);
58
+ const title = (candidate.title ?? '').toLowerCase(), snippet = (candidate.snippet ?? '').toLowerCase(), domain = (candidate.domain ?? '').toLowerCase();
59
+ const nameIn = value => name && (normalizeName(value).includes(name) || (parts.length >= 2 && normalizeName(value).includes(parts[0]) && normalizeName(value).includes(parts.at(-1))));
60
+ let amount = 0; const signals = [];
61
+ if (nameIn(title) || nameIn(snippet)) { amount += 0.4; signals.push('name_match'); }
62
+ const companyDomain = (profile.companyDomain ?? '').toLowerCase().replace(/^www\./, '').split('/')[0];
63
+ // Preserve V10's advisory substring weights, not an authorization predicate.
64
+ if (companyDomain && domain && (domain.replace(/^www\./, '').split('/')[0] === companyDomain || domain.includes(companyDomain))) { amount += 0.3; signals.push('company_domain'); }
65
+ if (trustedSources.some(value => domain.includes(value))) { amount += 0.2; signals.push('trusted_source'); }
66
+ if (socialSources.some(value => domain.includes(value))) { amount += 0.1; signals.push('social_platform'); }
67
+ const a = tokens([profile.displayName, profile.bio, profile.headline, profile.description].filter(Boolean).join(' ')), b = tokens(`${title} ${snippet}`);
68
+ const union = new Set([...a, ...b]).size, intersection = [...a].filter(value => b.has(value)).length;
69
+ if (union && intersection / union >= 0.2) { amount += 0.15; signals.push('text_overlap'); }
70
+ return { score: Math.round(amount * 100) / 100, signals };
71
+ }
72
+
73
+ function candidates({ profile, candidates: rows, limit = 12 }) {
74
+ const seen = new Set(), ranked = []; let duplicatesRemoved = 0;
75
+ for (const row of rows) {
76
+ const key = row.profileId ? `profile:${row.profileId}` : `raw:${row.url}`;
77
+ if (seen.has(key)) { duplicatesRemoved++; continue; }
78
+ seen.add(key);
79
+ const result = score(profile, row);
80
+ ranked.push({ ...row, ...result, identityStatus: row.profileId ? 'saved_reference' : 'unresolved', suggestedAction: result.score < 0.4 ? 'skip_low_score' : result.score > 0.7 ? 'inspect_page' : 'inspect_snippet' });
81
+ }
82
+ ranked.sort((a, b) => b.score - a.score);
83
+ return { version: SKILL_VERSION, candidates: ranked.slice(0, limit), duplicatesRemoved, omitted: Math.max(0, ranked.length - limit) };
84
+ }
85
+
86
+ function completeness({ requiredFields, fields }) {
87
+ const available = [], absent = [], needsEvidence = [];
88
+ for (const field of unique(requiredFields)) {
89
+ const states = unique(fields.filter(row => row.name === field).map(row => row.state));
90
+ const state = states.length > 1 ? 'conflicting' : states[0] ?? 'not_reported';
91
+ if (state === 'available') available.push(field);
92
+ else if (state === 'absent') absent.push(field);
93
+ else needsEvidence.push({ field, state });
94
+ }
95
+ return { version: SKILL_VERSION, authority: 'planning_only', allFieldsKnown: needsEvidence.length === 0, available, absent, needsEvidence };
96
+ }
97
+
98
+ // V10 Utils::TextScanner and Profiles::Writer prepare observations. URL identity
99
+ // normalization and authoritative citation/identity writes remain server-owned.
100
+ const observedUrlPattern = /https?:\/\/[^\s<>"')]+/g;
101
+ const sensitiveUrlKey = value => /token$|secret|password|signature|credential|accesskey|^(auth|authorization|key|code|sig|apikey|sas)$/.test(value.replace(/[^a-z0-9]/gi, '').toLowerCase());
102
+ function evidence({ profileId, observationId, platform, bio = '', websiteUrl = '' }) {
103
+ const links = new Map(); let rejectedLinks = 0;
104
+ const add = (raw, field) => {
105
+ const url = raw.trim().replace(/\.$/, '').replace(/,$/, '').replace(/!$/, '');
106
+ try {
107
+ const parsed = new URL(url);
108
+ if (url.length > 2048 || /\s/.test(url) || !['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || [...parsed.searchParams.keys(), ...new URLSearchParams(parsed.hash.slice(1)).keys()].some(sensitiveUrlKey)) throw new Error();
109
+ } catch { rejectedLinks++; return; }
110
+ const fields = links.get(url) ?? [];
111
+ if (!fields.includes(field)) fields.push(field);
112
+ links.set(url, fields);
113
+ };
114
+ if (websiteUrl.trim()) add(websiteUrl, 'websiteUrl');
115
+ for (const match of bio.matchAll(observedUrlPattern)) add(match[0], 'bio');
116
+ let handles = [];
117
+ if (platform) {
118
+ // Unlike V10's unbounded @ regex, require a token boundary so an email
119
+ // address cannot become a proposed social identity. A mention is never a URL.
120
+ const pattern = platform === 'reddit' ? /\bu\/([A-Za-z0-9_-]+)/g
121
+ : ['twitter', 'x'].includes(platform) ? /(?<![\p{L}\p{N}_@./])@([A-Za-z0-9_]{1,15})(?![A-Za-z0-9_])/gu
122
+ : /(?<![\p{L}\p{N}_@./])@([A-Za-z0-9_.]+[A-Za-z0-9_])/gu;
123
+ const mentionText = bio.replace(observedUrlPattern, ' ');
124
+ handles = unique([...mentionText.matchAll(pattern)].map(match => match[1].toLowerCase()));
125
+ }
126
+ const selectedHandles = handles.filter(handle => handle.length <= 200).slice(0, 50);
127
+ return { version: SKILL_VERSION, authority: 'caller_supplied', ...(profileId ? { profileId } : {}), ...(observationId ? { observationId } : {}),
128
+ links: [...links].slice(0, 50).map(([url, fields]) => ({ url, fields })), mentions: selectedHandles.map(handle => ({ platform, handle, field: 'bio' })),
129
+ rejectedLinks, omittedLinks: Math.max(0, links.size - 50), omittedMentions: handles.length - selectedHandles.length };
130
+ }
131
+
132
+ export function runHelper(name, input) {
133
+ validateInput(name, input);
134
+ if (name === 'profile-queries') return queries(input);
135
+ if (name === 'profile-candidates') return candidates(input);
136
+ if (name === 'profile-completeness') return completeness(input);
137
+ return evidence(input);
138
+ }