@audienti/cli 0.1.145 → 0.2.0-exora.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -902
- package/bin/audienti-mcp.mjs +2 -0
- package/cli/account-intake-operations.mjs +28 -0
- package/cli/account-intake.mjs +33 -0
- package/cli/account-replies.mjs +35 -0
- package/cli/account-reply-operations.mjs +30 -0
- package/cli/agents.mjs +107 -0
- package/cli/approach-operations.mjs +12 -0
- package/cli/assignment-operations.mjs +218 -0
- package/cli/assignment.mjs +116 -0
- package/cli/audience-operations.mjs +16 -0
- package/cli/audiences.mjs +26 -0
- package/cli/audienti.mjs +124 -0
- package/cli/connection-registry-operations.mjs +7 -0
- package/cli/content.mjs +29 -0
- package/cli/discovery-operations.mjs +27 -0
- package/cli/discovery.mjs +14 -0
- package/cli/experiment-operations.mjs +25 -0
- package/cli/experiments.mjs +26 -0
- package/cli/json-file.mjs +13 -0
- package/cli/mcp.mjs +47 -0
- package/cli/offer-operations.mjs +12 -0
- package/cli/offers.mjs +24 -0
- package/cli/owner-account-client.mjs +18 -0
- package/cli/profiles.mjs +29 -0
- package/cli/progress.mjs +12 -0
- package/cli/prospecting-settings-operations.mjs +16 -0
- package/cli/prospecting-settings.mjs +15 -0
- package/cli/recipient-operations.mjs +13 -0
- package/cli/recipients.mjs +30 -0
- package/cli/sender-account-operations.mjs +24 -0
- package/cli/sender-accounts.mjs +23 -0
- package/cli/skills-mcp.mjs +24 -0
- package/cli/skills.mjs +56 -0
- package/cli/suppression-operations.mjs +8 -0
- package/cli/suppression.mjs +12 -0
- package/cli/tools-operations.mjs +70 -0
- package/cli/tools.mjs +72 -0
- package/cli/work-execution-operations.mjs +35 -0
- package/cli/work-execution.mjs +29 -0
- package/cli/work-feedback-operations.mjs +17 -0
- package/cli/work-feedback.mjs +18 -0
- package/cli/work-operations.mjs +28 -0
- package/cli/work.mjs +27 -0
- package/cli/workspaces.mjs +158 -0
- package/package.json +15 -26
- package/skills/audienti/SKILL.md +61 -323
- package/skills/audienti/references/helpers.md +149 -0
- package/skills/audienti/references/profile-workflows.md +212 -0
- package/skills/audienti/references/workflow-receipt.md +49 -0
- package/skills/audienti/scripts/contracts.mjs +72 -0
- package/skills/audienti/scripts/profile-helpers.mjs +138 -0
- package/skills/audienti/scripts/run.mjs +40 -0
- package/source-manifest.json +59 -0
- package/.mcp.json +0 -13
- package/CHANGELOG.md +0 -1018
- package/bin/audienti-mcp.js +0 -5
- package/bin/audienti.js +0 -6
- package/install +0 -83
- package/skills/audienti/references/methodology.md +0 -111
- package/skills/audienti-gift-research/SKILL.md +0 -47
- package/src/agent-skills.js +0 -60
- package/src/api-client.js +0 -1639
- package/src/cli.js +0 -16992
- package/src/config.js +0 -130
- 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
|
+
}
|