@audienti/cli 0.2.0-exora.5 → 0.2.0-exora.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/audienti.mjs CHANGED
@@ -49,7 +49,7 @@ Audiences: audience options --kind industries|locations|sizes|types|seniorities|
49
49
  audience summary [--audience ID]|people ID|companies ID
50
50
  audience test ID --url LINKEDIN_URL --request-id UUID (spends hosted credits unless the profile is saved)
51
51
  Files: file upload PATH --type MIME|list|prepare|register|download|delete ID (personal files, human token only)
52
- 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)
52
+ 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)
53
53
  Outcome checks: audienti outcomes report TASK_ID --file RESULTS.json (results for the people an outcomes.check task lists)
54
54
  Questions: audienti ask "QUESTION" [--context TEXT] [--option TEXT]... [--campaign ID] [--task ID] (questions for your user go to Operator, not chat)
55
55
  audienti questions [list] [--state open|answered|withdrawn|all] | show ID | withdraw ID | answer ID --answer TEXT
@@ -61,7 +61,7 @@ Offer options: --file FIELDS.json --request-id UUID --expected-revision N --revi
61
61
  Account intake: prospects intake --workspace ID --file INPUT.json
62
62
  prospects conversation append|list PERSON --workspace ID
63
63
  Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|citation create|evidence create
64
- prospects contacts list|record PERSON [--file INPUT.json] --workspace ID
64
+ prospects contacts list|record PERSON [--file INPUT.json] [--request-id UUID] --workspace ID
65
65
  prospects contacts get|retire PERSON CONTACT --workspace ID
66
66
  prospects suppression get|set ID --workspace ID
67
67
  guardrails show|pause|resume --workspace ID
@@ -69,7 +69,7 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
69
69
  dnc import --file LIST.csv --workspace ID (at most 5,000 rows; reports added, skipped and invalid rows)
70
70
  dnc export [--output FILE.csv] --workspace ID
71
71
  secret list [--workspace ID] (names only)
72
- secret get NAME [--workspace ID] [--json] (prints the value; every read is logged)
72
+ secret get NAME [--workspace ID] [--json] (prints the value; the secret records who read it last and when)
73
73
  budgets show --workspace ID
74
74
  budgets alerts --workspace ID [--limit N]
75
75
  usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv]
@@ -78,6 +78,8 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
78
78
  Qualification: campaigns qualification submit --file INPUT.json
79
79
  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)
80
80
  campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
81
+ Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
82
+ campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
81
83
  Playbooks: playbook list|show ID|save --file REQUEST.json|progress ID|record --file REQUEST.json
82
84
  Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
83
85
  analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
@@ -103,7 +105,7 @@ Start local MCP stdio server: mcp
103
105
 
104
106
  // Example inputs for the commands that save prospects, so `--help` shows a working shape.
105
107
  const examples = {
106
- 'prospects observation create': { requestId: 'UUID', kind: 'person', source: { provider: 'web', url: 'https://example.com/team/jane' }, 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' } },
108
+ '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' } },
107
109
  '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' }] },
108
110
  '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' } },
109
111
  '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' } },
@@ -7,10 +7,9 @@ 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(),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(),targets:z.discriminatedUnion('kind',[
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)])));
@@ -24,6 +23,8 @@ export const campaignOperations={
24
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.'},
25
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.'},
26
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.'},
27
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.'},
28
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},
29
30
  };
package/cli/campaigns.mjs CHANGED
@@ -3,7 +3,7 @@ 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','per-day','count','max-credits','url'].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
9
  const command=position[1];
@@ -21,8 +21,21 @@ export async function campaignCli(args){
21
21
  const {randomUUID}=await import('node:crypto');
22
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
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
+ }
24
37
  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];
25
- if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|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.');
38
+ if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|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].');
26
39
  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.');
27
40
  const input={workspaceId:flags.workspace};
28
41
  if(position[2])input.campaignId=position[2];
