@audienti/cli 0.2.0-exora.25 → 0.2.0-exora.27

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
@@ -114,7 +114,8 @@ Search: workspace search --workspace ID --query TEXT [--people-only true] [--rec
114
114
  Invitation commands: workspace invitation show|accept|decline|retry|cancel ID
115
115
  Options: --workspace ID --name NAME --request-id UUID --email EMAIL --generation N
116
116
  Optional private profile: --profile PATH (outside this repository), or the AUDIENTI_PROFILE variable. --profile wins.
117
- Save an environment token: profile save --scope account:read|workspace:access|agents:manage --profile PATH
117
+ Save an environment token: profile save --scope account:read|workspace:read|workspace:access|agents:manage --profile PATH
118
+ Tokens: token show [--profile PATH] (what the token can do; workspace:read can look but is refused on every change)
118
119
  Start local MCP stdio server: mcp (audienti mcp --help for the settings)
119
120
  --json writes the API response envelope.
120
121
  `;
@@ -201,7 +202,7 @@ async function main() {
201
202
  if(args[0]==='offer'){const {offerCli}=await import('./offers.mjs');await offerCli(args);return;}
202
203
  if (args[0] === 'agent') { const { agentCli } = await import('./agents.mjs'); await agentCli(args); return; }
203
204
  if (isAssignmentCommand(args)) { await assignmentCli(args); return; }
204
- if (['workspace', 'profile'].includes(args[0])) { const { workspaceCli } = await import('./workspaces.mjs'); await workspaceCli(args); return; }
205
+ if (['workspace', 'profile', 'token'].includes(args[0])) { const { workspaceCli } = await import('./workspaces.mjs'); await workspaceCli(args); return; }
205
206
  if (args[0] === 'profiles') { const { profilesCli } = await import('./profiles.mjs'); await profilesCli(args); return; }
206
207
  if (args[0] === 'skills') { const { skillsCli } = await import('./skills.mjs'); await skillsCli(args); return; }
207
208
  if (args[0] === 'mcp' && args.length === 1) { await import('./mcp.mjs'); return; }
@@ -85,6 +85,7 @@ export const operations = {
85
85
  workspace_member: {schema:z.object({...workspace,userId:uuid}).strict(),method:'GET',path:a=>`/workspaces/${a.workspaceId}/members/${a.userId}`,scoped:true},
86
86
  workspace_agent_access: { schema:z.object(workspace).strict(), method:'GET', path:a=>`/workspaces/${a.workspaceId}/agent-access`, scoped:true },
87
87
  workspace_agent_access_set: { schema:z.object({...workspace,userId:uuid,blocked:z.boolean()}).strict(), method:'POST', path:a=>`/workspaces/${a.workspaceId}/members/${a.userId}/agent-access`, body:a=>({blocked:a.blocked}), scoped:true },
88
+ token_show: { schema: z.object({}).strict(), method: 'GET', path: () => '/token', description: 'Say what the credential you are using can do: its permission and whether it can change things. Works with any valid user API token or agent credential. A read-only token (workspace:read) is refused on every change; ask your user for a workspace:access token to change things.' },
88
89
  workspace_initialize: { schema: z.object({}).strict(), method: 'POST', path: () => '/workspaces/bootstrap' },
89
90
  workspace_list: { schema: z.object({}).strict(), method: 'GET', path: () => '/workspaces' },
90
91
  workspace_create: { schema: z.object({ name: z.string().trim().min(1).max(80), requestId: uuid }).strict(), method: 'POST', path: () => '/workspaces', body: a => a },
@@ -134,7 +135,7 @@ export function configuration(path, scope='workspace:access') {
134
135
  path=profileFrom(path);
135
136
  const profile=readProfile(path);
136
137
  const agentProfile = Boolean(profile.agent || profile.pendingAgent || profile.connectionSecret);
137
- return { url:agentProfile ? profile.url : process.env.AUDIENTI_API_URL ?? profile.url, token:agentProfile ? (scope === 'workspace:access' ? profile.tokens?.['workspace:read'] : undefined) : process.env.AUDIENTI_API_TOKEN ?? (scope === 'workspace:access' ? profile.tokens?.['workspace:read'] ?? profile.tokens?.[scope] : profile.tokens?.[scope]), workspaceId:profile.workspaceId, profile };
138
+ return { url:agentProfile ? profile.url : process.env.AUDIENTI_API_URL ?? profile.url, token:agentProfile ? (scope === 'workspace:access' ? profile.tokens?.['workspace:read'] : undefined) : process.env.AUDIENTI_API_TOKEN ?? (scope === 'workspace:access' ? profile.tokens?.['workspace:access'] ?? profile.tokens?.['workspace:read'] : profile.tokens?.[scope]), workspaceId:profile.workspaceId, profile };
138
139
  }
139
140
  export function validateUrl(value) {
140
141
  let url;try {url=new URL(value);}catch{throw new Error('Set AUDIENTI_API_URL to an API URL ending with /api/v1.');}
@@ -163,6 +164,10 @@ export function localGuidance(name, issues, args) {
163
164
  const more = playbook && last === 'actionId' ? ' List every action id with its channel: audienti playbook actions.' : playbook && last === 'when' ? ' List the event names: audienti playbook actions.' : '';
164
165
  return {field, ...(allowed.length ? {allowed} : {}), next: `Use one of the allowed values in place of ${shown}.${phone}${more}`};
165
166
  }
167
+ if(issue.code === 'invalid_union' && issue.discriminator && Array.isArray(issue.options)) {
168
+ const allowed = issue.options.filter(value => typeof value === 'string' && value.length <= 80).slice(0, 60);
169
+ return {field, ...(allowed.length ? {allowed} : {}), next: `Use one of the allowed values in place of ${shown}, spelled exactly.${playbook ? ` Field rules: ${FIELD_RULES}` : ''}`};
170
+ }
166
171
  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.'}`};
