@audienti/cli 0.2.0-exora.16 → 0.2.0-exora.18

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
@@ -90,7 +90,7 @@ Find prospects: campaign find CAMPAIGN_ID --count N --max-credits C (searches w
90
90
  campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
91
91
  Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
92
92
  campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
93
- Playbooks: playbook list [--archived] [--limit N --cursor ID]|actions|show ID [--revision N]|save --file REQUEST.json|archive ID|restore ID|delete ID|progress PARTICIPATION_ID|record --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
93
+ 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 (audienti playbook SUBCOMMAND --help for the request shape)
94
94
  Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
95
95
  analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
96
96
  analytics activity [--workspace ID] [--days N | --from ISO --to ISO] [--format json|csv]
@@ -109,10 +109,17 @@ Invitation commands: workspace invitation show|accept|decline|retry|cancel ID
109
109
  Options: --workspace ID --name NAME --request-id UUID --email EMAIL --generation N
110
110
  Optional private profile: --profile PATH (outside this repository), or the AUDIENTI_PROFILE variable. --profile wins.
111
111
  Save an environment token: profile save --scope account:read|workspace:access|agents:manage --profile PATH
112
- Start local MCP stdio server: mcp
112
+ Start local MCP stdio server: mcp (audienti mcp --help for the settings)
113
113
  --json writes the API response envelope.
114
114
  `;
115
115
 
116
+ 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.
117
+ Settings come from the environment of that command:
118
+ AUDIENTI_PROFILE the profile file made by agent connect or profile save (the usual way)
119
+ AUDIENTI_API_URL and AUDIENTI_API_TOKEN instead of a profile: the API URL ending in /api/v1 and a token
120
+ 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.
121
+ Tool names follow the commands: playbook_get for playbook show, playbook_draft_get for playbook draft show.
122
+ Errors name the MCP tool to call next, not the command.`;
116
123
  // Example inputs for the commands that save prospects, so `--help` shows a working shape.
117
124
  const examples = {
118
125
  'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
@@ -146,7 +153,8 @@ function unknownCommandMessage(words) {
146
153
  }
147
154
 
148
155
  async function main() {
149
- if (args[0] === 'playbook' && args.slice(1).some(value => value === '--help' || value === '-h')) { console.log(playbookHelp(args[1]?.startsWith('-') ? undefined : args[1])); return; }
156
+ 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; }
157
+ if (args[0] === 'mcp' && args.length === 2 && (args[1] === '--help' || args[1] === '-h')) { console.log(MCP_HELP); return; }
150
158
  if (args.length > 1 && args.slice(1).some(value => value === '--help' || value === '-h') && !['tools', 'call', 'skills'].includes(args[0])) { commandHelp(); return; }
151
159
  if(args[0]==='prospects'&&['intake','conversation'].includes(args[1])){const {accountIntakeCli}=await import('./account-intake.mjs');await accountIntakeCli(args);return;}
152
160
  if(args[0]==='sender'){const {senderAccountsCli}=await import('./sender-accounts.mjs');await senderAccountsCli(args);return;}
@@ -13,6 +13,6 @@ export async function callOwnerAccountOperation(operation,args,profilePath=proce
13
13
  const url=validateUrl(config.url);let response;
14
14
  try{response=await fetch(new URL('/api/v1'+operation.path(parsed.data),url),{method:operation.method,headers:{Authorization:`Bearer ${config.token}`,Accept:'application/json','Content-Type':'application/json'},...(operation.method==='POST'?{body:JSON.stringify(operation.body?operation.body(parsed.data):parsed.data)}:{}),redirect:'error',signal:AbortSignal.timeout(10000)});}catch{throw Error('Could not reach the API. Redirects are not followed.');}
15
15
  let result;try{result=await response.json();}catch{throw Error('The API returned an invalid response.');}
16
- if(!response.ok){const code=typeof result?.error?.code==='string'&&/^[a-z_]+$/.test(result.error.code)?result.error.code:'request_failed';throw new WorkspaceRequestError(response.status,code,`${label} request failed: ${code} (HTTP ${response.status}).`,undefined,undefined,undefined,errorGuidance(result?.error,config.token));}
16
+ if(!response.ok){const code=typeof result?.error?.code==='string'&&/^[a-z_]+$/.test(result.error.code)?result.error.code:'request_failed';throw new WorkspaceRequestError(response.status,code,`${label} request failed: ${code} (HTTP ${response.status}).${response.status===422&&code==='hours_not_narrower'?' An account can only narrow your working hours. Choose hours inside them, or change your own working hours first.':''}`,undefined,undefined,undefined,errorGuidance(result?.error,config.token));}
17
17
  if(!result||!Object.hasOwn(result,'data'))throw Error('The API returned an invalid response.');return result;
18
18
  }
@@ -40,6 +40,22 @@ export const playbookCommands = {
40
40
  usage: 'playbook restore PLAYBOOK_ID [--workspace ID] [--profile PATH]',
41
41
  about: 'Bring an archived playbook back. Find archived playbooks with playbook list --archived. Safe to repeat.'
42
42
  },
43
+ draft_show: {
44
+ usage: 'playbook draft show PLAYBOOK_ID [--workspace ID] [--profile PATH]',
45
+ about: 'Your draft of a playbook: {draft: {baseRevision, fields, updatedAt, savedBy}} (savedBy is person or agent), or {draft: null} when there is none. The draft is the unsaved work in the playbook editor. It belongs to one person; an agent reads and changes the draft of the person who owns the agent. A campaign cannot use a draft, and playbook show and playbook save never touch it.'
46
+ },
47
+ draft_save: {
48
+ usage: 'playbook draft save PLAYBOOK_ID --file REQUEST.json [--workspace ID] [--profile PATH]',
49
+ about: 'Keep your edits as the draft. The file is {"baseRevision": N, "expectedUpdatedAt": T, "fields": {...}}. expectedUpdatedAt is the updatedAt that playbook draft show returned, or null when it returned {draft: null}. If the draft changed since (another tab, your agent or the person), nothing is replaced: you get draft_conflict with currentUpdatedAt. Run playbook draft show, combine the changes, and save again. baseRevision is the version the edits start from: currentRevision from playbook show, or baseRevision from playbook draft show. A lower baseRevision than the current version is kept; publish checks it and refuses a draft that started from an older version (revision_conflict). The draft is replaced as a whole, so send every field (take them from playbook draft show, or playbook show when there is no draft). It never makes a version. The person sees the change in the editor within a few seconds. Fields follow the same rules as playbook save. The answer carries check: the problems and warnings a publish would see. They never stop a draft save. The draft is not fully checked until publish. Starter and archived playbooks are refused.'
50
+ },
51
+ draft_discard: {
52
+ usage: 'playbook draft discard PLAYBOOK_ID [--expected-updated-at T] [--workspace ID] [--profile PATH]',
53
+ about: 'Throw your draft away. The saved versions stay. Safe to repeat: discarded says whether a draft existed. With --expected-updated-at T (the updatedAt from playbook draft show), a draft saved since is kept and you get draft_conflict with currentUpdatedAt.'
54
+ },
55
+ draft_publish: {
56
+ usage: 'playbook draft publish PLAYBOOK_ID [--request-id UUID] [--workspace ID] [--profile PATH]',
57
+ about: 'Save your draft as the next version and remove the draft. It returns the new version, as playbook save does. If a newer version was saved after the draft started, nothing is saved: you get revision_conflict with currentRevision. Run playbook draft show, then playbook draft save with baseRevision set to currentRevision and your changes applied to the newer version, then publish again. No draft gives no_draft. Without --request-id a fresh one is made and printed to stderr. To retry a call that may have worked, pass the same --request-id: a repeat of a finished publish returns the same version. If a draft was saved while this ran you get draft_conflict: read it with playbook draft show and publish again.'
58
+ },
43
59
  delete: {
44
60
  usage: 'playbook delete PLAYBOOK_ID [--workspace ID] [--profile PATH]',
45
61
  about: 'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses, in any state, is refused with playbook_in_use and the campaigns are named; archive it instead. Starters cannot be deleted.'
@@ -57,7 +73,7 @@ const lines = Object.values(playbookCommands).map(command => command.usage);
57
73
  export const playbookUsageError = `Unknown or missing playbook subcommand. Usage:\n${lines.map(line => ` audienti ${line}`).join('\n')}\nRun audienti playbook SUBCOMMAND --help for the request shape.`;
58
74
  /** Help for `audienti playbook [SUBCOMMAND] --help`. */
59
75
  export function playbookHelp(subcommand) {
60
- const names = playbookCommands[subcommand] ? [subcommand] : Object.keys(playbookCommands);
76
+ const names = playbookCommands[subcommand] ? [subcommand] : subcommand === 'draft' ? Object.keys(playbookCommands).filter(name => name.startsWith('draft_')) : Object.keys(playbookCommands);
61
77
  const text = names.map(name => {
62
78
  const command = playbookCommands[name];
63
79
  return `audienti ${command.usage}\n ${command.about}${command.example ? `\n\nExample REQUEST.json (replace each UUID with a fresh one; reuse the same UUID where a key repeats):\n${JSON.stringify(command.example, null, 2)}` : ''}`;
@@ -27,6 +27,10 @@ export const playbookOperations={
27
27
  playbook_restore:{schema:z.object({...scope,id:uuid}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/restore`,scoped:true,description:'Bring an archived playbook back to the list and the campaign picker. Safe to repeat.'},
28
28
  playbook_delete:{schema:z.object({...scope,id:uuid}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}`,scoped:true,description:'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses (a campaign of any state, finished or archived included, that was ever set up with a version of it) is refused with 409 playbook_in_use, naming the campaigns; archive it instead. Starter playbooks are refused with starter_playbook.'},
29
29
  playbook_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative().describe('0 for a new playbook, otherwise currentRevision from playbook_get. A stale value is refused with revision_conflict, which carries currentRevision.'),fields}).strict(),method:'POST',path:a=>`${base(a)}/playbooks`,body:({workspaceId,...a})=>a,scoped:true,description:'Create or revise a playbook with optimistic version and idempotent request. A starter playbook refuses revision (409 starter_playbook); save its fields without an id to make an editable copy. Does not change existing campaign or participation pins. A playbook that cannot work (no off-ramp, or a stage nobody can reach) is refused with 422 playbook_cannot_work and the list in errors. A saved playbook returns warnings, the Playbook check, for what is legal but probably wrong. The save is listed in the audit log (category playbooks) with who made it; a save by an agent of a playbook no campaign uses yet marks it reviewPending until a person opens it. A step with channel Custom is done by a person: put what the person does in instructions (for example call, leave a voicemail, handwrite a note, record a video, send a gift); your agent prepares the script, text or choice and the person records the outcome (hand_task_* commands). A Custom step can wait for a named event with wait {until: event, event: name, days}.'},
30
+ playbook_draft_get:{schema:z.object({...scope,id:uuid}).strict(),method:'GET',path:a=>`${base(a)}/playbooks/${a.id}/draft`,scoped:true,description:'Read your draft of a playbook: {draft: {baseRevision, fields, updatedAt, savedBy}} or {draft: null}. savedBy is person or agent. The draft is the unsaved work in the playbook editor. It belongs to one person, and an agent reads and changes the draft of the person who owns the agent. No campaign can use a draft. playbook_get and playbook_save never touch it.'},
31
+ playbook_draft_save:{schema:z.object({...scope,id:uuid,baseRevision:z.number().int().positive().describe('The version the edits start from: currentRevision from playbook_get, or the baseRevision of the draft you read. A value above the current version is refused with revision_conflict, which carries currentRevision. A lower value is kept; playbook_draft_publish checks it and refuses a draft that started from an older version.'),expectedUpdatedAt:z.string().datetime().nullable().describe('The updatedAt of the draft you read with playbook_draft_get, or null when it returned no draft. If the draft changed since (another tab, your agent or the person), the save is refused with draft_conflict and currentUpdatedAt, and nothing is replaced. Read the draft again, combine the changes, then save.'),fields}).strict(),method:'PUT',path:a=>`${base(a)}/playbooks/${a.id}/draft`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Keep your edits as the draft of a playbook. The draft is replaced as a whole, so send every field: read it with playbook_draft_get (or playbook_get when there is no draft) and change what you need. It never makes a version and uses no requestId. The person who owns the draft sees the change in the editor within a few seconds. A stale expectedUpdatedAt is refused with draft_conflict (HTTP 409). Same field rules as playbook_save, but problems do not stop a draft save: the answer carries check (the problems and warnings a publish would see). The draft is not fully checked until publish. Starter and archived playbooks are refused (starter_playbook, playbook_archived).'},
32
+ playbook_draft_discard:{schema:z.object({...scope,id:uuid,expectedUpdatedAt:z.string().datetime().optional().describe('The updatedAt of the draft you read. When given and the draft changed since, the draft is kept and the call is refused with draft_conflict and currentUpdatedAt. Leave it out to discard whatever the draft holds.')}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}/draft${a.expectedUpdatedAt?`?expectedUpdatedAt=${encodeURIComponent(a.expectedUpdatedAt)}`:''}`,scoped:true,description:'Throw your draft of a playbook away. The saved versions stay. Safe to repeat: {id, discarded} says whether a draft existed. Pass expectedUpdatedAt to keep a draft that was saved since you read it (draft_conflict).'},
33
+ playbook_draft_publish:{schema:z.object({...scope,id:uuid,requestId:uuid.describe('A fresh UUID. Reuse it only to retry the same call.')}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/draft/versions`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Save your draft of a playbook as the next version and remove the draft, in one step. Returns the new version, as playbook_save does, with the same checks (including 422 playbook_cannot_work). If a newer version was saved after the draft started, nothing is saved and the call is refused with 409 revision_conflict and currentRevision; read the draft and the new version, then save the draft again with the new baseRevision. No draft gives 404 no_draft. A retry with the same requestId returns the same version.'},
30
34
  workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Free CRM read.'},
31
35
  playbook_progress_get:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.')}).strict(),method:'GET',path:a=>`${base(a)}/participations/${a.participationId}/progress`,scoped:true,description:'Read the participation’s admitted playbook pin and recorded stage/outcome history. Takes a participation ID (a person in a campaign; each person in the pipeline has one), not a playbook ID.'},
32
36
  playbook_progress_record:{schema:z.object({...scope,participationId:uuid,expectedVersion:z.number().int().nonnegative(),requestId:uuid,stageKey:uuid.optional(),offRampKey:uuid.optional(),finishStage:z.literal(true).optional(),reason:z.string().trim().min(1).max(2000),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20)}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/progress`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Report a stage, off-ramp, or finishStage:true after externally completing the current play and waits. Finish resolves next/stay/off-ramp from the admitted version. Stored evidence required for agents; human-review and backward-move rules apply. Assigned owner/Admin only. Goal ends prospecting; post-goal forward reporting never reactivates. No send or provider call.'},
package/cli/progress.mjs CHANGED
@@ -1,16 +1,24 @@
1
+ import {randomUUID} from 'node:crypto';
1
2
  import {callWorkspace,configuration} from './workspaces.mjs';
2
3
  import {readJsonFile} from './json-file.mjs';
3
4
  import {playbookUsageError,playbookCommands} from './playbook-help.mjs';
4
5
  export async function progressCli(args){
5
- const positions=[],flags={};for(let i=0;i<args.length;i++){const arg=args[i];if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!['profile','workspace','file','cursor','limit','revision','from','to','campaign','playbook-version','kind','user','format','owner','offer','audience','tag','archived'].includes(key)||(key!=='archived'&&(!args[i+1]||args[i+1].startsWith('--')))||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);flags[key]=key==='archived'&&(!args[i+1]||args[i+1].startsWith('--'))?'true':args[++i];}
6
- const names={playbook:{list:'playbook_list',actions:'playbook_actions_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
6
+ const positions=[],flags={};for(let i=0;i<args.length;i++){const arg=args[i];if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!['profile','workspace','file','cursor','limit','revision','from','to','campaign','playbook-version','kind','user','format','owner','offer','audience','tag','archived','request-id','expected-updated-at'].includes(key)||(key!=='archived'&&(!args[i+1]||args[i+1].startsWith('--')))||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);flags[key]=key==='archived'&&(!args[i+1]||args[i+1].startsWith('--'))?'true':args[++i];}
7
+ const names={playbook:{list:'playbook_list',actions:'playbook_actions_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',draft_show:'playbook_draft_get',draft_save:'playbook_draft_save',draft_discard:'playbook_draft_discard',draft_publish:'playbook_draft_publish',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
8
+ // playbook draft show|save|discard|publish ID is one command name in two words.
9
+ if(positions[0]==='playbook'&&positions[1]==='draft'&&['show','save','discard','publish'].includes(positions[2]))positions.splice(1,2,`draft_${positions[2]}`);
7
10
  const name=names[positions[0]]?.[positions[1]];if(!name)throw new Error(positions[0]==='playbook'?playbookUsageError:'Use analytics show --from ISO --to ISO; pipeline show; connections list|register --file PATH|report --file PATH|disconnect --file PATH.');
8
11
  if(flags.format&&(name!=='workspace_analytics_get'||!['json','csv'].includes(flags.format)))throw new Error('--format json|csv is only accepted for analytics show.');
9
- const fileNames=['playbook_save','playbook_progress_record','workspace_connection_register','workspace_connection_report','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get','playbook_archive','playbook_restore','playbook_delete'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
12
+ const fileNames=['playbook_save','playbook_draft_save','playbook_progress_record','workspace_connection_register','workspace_connection_report','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get','playbook_archive','playbook_restore','playbook_delete','playbook_draft_get','playbook_draft_save','playbook_draft_discard','playbook_draft_publish'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
10
13
  if(fileNames.includes(name)&&!flags.file)throw new Error('Supply a JSON request with --file PATH.');if(!fileNames.includes(name)&&flags.file)throw new Error('--file is only accepted for mutations.');
11
14
  const input=fileNames.includes(name)?readJsonFile(flags.file,65536,'Request file'):{};if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('Request must be an object.');if(flags.workspace)input.workspaceId=flags.workspace;if(withId)input[name==='playbook_progress_get'?'participationId':'id']=positions[2];
15
+ if(flags['request-id']!==undefined&&name!=='playbook_draft_publish')throw new Error('--request-id is only accepted for playbook draft publish.');
16
+ if(flags['expected-updated-at']!==undefined&&name!=='playbook_draft_discard')throw new Error('--expected-updated-at is only accepted for playbook draft discard.');
17
+ if(flags['expected-updated-at']!==undefined)input.expectedUpdatedAt=flags['expected-updated-at'];
18
+ // A generated request ID is printed, so a retry of a call that may have worked can use the same one.
19
+ if(name==='playbook_draft_publish'){input.requestId=flags['request-id']??randomUUID();if(flags['request-id']===undefined)console.error(`requestId ${input.requestId} (retry with --request-id ${input.requestId} if this call may have worked)`);}
12
20
  if(flags.archived!==undefined&&(name!=='playbook_list'||!['true','false'].includes(flags.archived)))throw new Error(name==='playbook_list'?'--archived takes true or false, or no value.':'--archived is only accepted for playbook list.');
13
- for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format'].includes(key))continue;if(key==='archived'){input.archived=value==='true';continue;}const field={campaign:'campaignId','playbook-version':'pinId',user:'userId',owner:'ownerUserId',offer:'offerId',audience:'audienceId'}[key]??key;input[field]=['limit','revision'].includes(key)?Number(value):value;}
21
+ for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format','request-id','expected-updated-at'].includes(key))continue;if(key==='archived'){input.archived=value==='true';continue;}const field={campaign:'campaignId','playbook-version':'pinId',user:'userId',owner:'ownerUserId',offer:'offerId',audience:'audienceId'}[key]??key;input[field]=['limit','revision'].includes(key)?Number(value):value;}
14
22
  const result=await callWorkspace(name,input,configuration(flags.profile));
15
23
  if(flags.format==='csv'){process.stdout.write(analyticsCsv(result?.data??result));return;}
16
24
  console.log(JSON.stringify(result));
@@ -124,6 +124,9 @@ export function writeProfile(path, profile) {
124
124
  }
125
125
  /** The profile path: --profile wins, then AUDIENTI_PROFILE (the variable the MCP server reads), else none (#448). */
126
126
  export const profileFrom = flag => flag ?? (process.env.AUDIENTI_PROFILE || undefined);
127
+ // The server words a next step with CLI commands. An MCP caller has tools, not commands, so the same step is said with the tool names (#507).
128
+ const MCP_WORDING = [['audienti playbook actions','playbook_actions_list'],['playbook draft show','playbook_draft_get'],['playbook draft save','playbook_draft_save'],['playbook draft discard','playbook_draft_discard'],['playbook draft publish','playbook_draft_publish'],['playbook show','playbook_get'],['playbook save','playbook_save'],['audienti playbook save --help','the playbook_save tool description'],['--workspace ID','workspaceId']];
129
+ export const surfaceWording = (text, config) => config?.interface === 'mcp' && typeof text === 'string' ? MCP_WORDING.reduce((out, [cli, tool]) => out.replaceAll(cli, tool), text) : text;
127
130
  export const profileHint = 'Pass --profile PATH, or set AUDIENTI_PROFILE to the profile file.';
128
131
  export function configuration(path, scope='workspace:access') {
129
132
  path=profileFrom(path);
@@ -159,6 +162,7 @@ export function localGuidance(name, issues, args) {
159
162
  return {field, ...(allowed.length ? {allowed} : {}), next: `Use one of the allowed values in place of ${shown}.${phone}${more}`};
160
163
  }
161
164
  if(issue.code === 'unrecognized_keys') return {field, next: `Remove ${issue.keys.map(key => `"${key}"`).join(', ')} or fix the spelling. ${playbook ? `The fields a playbook has are in the example of: audienti playbook save --help. Field rules: ${FIELD_RULES}` : 'Run audienti --help for the options of this command.'}`};
165
+ if(issue.code === 'invalid_type' && given === undefined && last === 'expectedUpdatedAt') return {field, next: 'Add expectedUpdatedAt (null when there is no draft; otherwise the updatedAt that playbook draft show returned).'};
162
166
  if(issue.code === 'invalid_type') return {field, next: given === undefined ? `Add ${last || 'the value'}.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}` : `Send a ${issue.expected} for ${last || field}.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}`};
163
167
  return field ? {field, next: `Fix this value and send the command again.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}`} : {};
164
168
  }
@@ -166,17 +170,17 @@ export function localGuidance(name, issues, args) {
166
170
  // Checked here too, so a response never puts the token or control text into the agent's context.
167
171
  // #519: a refused label already says in plain words what is wrong; the code and the next step travel beside it.
168
172
  const INTENT_REFUSALS = new Set(['prospect_not_found', 'conversation_evidence_not_found', 'participation_not_found', 'payload_too_large', 'idempotency_conflict', 'evidence_has_no_body', 'too_many_labels', 'unknown_label', 'already_recorded', 'label_not_available', 'label_direction', 'label_channel', 'duplicate_label', 'must_stand_alone', 'labels_conflict', 'result_required', 'result_mismatch', 'quote_required', 'quote_not_in_message', 'reason_required', 'reason_not_plain', 'detail_not_allowed']);
169
- export function errorGuidance(error, token) {
173
+ export function errorGuidance(error, token, config) {
170
174
  const clean = text => typeof text === 'string' && !/[\u0000-\u001f\u007f]/.test(text) && !(token && text.includes(token));
171
175
  return {
172
- next: clean(error?.next) && error.next.length <= 600 ? error.next : undefined,
176
+ next: clean(error?.next) && error.next.length <= 600 ? surfaceWording(error.next, config) : undefined,
173
177
  field: clean(error?.field) && /^[A-Za-z0-9_.[\]-]{1,200}$/.test(error.field) ? error.field : undefined,
174
178
  allowed: Array.isArray(error?.allowed) && error.allowed.length > 0 && error.allowed.length <= 60 && error.allowed.every(value => clean(value) && value.length <= 80) ? error.allowed : undefined
175
179
  };
176
180
  }
177
181
  export class WorkspaceRequestError extends Error {
178
182
  constructor(status, code, message, retryAt, blockers, requestTraceId, guidance = {}) { super(message); this.status = status; this.code = code; this.retryAt = retryAt; this.blockers = blockers; this.requestTraceId = requestTraceId; this.next = guidance.next; this.field = guidance.field; this.allowed = guidance.allowed; }
179
- toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.field ? {field:this.field} : {}), ...(this.allowed ? {allowed:this.allowed} : {}), ...(this.next ? {next:this.next} : {}), ...(this.currentRevision!==undefined ? {currentRevision:this.currentRevision} : {}), ...(this.campaigns ? {campaigns:this.campaigns,campaignCount:this.campaignCount} : {}), ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.errors ? {errors:this.errors} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
183
+ toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.field ? {field:this.field} : {}), ...(this.allowed ? {allowed:this.allowed} : {}), ...(this.next ? {next:this.next} : {}), ...(this.currentRevision!==undefined ? {currentRevision:this.currentRevision} : {}), ...(this.currentUpdatedAt!==undefined ? {currentUpdatedAt:this.currentUpdatedAt} : {}), ...(this.campaigns ? {campaigns:this.campaigns,campaignCount:this.campaignCount} : {}), ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.errors ? {errors:this.errors} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
180
184
  }
181
185
  export class HostedResponseUnknown extends Error {
182
186
  constructor(requestId, reason, requestTraceId) { super('The server response is unknown. Paid work may have started. Retry using the same requestId and principal; never generate a new ID to recover this call.'); this.requestId=requestId; this.reason=reason; this.requestTraceId=requestTraceId; }
@@ -188,7 +192,7 @@ export async function callWorkspace(name, args, config) {
188
192
  const input={...parsed.data};
189
193
  if(operation.localUpload)return uploadFileContent(input,(operationName,fields)=>callWorkspace(operationName,fields,config));
190
194
  // #333: an agent's own queue covers all its workspaces; a user's reads the selected workspace, as before.
191
- if(operation.scoped || (operation.agentWide && !/^agt_/.test(config.token ?? ''))) { input.workspaceId ??= config.workspaceId;if(!uuid.safeParse(input.workspaceId).success) throw new Error(`Select a workspace or supply --workspace ID. ${config.profile&&Object.keys(config.profile).length?'Run workspace use WORKSPACE_ID once for this profile.':profileHint}`); }
195
+ if(operation.scoped || (operation.agentWide && !/^agt_/.test(config.token ?? ''))) { input.workspaceId ??= config.workspaceId;if(!uuid.safeParse(input.workspaceId).success) throw new Error(config.interface==='mcp'?`Pass workspaceId in the tool input, or call workspace_select once. ${config.profile&&Object.keys(config.profile).length?'':profileHint}`.trim():`Select a workspace or supply --workspace ID. ${config.profile&&Object.keys(config.profile).length?'Run workspace use WORKSPACE_ID once for this profile.':`${profileHint} An agent that is not connected yet runs agent connect first; then run workspace use WORKSPACE_ID once.`}`); }
192
196
  const url=validateUrl(config.url); if(!config.token) throw new Error(`A workspace:access token is required. ${profileHint} Or set AUDIENTI_API_URL and AUDIENTI_API_TOKEN.`);
193
197
  const target=new URL(`/api/v1${operation.path(input)}`,url);
194
198
  let response;try {response=await fetch(target,{method:operation.method,headers:{Authorization:`Bearer ${config.token}`,Accept:'application/json','Content-Type':'application/json',...((operation.traced||name.startsWith('tools_'))?{'X-Audienti-Interface':config.interface==='mcp'?'mcp':'cli'}:{})},...(['POST','PUT','PATCH'].includes(operation.method)?{body:JSON.stringify(operation.body?.(input)??{})}:{}),redirect:'error',signal:AbortSignal.timeout(operation.timeoutMs ?? 10000)});}catch(error){if(isPaidToolOperation(name,input)||operation.recoverable)throw new HostedResponseUnknown(input.requestId,['TimeoutError','AbortError'].includes(error?.name)?'timeout':'transport_failure');throw new Error('Could not reach the API. Redirects are not followed.');}
@@ -198,10 +202,11 @@ export async function callWorkspace(name, args, config) {
198
202
  if(!response.ok) {
199
203
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
200
204
  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;
201
- const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000: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() : '';
205
+ const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && (code==='not_found' || code==='no_draft')) || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='draft_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000: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() : '';
202
206
  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;
203
- const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? detail : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token));
207
+ const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? surfaceWording(detail, config) : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${surfaceWording(detail, config)}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token, config));
204
208
  if(code==='playbook_cannot_work'&&Array.isArray(result?.error?.errors)&&result.error.errors.length<=30)error.errors=result.error.errors.filter(item=>item&&typeof item.id==='string'&&typeof item.message==='string').map(item=>({id:item.id,level:item.level,message:item.message,...(typeof item.stageKey==='string'?{stageKey:item.stageKey}:{}),...(item.fix&&typeof item.fix.label==='string'?{fix:{id:item.fix.id,label:item.fix.label}}:{})}));
209
+ if(code==='draft_conflict'&&(result?.error?.currentUpdatedAt===null||/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(result?.error?.currentUpdatedAt??'')))error.currentUpdatedAt=result.error.currentUpdatedAt;
205
210
  if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
206
211
  // A playbook that is in use names the campaigns that use it (#459).
207
212
  if(code==='playbook_in_use'&&Array.isArray(result?.error?.campaigns)&&Number.isInteger(result.error.campaignCount)){error.campaigns=result.error.campaigns.slice(0,20).filter(c=>uuid.safeParse(c?.id).success&&typeof c.name==='string'&&c.name.length<=200&&!/[\u0000-\u001f\u007f]/.test(c.name)).map(c=>({id:c.id,name:c.name}));error.campaignCount=result.error.campaignCount;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.16",
3
+ "version": "0.2.0-exora.18",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "94242d4dc776cdc9a6761f9c0b40fbca17e8b1c6",
3
+ "sourceCommit": "4cab4a5e992c5dea08547078bee1839d951a6d22",
4
4
  "files": {
5
5
  "LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
6
6
  "README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
@@ -16,7 +16,7 @@
16
16
  "cli/attachments.mjs": "2fee6b4abbf82da9847d4a927a3d4d3ca947a198a903a78927596a2ea858cbf4",
17
17
  "cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
18
18
  "cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
19
- "cli/audienti.mjs": "6cf78f0359a450bfc456599aa047efcf93f0f9353e810449957f932ea0ce1a43",
19
+ "cli/audienti.mjs": "e9789a8fc1700fa82c2fafba2147d64a0f8818052d4728690aff503c0434233c",
20
20
  "cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
21
21
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
22
22
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
@@ -49,11 +49,11 @@
49
49
  "cli/outbound.mjs": "23903eb0fec4d7af3aa0031d63488ed9a8697e07ccf1314d194b48e109d15f82",
50
50
  "cli/outcome-operations.mjs": "cf133097799f240a006264a38a4c6bd4094078d6dec78a71679700ac3bdeacd5",
51
51
  "cli/outcomes.mjs": "765ccb112a90fc4d228b2a41e5e19c5e1f3e2fe14bc1d8128b0090fc95cdbf3f",
52
- "cli/owner-account-client.mjs": "e7912f7c67571676cb94bcc9424b7d4a3bf9ef1461c9a5d6c31a2651e546a9ea",
53
- "cli/playbook-help.mjs": "541bba97695e9761cf73d182f62900e8117bddb23a39c77f1d18258cd7e6f5a5",
54
- "cli/playbook-operations.mjs": "3994739d63dbd6658d5e14eea889c56ce1dc19a9c639f178335a56387793717a",
52
+ "cli/owner-account-client.mjs": "91ab4d9b4b93c7feea668e5a3087cd9aa0766cf4cc657b7cac34e606d3a4b7a5",
53
+ "cli/playbook-help.mjs": "cb3ccb3ac3538bdc360f505b6eb317cf07a9c58e75b9ddcf3b2bfbd96635eaf6",
54
+ "cli/playbook-operations.mjs": "e8c065ce2ffeaf15994a218ed730e1aeb5563551d877c678412e116e62ac26a3",
55
55
  "cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
56
- "cli/progress.mjs": "c394eae609596accba0d170d376fe35b58cddff2cf7b106f670da7eaed76dd5f",
56
+ "cli/progress.mjs": "a3052d8ce40fbbf2f38ac0efef035d8f0a964838350d6e63e5fc38a738e0fa34",
57
57
  "cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
58
58
  "cli/prospecting-settings.mjs": "b09f47c84416d9892d7d5b17dcf684cbb560e1cacb25513826ff04ef9307626e",
59
59
  "cli/question-operations.mjs": "c7070dd1b5ec9c5b0df93012ef08d0d6b4d6e5c8da267a37beb1acc5c3cf4ea1",
@@ -83,8 +83,8 @@
83
83
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
84
84
  "cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
85
85
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
86
- "cli/workspaces.mjs": "5694825a763008bf0dcfa4e17bfe79a1d593207099f500bfa21184f899e83971",
87
- "package.json": "4f431e7f9111cd5ed691aac68cf1c5ba0a6f58905dd502250f86fb9093e2edd8",
86
+ "cli/workspaces.mjs": "eebe2c4bfc6ddedb2fb0dd551d5fa348eeda2fdf2105babd514b29a97f4d6225",
87
+ "package.json": "95dfaba2962c0a73f054289dfa3cdcad7e667dc932ba7d0fcfa35517e7ad1513",
88
88
  "skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
89
89
  "skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
90
90
  "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",