@audienti/cli 0.2.0-exora.3 → 0.2.0-exora.30
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 +2 -2
- package/cli/account-replies.mjs +16 -2
- package/cli/account-reply-operations.mjs +28 -1
- package/cli/agents.mjs +56 -14
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audience-operations.mjs +1 -1
- package/cli/audienti.mjs +118 -27
- package/cli/audit-operations.mjs +2 -2
- package/cli/campaign-find.mjs +155 -0
- package/cli/campaign-operations.mjs +8 -4
- package/cli/campaigns.mjs +38 -3
- package/cli/cohort-operations.mjs +1 -1
- package/cli/content.mjs +2 -0
- package/cli/decision-operations.mjs +35 -0
- package/cli/decision.mjs +51 -0
- package/cli/discovery-operations.mjs +2 -1
- package/cli/discovery.mjs +3 -2
- package/cli/duplicate-operations.mjs +22 -0
- package/cli/duplicates.mjs +22 -0
- package/cli/hand-task-operations.mjs +11 -0
- package/cli/hand-tasks.mjs +18 -0
- package/cli/intent-operations.mjs +44 -0
- package/cli/intent.mjs +109 -0
- package/cli/json-file.mjs +1 -1
- package/cli/kv-operations.mjs +17 -0
- package/cli/kv.mjs +58 -0
- package/cli/mcp.mjs +4 -4
- package/cli/outcome-operations.mjs +28 -0
- package/cli/outcomes.mjs +16 -0
- package/cli/owner-account-client.mjs +5 -4
- package/cli/playbook-help.mjs +87 -0
- package/cli/playbook-operations.mjs +40 -0
- package/cli/progress.mjs +19 -8
- package/cli/prospecting-settings-operations.mjs +1 -1
- package/cli/question-operations.mjs +4 -4
- package/cli/questions.mjs +4 -4
- package/cli/queue-operations.mjs +7 -6
- package/cli/queue.mjs +4 -3
- package/cli/recipient-operations.mjs +1 -1
- package/cli/recipients.mjs +6 -2
- package/cli/rooms-operations.mjs +26 -5
- package/cli/rooms.mjs +32 -6
- package/cli/secret-operations.mjs +7 -0
- package/cli/secrets.mjs +30 -0
- package/cli/sender-account-operations.mjs +8 -3
- package/cli/sender-accounts.mjs +20 -10
- package/cli/skills-mcp.mjs +6 -2
- package/cli/skills.mjs +55 -12
- package/cli/tools-operations.mjs +19 -10
- package/cli/tools.mjs +48 -20
- package/cli/work-execution-operations.mjs +7 -0
- package/cli/work-execution.mjs +3 -3
- package/cli/work-operations.mjs +5 -4
- package/cli/work.mjs +2 -2
- package/cli/workspaces.mjs +104 -22
- package/package.json +2 -2
- package/skills/agent-rooms/SKILL.md +140 -0
- package/skills/agent-rooms/references/commands.md +78 -0
- package/skills/audienti/SKILL.md +111 -5
- package/skills/audienti/references/helpers.md +15 -2
- package/skills/audienti/references/profile-workflows.md +4 -2
- package/skills/audienti/scripts/contracts.mjs +1 -1
- package/skills/getting-started/SKILL.md +188 -154
- package/skills/getting-started/references/audience.md +72 -7
- package/skills/getting-started/references/campaign-draft.md +56 -13
- package/skills/getting-started/references/capabilities.md +238 -0
- package/skills/getting-started/references/mailbox-check.md +64 -0
- package/skills/getting-started/references/outcome-checks.md +125 -0
- package/skills/getting-started/references/replies.md +314 -0
- package/skills/getting-started/references/reply-checks.md +54 -0
- package/skills/playbook-design/SKILL.md +139 -0
- package/skills/playbook-design/references/fields.md +145 -0
- package/source-manifest.json +74 -48
- package/cli/approach-operations.mjs +0 -24
- package/skills/getting-started/references/inventory.md +0 -52
package/cli/audienti.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { isAssignmentCommand, assignmentCli } from './assignment.mjs';
|
|
3
|
+
import { playbookHelp } from './playbook-help.mjs';
|
|
3
4
|
// The CLI is an HTTP client. It has no database access or separate business rules.
|
|
4
5
|
const args = process.argv.slice(2);
|
|
5
6
|
const usage = `Profiles: audienti profiles list|search|get ID | match evaluate|apply SOURCE CANDIDATE | history RESOLUTION (saved workspace records)
|
|
6
|
-
Skills: audienti skills list|show
|
|
7
|
-
Tools: audienti tools
|
|
8
|
-
Sender: audienti sender accounts list|get|alias|register|bind|unbind|state|automation|activity report [--profile PATH]
|
|
7
|
+
Skills: audienti skills list [--compact]|show SKILL|read SKILL RESOURCE|helper NAME --file INPUT.json|- [--json]
|
|
8
|
+
Tools: audienti call TOOL ['JSON'] | tools search "WORDS"|get TOOL|list|runs|balance [--help]
|
|
9
|
+
Sender: audienti sender accounts list|get|alias|register|bind|unbind|state|workspaces|automation|activity report [--profile PATH]
|
|
10
|
+
Managed LinkedIn: audienti sender managed list|connect [--workspace ID] [--profile PATH] (connect: the user only; returns a sign-in link)
|
|
11
|
+
audienti sender managed plan MANAGED_ACCOUNT_ID --plan free|premium_career|premium_business|premium_all_in_one|sales_navigator_core|sales_navigator_advanced|recruiter_lite|none [--workspace ID] (the user only; sets the connection note length; none clears it)
|
|
9
12
|
Prospecting: audienti prospecting settings show|save|accounts|account-show|account-save|history [--profile PATH]
|
|
10
13
|
Usage: audienti whoami [--json]
|
|
11
14
|
audienti workspace assignments list [--workspace ID]
|
|
@@ -23,61 +26,87 @@ Required environment:
|
|
|
23
26
|
AUDIENTI_API_TOKEN Account API token (create it from your Account page)
|
|
24
27
|
|
|
25
28
|
Use HTTPS for remote hosts. Local HTTP is supported on loopback only.
|
|
26
|
-
Agent commands: agent connect --name NAME [--capabilities find,enrich,
|
|
29
|
+
Agent commands: agent connect --name NAME [--write (ask for Allow workspace changes up front)] [--capabilities find,enrich,expand,research,engage,sync,notify,approve]|poll|cancel|list|revoke ID --profile PATH
|
|
27
30
|
Owner permission change: agent permission ID --scope workspace:read|workspace:access --profile USER_PROFILE
|
|
28
|
-
Owner capability change: agent capabilities ID --set find,enrich,
|
|
29
|
-
Agent capabilities: agent capabilities report --file
|
|
31
|
+
Owner capability change: agent capabilities ID --set find,enrich,expand,research,engage,sync,notify,approve --profile USER_PROFILE (agents are refused outside this set)
|
|
32
|
+
Agent capabilities: agent capabilities report --file CAPABILITIES.json --profile AGENT_PROFILE (capability rows; the old per-account shape still works)
|
|
33
|
+
What the agent can do: agent capabilities [--workspace ID] [--json] --profile AGENT_PROFILE (works now, and works with Audienti tools)
|
|
30
34
|
Read capabilities: agent capabilities show [--id AGENT_ID] --profile PROFILE
|
|
31
|
-
Getting started: skills show getting-started, then agent
|
|
32
|
-
Read tool inventory: agent inventory show [--id AGENT_ID] --profile PROFILE
|
|
35
|
+
Getting started: skills show getting-started, then agent capabilities report --file CAPABILITIES.json --profile AGENT_PROFILE
|
|
36
|
+
Read tool inventory: agent inventory show [--id AGENT_ID] --profile PROFILE (agent inventory report still works; capabilities report is current)
|
|
33
37
|
Account authorization: agent accounts show [--id AGENT_ID] --profile PROFILE
|
|
34
38
|
Authorize/revoke: agent accounts authorize --id AGENT_ID --provider email|linkedin --account REF --authorized true|false --profile OWNER_PROFILE
|
|
35
39
|
Sharing: agent capabilities sharing --id AGENT_ID --profile OWNER_PROFILE
|
|
36
40
|
Share/unshare: agent capabilities share --id AGENT_ID --provider email|linkedin --account REF --workspace ID --shared true|false --profile OWNER_PROFILE
|
|
37
41
|
Team member: workspace member USER_ID [--workspace ID] --profile PROFILE
|
|
38
|
-
Work execution: work execution claim|start|get PLAN --action ID; release|lookup --claim ID; report --attempt ID
|
|
42
|
+
Work execution: work execution claim|start|send|get PLAN --action ID (send: managed LinkedIn accounts, --file with claimId and requestId); release|lookup --claim ID; report --attempt ID
|
|
39
43
|
Work feedback: work changes request|list|get|respond|withdraw PLAN --workspace ID
|
|
40
|
-
Work: work plan save|list|show|approve|withdraw-approval|cancel|eligibility
|
|
44
|
+
Work: work plan save|list|show|approve|withdraw-approval|cancel|skip|eligibility
|
|
41
45
|
Activity: work activity --workspace ID [--search TEXT] [--cursor-at ISO --cursor-id ID]
|
|
42
46
|
Work list: work list --workspace ID [--order oldest|priority] [--queue review|blocked|activity]
|
|
47
|
+
a plan that only waits for its cadence is in neither review nor blocked; it returns to review when the wait ends
|
|
43
48
|
Work writes: --file INPUT.json; human review needs a human workspace:access token
|
|
44
|
-
Audiences: audience options --kind industries|locations|sizes|types|seniorities|titles|functions
|
|
49
|
+
Audiences: audience options --kind industries|locations|sizes|types|seniorities|titles|functions [--q TEXT]
|
|
45
50
|
audience list|show ID|create|update ID|archive ID|restore ID
|
|
46
51
|
audience summary [--audience ID]|people ID|companies ID
|
|
47
52
|
audience test ID --url LINKEDIN_URL --request-id UUID (spends hosted credits unless the profile is saved)
|
|
48
53
|
Files: file upload PATH --type MIME|list|prepare|register|download|delete ID (personal files, human token only)
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
Hand tasks: hand-tasks [list [--state open|ready|done|skipped]]|show ID|done ID --outcome TEXT [--notes TEXT] [--result replied|meeting_booked]|skip ID --reason TEXT|event ID [--name TEXT] (a person does a Custom step by hand and records what happened)
|
|
55
|
+
Queue: audienti queue [list]|next|claim ID [--note TEXT]|done ID [--note TEXT]|skip ID --note TEXT|release ID [--workspace ID] (start here after you connect; without --workspace an agent reads every workspace its user approved it in; done or skip again with a new --note corrects the note of a finished task)
|
|
56
|
+
Attachments: audienti attachments upload FILE [--duration SECONDS] (store an image or voice note, get its id for content.attachments)
|
|
57
|
+
Outcome checks: audienti outcomes report TASK_ID --file RESULTS.json (results for the people an outcomes.check task lists)
|
|
58
|
+
Saved posts: audienti posts list [--topic-id ID]|get ID (savedByOperationId names the tool run that saved a post; read it with audienti tools runs get ID)
|
|
59
|
+
Saved topics: audienti topics list|get ID|create --input FILE --request-id UUID
|
|
60
|
+
Questions: audienti ask "QUESTION" [--context TEXT] [--option TEXT]... [--campaign ID] [--task ID] (questions for your user go to Operator, not chat)
|
|
51
61
|
audienti questions [list] [--state open|answered|withdrawn|all] | show ID | withdraw ID | answer ID --answer TEXT
|
|
52
|
-
Rooms: audienti rooms directory|inbox|ack|send|thread|threads|create|list|retire|restore|redact|resume|pause|profile|hook|watch|wait (audienti rooms help)
|
|
62
|
+
Rooms: audienti rooms directory|inbox|ack|send|thread|threads|create|list|read|retire|restore|redact|resume|pause|profile|hook|watch|wait (audienti rooms help)
|
|
63
|
+
KV: audienti kv get KEY|set KEY VALUE [--json] [--nx] [--if-version N] [--ttl 7d|--expires-at TIME]|list [--prefix P]|search [--key TEXT] [--value TEXT]|delete KEY (small state between runs; never credentials)
|
|
53
64
|
Offers: offer list|show ID|create|update ID|archive ID|restore ID
|
|
54
65
|
Offer options: --file FIELDS.json --request-id UUID --expected-revision N --revision N --archived true|false
|
|
55
66
|
audienti prospects replies set|get|list PERSON --workspace ID [--evidence EVIDENCE] [--file INPUT.json]
|
|
56
67
|
Account intake: prospects intake --workspace ID --file INPUT.json
|
|
57
68
|
prospects conversation append|list PERSON --workspace ID
|
|
58
|
-
Discovery: prospects list|get ID|observation create|citation create|evidence create
|
|
59
|
-
prospects contacts list|record PERSON --workspace ID
|
|
69
|
+
Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|citation create|evidence create
|
|
70
|
+
prospects contacts list|record PERSON [--file INPUT.json] [--request-id UUID] --workspace ID
|
|
60
71
|
prospects contacts get|retire PERSON CONTACT --workspace ID
|
|
61
72
|
prospects suppression get|set ID --workspace ID
|
|
73
|
+
prospects duplicates list --workspace ID | get|merge|dismiss PERSON --workspace ID [--primary PROFILE_ID] (same name and photo: the newer record is held until you decide)
|
|
62
74
|
guardrails show|pause|resume --workspace ID
|
|
63
75
|
dnc list|add VALUE|remove VALUE --workspace ID
|
|
64
76
|
dnc import --file LIST.csv --workspace ID (at most 5,000 rows; reports added, skipped and invalid rows)
|
|
65
77
|
dnc export [--output FILE.csv] --workspace ID
|
|
78
|
+
secret list [--workspace ID] (names only)
|
|
79
|
+
secret get NAME [--workspace ID] [--json] (prints the value; the secret records who read it last and when)
|
|
66
80
|
budgets show --workspace ID
|
|
67
81
|
budgets alerts --workspace ID [--limit N]
|
|
68
82
|
usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv]
|
|
83
|
+
workspace usage summary|callers|calls [--after ID] [--caller user:ID|agent:ID|key:ID] [--status all|succeeded|failed|pending]|call ID [--workspace ID] (paid tool use, last 30 days, and single calls)
|
|
84
|
+
intent list [--playbook ID] [--workspace ID] (the closed set of message labels: name, direction, event, channels, typed detail; --playbook adds that playbook's own labels as customLabels; output is JSON)
|
|
85
|
+
intent suggest (--text TEXT | --text-file PATH) --channel email|linkedin [--direction inbound|outbound] [--context TEXT] [--request-id UUID] [--workspace ID] (paid: what a message may mean, read by a model; prints the price per 1M input tokens first; suggests labels with a quote and reason, records nothing; keep one with intent record)
|
|
86
|
+
intent record --person ID --evidence ID --label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] [--request-id UUID] [--participation ID] [--workspace ID] (what a stored message means; get EVIDENCE from prospects conversation append|list; --file LABELS.json for several labels or a typed detail such as a stop result or a meeting time; every label is checked and a refusal says what to fix; a message you sent that asks for a meeting takes --label meeting_requested on its outbound evidence)
|
|
87
|
+
intent correct --person ID --event ID (--label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] | --file LABEL.json | --retract) [--request-id UUID] [--workspace ID] (a person changes or withdraws one label; the old one stays in the history; a change that routes the person elsewhere opens a decision and a change away from a stop never lifts it until a person confirms; an agent token is refused)
|
|
88
|
+
decision list [--state open|decided|all] [--person ID] [--participation ID] [--limit N] [--workspace ID] (what Operator asks the person when the rules do not decide; an open decision holds its person)
|
|
89
|
+
decision options ID [--workspace ID] (one decision with the options the playbook allows)
|
|
90
|
+
decision ask --participation ID [--reason TEXT] [--request-id UUID] [--workspace ID] (ask the person to decide about one running person)
|
|
91
|
+
decision prepare ID --recommended OPTION_ID --option OPTION_ID=ANALYSIS ... [--file PREPARE.json] [--request-id UUID] [--workspace ID] (an agent adds its view: two to five options, one line each)
|
|
92
|
+
decision answer ID --option OPTION_ID [--workspace ID] (a person picks; the move runs once)
|
|
69
93
|
audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
|
|
70
94
|
budgets set --workspace ID [--agent ID --cap N|none] [--new-agent-cap N|none] [--at-limit pause|ask] [--alert80 on|off] (alert80 covers Operator and email at 80%)
|
|
71
95
|
Qualification: campaigns qualification submit --file INPUT.json
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
Campaign summary: campaign summary [--workspace ID] (campaign counts per state, and per campaign its audience, offer, playbook, goal stage, people, engaged and at-goal counts)
|
|
97
|
+
Find prospects: campaign find CAMPAIGN_ID --count N --max-credits C (searches with the audience filters, checks each person, admits only fits; stops at N fits or the credit cap and reports the spend)
|
|
98
|
+
campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
|
|
99
|
+
Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
|
|
100
|
+
campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
|
|
101
|
+
Playbooks: playbook list [--archived] [--limit N --cursor ID]|actions|show ID [--revision N]|save --file REQUEST.json|archive ID|restore ID|draft show ID|save ID --file REQUEST.json|discard ID [--expected-updated-at T]|publish ID [--request-id UUID]|delete ID|progress PARTICIPATION_ID|record --file REQUEST.json|event PARTICIPATION_ID --kind KIND --reason TEXT (audienti playbook SUBCOMMAND --help for the request shape)
|
|
102
|
+
Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
|
|
74
103
|
analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
|
|
75
104
|
analytics activity [--workspace ID] [--days N | --from ISO --to ISO] [--format json|csv]
|
|
76
105
|
analytics cohorts [--workspace ID] [--stage goal|KEY] [--cohorts N] [--horizon N] [filters] [--format json|csv]
|
|
77
106
|
analytics aging [--workspace ID] [--threshold-days N] [filters] [--format json|csv]
|
|
78
107
|
analytics filters [--workspace ID] (lists campaign, owner, offer, audience and tag values)
|
|
79
108
|
(filters: --campaign ID --owner ID --offer ID --audience ID --tag TAG; also accepted by analytics show)
|
|
80
|
-
Pipeline: pipeline show [--campaign ID] [--
|
|
109
|
+
Pipeline: pipeline show [--campaign ID] [--playbook-version ID]
|
|
81
110
|
Connections: connections list|register --file REQUEST.json|report --file REQUEST.json
|
|
82
111
|
Discovery writes use --file INPUT.json, --workspace ID and stable --request-id UUID
|
|
83
112
|
Workspace commands: workspace init|list|create|show|search|invite|invitations|use
|
|
@@ -86,23 +115,74 @@ Read state: workspace notification-read --workspace ID --key KEY --read true|fal
|
|
|
86
115
|
Search: workspace search --workspace ID --query TEXT [--people-only true] [--recent FILE.json]
|
|
87
116
|
Invitation commands: workspace invitation show|accept|decline|retry|cancel ID
|
|
88
117
|
Options: --workspace ID --name NAME --request-id UUID --email EMAIL --generation N
|
|
89
|
-
Optional private profile: --profile PATH (outside this repository)
|
|
90
|
-
Save an environment token: profile save --scope account:read|workspace:access|agents:manage --profile PATH
|
|
91
|
-
|
|
118
|
+
Optional private profile: --profile PATH (outside this repository), or the AUDIENTI_PROFILE variable. --profile wins.
|
|
119
|
+
Save an environment token: profile save --scope account:read|workspace:read|workspace:access|agents:manage --profile PATH
|
|
120
|
+
Tokens: token show [--profile PATH] (what the token can do; workspace:read can look but is refused on every change)
|
|
121
|
+
Start local MCP stdio server: mcp (audienti mcp --help for the settings)
|
|
92
122
|
--json writes the API response envelope.
|
|
93
123
|
`;
|
|
94
124
|
|
|
125
|
+
const MCP_HELP = `audienti mcp starts the MCP server on stdio. An agent app runs it as a command; do not type it in a shell.
|
|
126
|
+
Settings come from the environment of that command:
|
|
127
|
+
AUDIENTI_PROFILE the profile file made by agent connect or profile save (the usual way)
|
|
128
|
+
AUDIENTI_API_URL and AUDIENTI_API_TOKEN instead of a profile: the API URL ending in /api/v1 and a token
|
|
129
|
+
The workspace: a tool that works in a workspace takes workspaceId in its input. Call workspace_select once with a workspaceId to set a default for the session.
|
|
130
|
+
Tool names follow the commands: playbook_get for playbook show, playbook_draft_get for playbook draft show.
|
|
131
|
+
Errors name the MCP tool to call next, not the command.`;
|
|
132
|
+
// Example inputs for the commands that save prospects, so `--help` shows a working shape.
|
|
133
|
+
const examples = {
|
|
134
|
+
'decision prepare': { recommended: 'snooze_30', options: [{ id: 'snooze_30', analysis: 'They said ask again in March, so wait a month.' }, { id: 'continue', analysis: 'Keep the plan; a follow-up may go this week.' }] },
|
|
135
|
+
'prospects conversation append': { requestId: 'UUID', item: { source: { provider: 'email', accountRef: 'you@yourcompany.com', itemId: 'PROVIDER_MESSAGE_ID', threadId: null }, payload: { direction: 'outbound', conversationType: 'direct', occurredAt: '2026-11-06T14:00:00Z', subject: 'Ten minutes on late invoices?', body: 'Hi Jordan, could we find 20 minutes next week?', participants: null } } },
|
|
136
|
+
'intent correct': { label: 'not_interested', quote: 'not a fit for us right now', reason: 'They decline, so this is not interest.' },
|
|
137
|
+
'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
|
|
138
|
+
'prospects observation create': { requestId: 'UUID', kind: 'person', source: { provider: 'linkedin', url: 'https://www.linkedin.com/in/jane-doe' }, fields: { name: 'Jane Doe', title: 'VP Sales', imageUrl: 'https://media.licdn.com/dms/image/PHOTO_ID' }, provenance: { url: 'https://example.com/team/jane' }, observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
|
|
139
|
+
'prospects citation create': { requestId: 'UUID', source: { kind: 'prospect', id: 'PERSON_PROSPECT_ID' }, target: { kind: 'prospect', id: 'COMPANY_PROSPECT_ID' }, edgeType: 'works_at', roles: [{ roleId: 'role-1', title: 'VP Sales', current: true, observedAt: '2026-01-01T00:00:00Z' }] },
|
|
140
|
+
'prospects evidence create': { requestId: 'UUID', subject: { kind: 'person', prospectId: 'PROSPECT_ID' }, campaignId: 'CAMPAIGN_ID', campaignRevision: 1, kind: 'interpretation', data: { conclusion: 'Leads a 40-person sales team', reason: 'Team page lists her as VP Sales' }, sourceUrl: 'https://example.com/team', observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
|
|
141
|
+
'prospects contacts record': { kind: 'email|linkedin|mobile|phone', value: 'jane@example.com, or +15551234567 for a mobile or a direct office phone', reason: 'Published on https://example.com/contact', requestId: 'UUID', provenance: { observationId: 'OBSERVATION_ID' } },
|
|
142
|
+
'campaigns qualification submit': { requestId: 'UUID', campaignId: 'CAMPAIGN_ID', campaignRevision: 1, prospectId: 'PERSON_PROSPECT_ID', companyProspectId: 'COMPANY_PROSPECT_ID', companyIcp: { status: 'pass|fail|unverified', evidenceIds: ['COMPANY_EVIDENCE_ID'] }, personIcp: { status: 'pass|fail|unverified', evidenceIds: ['PERSON_EVIDENCE_ID'] }, signal: { status: 'match|no_match|unverified|not_required', evidenceIds: [] }, fit: { status: 'high|moderate|low|no_fit|unscored', evidenceIds: ['PERSON_EVIDENCE_ID'], reason: 'why' }, skill: { id: 'signal-prospect-research', version: '1' } },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function commandHelp() {
|
|
146
|
+
const words = args.slice(0, args.findIndex(value => value.startsWith('-'))).filter(Boolean);
|
|
147
|
+
const lines = usage.split('\n');
|
|
148
|
+
let shown = [];
|
|
149
|
+
for (let n = Math.min(words.length, 2); n > 0 && shown.length === 0; n--) {
|
|
150
|
+
const prefix = words.slice(0, n).join(' ');
|
|
151
|
+
shown = lines.filter(line => line.includes(prefix));
|
|
152
|
+
}
|
|
153
|
+
console.log(shown.length ? shown.map(line => line.trim()).join('\n') : usage);
|
|
154
|
+
const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command) || (words.length > 0 && command.startsWith(words.join(' '))));
|
|
155
|
+
if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('decision') ? 'Choose option ids from `audienti decision options ID`. One plain line each, at most 140 characters, no IDs or error codes.' : example[0].startsWith('intent') ? `\nA message you sent that asks for a meeting (store it first with prospects conversation append, direction outbound):\naudienti intent record --person PERSON --evidence EVIDENCE --label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."\nRules: the "Label a message with its words" and "Label a message you sent" sections of the getting-started replies reference.` : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Says what is wrong with a command no handler took, instead of one message for every case. */
|
|
159
|
+
function unknownCommandMessage(words) {
|
|
160
|
+
const first = words[0];
|
|
161
|
+
if (first === undefined) return 'Give a command. Use audienti --help.';
|
|
162
|
+
if (first.startsWith('-')) return `Put the command first, then its options: audienti COMMAND [--workspace ID] [--profile PATH]. "${first}" is an option, not a command. Use audienti --help.`;
|
|
163
|
+
if (first === 'mcp') return 'mcp starts the local MCP server and takes no options. Run: audienti mcp';
|
|
164
|
+
return `"${first}" is not an audienti command. Use audienti --help for the commands. Most commands also take --workspace ID, and --profile PATH when the profile is not set in AUDIENTI_PROFILE.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
95
167
|
async function main() {
|
|
168
|
+
if (args[0] === 'playbook' && args.slice(1).some(value => value === '--help' || value === '-h')) { console.log(playbookHelp(args[1]?.startsWith('-') ? undefined : args[1] === 'draft' && args[2] && !args[2].startsWith('-') ? `draft_${args[2]}` : args[1])); return; }
|
|
169
|
+
if (args[0] === 'mcp' && args.length === 2 && (args[1] === '--help' || args[1] === '-h')) { console.log(MCP_HELP); return; }
|
|
170
|
+
if (args.length > 1 && args.slice(1).some(value => value === '--help' || value === '-h') && !['tools', 'call', 'skills'].includes(args[0])) { commandHelp(); return; }
|
|
96
171
|
if(args[0]==='prospects'&&['intake','conversation'].includes(args[1])){const {accountIntakeCli}=await import('./account-intake.mjs');await accountIntakeCli(args);return;}
|
|
97
172
|
if(args[0]==='sender'){const {senderAccountsCli}=await import('./sender-accounts.mjs');await senderAccountsCli(args);return;}
|
|
98
173
|
if(args[0]==='prospecting'){const {prospectingSettingsCli}=await import('./prospecting-settings.mjs');await prospectingSettingsCli(args);return;}
|
|
99
174
|
if(args[0]==='prospects'&&args[1]==='replies'){const {accountRepliesCli}=await import('./account-replies.mjs');await accountRepliesCli(args);return;}
|
|
100
175
|
if(args[0]==='prospects'&&args[1]==='contacts'){const {recipientsCli}=await import('./recipients.mjs');await recipientsCli(args);return;}
|
|
101
176
|
if(args[0]==='usage'){const {usageCli}=await import('./usage.mjs');await usageCli(args);return;}
|
|
177
|
+
if(args[0]==='decision'){const {decisionCli}=await import('./decision.mjs');await decisionCli(args);return;}
|
|
178
|
+
if(args[0]==='intent'){const {intentCli}=await import('./intent.mjs');await intentCli(args);return;}
|
|
102
179
|
if(args[0]==='audit'){const {auditCli}=await import('./audit.mjs');await auditCli(args);return;}
|
|
180
|
+
if(args[0]==='secret'||args[0]==='secrets'){const {secretsCli}=await import('./secrets.mjs');await secretsCli(args);return;}
|
|
103
181
|
if(args[0]==='budgets'){const {budgetsCli}=await import('./budgets.mjs');await budgetsCli(args);return;}
|
|
104
182
|
if(args[0]==='guardrails'||args[0]==='dnc'){const {guardrailsCli}=await import('./guardrails.mjs');await guardrailsCli(args);return;}
|
|
105
183
|
if(args[0]==='prospects'&&args[1]==='suppression'){const {suppressionCli}=await import('./suppression.mjs');await suppressionCli(args);return;}
|
|
184
|
+
if(args[0]==='prospects'&&args[1]==='duplicates'){const {duplicatesCli}=await import('./duplicates.mjs');await duplicatesCli(args);return;}
|
|
185
|
+
if(args[0]==='call'){const {callCli}=await import('./tools.mjs');await callCli(args);return;}
|
|
106
186
|
if(args[0]==='tools'){const {toolsCli}=await import('./tools.mjs');await toolsCli(args);return;}
|
|
107
187
|
if(['topics','posts'].includes(args[0])){const {contentCli}=await import('./content.mjs');await contentCli(args);return;}
|
|
108
188
|
if(args[0]==='work'&&args[1]==='execution'){const {workExecutionCli}=await import('./work-execution.mjs');await workExecutionCli(args);return;}
|
|
@@ -111,18 +191,22 @@ async function main() {
|
|
|
111
191
|
if(['prospects','campaigns'].includes(args[0])){const {discoveryCli}=await import('./discovery.mjs');await discoveryCli(args);return;}
|
|
112
192
|
if(args[0]==='analytics'&&['cohorts','aging','filters'].includes(args[1])){const {cohortCli}=await import('./cohorts.mjs');await cohortCli(args);return;}
|
|
113
193
|
if(args[0]==='analytics'&&['outbound','activity'].includes(args[1])){const {outboundCli}=await import('./outbound.mjs');await outboundCli(args);return;}
|
|
114
|
-
if(['
|
|
194
|
+
if(['playbook','analytics','pipeline','connections'].includes(args[0])){const {progressCli}=await import('./progress.mjs');await progressCli(args);return;}
|
|
115
195
|
if(args[0]==='campaign'){const {campaignCli}=await import('./campaigns.mjs');await campaignCli(args);return;}
|
|
116
196
|
if(args[0]==='audience'){const {audienceCli}=await import('./audiences.mjs');await audienceCli(args);return;}
|
|
117
197
|
if(args[0]==='file'){const {fileCli}=await import('./files.mjs');await fileCli(args);return;}
|
|
118
198
|
if(args[0]==='ask'){const {askCli}=await import('./questions.mjs');await askCli(args);return;}
|
|
119
199
|
if(args[0]==='questions'){const {questionsCli}=await import('./questions.mjs');await questionsCli(args);return;}
|
|
200
|
+
if(args[0]==='hand-tasks'){const {handTasksCli}=await import('./hand-tasks.mjs');await handTasksCli(args);return;}
|
|
120
201
|
if(args[0]==='queue'){const {queueCli}=await import('./queue.mjs');await queueCli(args);return;}
|
|
202
|
+
if(args[0]==='attachments'){const {attachmentsCli}=await import('./attachments.mjs');await attachmentsCli(args);return;}
|
|
203
|
+
if(args[0]==='outcomes'){const {outcomesCli}=await import('./outcomes.mjs');await outcomesCli(args);return;}
|
|
121
204
|
if(args[0]==='rooms'){const {roomsCli}=await import('./rooms.mjs');await roomsCli(args);return;}
|
|
205
|
+
if(args[0]==='kv'){const {kvCli}=await import('./kv.mjs');await kvCli(args);return;}
|
|
122
206
|
if(args[0]==='offer'){const {offerCli}=await import('./offers.mjs');await offerCli(args);return;}
|
|
123
207
|
if (args[0] === 'agent') { const { agentCli } = await import('./agents.mjs'); await agentCli(args); return; }
|
|
124
208
|
if (isAssignmentCommand(args)) { await assignmentCli(args); return; }
|
|
125
|
-
if (['workspace', 'profile'].includes(args[0])) { const { workspaceCli } = await import('./workspaces.mjs'); await workspaceCli(args); return; }
|
|
209
|
+
if (['workspace', 'profile', 'token'].includes(args[0])) { const { workspaceCli } = await import('./workspaces.mjs'); await workspaceCli(args); return; }
|
|
126
210
|
if (args[0] === 'profiles') { const { profilesCli } = await import('./profiles.mjs'); await profilesCli(args); return; }
|
|
127
211
|
if (args[0] === 'skills') { const { skillsCli } = await import('./skills.mjs'); await skillsCli(args); return; }
|
|
128
212
|
if (args[0] === 'mcp' && args.length === 1) { await import('./mcp.mjs'); return; }
|
|
@@ -132,12 +216,19 @@ async function main() {
|
|
|
132
216
|
if (args[0] === 'whoami' && profileIndex > 0 && args[profileIndex + 1]) {
|
|
133
217
|
const { configuration } = await import('./workspaces.mjs');
|
|
134
218
|
profileConfig = configuration(args[profileIndex + 1], 'account:read');
|
|
219
|
+
if (!profileConfig.token) throw new Error('whoami needs a human account token. Agents confirm their connection with: audienti workspace list --profile PATH');
|
|
135
220
|
args.splice(profileIndex, 2);
|
|
221
|
+
} else if (args[0] === 'whoami' && profileIndex < 0 && process.env.AUDIENTI_PROFILE && !(process.env.AUDIENTI_API_URL && process.env.AUDIENTI_API_TOKEN)) {
|
|
222
|
+
// The profile named by AUDIENTI_PROFILE (#448), unless the URL and token are in the environment.
|
|
223
|
+
const { configuration } = await import('./workspaces.mjs');
|
|
224
|
+
profileConfig = configuration(undefined, 'account:read');
|
|
225
|
+
if (!profileConfig.token) throw new Error('whoami needs a human account token, and AUDIENTI_PROFILE holds none. Agents confirm their connection with: audienti workspace list');
|
|
136
226
|
}
|
|
137
|
-
if (args[0] !== 'whoami'
|
|
227
|
+
if (args[0] !== 'whoami') throw new Error(unknownCommandMessage(args));
|
|
228
|
+
if (args.length > 2 || (args[1] && args[1] !== '--json')) throw new Error('whoami takes only --json and --profile PATH. Use audienti --help.');
|
|
138
229
|
const rawUrl = profileConfig ? profileConfig.url : process.env.AUDIENTI_API_URL;
|
|
139
230
|
const token = profileConfig ? profileConfig.token : process.env.AUDIENTI_API_TOKEN;
|
|
140
|
-
if (!rawUrl || !token) throw new Error('Set AUDIENTI_API_URL and AUDIENTI_API_TOKEN. Create a token from your Account page.');
|
|
231
|
+
if (!rawUrl || !token) throw new Error('Set AUDIENTI_API_URL and AUDIENTI_API_TOKEN, or pass --profile PATH (or set AUDIENTI_PROFILE) to a profile with an account:read token. Create a token from your Account page.');
|
|
141
232
|
let base;
|
|
142
233
|
try { base = new URL(rawUrl); } catch { throw new Error('AUDIENTI_API_URL must be a valid API base URL.'); }
|
|
143
234
|
if (base.username || base.password || base.search || base.hash) throw new Error('The API URL must not contain credentials, query parameters, or a fragment.');
|
|
@@ -163,4 +254,4 @@ async function main() {
|
|
|
163
254
|
console.log(args[1] === '--json' ? JSON.stringify({ data: profile }) : `${profile.email ?? profile.username ?? profile.id}\nAccount ID: ${profile.id}`);
|
|
164
255
|
}
|
|
165
256
|
|
|
166
|
-
main().catch(error => { console.error(
|
|
257
|
+
main().catch(error => { console.error(JSON.stringify({ error: error.toJSON ? error.toJSON() : { code: 'client_error', message: error.message } })); process.exitCode = 1; });
|
package/cli/audit-operations.mjs
CHANGED
|
@@ -5,13 +5,13 @@ export const auditOperations = {
|
|
|
5
5
|
schema: z.object({
|
|
6
6
|
workspaceId: z.string().uuid().optional(),
|
|
7
7
|
actor: z.enum(['person', 'agent']).optional(),
|
|
8
|
-
category: z.enum(['guardrails', 'budgets', 'approvals', 'suppression', 'members', 'agents']).optional(),
|
|
8
|
+
category: z.enum(['guardrails', 'budgets', 'approvals', 'suppression', 'members', 'agents', 'replies', 'billing', 'playbooks']).optional(),
|
|
9
9
|
since: z.string().datetime().optional(),
|
|
10
10
|
until: z.string().datetime().optional(),
|
|
11
11
|
cursor: z.string().max(500).optional(),
|
|
12
12
|
limit: z.number().int().min(1).max(200).optional(),
|
|
13
13
|
}).strict(),
|
|
14
14
|
method: 'GET', path: a => `/workspaces/${a.workspaceId}/audit-events?${query(a)}`, scoped: true,
|
|
15
|
-
description: 'List the read-only workspace audit log: who changed guardrails, budgets, approvals, suppression, members and
|
|
15
|
+
description: 'List the read-only workspace audit log: who changed guardrails, budgets, approvals, suppression, members, agent access and playbooks (who saved which version), newest first. Admin only. Page with cursor.',
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {HostedResponseUnknown} from './workspaces.mjs';
|
|
3
|
+
// #257: find N prospects for a Campaign. Searches LinkedIn with the Audience filters, then asks the server
|
|
4
|
+
// to check each new person. The server fetches, runs the rules and records the result; this file only
|
|
5
|
+
// drives the loop and adds up the spend. Amounts are pico USD (1 credit = 10,000,000,000).
|
|
6
|
+
export const PICO_PER_CREDIT=10_000_000_000n;
|
|
7
|
+
// Waits before each retry of a check whose outcome is unknown, in milliseconds.
|
|
8
|
+
const RECOVERY_WAITS=[2000,5000,15000];
|
|
9
|
+
const OUTCOMES=['fit','held','not_fit','already_found'];
|
|
10
|
+
// Only a definite answer lets the run go on. A check that is still open, a lost response or a server
|
|
11
|
+
// error may still be paying, so the same request ID is retried; anything else unexpected stops the run.
|
|
12
|
+
const pending=error=>error instanceof HostedResponseUnknown||error?.code==='check_running'||error?.status>=500&&!['tool_unavailable','history_policy_unavailable'].includes(error?.code);
|
|
13
|
+
// A refusal about this one person (a bad URL, no LinkedIn profile) is final, and the run moves on.
|
|
14
|
+
const skippable=error=>[400,404,422].includes(error?.status);
|
|
15
|
+
const MAX_SEARCHES=12,MAX_PAGES=10;
|
|
16
|
+
const positive=list=>(list??[]).filter(item=>!item.negative);
|
|
17
|
+
const numeric=value=>typeof value==='string'&&/^\d{1,20}$/.test(value);
|
|
18
|
+
// A charge the server reported for one request. Charges are cumulative per request ID, so the largest one
|
|
19
|
+
// seen across attempts is the request's charge so far.
|
|
20
|
+
const reportedCharge=value=>numeric(value)?BigInt(value):0n;
|
|
21
|
+
|
|
22
|
+
/** One profiles.search input per title, location and keyword combination the Audience and Campaign name. */
|
|
23
|
+
export function planSearches(audience,signals=[]){
|
|
24
|
+
const titles=positive(audience.person?.currentTitles).map(item=>item.name).filter(Boolean).slice(0,5);
|
|
25
|
+
// #281: the people search has no company region filter, so with no person location the company's region is
|
|
26
|
+
// searched as where the person is. Without it the search pays to check people anywhere in the world.
|
|
27
|
+
const places=positive(audience.person?.locations).length?positive(audience.person?.locations):positive(audience.company?.locations);
|
|
28
|
+
const locations=places.slice(0,3).map(item=>numeric(item.providerId)?{geoId:item.providerId}:{location:item.name});
|
|
29
|
+
// #322: the search takes 20 industry ids at a time, so a longer list is split across searches, never cut.
|
|
30
|
+
const industryIds=[...new Set(positive(audience.company?.industries).map(item=>item.providerId).filter(numeric))];
|
|
31
|
+
const industryChunks=[];for(let i=0;i<industryIds.length;i+=20)industryChunks.push(industryIds.slice(i,i+20));
|
|
32
|
+
// A topic signal is a keyword people use; research and profile signals are judged after the fetch, not searched.
|
|
33
|
+
const queries=[...new Set([audience.discoveryKeyword,...signals.filter(signal=>signal.kind==='topic').map(signal=>signal.topic)].map(value=>value?.trim()).filter(Boolean))].slice(0,3);
|
|
34
|
+
// At most MAX_SEARCHES title, place and keyword combinations. Each one is searched across every industry chunk, so
|
|
35
|
+
// a long industry list never pushes a title out of the plan. The credit cap still bounds what the searches cost.
|
|
36
|
+
const combos=[];
|
|
37
|
+
for(const title of titles.length?titles:[undefined])for(const place of locations.length?locations:[{}])for(const query of queries.length?queries:[undefined])combos.push({kind:'person',...(title?{title}:{}),...place,...(query?{query}:{})});
|
|
38
|
+
const searches=[];
|
|
39
|
+
for(const combo of combos.slice(0,MAX_SEARCHES))for(const ids of industryChunks.length?industryChunks:[undefined]){
|
|
40
|
+
const input={...combo,...(ids?{industryIds:ids}:{})};
|
|
41
|
+
if(input.title||input.query||input.geoId||input.location||input.industryIds)searches.push(input);
|
|
42
|
+
}
|
|
43
|
+
return searches;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseCredits(value){
|
|
47
|
+
if(typeof value!=='string'||!/^\d{1,7}(\.\d{1,4})?$/.test(value))throw new Error('Set --max-credits to a number of credits, like 50 or 12.5.');
|
|
48
|
+
const [whole,fraction='']=value.split('.');
|
|
49
|
+
return BigInt(whole)*PICO_PER_CREDIT+BigInt(fraction.padEnd(4,'0'))*(PICO_PER_CREDIT/10_000n);
|
|
50
|
+
}
|
|
51
|
+
export const credits=pico=>{const whole=pico/PICO_PER_CREDIT,rest=(pico%PICO_PER_CREDIT)*10_000n/PICO_PER_CREDIT;return rest?`${whole}.${rest.toString().padStart(4,'0').replace(/0+$/,'')}`:`${whole}`;};
|
|
52
|
+
const charged=data=>data?.billing?.status==='charged'&&/^\d+$/.test(data.billing.amount??'')?BigInt(data.billing.amount):0n;
|
|
53
|
+
// A search is final once its outcome is known and its charge has settled.
|
|
54
|
+
const searchSettled=data=>['succeeded','failed','rejected'].includes(data?.status)&&(data.billing?.status==='not_charged'||charged(data)>0n||data.billing?.status==='charged'&&data.billing.amount==='0');
|
|
55
|
+
const personUrl=url=>typeof url==='string'&&/^https:\/\/([a-z]{2,3}\.)?(www\.)?linkedin\.com\/in\//i.test(url);
|
|
56
|
+
|
|
57
|
+
async function settled(call,workspaceId,data,wait){
|
|
58
|
+
for(let tries=0;data?.operationId&&!searchSettled(data)&&data.status!=='uncertain'&&tries<30;tries++){await wait(2000);data=(await call('tools_runs_get',{workspaceId,operationId:data.operationId})).data;}
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Searches and checks until `count` new fits are found, the searches run out, or the next paid call could pass
|
|
64
|
+
* `maxCredits`. A search runs only when its published price fits what is left. Each check sends what is left as
|
|
65
|
+
* `maxCharge`, and the server refuses a check whose worst case (both profile fetches paid) could pass it.
|
|
66
|
+
*/
|
|
67
|
+
export async function findCampaignProspects(call,{workspaceId,campaignId,count,maxCredits},{wait=ms=>new Promise(resolve=>setTimeout(resolve,ms)),progress=()=>{}}={}){
|
|
68
|
+
const {data:campaign}=await call('campaign_get',{workspaceId,campaignId});
|
|
69
|
+
const fields=campaign.revision.fields;
|
|
70
|
+
if(!['Draft','Preparing','Active'].includes(campaign.campaign.state))throw new Error('This campaign is not finding prospects. Pick a Draft, Preparing or Active campaign.');
|
|
71
|
+
const {data:audience}=await call('audience_get',{workspaceId,audienceId:fields.audienceId,revision:fields.audienceRevision});
|
|
72
|
+
// #322: the search cannot apply an older audience's industry group, so it would pay to check people in any
|
|
73
|
+
// industry. Refuse before any paid call; named industries are searched.
|
|
74
|
+
const company=audience.revision.fields.company;
|
|
75
|
+
if(company?.industryGroups?.length&&!positive(company.industries).length)throw new Error("This campaign's audience uses an older industry group, which the LinkedIn search cannot apply. Name the industries in the audience instead (audienti audience options --kind industries --q TEXT), then try again.");
|
|
76
|
+
const searches=planSearches(audience.revision.fields,fields.signals);
|
|
77
|
+
// #323: rules that will reject people (a named title below the seniority floor) are shown before the first paid call.
|
|
78
|
+
const warnings=audience.revision.warnings??[];
|
|
79
|
+
for(const warning of warnings)progress({warning});
|
|
80
|
+
if(!searches.length)throw new Error("This campaign's audience has no title, location, industry or keyword to search by. Add one, then try again.");
|
|
81
|
+
const {data:tool}=await call('tools_get',{workspaceId,tool:'profiles.search'});
|
|
82
|
+
if(!/^\d{1,20}$/.test(tool?.price?.picoUsd??''))throw new Error('The LinkedIn search has no fixed price right now, so the credit cap cannot be kept. Try again later.');
|
|
83
|
+
const searchPrice=BigInt(tool.price.picoUsd);
|
|
84
|
+
const report={campaignId,state:campaign.campaign.state,target:count,maxCredits:credits(maxCredits),spentCredits:'0',spent:'0',searches:0,checked:0,fit:0,held:0,notFit:0,alreadyFound:0,failed:0,stoppedBecause:'searches_exhausted',fits:[],heldPeople:[],...(warnings.length?{audienceWarnings:warnings}:{})};
|
|
85
|
+
let spent=0n;const seen=new Set();
|
|
86
|
+
const finish=reason=>{report.stoppedBecause=reason;report.spent=spent.toString();report.spentCredits=credits(spent);return report;};
|
|
87
|
+
// The check may have paid more than it reported; the rest is recovered with the same request ID.
|
|
88
|
+
const unknown=(url,requestId,error)=>{report.unknownCheck={url,requestId,code:error?.code??'response_unknown'};return finish('outcome_unknown');};
|
|
89
|
+
for(const input of searches){
|
|
90
|
+
for(let page=1;page&&page<=MAX_PAGES;){
|
|
91
|
+
if(report.fit>=count)return finish('count_reached');
|
|
92
|
+
if(spent+searchPrice>maxCredits)return finish('credit_cap');
|
|
93
|
+
// Search results are not saved as People; only a person the check fetches joins People.
|
|
94
|
+
// Nothing else pays until this search has settled: a search still running, uncertain or with a charge not
|
|
95
|
+
// yet final is retried with the same request ID, and if it never settles the run stops and names it.
|
|
96
|
+
const search={workspaceId,requestId:randomUUID(),input:{...input,...(page>1?{page}:{}),saveProfiles:false}};
|
|
97
|
+
let data=null,failure=null,paid=0n;
|
|
98
|
+
for(let attempt=0;;attempt++){
|
|
99
|
+
try{data=await settled(call,workspaceId,(await call('tools_profiles_search',search)).data,wait);failure=null;}
|
|
100
|
+
catch(error){if(!pending(error))throw error;failure=error;}
|
|
101
|
+
if(!failure&&charged(data)>paid)paid=charged(data);
|
|
102
|
+
if(!failure&&searchSettled(data))break;
|
|
103
|
+
if(attempt>=RECOVERY_WAITS.length){
|
|
104
|
+
spent+=paid;report.searches++;
|
|
105
|
+
report.unknownSearch={requestId:search.requestId,operationId:data?.operationId??null,code:failure?.code??data?.error?.code??data?.status??'response_unknown'};
|
|
106
|
+
return finish('outcome_unknown');
|
|
107
|
+
}
|
|
108
|
+
await wait(RECOVERY_WAITS[attempt]);
|
|
109
|
+
}
|
|
110
|
+
spent+=paid;report.searches++;
|
|
111
|
+
if(data?.status!=='succeeded'){if(data?.error?.code==='insufficient_credits')return finish('no_credits');break;}
|
|
112
|
+
const items=(data.result?.items??[]).filter(item=>personUrl(item.url)&&!seen.has(item.url));
|
|
113
|
+
for(const item of items){
|
|
114
|
+
seen.add(item.url);
|
|
115
|
+
if(report.fit>=count)return finish('count_reached');
|
|
116
|
+
if(spent>=maxCredits)return finish('credit_cap');
|
|
117
|
+
// Nothing else pays until this check has a definite answer. The same request ID replays the stored
|
|
118
|
+
// outcome and charge; if no definite answer comes, the run stops and names the check to recover.
|
|
119
|
+
const args={workspaceId,campaignId,url:item.url,requestId:randomUUID(),maxCharge:(maxCredits-spent).toString()};
|
|
120
|
+
// Every attempt's reported charge is kept, and the largest is counted once on every way out.
|
|
121
|
+
let result,failure,paid=0n;
|
|
122
|
+
for(let attempt=0;;attempt++){
|
|
123
|
+
try{result=(await call('campaign_prospect_check',args)).data;failure=null;}
|
|
124
|
+
catch(error){result=null;failure=error;}
|
|
125
|
+
const reported=reportedCharge(failure?failure.charged:result?.charged);
|
|
126
|
+
if(reported>paid)paid=reported;
|
|
127
|
+
if(!failure&&!OUTCOMES.includes(result?.outcome)){failure={code:'invalid_response'};break;}
|
|
128
|
+
if(!failure||!pending(failure)||attempt>=RECOVERY_WAITS.length)break;
|
|
129
|
+
await wait(RECOVERY_WAITS[attempt]);
|
|
130
|
+
}
|
|
131
|
+
spent+=paid;
|
|
132
|
+
if(failure){
|
|
133
|
+
const error=failure;
|
|
134
|
+
if(pending(error)||error.code==='invalid_response')return unknown(item.url,args.requestId,error);
|
|
135
|
+
if(error.code==='check_over_budget')return finish('credit_cap');
|
|
136
|
+
if(error.status===402)return finish('no_credits');
|
|
137
|
+
if(error.code==='campaign_not_active')return finish('campaign_not_active');
|
|
138
|
+
if(['tool_unavailable','history_policy_unavailable'].includes(error.code))return finish('tool_unavailable');
|
|
139
|
+
if(!skippable(error))return unknown(item.url,args.requestId,error);
|
|
140
|
+
report.failed++;progress({url:item.url,outcome:'failed',code:error.code,spent:credits(spent)});continue;
|
|
141
|
+
}
|
|
142
|
+
report.checked++;
|
|
143
|
+
const who={prospectId:result.prospectId,url:item.url,name:item.name??null,reason:result.assessment?.fit?.reason??null};
|
|
144
|
+
if(result.outcome==='fit'){report.fit++;report.fits.push({...who,admission:result.assessment?.admission});}
|
|
145
|
+
else if(result.outcome==='held'){report.held++;report.heldPeople.push({...who,reason:result.assessment?.reason??null});}
|
|
146
|
+
else if(result.outcome==='not_fit')report.notFit++;
|
|
147
|
+
else report.alreadyFound++;
|
|
148
|
+
progress({url:item.url,outcome:result.outcome,spent:credits(spent)});
|
|
149
|
+
}
|
|
150
|
+
// Follow the next page even when this one had only people already seen; MAX_PAGES still bounds it.
|
|
151
|
+
page=data.result?.nextPage>page?data.result.nextPage:0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return finish(report.fit>=count?'count_reached':'searches_exhausted');
|
|
155
|
+
}
|
|
@@ -7,20 +7,24 @@ const signal=z.discriminatedUnion('kind',[
|
|
|
7
7
|
z.object({...signalBase,kind:z.literal('topic'),topic:z.string()}).strict(),
|
|
8
8
|
z.object({...signalBase,kind:z.literal('profile'),url:z.string(),track:z.enum(['audience','activity']).optional(),interactions:z.array(z.enum(['comments','reactions'])).optional()}).strict(),
|
|
9
9
|
]);
|
|
10
|
-
const fields=z.object({name:z.string(),audienceId:uuid,audienceRevision:z.number().int(),offerId:uuid,offerRevision:z.number().int(),premise:z.string().optional(),
|
|
10
|
+
const fields=z.object({name:z.string(),audienceId:uuid,audienceRevision:z.number().int(),offerId:uuid,offerRevision:z.number().int(),premise:z.string().optional(),playbookPin:z.object({id:uuid,revision:z.number().int().positive()}).strict().optional(),notes:z.string().optional(),ownerUserId:uuid.nullable().optional(),tags:z.array(z.string()).optional(),signalMatch:z.enum(['any','all']).optional(),signals:z.array(signal).optional(),method:z.enum(['signals','listen','track-audience','track-engagement','custom']).optional(),findingInstructions:z.string().optional(),targets:z.discriminatedUnion('kind',[
|
|
11
11
|
z.object({kind:z.literal('none'),values:z.array(z.never())}).strict(),
|
|
12
12
|
z.object({kind:z.literal('companies'),values:z.array(z.object({type:z.enum(['domain','linkedin-company']),value:z.string()}).strict())}).strict(),
|
|
13
|
-
z.object({kind:z.literal('people'),values:z.array(z.object({type:z.literal('linkedin-person'),value:z.string()}).strict())}).strict(),
|
|
14
13
|
]).optional(),channels:z.array(z.enum(['email','linkedin'])).optional(),approvalMode:z.literal('manual').optional(),contentOpsEnabled:z.boolean().optional(),schedule:z.object({startAt:z.string().nullable().optional(),endAt:z.string().nullable().optional(),maxCompanies:z.number().int().nullable().optional()}).strict().optional()}).strict();
|
|
15
14
|
const path=a=>`/workspaces/${a.workspaceId}/campaigns`,detail=a=>path(a)+`/${a.campaignId}`;
|
|
16
15
|
const qs=a=>new URLSearchParams(Object.fromEntries(Object.entries(a).filter(([k,v])=>!['workspaceId','campaignId'].includes(k)&&v!==undefined).map(([k,v])=>[k,String(v)])));
|
|
17
16
|
export const campaignOperations={
|
|
18
|
-
campaign_summary:{schema:z.object({...workspace}).strict(),method:'GET',path:a=>`/workspaces/${a.workspaceId}/campaign-summary`,scoped:true,description:'Counts of Campaigns per state, and per Campaign its Audience, offer,
|
|
17
|
+
campaign_summary:{schema:z.object({...workspace}).strict(),method:'GET',path:a=>`/workspaces/${a.workspaceId}/campaign-summary`,scoped:true,description:'Counts of Campaigns per state, and per Campaign its Audience, offer, playbook, goal stage, methods, people, engaged and at-goal counts since start.'},
|
|
19
18
|
campaign_list:{schema:z.object({...workspace,state:state.optional(),archived:z.boolean().optional(),q:z.string().optional(),limit:z.number().int().optional(),after:uuid.optional()}).strict(),method:'GET',path:a=>path(a)+'?'+qs(a),scoped:true},
|
|
20
19
|
campaign_get:{schema:z.object({...locator,revision:z.number().int().optional()}).strict(),method:'GET',path:a=>detail(a)+'?'+qs(a),scoped:true},
|
|
21
20
|
campaign_events:{schema:z.object({...locator,limit:z.number().int().optional(),after:uuid.optional()}).strict(),method:'GET',path:a=>detail(a)+'/events?'+qs(a),scoped:true},
|
|
22
21
|
campaign_create:{schema:z.object({...workspace,requestId:uuid,fields}).strict(),method:'POST',path,body:a=>({requestId:a.requestId,fields:a.fields}),scoped:true},
|
|
23
22
|
campaign_update:{schema:z.object({...locator,expectedRevision:z.number().int(),fields}).strict(),method:'POST',path:a=>detail(a)+'/revisions',body:a=>({expectedRevision:a.expectedRevision,fields:a.fields}),scoped:true},
|
|
24
|
-
campaign_transition:{schema:z.object({...locator,expectedState:state,operation:z.enum(['prepare','activate','pause','resume','close','archive','restore']),reason:z.string().optional()}).strict(),method:'POST',path:a=>detail(a)+'/transitions',body:a=>({expectedState:a.expectedState,operation:a.operation,...(a.reason===undefined?{}:{reason:a.reason})}),scoped:true,description:'Move a Campaign through its lifecycle. prepare: Draft to Preparing (signal tracking and prospect finding run, no engagement). activate: Draft or Preparing to Active (finding and engagement run). pause: stop automation. resume: back to the paused state. close: Active to Closing (finding stops, engagement continues). archive: stop everything and hide it. restore: Archived to Paused, or Draft for a draft. Agents do not start or activate campaigns; the
|
|
23
|
+
campaign_transition:{schema:z.object({...locator,expectedState:state,operation:z.enum(['prepare','activate','pause','resume','close','archive','restore']),reason:z.string().optional()}).strict(),method:'POST',path:a=>detail(a)+'/transitions',body:a=>({expectedState:a.expectedState,operation:a.operation,...(a.reason===undefined?{}:{reason:a.reason})}),scoped:true,description:'Move a Campaign through its lifecycle. prepare: Draft to Preparing (signal tracking and prospect finding run, no engagement). activate: Draft or Preparing to Active (finding and engagement run). pause: stop automation. resume: back to the paused state. close: Active to Closing (finding stops, engagement continues). archive: stop everything and hide it. restore: Archived to Paused, or Draft for a draft. Agents do not start or activate campaigns; the user does. A campaign that cannot run is refused with campaign_not_ready and its missing items in blockers.'},
|
|
24
|
+
campaign_pace_get:{schema:z.object({...locator}).strict(),method:'GET',path:a=>detail(a)+'/pace',scoped:true,description:'Read a Campaign pace: new people a day. Derived is 25 a day per confirmed LinkedIn account, each capped by its reported connection-request limit; an email-only Campaign uses its own number, 50 by default, within mailbox limits. The reply names the LinkedIn accounts, the weekly figure and the expected daily credits for hosted lookups.'},
|
|
25
|
+
campaign_pace_set:{schema:z.object({...locator,perDay:z.union([z.number().int().min(1).max(1000),z.literal('derived')]).optional(),maxCredits:z.number().int().min(1).max(100000).nullable().optional()}).strict().refine(a=>a.perDay!==undefined||a.maxCredits!==undefined,{message:'Send perDay, maxCredits or both.'}),method:'POST',path:a=>detail(a)+'/pace',body:a=>({...(a.perDay===undefined?{}:{perDay:a.perDay}),...(a.maxCredits===undefined?{}:{maxCredits:a.maxCredits})}),scoped:true,description:'Save a Campaign pace. Pass the number the user gave, or "derived" to follow the confirmed sending accounts. An owner number stays until changed and is still capped by LinkedIn account and mailbox limits. maxCredits saves the credits a day the user approved for finding (null removes it); each day\'s find task carries it.'},
|
|
26
|
+
campaign_targets_get:{schema:z.object({...locator,limit:z.number().int().min(1).max(500).optional(),after:z.string().regex(/^\d{1,7}$/).optional()}).strict(),method:'GET',path:a=>detail(a)+'/targets?'+qs(a),scoped:true,description:'Read a Campaign\'s target companies in pages of up to 500: domains and LinkedIn company URLs. Only people whose current company is one of them can join.'},
|
|
27
|
+
campaign_targets_change:{schema:z.object({...locator,expectedRevision:z.number().int().positive(),add:z.array(z.string()).optional(),remove:z.array(z.string()).optional(),set:z.array(z.string()).optional()}).strict().refine(a=>(a.set===undefined)!==(a.add===undefined&&a.remove===undefined),{message:'Send set, or add and or remove.'}),method:'POST',path:a=>detail(a)+'/targets',body:a=>({expectedRevision:a.expectedRevision,...Object.fromEntries(['add','remove','set'].filter(k=>a[k]!==undefined).map(k=>[k,a[k]]))}),scoped:true,description:'Change a Campaign\'s target companies: add or remove domains (acme.com) and LinkedIn company URLs, or replace them all with set. Saves a new Campaign revision; an empty set removes the targets.'},
|
|
28
|
+
campaign_prospect_check:{schema:z.object({...locator,url:z.string().max(1024),requestId:uuid,maxCharge:z.string().regex(/^\d{1,20}$/).optional()}).strict(),method:'POST',path:a=>detail(a)+'/prospect-checks',body:a=>({url:a.url,requestId:a.requestId,...(a.maxCharge!==undefined?{maxCharge:a.maxCharge}:{})}),scoped:true,recoverable:true,timeoutMs:90000,description:'Check one LinkedIn person URL for a Campaign and record the result. Fetches the person and current company (saved profiles are reused free; otherwise each fetch spends hosted credits), runs the pinned Audience rules and records a qualification with the profiles as evidence. outcome fit: admitted, or shortlisted while the Campaign is a Draft. held: an agent must judge a smart rule, a signal or an undecided rule. not_fit: a rule failed. already_found: already in or shortlisted for this Campaign; nothing written. charged and balance are pico USD (1 credit = 10,000,000,000). Generate requestId once and reuse it after an interruption so a retry never pays twice.'},
|
|
25
29
|
campaign_successor:{schema:z.object({...locator,requestId:uuid,expectedRevision:z.number().int(),fields}).strict(),method:'POST',path:a=>detail(a)+'/successors',body:a=>({requestId:a.requestId,expectedRevision:a.expectedRevision,fields:a.fields}),scoped:true},
|
|
26
30
|
};
|
package/cli/campaigns.mjs
CHANGED
|
@@ -3,11 +3,44 @@ import {readJsonFile} from './json-file.mjs';
|
|
|
3
3
|
export async function campaignCli(args){
|
|
4
4
|
const position=[],flags={};for(let i=0;i<args.length;i++){
|
|
5
5
|
if(!args[i].startsWith('--')){position.push(args[i]);continue}
|
|
6
|
-
const key=args[i].slice(2);if(!['profile','workspace','file','request-id','expected-revision','revision','archived','limit','after','q','state','operation','expected-state','reason'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete campaign option.');
|
|
6
|
+
const key=args[i].slice(2);if(!['profile','workspace','file','request-id','expected-revision','revision','archived','limit','after','q','state','operation','expected-state','reason','per-day','count','max-credits','url','add','remove','set'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete campaign option.');
|
|
7
7
|
flags[key]=args[++i];
|
|
8
8
|
}
|
|
9
|
-
const command=position[1]
|
|
10
|
-
if(
|
|
9
|
+
const command=position[1];
|
|
10
|
+
if(command==='find'){
|
|
11
|
+
if(position.length!==3)throw new Error('Use campaign find ID --count N --max-credits C.');
|
|
12
|
+
if(!/^\d{1,3}$/.test(flags.count??'')||Number(flags.count)<1||Number(flags.count)>500)throw new Error('Set --count to how many fits to find, from 1 to 500.');
|
|
13
|
+
if(flags['max-credits']===undefined)throw new Error('Set --max-credits to the most credits this run may spend.');
|
|
14
|
+
const {findCampaignProspects,parseCredits}=await import('./campaign-find.mjs');
|
|
15
|
+
const config=configuration(flags.profile);
|
|
16
|
+
const report=await findCampaignProspects((operation,input)=>callWorkspace(operation,input,config),{workspaceId:flags.workspace,campaignId:position[2],count:Number(flags.count),maxCredits:parseCredits(flags['max-credits'])},{progress:step=>console.error(JSON.stringify(step))});
|
|
17
|
+
console.log(JSON.stringify({data:report}));return;
|
|
18
|
+
}
|
|
19
|
+
if(command==='check'){
|
|
20
|
+
if(position.length!==3||!flags.url)throw new Error('Use campaign check ID --url LINKEDIN_URL [--request-id UUID].');
|
|
21
|
+
const {randomUUID}=await import('node:crypto');
|
|
22
|
+
console.log(JSON.stringify(await callWorkspace('campaign_prospect_check',{workspaceId:flags.workspace,campaignId:position[2],url:flags.url,requestId:flags['request-id']??randomUUID()},configuration(flags.profile))));return;
|
|
23
|
+
}
|
|
24
|
+
if(command==='targets'){
|
|
25
|
+
if(position.length!==3)throw new Error('Use campaign targets ID [--add a.com,b.com] [--remove a.com] [--set FILE|-] [--expected-revision N].');
|
|
26
|
+
const config=configuration(flags.profile),input={workspaceId:flags.workspace,campaignId:position[2]};
|
|
27
|
+
const change=['add','remove','set'].some(k=>flags[k]!==undefined);
|
|
28
|
+
if(!change){if(flags.limit!==undefined)input.limit=Number(flags.limit);if(flags.after)input.after=flags.after;console.log(JSON.stringify(await callWorkspace('campaign_targets_get',input,config)));return;}
|
|
29
|
+
if(flags.set!==undefined&&(flags.add!==undefined||flags.remove!==undefined))throw new Error('Use --set on its own, or --add and --remove.');
|
|
30
|
+
const split=v=>v.split(',').map(x=>x.trim()).filter(Boolean);
|
|
31
|
+
if(flags.add!==undefined)input.add=split(flags.add);
|
|
32
|
+
if(flags.remove!==undefined)input.remove=split(flags.remove);
|
|
33
|
+
if(flags.set!==undefined){const {readFileSync}=await import('node:fs');input.set=readFileSync(flags.set==='-'?0:flags.set,'utf8').split(/\r?\n/).map(x=>x.trim()).filter(x=>x&&!x.startsWith('#'));}
|
|
34
|
+
input.expectedRevision=flags['expected-revision']!==undefined?Number(flags['expected-revision']):(await callWorkspace('campaign_targets_get',{workspaceId:flags.workspace,campaignId:position[2],limit:1},config)).data.revision;
|
|
35
|
+
console.log(JSON.stringify(await callWorkspace('campaign_targets_change',input,config)));return;
|
|
36
|
+
}
|
|
37
|
+
if(command==='summary'){
|
|
38
|
+
if(position.length!==2)throw new Error('Use campaign summary [--workspace ID].');
|
|
39
|
+
console.log(JSON.stringify(await callWorkspace('campaign_summary',{workspaceId:flags.workspace},configuration(flags.profile))));return;
|
|
40
|
+
}
|
|
41
|
+
const name={list:'campaign_list',show:'campaign_get',create:'campaign_create',update:'campaign_update',transition:'campaign_transition',successor:'campaign_successor',events:'campaign_events',pace:flags['per-day']===undefined&&flags['max-credits']===undefined?'campaign_pace_get':'campaign_pace_set'}[command];
|
|
42
|
+
if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|summary|show ID|create|update ID|transition ID|successor ID|events ID|pace ID [--per-day N|derived] [--max-credits C|none]|find ID --count N --max-credits C|check ID --url URL|targets ID [--add|--remove|--set].');
|
|
43
|
+
if(flags['per-day']!==undefined&&command!=='pace')throw new Error('Set the pace with campaign pace ID --per-day N|derived, after the campaign is saved.');
|
|
11
44
|
const input={workspaceId:flags.workspace};
|
|
12
45
|
if(position[2])input.campaignId=position[2];
|
|
13
46
|
if(['list','events'].includes(command)){
|
|
@@ -22,5 +55,7 @@ export async function campaignCli(args){
|
|
|
22
55
|
if(['create','successor'].includes(command))input.requestId=flags['request-id'];
|
|
23
56
|
if(['update','successor'].includes(command))input.expectedRevision=Number(flags['expected-revision']);
|
|
24
57
|
if(command==='transition'){input.operation=flags.operation;input.expectedState=flags['expected-state'];if(flags.reason!==undefined)input.reason=flags.reason;}
|
|
58
|
+
if(command==='pace'&&flags['per-day']!==undefined){const v=flags['per-day'];if(v!=='derived'&&!/^\d+$/.test(v))throw new Error('Pace must be a whole number of people a day, or derived.');input.perDay=v==='derived'?v:Number(v);}
|
|
59
|
+
if(command==='pace'&&flags['max-credits']!==undefined){const v=flags['max-credits'];if(v!=='none'&&!/^\d+$/.test(v))throw new Error('The credit cap must be a whole number of credits a day, or none.');input.maxCredits=v==='none'?null:Number(v);}
|
|
25
60
|
console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
|
|
26
61
|
}
|
|
@@ -9,7 +9,7 @@ const path = name => a => {
|
|
|
9
9
|
export const cohortOperations = {
|
|
10
10
|
workspace_analytics_cohorts: {
|
|
11
11
|
schema: z.object({ ...filters, stage: z.string().min(1).max(100).optional(), cohorts: z.number().int().min(1).max(52).optional(), horizon: z.number().int().min(1).max(26).optional() }).strict(), method: 'GET', path: path('cohorts'), scoped: true,
|
|
12
|
-
description: `Weekly entry cohorts: people grouped by the UTC week (Monday start) they were admitted to a campaign, and for each week since admission the share that reached a stage. stage is "goal" (default, each
|
|
12
|
+
description: `Weekly entry cohorts: people grouped by the UTC week (Monday start) they were admitted to a campaign, and for each week since admission the share that reached a stage. stage is "goal" (default, each playbook's goal) or a stage key from the stages list in the response. cohorts is the number of weeks (default 12, at most 52); horizon is the number of weeks since entry shown after week 0 (default 8, at most 26). A cell is empty (null) until every member of the cohort has had that long. ${FILTER_TEXT} Same figures as Analytics > Cohorts.`,
|
|
13
13
|
},
|
|
14
14
|
workspace_stage_aging: {
|
|
15
15
|
schema: z.object({ ...filters, thresholdDays: z.number().int().min(1).max(365).optional() }).strict(), method: 'GET', path: path('stage-aging'), scoped: true,
|
package/cli/content.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const help = `Usage:
|
|
|
10
10
|
|
|
11
11
|
Topic input: {"name":"Topic name","slug":"optional-slug","description":"optional"}.
|
|
12
12
|
These commands read saved records or create a topic; they do not call a paid provider.
|
|
13
|
+
Each post names the tool run that last saved it in savedByOperationId; read the run with audienti tools runs get OPERATION_ID.
|
|
13
14
|
Retrieve fresh data with audienti tools posts.fetch|posts.search|posts.comments|posts.reactions|profiles.posts.
|
|
14
15
|
Use audienti tools describe NAME --json for the exact contract.
|
|
15
16
|
All commands accept --profile PATH, --workspace UUID and --json.`;
|
|
@@ -21,6 +22,7 @@ export async function contentCli(argv) {
|
|
|
21
22
|
let args = values.workspace ? { workspaceId: values.workspace } : {};
|
|
22
23
|
if (verb === 'list' && positionals.length === 1) args = { ...args, cursor: values.cursor, ...(kind === 'posts' ? { topicId: values['topic-id'] } : {}) };
|
|
23
24
|
else if (verb === 'get' && positionals.length === 2) args.id = id;
|
|
25
|
+
else if (verb === 'get') throw new Error(`Name the ${kind === 'posts' ? 'post' : 'topic'} to read: audienti ${kind} get ID.`);
|
|
24
26
|
else if (kind === 'topics' && verb === 'create' && positionals.length === 1) {
|
|
25
27
|
if (!values.input || !values['request-id']) throw new Error('Topic create requires --input FILE and --request-id UUID.');
|
|
26
28
|
args = { ...readJsonFile(values.input, 32768, 'Topic input'), ...args, requestId: values['request-id'] };
|