@@ -5,7 +5,7 @@ const page=z.object({cursor:uuid.optional(),limit:z.number().int().min(1).max(10
5
5
  const endpoint=z.object({kind:z.enum(['prospect','profile']),id:uuid}).strict();
6
6
  const subject=z.object({kind:z.enum(['person','company']),prospectId:uuid}).strict();
7
7
  const text=z.string().nullable().optional();
8
- const fields=z.object({name:text,title:text,location:text,bio:text,domain:text,description:text,imageUrl:text}).strict();
8
+ const fields=z.object({name:text,title:text,companyName:text,location:text,bio:text,domain:text,description:text,imageUrl:text}).strict();
9
9
  const status=(values)=>z.object({status:z.enum(values),evidenceIds:z.array(uuid)}).strict();
10
10
  const root=a=>`/workspaces/${a.workspaceId}/prospects`;
11
11
  const body=a=>Object.fromEntries(Object.entries(a).filter(([k])=>k!=='workspaceId'));
package/cli/progress.mjs CHANGED
@@ -14,7 +14,7 @@ export async function progressCli(args){
14
14
  console.log(JSON.stringify(result));
15
15
  }
16
16
 
17
- /** Same columns as Export CSV on the Analytics page. Untracked outcome values are left blank. */
17
+ /** Daily analytics rows plus a total row. Untracked outcome values are left blank. */
18
18
  export function analyticsCsv(data){
19
19
  if(!data||!Array.isArray(data.daily)||!data.total)throw new Error('Analytics response has no daily rows.');
20
20
  const cell=value=>{const text=String(value);return /[",\n]/.test(text)?`"${text.replace(/"/g,'""')}"`:text;};
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { callWorkspace, configuration } from './workspaces.mjs';
3
3
  export async function recipientsCli(args) {
4
4
  const flags = {}, positions = [];
5
- const allowed = ['workspace','profile','status','cursor','limit','history-cursor','history-limit','file'];
5
+ const allowed = ['workspace','profile','status','cursor','limit','history-cursor','history-limit','file','request-id'];
6
6
  for (let i=0;i<args.length;i++) {
7
7
  const arg=args[i]; if(arg==='--json')continue;
8
8
  if(!arg.startsWith('--')){positions.push(arg);continue;}
@@ -12,7 +12,7 @@ export async function recipientsCli(args) {
12
12
  }
13
13
  const command=positions[2];
14
14
  if(positions[0]!=='prospects'||positions[1]!=='contacts'||!['list','get','record','retire'].includes(command)||positions.length!==(['get','retire'].includes(command)?5:4))throw Error('Use prospects contacts list|record PERSON or get|retire PERSON CONTACT --workspace ID.');
15
- const valid=['workspace','profile',...(command==='list'?['status','cursor','limit']:command==='get'?['history-cursor','history-limit']:['file'])];
15
+ const valid=['workspace','profile',...(command==='list'?['status','cursor','limit']:command==='get'?['history-cursor','history-limit']:['file','request-id'])];
16
16
  if(Object.keys(flags).some(k=>!valid.includes(k)))throw Error('Option does not apply to this contact command.');
17
17
  let input={};
18
18
  if(['record','retire'].includes(command)){
@@ -21,6 +21,10 @@ export async function recipientsCli(args) {
21
21
  try{input=JSON.parse(raw.toString('utf8'));}catch{throw Error('validation_error');}
22
22
  if(!input||typeof input!=='object'||Array.isArray(input)||['workspaceId','prospectId','contactId'].some(k=>Object.hasOwn(input,k)))throw Error('invalid_fields');
23
23
  }
24
+ if(flags['request-id']!==undefined){
25
+ if(input.requestId!==undefined&&input.requestId!==flags['request-id'])throw Error('--request-id differs from requestId in the file.');
26
+ input.requestId=flags['request-id'];
27
+ }
24
28
  Object.assign(input,{workspaceId:flags.workspace,prospectId:positions[3]});
25
29
  if(['get','retire'].includes(command))input.contactId=positions[4];
26
30
  if(command==='list')Object.assign(input,{...(flags.status===undefined?{}:{status:flags.status}),...(flags.cursor===undefined?{}:{cursor:flags.cursor}),...(flags.limit===undefined?{}:{limit:Number(flags.limit)})});
@@ -3,5 +3,5 @@ const workspace = { workspaceId: z.string().uuid().optional() };
3
3
  export const secretName = z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/, 'A secret name uses capital letters, digits and underscores, for example HUBSPOT_TOKEN.');
4
4
  export const secretOperations = {
5
5
  workspace_secret_list: { schema: z.object(workspace).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets`, scoped: true, description: 'List the names of the secrets in the workspace Vault. Names only, never values. Use workspace_secret_get for one value.' },
6
- workspace_secret_get: { schema: z.object({ ...workspace, name: secretName }).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets/${encodeURIComponent(a.name)}`, scoped: true, description: 'Get one secret by name from the workspace Vault (for example HUBSPOT_TOKEN). Every agent in the workspace can read every secret and reading is free. Each read is logged with the agent and the time. Use the value for the task only: never print it, paste it into a message or save it to a file.' },
6
+ workspace_secret_get: { schema: z.object({ ...workspace, name: secretName }).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets/${encodeURIComponent(a.name)}`, scoped: true, description: 'Get one secret by name from the workspace Vault (for example HUBSPOT_TOKEN). Every agent in the workspace can read every secret and reading is free. Each read records who read it last and when. Use the value for the task only: never print it, paste it into a message or save it to a file.' },
7
7
  };
@@ -151,7 +151,7 @@ export async function callWorkspace(name, args, config) {
151
151
  if(!response.ok) {
152
152
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
153
153
  const retryAt = typeof result?.error?.retryAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(result.error.retryAt) ? result.error.retryAt : undefined;
154
- const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===409 && (code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready'))) && typeof result?.error?.message==='string' && result.error.message.length<=1000 && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
154
+ const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===409 && (code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=1000 && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
155
155
  const blockers = (code==='execution_blocked' || code==='campaign_not_ready') && Array.isArray(result?.error?.blockers) && result.error.blockers.length<=28 && result.error.blockers.every(v=>typeof v==='string' && /^[a-z_]+$/.test(v)) ? result.error.blockers : undefined;
156
156
  const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId);
157
157
  // What a failed paid check already spent (pico USD), so a caller counting its spend never loses it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.5",
3
+ "version": "0.2.0-exora.6",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,9 +71,12 @@ takes `--file INPUT.json`; the request fields are in the API schema at
71
71
  `/api/v1/openapi.json`.
72
72
 
73
73
  1. `audienti prospects observation create --file person.json` saves a person or company
74
- profile with its source URL and returns the prospect ID. Add `fields.imageUrl` with
74
+ profile with its source URL and returns the prospect ID. A person's source is their
75
+ own LinkedIn profile URL, email, phone or provider ID (or pass the existing person's
76
+ `prospectId`); a web page that names them goes in `provenance.url`, because a plain
77
+ `web` source never identifies a person. Add `fields.imageUrl` with
75
78
  the LinkedIn profile photo or company logo URL when you have it; People and
76
- Companies show it instead of initials.
79
+ Companies show it instead of initials. A company's source can be its own site.
77
80
  2. `audienti prospects citation create --file employment.json` links a person to their
78
81
  company (`edgeType` `works_at`, one current role). The first current employer
79
82
  becomes the person's company in People.
@@ -101,9 +104,11 @@ value may follow in brackets, never on its own. The command:
101
104
  - searches LinkedIn with the audience's titles, locations, industries and keyword, plus
102
105
  the campaign's topic signals. With no person location, it searches the company
103
106
  locations as where the person is;
104
- - checks each new person against the campaign's audience rules, using their profile and
105
- current company as evidence. A person who fails a person rule is not a fit, and their
106
- company is not paid for;
107
+ - checks each new person against the campaign's audience rules, cheapest first: it reads
108
+ what the workspace already holds, fetches the person only if that is missing or old,
109
+ and fetches the company only when the audience has company rules or the campaign
110
+ targets companies. A person who fails a person rule is not a fit, and their company is
111
+ not paid for. The check also records where the person works as their primary employer;
107
112
  - stops at N fits or before the next paid call would pass the cap.
108
113
 
109
114
  The report lists the fits, the people held for your judgment, the not-fit count and the
@@ -101,6 +101,9 @@ capability report and ask your user to approve it in **Settings → Sending**; t
101
101
  Audienti gives you a queue task to send one test email from the mailbox to itself, read it
102
102
  back and report its code; the mailbox counts once you do
103
103
  ([the mailbox test email](references/mailbox-check.md)). Never ask your user for the code.
104
+ A campaign can run without a mailbox: its people with an email step wait at that step, and their
105
+ LinkedIn steps go on. Nothing is skipped. They go on once a mailbox is confirmed, so the fix is the
106
+ email connector.
104
107
 
105
108
  ## 2. Introduce yourself and propose a first campaign
106
109
 
@@ -209,7 +212,12 @@ Audienti hosted tool and the workspace has no credits left: tell the user once t
209
212
  waits on credits and runs when they add credits in Billing, and do not ask again. When
210
213
  credits are added or run out, or the user adds or removes one of their own provider keys
211
214
  in the Vault, every agent gets a notice task: run `audienti agent capabilities` again,
212
- then do what its steps say. When the queue is empty, your timer runs it
215
+ then do what its steps say. If you reported an `owner.notify` capability, Audienti puts one
216
+ **Tell your user about an alert** task in your queue for each alert it records. Its steps
217
+ give the message and a link: tell your user that message on whatever channel you use with
218
+ them (chat, push, Slack, SMS or email), include the link, and mark the task done once it
219
+ is delivered. Done means delivered: a done task is not created again for the same alert.
220
+ An agent that did not report `owner.notify` gets no such task. When the queue is empty, your timer runs it
213
221
  again after `pollAfterSeconds`; when `ask` or `queue next` returns an `announce` line,
214
222
  say it in your chat and point to Operator. Keep machine state in Audienti, not in your
215
223
  own memory: `audienti kv set KEY VALUE` stores cursors, change tokens, hashes, ids and
@@ -23,8 +23,10 @@ Fill this before you ask the user anything. Mark each value with where it came f
23
23
  ## Signals: the through line
24
24
 
25
25
  Prospect finding starts from signals: they find the companies that need the offer
26
- today. A campaign with no signal and no target list cannot start, so every campaign you
27
- draft has at least one. A signal is not "they might want to buy". It is an
26
+ today. A campaign with no signal, no target companies and no finding instructions cannot
27
+ start, so every signal campaign you draft has at least one signal. When the user already
28
+ has the people, or a source Audienti does not have, draft a custom campaign instead:
29
+ `method: "custom"`, finding instructions in the user's words and no signals. A signal is not "they might want to buy". It is an
28
30
  observable event that shows a company has the problem the offer solves, now. Build the
29
31
  line in this order:
30
32
 
@@ -183,6 +183,11 @@ user, but no task requires them yet.
183
183
  | `webhook.receive` | none | read | Take a push instead of polling | later |
184
184
  | `webhook.send` | none | send | Post events to the user's URL | later |
185
185
 
186
+ `owner.notify` means you can reach your user outside Audienti: chat, push, Slack, SMS or email. Report it
187
+ and Audienti puts a **Tell your user about an alert** task in your queue for each alert it records in
188
+ the workspace; the task carries the message and a link, and you mark it done once you delivered it. An
189
+ agent that has not reported `owner.notify` gets no such task.
190
+
186
191
  ### Tools you use for yourself
187
192
 
188
193
  | Capability | Providers | Access | What it does | Launch |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "9ef981e46b0c80a41b31c8352ff6b729180270af",
3
+ "sourceCommit": "213901acb79c89cc1b217fc723f843355358b7c6",
4
4
  "files": {
5
5
  "LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
6
6
  "README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
@@ -14,19 +14,19 @@
14
14
  "cli/assignment.mjs": "90cb85e0df8aadca2ff8391c99e778b41261183b5f38e8c6c32afee2f98f871a",
15
15
  "cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
16
16
  "cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
17
- "cli/audienti.mjs": "87b2b44472c1d4da1e2ac9de08eba50a0dc74b3599bc3e0c544cb1708944de72",
17
+ "cli/audienti.mjs": "7a2ed9483bb971f09d156e43c8bf68e38d3698baf22eea171910b24672954ec6",
18
18
  "cli/audit-operations.mjs": "184eb58bc479225314b77d6019a185670cd843895a10d9bf58a28de7f1355d91",
19
19
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
20
20
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
21
21
  "cli/budgets.mjs": "683e06cb4d410c851286fbd1cdd4fb9f1902726c60fd4fcddb2c13d164786525",
22
22
  "cli/campaign-find.mjs": "9c0a44c69ab7ad3e24a0a2edc97b8f3a14f602e3b6caf7e1943b8e1b93609c4a",
23
- "cli/campaign-operations.mjs": "ea236b7b456be2377f7c43f987ffe4cfae6aa2cfea4a8a489518dcca60cc645f",
24
- "cli/campaigns.mjs": "8bda546e7f44c9ba7e1bf2f39d8e8cc12d231ee9051bb32ad31bde7bcecc794f",
23
+ "cli/campaign-operations.mjs": "c1ac42f53f8bf19957fac41cc2a80ab4e26a743a7c1cc0b42c8c0e85fcf6e61b",
24
+ "cli/campaigns.mjs": "63a65f3af9964ede1a62a5946c9d751110aa29b69cec0a67a451241e73ad8c1d",
25
25
  "cli/cohort-operations.mjs": "91b1d1fc48e75581bb315f42d735ef94e21687dc216f352337d8a5d180bb691e",
26
26
  "cli/cohorts.mjs": "c71940903430f3e6834608f113534910b75a98b3e410a5942f04051607488485",
27
27
  "cli/connection-registry-operations.mjs": "888ea440d00ffc9ed68d4cc084ff73a5a00d0e60bd6293baba8bb4539a25ae62",
28
28
  "cli/content.mjs": "db3fcbf7f96c314227a8172a442b70b46405126e0d61f8358cee1bfe6c627ebd",
29
- "cli/discovery-operations.mjs": "c029935ddd5a113ba617b5948d0df1f6edc5799c2ef7ea6cee7f6b2122ee358f",
29
+ "cli/discovery-operations.mjs": "cc132abae230cc84439bcd24da813e2798ee1eb83a06a7d1141202ab9c68215a",
30
30
  "cli/discovery.mjs": "34b3794ee6ca539dbbccef77ec2085fd5411023046bdfb9b83e2537414542a41",
31
31
  "cli/file-operations.mjs": "564f334dd9d52e9c56f44d58b5c3b0bda59ff72f54126b755bc472144069349e",
32
32
  "cli/files.mjs": "2221b067d6ade7ef5cc8965929aea2eb485781953d3f2b50600995b65fbdedae",
@@ -46,7 +46,7 @@
46
46
  "cli/owner-account-client.mjs": "beb9736ac84a7ab57220ac1f1ea089b6401cf92fcf7c549417014d285f2340dc",
47
47
  "cli/playbook-operations.mjs": "25c593a2276b06c7e35377e3f132e6eee15c0e34542df62f1cf37a339d4e4ad2",
48
48
  "cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
49
- "cli/progress.mjs": "c44af4937423432e92cd922afc6afefca0bd7e14bb07a82dd07df35549cffc9f",
49
+ "cli/progress.mjs": "5d0f6b6094f212fb80aef0716e0917cf177eb2349490421784acc6f7cb576c44",
50
50
  "cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
51
51
  "cli/prospecting-settings.mjs": "b09f47c84416d9892d7d5b17dcf684cbb560e1cacb25513826ff04ef9307626e",
52
52
  "cli/question-operations.mjs": "5d7a885f2d4e0d2ce2249aadce2b4478b95843d21761f25eabdfe2759582a3b6",
@@ -54,11 +54,11 @@
54
54
  "cli/queue-operations.mjs": "bc5f5b8f79f056f579c2d3965d143b45ded4665629e2c4d427adfbaf7e9687fb",
55
55
  "cli/queue.mjs": "046f4dba92ac9eacbb1f12215ab2b49623bae597342da6b415f7afb3c3a1344e",
56
56
  "cli/recipient-operations.mjs": "7d41c17ef2d59dbed982893e0989c9c076317b91adcd3d752cbe1d2e0b8c41b2",
57
- "cli/recipients.mjs": "e5e30fc30ab325a43cffdc51ad8872cb657ce0f5d3cb304dd8af65c6d9224c75",
57
+ "cli/recipients.mjs": "dc93d4a23ca1df9c84a0c9734d09491af179644eed48d856d1f1a24d42276ea8",
58
58
  "cli/rooms-operations.mjs": "09d36003183eccf2f6d2ee1ed6c4981519b8ae34fbffd0bb1a2234a8fc6cde68",
59
59
  "cli/rooms.mjs": "7fd05b4cead1bab2e18bf724c56034e12b4304f55cc69e3fc7ea5c4536e5305c",
60
60
  "cli/search-operations.mjs": "903f12fffb6824872160c4136925ab5638142bb4e39236b37f927cd2c6a43776",
61
- "cli/secret-operations.mjs": "2b3481d608afcf8f243cbfc6b0d37aa633383d47f8224f69c903ee882eacb096",
61
+ "cli/secret-operations.mjs": "faefe9c9ece81087dc7aab32e88f331ed942d0757ce5a978b5f0722d514f377f",
62
62
  "cli/secrets.mjs": "cab4ea932e724ce3c807c7825869810b44fb8d75cdacec32f8101353f0e7c5f5",
63
63
  "cli/sender-account-operations.mjs": "3f5bc6b047916019a9266feb64041a7a338da94308910aa90eed6cb33e143ed0",
64
64
  "cli/sender-accounts.mjs": "0c9be3e441aebf4bea2d2674ee54c4b17ed246c7a8476bcb4debbdad7cc1ec10",
@@ -76,21 +76,21 @@
76
76
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
77
77
  "cli/work-operations.mjs": "addcfcd5049168e27d224ff879e9fcccd146228e876bcae25c9dd7cfeaa3c9a9",
78
78
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
79
- "cli/workspaces.mjs": "722c17a3daa7cc6f13eb50387bb53b3e5129e65330aeaf4c17127aaf3ee14973",
80
- "package.json": "70015dd167a28f668a9a989d7c208932256b3ffcac189dc593dffeefcc6b39c0",
79
+ "cli/workspaces.mjs": "09ea1fb6a416ef4810a72eab2b8bbe0fe7271e1dfe4ba29a214f31afd1af9707",
80
+ "package.json": "790a0840cdb4991f42dcc9883085932e4864b3001aa365b6644de1cce690be57",
81
81
  "skills/agent-rooms/SKILL.md": "3da993292bf50f1220d2d724d74b5f46af8c8f5f054ce18bb7cf27ef29d0437d",
82
82
  "skills/agent-rooms/references/commands.md": "7cff1ecefd74a838146344ad928b3936dafc8eb14f6747ec02c0215f207818e9",
83
- "skills/audienti/SKILL.md": "e6c366a14d9899def4980adda40799086013b15d92cbc11bfd6d30287118bca6",
83
+ "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
84
84
  "skills/audienti/references/helpers.md": "e27aff0aa07b92f575d617612ed3def824765bac6ce1304cf227a4aca223369a",
85
85
  "skills/audienti/references/profile-workflows.md": "80669b75eb5e8aeb614ce55ac046236d671af0de3ddd238853324579d5a3b251",
86
86
  "skills/audienti/references/workflow-receipt.md": "349f9a85472832b8bdc6a5d16c5b10b8e32d48d3e81523ad8d42f9a03b3655ff",
87
87
  "skills/audienti/scripts/contracts.mjs": "900428e067b7f8dae544012387bc3752f7b0d27777a80cd16ec0985848983f70",
88
88
  "skills/audienti/scripts/profile-helpers.mjs": "7f62afdefd8e1b1bfec95f1ca6f48cfc4e204d1bff35f805261c45c6d5fcb46a",
89
89
  "skills/audienti/scripts/run.mjs": "652094ff8b642d13f5e0bb30756bb49464c051511c37f202249fde917b319208",
90
- "skills/getting-started/SKILL.md": "a03a18cad9c11fbf6c6cd5fc429709daed25b020add6cf23d49e85dbe11b3468",
90
+ "skills/getting-started/SKILL.md": "902fffe321a91acd2dc3583a372801df6952c7e974179198ce688b62ca8b8f21",
91
91
  "skills/getting-started/references/audience.md": "6a2c061be5e299b3cbbd236578693ae5084d216425c500d3639a4d1fcb86ccb7",
92
- "skills/getting-started/references/campaign-draft.md": "8a39eb73cca96705aee2e38c0038ba2d618365a2a9869c3a7566ae0a42707a29",
93
- "skills/getting-started/references/capabilities.md": "badeb374537e74bd51e62325afe7b8c3fe13b293bef5559be4d282484558fb0d",
92
+ "skills/getting-started/references/campaign-draft.md": "c2cbeb7de95734889698a5cd852268b9bed69e56f661153bb7ada237241a1a6b",
93
+ "skills/getting-started/references/capabilities.md": "67dc33dc6e3f5a73d77cfc14055e330b58c8f03c7a8268f7b5baa97f65e667bd",
94
94
  "skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
95
95
  "skills/getting-started/references/outcome-checks.md": "05b2431b5f89bf05026ee4e5bac3ca8df594be97640eaca6a5e38907eca6e3dd",
96
96
  "skills/getting-started/references/replies.md": "019247b3eb260b21bf66689409748a9bb140d768fd0470a73992162fa8a5efd2",