167
172
  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).'};
168
173
  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}` : ''}`};
@@ -197,7 +202,7 @@ export async function callWorkspace(name, args, config) {
197
202
  if(operation.localUpload)return uploadFileContent(input,(operationName,fields)=>callWorkspace(operationName,fields,config));
198
203
  // #333: an agent's own queue covers all its workspaces; a user's reads the selected workspace, as before.
199
204
  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.`}`); }
200
- 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.`);
205
+ const url=validateUrl(config.url); if(!config.token) throw new Error(`A workspace token is required: workspace:access to read and change, or workspace:read to only read. ${profileHint} Or set AUDIENTI_API_URL and AUDIENTI_API_TOKEN.`);
201
206
  const target=new URL(`/api/v1${operation.path(input)}`,url);
202
207
  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.');}
203
208
  const headerTrace=(operation.traced||name.startsWith('tools_'))&&uuid.safeParse(response.headers.get('x-request-trace-id')).success?response.headers.get('x-request-trace-id'):undefined;
@@ -206,7 +211,7 @@ export async function callWorkspace(name, args, config) {
206
211
  if(!response.ok) {
207
212
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
208
213
  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;
209
- const detail = (INTENT_REFUSALS.has(code) || (name.startsWith('decision_') && DECISION_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() : '';
214
+ const detail = (INTENT_REFUSALS.has(code) || (name.startsWith('decision_') && DECISION_REFUSALS.has(code)) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===403 && code==='insufficient_scope' && /^This API token can read but not change\./.test(result?.error?.message ?? '')) || (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() : '';
210
215
  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;
211
216
  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));
212
217
  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}}:{})}));
@@ -233,11 +238,13 @@ export async function workspaceCli(args) {
233
238
  else position.push(args[i]);
234
239
  }
235
240
  if(position[0]==='profile' && position[1]==='save') {
236
- if(!['account:read','workspace:access','agents:manage'].includes(flags.scope)) throw new Error('Specify --scope account:read, workspace:access or agents:manage.');
241
+ if(!['account:read','workspace:read','workspace:access','agents:manage'].includes(flags.scope)) throw new Error('Specify --scope account:read, workspace:read, workspace:access or agents:manage.');
237
242
  const url=validateUrl(process.env.AUDIENTI_API_URL).toString().replace(/\/$/,'');const token=process.env.AUDIENTI_API_TOKEN;if(!token) throw new Error('Set AUDIENTI_API_TOKEN.');
238
243
  const profile=readProfile(profileFrom(flags.profile));if(profile.agent || profile.pendingAgent || profile.connectionSecret) throw new Error('Use a separate user profile; agent profiles cannot store user API tokens.');writeProfile(profileFrom(flags.profile),{...profile,url,tokens:{...profile.tokens,[flags.scope]:token}});console.log('Profile saved.');return;
239
244
  }
240
245
  const config=configuration(flags.profile);const command=position.slice(1).join(' ');
246
+ // token show works with any saved user token, so it also tries the other slots when there is no workspace token.
247
+ if(position[0]==='token'&&!config.token&&!process.env.AUDIENTI_API_TOKEN&&!(config.profile.agent||config.profile.pendingAgent||config.profile.connectionSecret))config.token=config.profile.tokens?.['agents:manage']??config.profile.tokens?.['account:read'];
241
248
  let name, input={};
242
249
  if(command==='sending-settings') {name='workspace_sending_settings_get';input={workspaceId:flags.workspace,...(flags.user?{userId:flags.user}:{})};}
243
250
  else if(command==='sending-settings save') {if(!flags.file) throw new Error('Supply the settings with --file PATH.');const body=readJsonFile(flags.file,65536,'Settings file');if(!body||typeof body!=='object'||Array.isArray(body)) throw new Error('Settings must be an object.');name='workspace_sending_settings_save';input={...body,workspaceId:flags.workspace??body.workspaceId,...(flags.user?{userId:flags.user}:{})};}
@@ -247,6 +254,7 @@ export async function workspaceCli(args) {
247
254
  else if(position[1]==='usage'&&position[2]==='call'&&position.length===4) {name='workspace_usage_call';input={workspaceId:flags.workspace,callId:position[3]};}
248
255
  else if(command==='agent-access') {name='workspace_agent_access';input={workspaceId:flags.workspace};}
249
256
  else if(['agent-access block','agent-access restore'].includes(command)) {name='workspace_agent_access_set';input={workspaceId:flags.workspace,userId:flags.user,blocked:command.endsWith('block')};}
257
+ else if(position[0]==='token'&&command==='show') name='token_show';
250
258
  else if(command==='init') name='workspace_initialize';
251
259
  else if(command==='list') name='workspace_list';
252
260
  else if(command==='notifications') {if(flags['unread-only']&&!['true','false'].includes(flags['unread-only']))throw new Error('--unread-only must be true or false.');name='workspace_notifications_list';input={workspaceId:flags.workspace,unreadOnly:flags['unread-only']==='true',...(flags.cursor?{cursor:readJsonFile(flags.cursor,4096,'Notification cursor')}:{})};}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.25",
3
+ "version": "0.2.0-exora.27",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@ When you change a playbook you already have, a part you leave out (`stages`, `go
35
35
  | a stage `completion` | A stage with steps that an agent works, other than the goal, closes people out as Unresponsive. Other stages stay. |
36
36
  | a step `condition` | `Only if connected` for a LinkedIn message, else `Always`. |
37
37
  | a step `instructions` | One coaching sentence for the kind of action. |
38
- | a main step `wait` | Only for a step that is new in this save. A step that sends waits 3 days for a reply; a connection request waits 7 days for the accept; a step that sends nothing has no wait. A step the playbook already has keeps what it has, and `"wait": null` means no wait. |
38
+ | a main step `wait` | Only for a step that is new in this save. A step that sends waits 3 days for a reply; a connection request waits 7 days for the accept; a step that sends nothing has no wait. A step the playbook already has keeps what it has, and `"wait": null` means no wait. The saved step carries the wait it got, so `playbook show` and the API read give the actual days. |
39
39
 
40
40
  ## The Playbook check
41
41
 
@@ -66,7 +66,7 @@ The other items are warnings; read them after each save and clear them:
66
66
  | `goalDays` | 0 to 365. The days after which a prospect in this stage is overdue (the pipeline card turns orange). 0 means never overdue. |
67
67
  | `enabled` | Defaults to true. `false` skips the stage: no one is moved into it. The first stage and the goal stage cannot be skipped. |
68
68
  | `steps` | Up to 20 steps. |
69
- | `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. |
69
+ | `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. Spell the kind exactly: `off_ramp`, not `offRamp`. A kind that matches none is refused with the list of kinds that work. |
70
70
 
71
71
  ## Step
72
72
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "d6cf3b19751f65dfd1bef97a2323148fa1991b99",
3
+ "sourceCommit": "22bd7faad251f47226c59035b267f222ea1943b4",
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": "fa7a7e479eb95d60678fa7b24dbbbbc84aacddcfdcd8edc7373f1859b86c8ba4",
19
+ "cli/audienti.mjs": "afdba3fa4acc93faaa708a0214fbaa57444bfee20204a2c2817d9fd320a53933",
20
20
  "cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
21
21
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
22
22
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
@@ -85,8 +85,8 @@
85
85
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
86
86
  "cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
87
87
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
88
- "cli/workspaces.mjs": "7ec6ec2a45a275d4425eb56731ccfc2d27abd979c5ae322294468e689c05c9ce",
89
- "package.json": "e534913c63064467c1bce0c249dcc2ec2c7605b4c3d48bb39853f9236012b59b",
88
+ "cli/workspaces.mjs": "5b179691b8492d00b9d639dd13521fe96828aa392190550de34dd91cd0bdba53",
89
+ "package.json": "af224a943c29b27cad8a385a5f670d6e2927de40915e97107721d4e7bafd7509",
90
90
  "skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
91
91
  "skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
92
92
  "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
@@ -105,6 +105,6 @@
105
105
  "skills/getting-started/references/replies.md": "a372a01078f7c0a750b0fb98aff23ca9e7d9155c6b4ac549dd621444325e402b",
106
106
  "skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
107
107
  "skills/playbook-design/SKILL.md": "56aa5692c1bbe3843f11819f0f9270988ddd0424b16f38db72a854864155407f",
108
- "skills/playbook-design/references/fields.md": "7af0c8a2ad7b2ee83ff006fe4707a90c3edacdb8ccf83c09fa17f692b5fa2c81"
108
+ "skills/playbook-design/references/fields.md": "c7ba7a0bf8ac1325e70a5e4d98734716d752c311588d064bf2aba849925357aa"
109
109
  }
110
110
  }