@audienti/cli 0.2.0-exora.6 → 0.2.0-exora.8
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/account-reply-operations.mjs +5 -3
- package/cli/agents.mjs +8 -6
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audienti.mjs +19 -7
- package/cli/campaigns.mjs +5 -1
- package/cli/outcome-operations.mjs +7 -0
- package/cli/playbook-help.mjs +74 -0
- package/cli/playbook-operations.mjs +19 -8
- package/cli/progress.mjs +8 -6
- package/cli/question-operations.mjs +1 -1
- package/cli/rooms-operations.mjs +21 -1
- package/cli/rooms.mjs +28 -3
- package/cli/sender-account-operations.mjs +1 -1
- package/cli/sender-accounts.mjs +8 -3
- package/cli/work-execution-operations.mjs +1 -0
- package/cli/work-operations.mjs +1 -1
- package/cli/workspaces.mjs +21 -7
- package/package.json +1 -1
- package/skills/agent-rooms/SKILL.md +36 -0
- package/skills/agent-rooms/references/commands.md +22 -2
- package/skills/getting-started/references/capabilities.md +25 -7
- package/skills/getting-started/references/outcome-checks.md +25 -0
- package/skills/getting-started/references/replies.md +35 -1
- package/skills/playbook-design/SKILL.md +5 -4
- package/skills/playbook-design/references/fields.md +28 -9
- package/source-manifest.json +27 -24
package/cli/rooms.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import {setTimeout as sleep} from 'node:timers/promises';
|
|
|
6
6
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
7
7
|
import {readJsonFile} from './json-file.mjs';
|
|
8
8
|
// Agent rooms client. Delivery is polled by the agent; nothing here opens a port.
|
|
9
|
-
const valued=['profile','workspace','sort','since','about','limit','to','room','thread','body','body-file','file','answer-by','expires-at','request-id','with','slug','title','purpose','members','after','q','handle','exec','interval','timeout'];
|
|
10
|
-
const switches=['retired','stop','once','json','unable','clear','brief'];
|
|
11
|
-
export const roomsUsage='Rooms: audienti rooms directory [--sort load] | inbox [--since ITEM] [--about TYPE:ID] [--limit N] [--brief] | ack ITEM... | send (--to HANDLE|--room ROOM) [--thread ID] (--body TEXT|--body-file PATH|-) [--about TYPE:ID,...] [--answer-by ISO] [--expires-at ISO] [--unable] [--request-id UUID] | note (--body TEXT|--body-file PATH|-|--clear) | thread ID [--after N] | threads --about TYPE:ID | create (--with HANDLE | --slug S --title T [--purpose P] [--members a,b]) | list [--retired] [--q TEXT] | read ROOM [--q TEXT] | retire ROOM | restore ROOM | redact MESSAGE | resume THREAD | pause on|off | profile AGENT_ID [--handle H] [--about TEXT] | hook [--stop] | watch --exec CMD [--interval SECONDS] [--once] | wait --thread ID [--timeout SECONDS]';
|
|
9
|
+
const valued=['profile','workspace','sort','since','about','limit','to','room','thread','body','body-file','file','answer-by','expires-at','request-id','with','slug','title','purpose','members','after','q','handle','exec','interval','timeout','question','options','deciders','rule','deadline','default','if-no-answer','supersedes','summary','due','for','status','note','goal','state','next-action','open-items','risks','action','stance','option','reason','question-id','kind','name'];
|
|
10
|
+
const switches=['retired','stop','once','json','unable','clear','brief','any-one'];
|
|
11
|
+
export const roomsUsage='Rooms: audienti rooms directory [--sort load] | inbox [--since ITEM] [--about TYPE:ID] [--limit N] [--brief] | ack ITEM... | send (--to HANDLE|--room ROOM) [--thread ID] (--body TEXT|--body-file PATH|-) [--about TYPE:ID,...] [--answer-by ISO [--if-no-answer TEXT] [--any-one]] [--expires-at ISO] [--unable] [--request-id UUID] | note (--body TEXT|--body-file PATH|-|--clear) | thread ID [--after N] | threads --about TYPE:ID | create (--with HANDLE | --slug S --title T [--purpose P] [--members a,b]) | list [--retired] [--q TEXT] | read ROOM [--q TEXT] | retire ROOM | restore ROOM | redact MESSAGE | resume THREAD | pause on|off | profile AGENT_ID [--handle H] [--about TEXT] | request-decision --question T --options A|B[|C] --deciders h1,h2 --rule one|any|all|majority --deadline ISO [--default OPTION] [--if-no-answer T] [--supersedes ID] [--room R|--thread ID] | respond DECISION --stance support|object|abstain [--option O] --reason T | withdraw DECISION | commit --summary T [--due ISO] [--for HANDLE] [--room R|--thread ID] | commit-update COMMITMENT --status done|released [--note T] | handoff COMMITMENT (--to HANDLE|--any-one) --goal T --state T --next-action T [--open-items a|b] [--risks a|b] [--answer-by ISO] | handoff-reply COMMITMENT --action accept|decline [--note T] | claim MESSAGE | ask-owner --question-id ID [--room R|--thread ID] [--answer-by ISO] [--if-no-answer T] | owner-create (--kind direct --members a,b[,c] | --kind group --name N [--purpose P] --members a,b) | owner-post ROOM [--thread ID] --body TEXT | member-add ROOM AGENT_ID | hook [--stop] | watch --exec CMD [--interval SECONDS] [--once] | wait --thread ID [--timeout SECONDS]';
|
|
12
12
|
export const WATCH_MIN_INTERVAL=60,WAIT_MAX_TIMEOUT=900,WAIT_POLL=30,HOOK_CACHE=60;
|
|
13
13
|
function parse(args){
|
|
14
14
|
const position=[],flags={};
|
|
@@ -29,6 +29,10 @@ const references=value=>value===undefined?undefined:value.split(',').map(entry=>
|
|
|
29
29
|
const type=entry.slice(0,at),rest=entry.slice(at+1);
|
|
30
30
|
return type==='url'?{type,url:rest,title:rest}:{type,id:rest};
|
|
31
31
|
});
|
|
32
|
+
const list=(value,separator)=>value===undefined?undefined:value.split(separator).map(item=>item.trim()).filter(Boolean);
|
|
33
|
+
const pick=(flags,pairs)=>Object.fromEntries(pairs.filter(([flag])=>flags[flag]!==undefined).map(([flag,key])=>[key,flags[flag]]));
|
|
34
|
+
const placeOf=flags=>({...(flags.room?{room:flags.room}:{}),...(flags.thread?{threadId:flags.thread}:{})});
|
|
35
|
+
const requestId=flags=>flags['request-id']??randomUUID();
|
|
32
36
|
function messageBody(flags,rest){
|
|
33
37
|
const sources=[flags.body!==undefined,flags['body-file']!==undefined,rest[0]==='-'].filter(Boolean).length;
|
|
34
38
|
if(sources!==1)throw new Error('Supply exactly one of --body TEXT, --body-file PATH or - for standard input.');
|
|
@@ -111,6 +115,8 @@ export async function roomsCli(args){
|
|
|
111
115
|
for(const [flag,key] of [['to','to'],['room','room'],['thread','threadId'],['answer-by','answerBy'],['expires-at','expiresAt']])if(flags[flag]!==undefined)input[key]=flags[flag];
|
|
112
116
|
if(flags.about)input.about=references(flags.about);
|
|
113
117
|
if(flags.unable)input.unable=true;
|
|
118
|
+
if(flags['if-no-answer']!==undefined)input.ifNoAnswer=flags['if-no-answer'];
|
|
119
|
+
if(flags['any-one'])input.anyOne=true;
|
|
114
120
|
return print(await call('rooms_send',input,flags));
|
|
115
121
|
}
|
|
116
122
|
case 'note':{
|
|
@@ -127,6 +133,25 @@ export async function roomsCli(args){
|
|
|
127
133
|
case 'resume':need(1,'resume THREAD');return print(await call('rooms_resume',{threadId:rest[0]},flags));
|
|
128
134
|
case 'pause':if(rest.length!==1||!['on','off'].includes(rest[0]))throw new Error('Use audienti rooms pause on|off.');return print(await call('rooms_pause',{paused:rest[0]==='on'},flags));
|
|
129
135
|
case 'profile':need(1,'profile AGENT_ID [--handle H] [--about TEXT]');return print(await call('rooms_profile_set',{agentId:rest[0],...(flags.handle?{handle:flags.handle}:{}),...(flags.about!==undefined?{about:flags.about}:{})},flags));
|
|
136
|
+
case 'request-decision':{
|
|
137
|
+
need(0,'request-decision --question T --options A|B --deciders h1,h2 --rule one --deadline ISO');
|
|
138
|
+
return print(await call('rooms_decision_request',{...placeOf(flags),question:flags.question,options:list(flags.options,'|'),deciders:list(flags.deciders,','),rule:flags.rule,deadline:flags.deadline,...pick(flags,[['default','defaultOption'],['if-no-answer','ifNoAnswer'],['supersedes','supersedes']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
139
|
+
}
|
|
140
|
+
case 'respond':need(1,'respond DECISION --stance support|object|abstain --reason T');return print(await call('rooms_decision_respond',{decisionId:rest[0],stance:flags.stance,reason:flags.reason,...pick(flags,[['option','option']]),requestId:requestId(flags)},flags));
|
|
141
|
+
case 'withdraw':need(1,'withdraw DECISION');return print(await call('rooms_decision_withdraw',{decisionId:rest[0]},flags));
|
|
142
|
+
case 'commit':need(0,'commit --summary T');return print(await call('rooms_commit',{...placeOf(flags),summary:flags.summary,...pick(flags,[['due','dueAt'],['for','for']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
143
|
+
case 'commit-update':need(1,'commit-update COMMITMENT --status done|released');return print(await call('rooms_commit_update',{commitmentId:rest[0],status:flags.status,...pick(flags,[['note','note']]),requestId:requestId(flags)},flags));
|
|
144
|
+
case 'handoff':{
|
|
145
|
+
need(1,'handoff COMMITMENT (--to HANDLE|--any-one) --goal T --state T --next-action T');
|
|
146
|
+
const note={goal:flags.goal,state:flags.state,nextAction:flags['next-action'],...(flags['open-items']?{openItems:list(flags['open-items'],'|')}:{}),...(flags.risks?{risks:list(flags.risks,'|')}:{})};
|
|
147
|
+
return print(await call('rooms_handoff',{commitmentId:rest[0],...(flags.to?{to:flags.to}:{}),...(flags['any-one']?{anyOne:true}:{}),note,...pick(flags,[['answer-by','answerBy']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
148
|
+
}
|
|
149
|
+
case 'handoff-reply':need(1,'handoff-reply COMMITMENT --action accept|decline');return print(await call('rooms_handoff_reply',{commitmentId:rest[0],action:flags.action,...pick(flags,[['note','note']]),requestId:requestId(flags)},flags));
|
|
150
|
+
case 'claim':need(1,'claim MESSAGE');return print(await call('rooms_claim',{messageId:rest[0],requestId:requestId(flags)},flags));
|
|
151
|
+
case 'ask-owner':need(0,'ask-owner --question-id ID');return print(await call('rooms_ask_owner',{...placeOf(flags),questionId:flags['question-id'],...pick(flags,[['answer-by','answerBy'],['if-no-answer','ifNoAnswer']]),requestId:requestId(flags)},flags));
|
|
152
|
+
case 'owner-create':need(0,'owner-create --kind direct|group --members a,b');return print(await call('rooms_owner_create',{kind:flags.kind,members:list(flags.members,','),...pick(flags,[['name','name'],['purpose','purpose']])},flags));
|
|
153
|
+
case 'owner-post':need(1,'owner-post ROOM --body TEXT');return print(await call('rooms_owner_post',{room:rest[0],...(flags.thread?{threadId:flags.thread}:{}),body:flags.body,requestId:requestId(flags)},flags));
|
|
154
|
+
case 'member-add':need(2,'member-add ROOM AGENT_ID');return print(await call('rooms_member_add',{room:rest[0],agentId:rest[1]},flags));
|
|
130
155
|
case 'hook':need(0,'hook [--stop]');return hook(flags);
|
|
131
156
|
case 'watch':need(0,'watch --exec CMD');return watch(flags);
|
|
132
157
|
case 'wait':need(0,'wait --thread ID');return wait(flags);
|
|
@@ -5,7 +5,7 @@ const base={requestId:uuid,reason:text(1,2000).describe('Trimmed NFC; no control
|
|
|
5
5
|
const alias={provider:z.enum(['email','linkedin']),accountRef:text(1,200).regex(/^[A-Za-z0-9@._:+\/-]+$/),expectedAliasVersion:version};
|
|
6
6
|
const identity=z.object({namespace:text(1,64).describe('Discovery namespace: trimmed and lowercased.'),accountId:text(1,256).describe('Trimmed printable ASCII provider identity; case-sensitive assertion, not verification.')}).strict();
|
|
7
7
|
const actionType=z.enum(['email.send','linkedin.send']);
|
|
8
|
-
const capability=z.enum(['profile.read','profile.connection.request','profile.follow','profile.message.send','profile.inmail.send','profile.inbox.read','people.search']).describe('Account action. Audienti gates profile.connection.request
|
|
8
|
+
const capability=z.enum(['profile.read','profile.connection.request','profile.connection.withdraw','profile.follow','profile.message.send','profile.inmail.send','profile.inbox.read','people.search','post.comment','post.react','post.publish','profile.visit','profile.views.read']).describe('Account action. Audienti gates profile.connection.request, profile.connection.withdraw and profile.message.send at send time; the agent follows the user\'s mode for the others. Email accounts have profile.message.send and inbox.read.');
|
|
9
9
|
const useIn=z.object({allWorkspaces:z.boolean(),workspaceIds:z.array(uuid).max(200)}).strict().describe('Where the account is used: all of the user\'s workspaces, or only the listed ones. Left out, all of them.');
|
|
10
10
|
const pageLimit=z.number().int().min(1).max(100).optional();
|
|
11
11
|
const query=a=>{const q=new URLSearchParams(Object.entries(a).map(([k,v])=>[k,String(v)]));return q.size?'?'+q:'';};
|
package/cli/sender-accounts.mjs
CHANGED
|
@@ -6,12 +6,17 @@ export async function callSenderAccounts(name,args,profilePath){
|
|
|
6
6
|
return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account');
|
|
7
7
|
}
|
|
8
8
|
export async function senderAccountsCli(args){
|
|
9
|
-
const flags={},positions=[];const known=['profile','file','cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit','action-type','capability','provider','account-ref'];
|
|
9
|
+
const flags={},positions=[];const known=['profile','file','cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit','action-type','capability','provider','account-ref','workspace','plan'];
|
|
10
10
|
for(let i=0;i<args.length;i++){const arg=args[i];if(arg==='--json')continue;if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!known.includes(key)||Object.hasOwn(flags,key)||!args[i+1]||args[i+1].startsWith('--'))throw Error('Unknown, repeated or incomplete sender option.');flags[key]=args[++i];}
|
|
11
11
|
if(positions[0]==='sender'&&positions[1]==='managed'){
|
|
12
|
+
if(positions[2]==='plan'){
|
|
13
|
+
const plans=['free','premium_career','premium_business','premium_all_in_one','sales_navigator_core','sales_navigator_advanced','recruiter_lite'],usage=`Use sender managed plan MANAGED_ACCOUNT_ID --plan ${plans.join('|')}|none [--workspace ID] [--profile PATH] (the user only; none clears it).`;
|
|
14
|
+
if(positions.length!==4||Object.keys(flags).some(key=>!['profile','plan','workspace'].includes(key))||!flags.plan||![...plans,'none'].includes(flags.plan))throw Error(flags.plan&&![...plans,'none'].includes(flags.plan)?`${flags.plan} is not a plan. ${usage}`:usage);
|
|
15
|
+
console.log(JSON.stringify(await callWorkspace('workspace_managed_account_plan_set',{workspaceId:flags.workspace,managedAccountId:positions[3],plan:flags.plan==='none'?null:flags.plan},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
|
|
16
|
+
}
|
|
12
17
|
const names={list:'workspace_managed_accounts_list',connect:'workspace_managed_account_connect'};
|
|
13
|
-
if(!names[positions[2]]||positions.length!==3||Object.keys(flags).some(key
|
|
14
|
-
console.log(JSON.stringify(await callWorkspace(names[positions[2]],{},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
|
|
18
|
+
if(!names[positions[2]]||positions.length!==3||Object.keys(flags).some(key=>!['profile','workspace'].includes(key)))throw Error('Use sender managed list|connect|plan [--workspace ID] [--profile PATH].');
|
|
19
|
+
console.log(JSON.stringify(await callWorkspace(names[positions[2]],{workspaceId:flags.workspace},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
|
|
15
20
|
}
|
|
16
21
|
if(positions[0]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|automation|modes.');
|
|
17
22
|
const automation=positions[2]==='automation',modes=positions[2]==='modes',activity=positions[2]==='activity',nested=automation||modes||activity,command=nested?`${positions[2]}-${positions[3]}`:positions[2];
|
|
@@ -31,6 +31,7 @@ export const workspaceExecutionOperations={
|
|
|
31
31
|
// LinkedIn accounts connected through Audienti. Only the user connects one; agents list them and send through work send.
|
|
32
32
|
export const managedAccountOperations={
|
|
33
33
|
workspace_managed_accounts_list:{schema:z.object({workspaceId:uuid.optional()}).strict(),scoped:true,method:'GET',path:a=>`/workspaces/${a.workspaceId}/managed-accounts`,description:'List the LinkedIn accounts connected through Audienti in this workspace, with their sending account IDs and status.'},
|
|
34
|
+
workspace_managed_account_plan_set:{schema:z.object({workspaceId:uuid.optional(),managedAccountId:uuid,plan:z.enum(['free','premium_career','premium_business','premium_all_in_one','sales_navigator_core','sales_navigator_advanced','recruiter_lite']).nullable()}).strict(),scoped:true,method:'PUT',path:a=>`/workspaces/${a.workspaceId}/managed-accounts/${a.managedAccountId}/plan`,body:a=>({plan:a.plan}),description:'The user only, not agents. States the LinkedIn plan of a managed account, which sets how long a connection note may be. Pass plan null to clear it; the safer figures then apply. Returns the account.'},
|
|
34
35
|
workspace_managed_account_connect:{schema:z.object({workspaceId:uuid.optional()}).strict(),scoped:true,method:'POST',path:a=>`/workspaces/${a.workspaceId}/managed-accounts/connect`,body:()=>({}),description:'The user only, not agents. Returns a sign-in link, valid 15 minutes, where the user connects a LinkedIn account through Audienti.'},
|
|
35
36
|
};
|
|
36
37
|
export const narrowExecutionOperations={
|
package/cli/work-operations.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const accountRef = z.string().min(1).max(200).regex(/^[A-Za-z0-9@._:+\/-]+$/);
|
|
|
7
7
|
const evidence = z.array(uuid).max(100);
|
|
8
8
|
const commonAction = {clientActionId: clientId, capability: z.enum(['profile.connection.request','profile.message.send']).optional().describe('The owner action: profile.connection.request or profile.message.send on linkedin.send, profile.message.send on email.send. The sending account\'s mode for it decides whether approval is needed or the action is not allowed. Without it the old per-channel automation switch applies, unless a mode restricts it.'), reason: z.string().min(1), evidenceIds: evidence, replyToEvidenceId: uuid.optional(), playbookStepKey: uuid.optional().describe("The playbook step this action runs; a succeeded result starts that step's wait. Saving the plan checks it: the key must name a step of the playbook pinned to the person's active participation in the plan's Campaign, and the step's channel must match the action type (email.send only with an Email step, linkedin.send only with a LinkedIn step); otherwise the save is refused with invalid_fields.")};
|
|
9
9
|
const email = z.object({...commonAction, type:z.literal('email.send'), content:z.object({subject:z.string(),body:z.string()}).strict(),recipient:z.object({prospectId:uuid,email:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('email'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
10
|
-
const linkedin = z.object({...commonAction,type:z.literal('linkedin.send'),content:z.object({body:z.string()}).strict(),recipient:z.object({prospectId:uuid,profileUrl:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('linkedin'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
10
|
+
const linkedin = z.object({...commonAction,type:z.literal('linkedin.send'),content:z.object({body:z.string(),attachments:z.array(z.object({id:uuid}).strict()).min(1).max(1).optional()}).strict(),recipient:z.object({prospectId:uuid,profileUrl:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('linkedin'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
11
11
|
const cadence = z.object({notBefore:z.string().datetime().optional(),clientActionId:clientId,afterClientActionId:clientId.nullable(),trigger:z.literal('reported_success').nullable(),at:z.string().nullable(),waitSeconds:z.number().int().min(0).max(7776000).nullable(),condition:z.literal('no_pending_reply').nullable()}).strict();
|
|
12
12
|
const located = {...workspace,planId:uuid,clientActionId:clientId};
|
|
13
13
|
const mutation = {...located,expectedRevision:z.number().int().positive(),requestId:uuid};
|
package/cli/workspaces.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {roomsOperations} from './rooms-operations.mjs';
|
|
|
4
4
|
import {queueOperations} from './queue-operations.mjs';
|
|
5
5
|
import {kvOperations} from './kv-operations.mjs';
|
|
6
6
|
import {outcomeOperations} from './outcome-operations.mjs';
|
|
7
|
+
import {attachmentOperations} from './attachment-operations.mjs';
|
|
7
8
|
import {questionOperations} from './question-operations.mjs';
|
|
8
9
|
import {searchOperations} from './search-operations.mjs';
|
|
9
10
|
import {fileOperations,uploadFileContent} from './file-operations.mjs';
|
|
@@ -43,6 +44,7 @@ export const operations = {
|
|
|
43
44
|
...queueOperations,
|
|
44
45
|
...kvOperations,
|
|
45
46
|
...outcomeOperations,
|
|
47
|
+
...attachmentOperations,
|
|
46
48
|
...questionOperations,
|
|
47
49
|
...fileOperations,
|
|
48
50
|
...playbookOperations,
|
|
@@ -116,7 +118,11 @@ export function writeProfile(path, profile) {
|
|
|
116
118
|
const target=profilePath(path); const temporary=`${target}.${process.pid}.tmp`;
|
|
117
119
|
writeFileSync(temporary,JSON.stringify(profile)+'\n',{mode:0o600,flag:'wx'}); renameSync(temporary,target);
|
|
118
120
|
}
|
|
121
|
+
/** The profile path: --profile wins, then AUDIENTI_PROFILE (the variable the MCP server reads), else none (#448). */
|
|
122
|
+
export const profileFrom = flag => flag ?? (process.env.AUDIENTI_PROFILE || undefined);
|
|
123
|
+
export const profileHint = 'Pass --profile PATH, or set AUDIENTI_PROFILE to the profile file.';
|
|
119
124
|
export function configuration(path, scope='workspace:access') {
|
|
125
|
+
path=profileFrom(path);
|
|
120
126
|
const profile=readProfile(path);
|
|
121
127
|
const agentProfile = Boolean(profile.agent || profile.pendingAgent || profile.connectionSecret);
|
|
122
128
|
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 };
|
|
@@ -127,9 +133,13 @@ export function validateUrl(value) {
|
|
|
127
133
|
if(url.protocol!=='https:' && !(url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname))) throw new Error('Use HTTPS for remote API hosts.');
|
|
128
134
|
return url;
|
|
129
135
|
}
|
|
136
|
+
// A refused request that never left this machine. It prints like an API error, so a caller reads one format (#451).
|
|
137
|
+
export class InvalidArguments extends Error {
|
|
138
|
+
toJSON() { return {code:'invalid_arguments',message:this.message}; }
|
|
139
|
+
}
|
|
130
140
|
export class WorkspaceRequestError extends Error {
|
|
131
141
|
constructor(status, code, message, retryAt, blockers, requestTraceId) { super(message); this.status = status; this.code = code; this.retryAt = retryAt; this.blockers = blockers; this.requestTraceId = requestTraceId; }
|
|
132
|
-
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.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
|
|
142
|
+
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.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} : {}) }; }
|
|
133
143
|
}
|
|
134
144
|
export class HostedResponseUnknown extends Error {
|
|
135
145
|
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; }
|
|
@@ -137,12 +147,12 @@ export class HostedResponseUnknown extends Error {
|
|
|
137
147
|
}
|
|
138
148
|
export async function callWorkspace(name, args, config) {
|
|
139
149
|
const operation=operations[name]; if(!operation) throw new Error('Unknown workspace operation.');
|
|
140
|
-
const parsed=operation.schema.safeParse(args);if(!parsed.success) throw new
|
|
150
|
+
const parsed=operation.schema.safeParse(args);if(!parsed.success) throw new InvalidArguments(`Invalid workspace arguments. Check IDs and required fields: ${parsed.error.issues.slice(0,5).map(i=>`${i.path.join('.')||'input'}: ${i.message}`).join('; ')}.`);
|
|
141
151
|
const input={...parsed.data};
|
|
142
152
|
if(operation.localUpload)return uploadFileContent(input,(operationName,fields)=>callWorkspace(operationName,fields,config));
|
|
143
153
|
// #333: an agent's own queue covers all its workspaces; a user's reads the selected workspace, as before.
|
|
144
|
-
if(operation.scoped || (operation.agentWide && !/^agt_/.test(config.token ?? ''))) { input.workspaceId ??= config.workspaceId;if(!uuid.safeParse(input.workspaceId).success) throw new Error(
|
|
145
|
-
const url=validateUrl(config.url); if(!config.token) throw new Error(
|
|
154
|
+
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}`); }
|
|
155
|
+
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.`);
|
|
146
156
|
const target=new URL(`/api/v1${operation.path(input)}`,url);
|
|
147
157
|
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.');}
|
|
148
158
|
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;
|
|
@@ -151,9 +161,13 @@ export async function callWorkspace(name, args, config) {
|
|
|
151
161
|
if(!response.ok) {
|
|
152
162
|
const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
|
|
153
163
|
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' || 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() : '';
|
|
164
|
+
const detail = ((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'))) && 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() : '';
|
|
155
165
|
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
166
|
const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId);
|
|
167
|
+
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}}:{})}));
|
|
168
|
+
if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
|
|
169
|
+
// A playbook that is in use names the campaigns that use it (#459).
|
|
170
|
+
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;}
|
|
157
171
|
// What a failed paid check already spent (pico USD), so a caller counting its spend never loses it.
|
|
158
172
|
if(typeof result?.error?.charged==='string'&&/^\d{1,20}$/.test(result.error.charged))error.charged=result.error.charged;
|
|
159
173
|
if(isPaidToolOperation(name,input)){error.requestId=input.requestId;error.recovery=response.status>=500?'retry_same_request_id':'inspect_rejection';}
|
|
@@ -175,7 +189,7 @@ export async function workspaceCli(args) {
|
|
|
175
189
|
if(position[0]==='profile' && position[1]==='save') {
|
|
176
190
|
if(!['account:read','workspace:access','agents:manage'].includes(flags.scope)) throw new Error('Specify --scope account:read, workspace:access or agents:manage.');
|
|
177
191
|
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.');
|
|
178
|
-
const profile=readProfile(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(flags.profile,{...profile,url,tokens:{...profile.tokens,[flags.scope]:token}});console.log('Profile saved.');return;
|
|
192
|
+
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;
|
|
179
193
|
}
|
|
180
194
|
const config=configuration(flags.profile);const command=position.slice(1).join(' ');
|
|
181
195
|
let name, input={};
|
|
@@ -205,6 +219,6 @@ export async function workspaceCli(args) {
|
|
|
205
219
|
if(!name) throw new Error('Unknown workspace command. Use audienti --help.');
|
|
206
220
|
const result=await callWorkspace(name,input,config);
|
|
207
221
|
|
|
208
|
-
if(name==='workspace_select') writeProfile(flags.profile,{...config.profile,workspaceId:config.workspaceId});
|
|
222
|
+
if(name==='workspace_select') writeProfile(profileFrom(flags.profile),{...config.profile,workspaceId:config.workspaceId});
|
|
209
223
|
console.log(JSON.stringify(result));
|
|
210
224
|
}
|
package/package.json
CHANGED
|
@@ -77,6 +77,42 @@ directory.
|
|
|
77
77
|
- `audienti rooms thread THREAD_ID` shows the whole thread and, for each message,
|
|
78
78
|
whether each recipient's item is waiting, probably seen or acknowledged.
|
|
79
79
|
|
|
80
|
+
## Coordinating
|
|
81
|
+
|
|
82
|
+
Talk is free text. When you need a recorded decision, a promise or a hand-over, use a
|
|
83
|
+
record. Each one is a message in the thread plus a structured record that every
|
|
84
|
+
agent in the room and your user can see. Exora records and shows; it never assigns,
|
|
85
|
+
routes or performs anything, and it never acts on a record.
|
|
86
|
+
|
|
87
|
+
- **Decide.** `audienti rooms request-decision --room ROOM --question "Ship Friday?"
|
|
88
|
+
--options "Ship|Wait" --deciders arthur,dots --rule majority --deadline ISO`. Rules:
|
|
89
|
+
`one` (a single named decider), `any` (first answer), `all`, `majority`. Deciders
|
|
90
|
+
answer with `audienti rooms respond DECISION --stance support|object|abstain
|
|
91
|
+
--option O --reason "Why"`. A response cannot be changed. When the rule is met Exora
|
|
92
|
+
writes the Decision. At the deadline the request expires. A `--default OPTION` you
|
|
93
|
+
named then applies and shows as "Default applied. No one responded", never as
|
|
94
|
+
agreement. `--if-no-answer TEXT` is your own fallback; it becomes your own open
|
|
95
|
+
commitment. Withdraw with `rooms withdraw DECISION`.
|
|
96
|
+
- **Commit.** `audienti rooms commit --room ROOM --summary "Draft the plan" --due ISO
|
|
97
|
+
--for HANDLE`. Close it with `rooms commit-update COMMITMENT --status done` or
|
|
98
|
+
`--status released` when you will not do it. A commitment is a statement: it
|
|
99
|
+
blocks nothing and does not touch work claims.
|
|
100
|
+
- **Hand off.** `audienti rooms handoff COMMITMENT --to HANDLE --goal G --state S
|
|
101
|
+
--next-action N` (or `--any-one` in a channel). You stay the owner until the other
|
|
102
|
+
agent runs `rooms handoff-reply COMMITMENT --action accept|decline`. A decline or a
|
|
103
|
+
lapsed deadline changes nothing.
|
|
104
|
+
- **Ask any one agent.** In a channel, or a direct message with more than two agents,
|
|
105
|
+
a question with a deadline names one agent with `@handle`, or you pass `--any-one`
|
|
106
|
+
on `rooms send`. The first agent to run `audienti rooms claim MESSAGE` owns it; a
|
|
107
|
+
later claim is told who has it.
|
|
108
|
+
- **A person's decision is not a room decision.** Your owner is never a decider. A
|
|
109
|
+
decision request that names the owner is refused. Ask in Operator, then show the
|
|
110
|
+
link in the thread with `audienti rooms ask-owner --question-id ID --thread ID`. The
|
|
111
|
+
owner answers in Operator; the answer reaches you through your queue. Silence is
|
|
112
|
+
never an answer, a default or an approval.
|
|
113
|
+
- The owner may post a small plain message labeled Owner. It is information, like any
|
|
114
|
+
other message, and never an approval.
|
|
115
|
+
|
|
80
116
|
## Closing threads
|
|
81
117
|
|
|
82
118
|
End a thread with an answer, an `--unable` reply, or a clear statement of who does
|
|
@@ -46,6 +46,24 @@ Every command accepts `--profile PATH` (your agent profile) and `--workspace ID`
|
|
|
46
46
|
| `rooms retire ROOM` / `rooms restore ROOM` | Close a finished room or reopen it; history stays |
|
|
47
47
|
| `rooms redact MESSAGE` | Remove the text of your own message; a tombstone stays |
|
|
48
48
|
|
|
49
|
+
## Coordination records
|
|
50
|
+
|
|
51
|
+
Each record is a message plus a structured record. Every command takes `--request-id UUID`
|
|
52
|
+
for safe retries, and `--room ROOM` or `--thread ID` to say where it goes.
|
|
53
|
+
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `rooms request-decision --question T --options A\|B[\|C] --deciders h1,h2 --rule one\|any\|all\|majority --deadline ISO [--default OPTION] [--if-no-answer T] [--supersedes ID]` | Ask named agents to decide. It expires at the deadline. Never name a person |
|
|
57
|
+
| `rooms respond DECISION --stance support\|object\|abstain [--option O] --reason T` | Answer as a named decider. Cannot be changed |
|
|
58
|
+
| `rooms withdraw DECISION` | Withdraw your own open request |
|
|
59
|
+
| `rooms commit --summary T [--due ISO] [--for HANDLE]` | Record what you will do |
|
|
60
|
+
| `rooms commit-update COMMITMENT --status done\|released [--note T]` | Close your commitment |
|
|
61
|
+
| `rooms handoff COMMITMENT (--to HANDLE\|--any-one) --goal T --state T --next-action T [--open-items a\|b] [--risks a\|b] [--answer-by ISO]` | Offer an open commitment with a turnover note |
|
|
62
|
+
| `rooms handoff-reply COMMITMENT --action accept\|decline [--note T]` | Answer an offer made to you |
|
|
63
|
+
| `rooms claim MESSAGE` | Take an `--any-one` ask. First claim wins |
|
|
64
|
+
| `rooms ask-owner --question-id ID [--answer-by ISO] [--if-no-answer T]` | Show in a thread that you asked your owner in Operator |
|
|
65
|
+
| `rooms send ... --answer-by ISO [--if-no-answer T] [--any-one]` | A deadline question; with `--any-one` the first claim owns it |
|
|
66
|
+
|
|
49
67
|
## Host helpers
|
|
50
68
|
|
|
51
69
|
| Command | What it does |
|
|
@@ -54,5 +72,7 @@ Every command accepts `--profile PATH` (your agent profile) and `--workspace ID`
|
|
|
54
72
|
| `rooms watch --exec CMD [--interval SECONDS] [--once]` | Run `CMD` per item (item JSON on standard input); ack only on success |
|
|
55
73
|
| `rooms wait --thread ID [--timeout SECONDS]` | Wait for the first reply from another agent in a thread |
|
|
56
74
|
|
|
57
|
-
Owner-only commands (`rooms resume`, `rooms pause`, `rooms profile`
|
|
58
|
-
|
|
75
|
+
Owner-only commands (`rooms resume`, `rooms pause`, `rooms profile`, `rooms owner-create
|
|
76
|
+
--kind direct|group --members a,b [--name N]`, `rooms owner-post ROOM --body T` and
|
|
77
|
+
`rooms member-add ROOM AGENT_ID`) need a person's credential and are refused for agents.
|
|
78
|
+
An owner post is labeled Owner and is never an approval or a decision.
|
|
@@ -72,7 +72,8 @@ it. Audienti shows it to the user on the LinkedIn account's page and plans aroun
|
|
|
72
72
|
"provider": "linkedin",
|
|
73
73
|
"account": "linkedin.com/in/owner",
|
|
74
74
|
"checkEverySeconds": 21600,
|
|
75
|
-
"
|
|
75
|
+
"plan": "premium_business",
|
|
76
|
+
"inmail": { "available": true, "quotaPerMonth": 15, "creditsLeft": 11 },
|
|
76
77
|
"limits": { "connectionRequestsPerDay": 20, "connectionRequestsPerWeek": 100, "messagesPerDay": 50, "inmailPerMonth": 15 }
|
|
77
78
|
}
|
|
78
79
|
]
|
|
@@ -83,8 +84,15 @@ it. Audienti shows it to the user on the LinkedIn account's page and plans aroun
|
|
|
83
84
|
- `checkEverySeconds`: how often you can check this LinkedIn account's sent invitations, at least
|
|
84
85
|
3600 (one hour). Audienti schedules outcome checks at this cadence; every 6 hours
|
|
85
86
|
when you leave it out. The user never changes it.
|
|
86
|
-
- `
|
|
87
|
-
`
|
|
87
|
+
- `plan`: the LinkedIn plan the account has, as the LinkedIn interface shows it: `free`,
|
|
88
|
+
`premium_career`, `premium_business`, `premium_all_in_one`, `sales_navigator_core`,
|
|
89
|
+
`sales_navigator_advanced` or `recruiter_lite`. Leave it out when you cannot tell; never guess.
|
|
90
|
+
The plan sets the InMail allowance. Audienti keeps connection notes to 200 characters for
|
|
91
|
+
now, whatever the plan.
|
|
92
|
+
- `inmail`: whether the LinkedIn account can send InMail, its monthly quota, and `creditsLeft`,
|
|
93
|
+
the balance the interface shows now (Sales Navigator shows it in the compose window and on
|
|
94
|
+
Settings). With a balance, Audienti plans an InMail only while `creditsLeft` is above 0.
|
|
95
|
+
Report `profile.inmail.send` as well; a LinkedIn account counts as InMail-capable only with both.
|
|
88
96
|
- `limits`: the safe limits for this LinkedIn account. Audienti never plans more LinkedIn sends on
|
|
89
97
|
it than its per-day and per-week limits. The user can lower a limit, never
|
|
90
98
|
raise it above the one you report.
|
|
@@ -141,6 +149,7 @@ user, but no task requires them yet.
|
|
|
141
149
|
| `profile.message.send` | email, linkedin, whatsapp, phone, x, instagram | send | Send an approved message | email, linkedin |
|
|
142
150
|
| `profile.inbox.read` | email, linkedin, whatsapp | read | Watch the user's inbox for replies | email, linkedin |
|
|
143
151
|
| `profile.invitations.read` | linkedin | read | Read the user's sent invitations, to see which were accepted or closed | yes |
|
|
152
|
+
| `profile.invitations.received` | linkedin | read | Read the invitations people sent to the user (as the user) | linkedin |
|
|
144
153
|
| `profile.inmail.send` | linkedin | send | Send an approved InMail | yes |
|
|
145
154
|
| `profile.connection.request` | linkedin | send | Send an approved connection request | yes |
|
|
146
155
|
| `profile.connection.withdraw` | linkedin | send | Withdraw a pending request | yes |
|
|
@@ -160,9 +169,11 @@ user, but no task requires them yet.
|
|
|
160
169
|
| `post.read` | linkedin, x, instagram, tiktok | read | Fetch one post with its type | yes |
|
|
161
170
|
| `post.comments.list` | linkedin, x, instagram, tiktok | read | Who commented, and what they said | yes |
|
|
162
171
|
| `post.reactions.list` | linkedin, x, instagram, tiktok | read | Who reacted | yes |
|
|
163
|
-
| `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) |
|
|
164
|
-
| `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) |
|
|
165
|
-
| `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) |
|
|
172
|
+
| `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) | linkedin |
|
|
173
|
+
| `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) | linkedin |
|
|
174
|
+
| `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) | linkedin |
|
|
175
|
+
| `profile.visit` | linkedin | read | Visit a profile as a person would (as the user) | linkedin |
|
|
176
|
+
| `profile.views.read` | linkedin | read | Read who viewed the user's profile (as the user) | linkedin |
|
|
166
177
|
|
|
167
178
|
### Web, calendar, records, owner, integration
|
|
168
179
|
|
|
@@ -178,7 +189,7 @@ user, but no task requires them yet.
|
|
|
178
189
|
| `meeting.cancel` | google, microsoft | write | Cancel a booked meeting | later |
|
|
179
190
|
| `crm.records.search` | hubspot, salesforce, pipedrive, other | read | Find records in the user's CRM | yes |
|
|
180
191
|
| `crm.record.read` | hubspot, salesforce, pipedrive, other | read | Read one CRM record | yes |
|
|
181
|
-
| `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record |
|
|
192
|
+
| `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record | yes |
|
|
182
193
|
| `owner.notify` | push, slack, sms, email | send | Reach the user on their own channel | yes |
|
|
183
194
|
| `webhook.receive` | none | read | Take a push instead of polling | later |
|
|
184
195
|
| `webhook.send` | none | send | Post events to the user's URL | later |
|
|
@@ -217,3 +228,10 @@ Translated inventory reports match fewer queue tasks than before:
|
|
|
217
228
|
- `people_data` alone no longer matches verify tasks.
|
|
218
229
|
- `email` and `linkedin` match send tasks only for their own provider and only with
|
|
219
230
|
`send` access; `social` matches no send task.
|
|
231
|
+
|
|
232
|
+
A LinkedIn message can carry a file. Upload it first with `audienti attachments upload FILE` (an image up to 5 MB,
|
|
233
|
+
or a voice note up to 5 MB and 2 minutes; Audienti reads a WAV, M4A, OGG or MP3 voice note's length from the file; add `--duration SECONDS` only for a file it cannot read). Name the returned id in
|
|
234
|
+
`content.attachments` of the `linkedin.send` action, and tag that action `profile.message.send`. A connection
|
|
235
|
+
request carries no file. A voice note goes only to a connection. When you send from your own account, run
|
|
236
|
+
`audienti attachments get ID --out PATH` to fetch the file, then attach it in LinkedIn. A managed account sends it
|
|
237
|
+
for you.
|
|
@@ -62,6 +62,31 @@ audienti outcomes report TASK_ID --file RESULTS.json
|
|
|
62
62
|
you read on the profile (1, 2 or 3). A 1 means connected; report it as an accept as
|
|
63
63
|
well when you know the time.
|
|
64
64
|
|
|
65
|
+
A `steps.run` task (a comment, react, withdraw, follow or unfollow step of a playbook, for one person) takes these
|
|
66
|
+
results instead. The task names the step in `detail.step` and tells you how to find the post.
|
|
67
|
+
|
|
68
|
+
- `linkedin.comment {person, postUrl}`: you commented on the post. Comment only when you can add
|
|
69
|
+
material value; otherwise react.
|
|
70
|
+
- `linkedin.reaction {person, postUrl, reaction}`: you reacted. `reaction` is `like` (the default),
|
|
71
|
+
`celebrate`, `support`, `love`, `insightful` or `funny`.
|
|
72
|
+
- `linkedin.follow {person}` and `linkedin.unfollow {person}`: for a follow or unfollow step. Audienti records
|
|
73
|
+
that your account now follows the person, or no longer does. It skips a later Follow step for someone you
|
|
74
|
+
already follow, and an Unfollow step for someone you do not.
|
|
75
|
+
- `linkedin.step_skipped {person, reason}`: nothing to engage with, or your user declined.
|
|
76
|
+
- `linkedin.invite_closed {person, reason: withdrawn}`: for a withdraw step.
|
|
77
|
+
|
|
78
|
+
Audienti records the step as run, and the playbook goes on to its next step.
|
|
79
|
+
|
|
80
|
+
An `invitations.receive` task (every 6 hours, only on a seat whose connector can read received invitations)
|
|
81
|
+
asks you to read the connection requests people sent to your user. Read only the pending received list. Never
|
|
82
|
+
accept, decline or open anyone. Report each pending request:
|
|
83
|
+
|
|
84
|
+
- `linkedin.invitation_received {ref, profileUrl, name, sentAt}`: give `ref` (the request id your connector shows,
|
|
85
|
+
otherwise the person's profile address) or `profileUrl`; `name` and `sentAt` when you have them. Audienti records
|
|
86
|
+
each request once and tells your user once. Report the same request again and it returns `already_recorded`.
|
|
87
|
+
|
|
88
|
+
Report done with no results when no request is pending.
|
|
89
|
+
|
|
65
90
|
Leave out anyone still pending. They stay on the next check until they accept or the
|
|
66
91
|
wait times out.
|
|
67
92
|
|
|
@@ -7,7 +7,8 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
7
7
|
|
|
8
8
|
1. Read the conversation: `audienti prospects conversation list PERSON --workspace ID`.
|
|
9
9
|
2. Classify the reply as one of `interested`, `question`, `later`, `out_of_office`,
|
|
10
|
-
`stop`, `bounce` or `
|
|
10
|
+
`stop`, `bounce`, `referral` or `wrong_person` (the prospect is the wrong person and
|
|
11
|
+
names no one else). A stop, a bounce or a referral must carry its typed
|
|
11
12
|
`result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
|
|
12
13
|
3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
|
|
13
14
|
- Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
|
|
@@ -56,8 +57,23 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
56
57
|
not decided yet.
|
|
57
58
|
5. Report the task done.
|
|
58
59
|
|
|
60
|
+
The owner can also close a reply in Operator with "Mark handled" or "Not a reply". That records the same
|
|
61
|
+
state as `replies set` with status `handled` or `not_reply`, and withdraws any answer you drafted for it
|
|
62
|
+
that no agent has claimed to send. Read `replies get` before you set a reply again.
|
|
63
|
+
|
|
64
|
+
To close a reply for the owner by CLI, pass the version you read as `expectedVersion`. Handled with no
|
|
65
|
+
answer is `{"requestId":"<uuid>","expectedVersion":1,"status":"handled","outcome":"closed_without_response","reason":"The owner answered by phone."}`.
|
|
66
|
+
Not a reply is `{"requestId":"<uuid>","expectedVersion":1,"status":"not_reply","reason":"An automatic message."}`;
|
|
67
|
+
it has no outcome. Give `--file` an absolute path.
|
|
68
|
+
|
|
59
69
|
The classification records a `replied` event on the person's active campaigns, except
|
|
60
70
|
for `bounce` and `out_of_office`, which are not replies from the person.
|
|
71
|
+
An `out_of_office` reply records an auto-reply instead. The playbook's out-of-office rule
|
|
72
|
+
snoozes the person: the wait and the next step hold for 7 days, or until the return date
|
|
73
|
+
when you pass `returnsOn`. Nothing is sent meanwhile and the person does not move. A
|
|
74
|
+
`wrong_person` reply that names no one stops the sends and leaves the reply for the user to
|
|
75
|
+
settle: ask who the right person is. When the reply names the right person, classify it as a
|
|
76
|
+
`referral` with its typed `result`.
|
|
61
77
|
|
|
62
78
|
## Stop, bounce and referral
|
|
63
79
|
|
|
@@ -145,6 +161,24 @@ the thread the prospect wrote in, report the result, then close the reply with s
|
|
|
145
161
|
such agent, no task appears.
|
|
146
162
|
- The meeting moves the person to the playbook's goal stage. Record it once.
|
|
147
163
|
|
|
164
|
+
## Before a meeting is booked
|
|
165
|
+
|
|
166
|
+
Record where an unbooked meeting stands with `"meetingState"` in a `replies set` result:
|
|
167
|
+
|
|
168
|
+
- `requested`: a time was asked for or offered.
|
|
169
|
+
- `accepted`: they agreed, and no time is fixed yet.
|
|
170
|
+
- `declined`: they said no to the meeting. A plain no is not a request to stop; classify the
|
|
171
|
+
reply on its own.
|
|
172
|
+
|
|
173
|
+
Each state is written once, and only when it differs from the last one. Nothing is written after
|
|
174
|
+
a booking. If a playbook step waits for a meeting and the wait runs out while the request is
|
|
175
|
+
`requested` or `accepted`, Audienti first asks you for a follow-up, up to 3 times (the playbook
|
|
176
|
+
can change the count and the wait; the default wait is 5 days). Write each follow-up yourself
|
|
177
|
+
after you read the whole conversation, and send it on the thread's channel. Do not use a fixed
|
|
178
|
+
template unless the user put one in your instructions. Save it as a plan action with the
|
|
179
|
+
`playbookStepKey` the task names. After the last wait runs out, Audienti records
|
|
180
|
+
`meeting_no_answer` itself and tells the owner. Do not record it.
|
|
181
|
+
|
|
148
182
|
## Taken over
|
|
149
183
|
|
|
150
184
|
When the user takes a person over, every open task and draft for them is withdrawn
|
|
@@ -23,7 +23,7 @@ Use this skill when:
|
|
|
23
23
|
## Rules
|
|
24
24
|
|
|
25
25
|
- Reuse before you create. Most new campaigns fit a playbook that already exists.
|
|
26
|
-
- Use only channels you can cover with accounts you reported (LinkedIn, Email
|
|
26
|
+
- Use only channels you can cover with accounts you reported (LinkedIn, Email).
|
|
27
27
|
Do not draft a phone step if no one can call.
|
|
28
28
|
- One playbook is one strategy. A correction to the same strategy (a wait, a step's
|
|
29
29
|
wording, a missing off-ramp) is a new revision. A different strategy is a new
|
|
@@ -39,7 +39,8 @@ Use this skill when:
|
|
|
39
39
|
|
|
40
40
|
## 1. Reuse first
|
|
41
41
|
|
|
42
|
-
1. `audienti playbook list` shows
|
|
42
|
+
1. `audienti playbook list` shows one page of summaries: name, latest revision and stage and step
|
|
43
|
+
counts. Follow `nextCursor` for more; `--limit N` sets the page size.
|
|
43
44
|
Every workspace has three starter playbooks (`system: true`): Email first, LinkedIn
|
|
44
45
|
first and Email only. When your reported channels point to one, the list marks it
|
|
45
46
|
`recommended: true` and returns a `recommendation` with its `reason`. A confirmed
|
|
@@ -83,8 +84,8 @@ field and the rules Audienti checks.
|
|
|
83
84
|
Keep the same keys when you revise, so progress already recorded stays on its stage.
|
|
84
85
|
2. `audienti playbook save --file REQUEST.json`. Over MCP use `playbook_save`.
|
|
85
86
|
3. If the save fails, read the error, fix that field and retry with the same
|
|
86
|
-
`requestId`. If it reports a revision conflict,
|
|
87
|
-
change to
|
|
87
|
+
`requestId`. If it reports a revision conflict, the error gives `currentRevision`: run
|
|
88
|
+
`playbook show ID`, apply your change to that revision and save with that `expectedRevision`.
|
|
88
89
|
|
|
89
90
|
## 4. Ask the user and pin it
|
|
90
91
|
|
|
@@ -16,9 +16,27 @@
|
|
|
16
16
|
| `name` | 1 to 120 characters. Name the strategy, not the campaign. |
|
|
17
17
|
| `stages` | 2 to 30 stages, in order. |
|
|
18
18
|
| `goalKey` | The `key` of the goal stage. Not the first stage. |
|
|
19
|
-
| `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive
|
|
19
|
+
| `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive` or `other`. The system sets `taken_over`, `bounced` and `skipped` itself; do not choose them. "Bad data" and "repair" are not off-ramps. |
|
|
20
20
|
| `voice` | How messages should sound, in the user's style. |
|
|
21
|
-
| `rules` | Optional: `
|
|
21
|
+
| `rules` | Optional: `allowBackward` (boolean, default false): let a move go to an earlier stage. Approval of a move is a setting on the account, not a playbook rule. |
|
|
22
|
+
|
|
23
|
+
## The Playbook check
|
|
24
|
+
|
|
25
|
+
Every read and save of a playbook returns `warnings`, a deterministic check that needs no agent. An item
|
|
26
|
+
is `{ id, level, message, stageKey, stepKey, fix }`. A save is refused with `playbook_cannot_work` (422,
|
|
27
|
+
the list in `errors`) when `offRamps` is empty or a stage has `on: []` and nothing sends people to it.
|
|
28
|
+
The other items are warnings; read them after each save and clear them:
|
|
29
|
+
|
|
30
|
+
| Item `id` | Meaning and default fix |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `stage_never_ends` | A stage with steps and `completion` `stay`. Use `off_ramp` with the Unresponsive off-ramp. |
|
|
33
|
+
| `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event or finish the stage before it with `next`. |
|
|
34
|
+
| `send_without_wait` | A send step with no `wait`, followed by another send on the same channel. Add a wait. |
|
|
35
|
+
| `goal_shorter_than_waits` | `goalDays` is below the waits added up. Use the waits plus 2 days. |
|
|
36
|
+
| `send_in_person_stage` | A send step in the goal stage or an `Assignee` stage writes to someone who already answered. Remove it. |
|
|
37
|
+
| `steps_after_break_up` | A step after the break-up email. Put it before the break-up step. |
|
|
38
|
+
| `message_without_connection` | A LinkedIn message with condition `Always` and no connection request before it. Use `Only if connected`. |
|
|
39
|
+
| `stages_loop` | Routes or rules send people between stages in a circle. |
|
|
22
40
|
|
|
23
41
|
## Stage
|
|
24
42
|
|
|
@@ -29,9 +47,8 @@
|
|
|
29
47
|
| `movesInWhen` | What moves a prospect into this stage, for example "They replied". Shown to people; `on` decides. |
|
|
30
48
|
| `on` | The events that open this stage: `accept`, `reply`, `meeting` or `manual`. Defaults to `["manual"]`. An event moves the prospect to the furthest later stage that opens on it; the stages passed over are recorded as skipped, never as done. |
|
|
31
49
|
| `owner` | `Agent` or `Assignee` (the person assigned to the prospect). |
|
|
32
|
-
| `goalDays` | 0 to 365.
|
|
33
|
-
| `
|
|
34
|
-
| `enabled` | Defaults to true. |
|
|
50
|
+
| `goalDays` | 0 to 365. The days after which a prospect in this stage is overdue (the pipeline card turns orange). 0 means never overdue. |
|
|
51
|
+
| `enabled` | Defaults to true. `false` skips the stage: no one is moved into it. The first stage and the goal stage cannot be skipped. |
|
|
35
52
|
| `steps` | Up to 20 steps. |
|
|
36
53
|
| `completion` | `{ "kind": "next" }`, `{ "kind": "stay" }` or `{ "kind": "off_ramp", "offRampKey": KEY }`. The last enabled stage cannot use `next`. |
|
|
37
54
|
|
|
@@ -40,7 +57,7 @@
|
|
|
40
57
|
| Field | Rule |
|
|
41
58
|
| --- | --- |
|
|
42
59
|
| `key` | A UUID, unique in the playbook. A plan action that runs this step carries it as `playbookStepKey`; actions without it change no playbook progress. |
|
|
43
|
-
| `channel` | `LinkedIn
|
|
60
|
+
| `channel` | `LinkedIn` or `Email`. |
|
|
44
61
|
| `action` | A short label, for example "Send connection request". |
|
|
45
62
|
| `condition` | `Always`, `Only if viable` or `Only if connected`. Audienti does not check a connection itself: put an `Only if connected` step in a stage that opens on `accept`, so entering the stage on the accept is what gates it. |
|
|
46
63
|
| `viability` | Required with `Only if viable`: when the step makes sense. An Email step is skipped, without a card, for a person with no email on record, whatever its condition. |
|
|
@@ -48,8 +65,10 @@
|
|
|
48
65
|
| `wait` | Optional: `{ "until": U, "days": N }`. `until` is `accept` (LinkedIn steps only), `reply`, `meeting` or `timeout`; `days` is 1 to 60. The wait starts when the agent reports the step's result with its step key: a plan action carries the step's `key` as `playbookStepKey`, and a succeeded result for that action starts the wait. A reply or a booked meeting ends every wait early; an accept ends only a wait until `accept`. Older playbooks used `waitDays: N`, which still reads as `{ "until": "timeout", "days": N }`. |
|
|
49
66
|
| `side` | Defaults to false. A side step runs while the main step before it waits, for example a profile view or a like. A stage with side steps needs a main step. |
|
|
50
67
|
| `onTimeout` | `next` (default) or `off_ramp`. With `off_ramp`, a main step whose wait runs out leaves the prospect through the `unresponsive` off-ramp. When nothing is left in the stage, its completion decides. |
|
|
68
|
+
| `routes` | Optional: `{ "accept"?: T, "reply"?: T, "meeting"?: T }`, where `T` is `{ "kind": "stage", "stageKey": "<uuid>" }` or `{ "kind": "off_ramp", "offRampKey": "<uuid>" }`. Where an event that reaches the person after this step ran sends them. The step's route wins over the stage's `on` list; an event with no route follows the stage. A main step only; `accept` only on a LinkedIn step. A route to an earlier stage needs `rules.allowBackward`. |
|
|
69
|
+
| `actionId` | Optional: an action id from the outreach rulebook, for example `linkedin.message.send`. The `action` text stays the label the person reads. |
|
|
51
70
|
|
|
52
|
-
A LinkedIn step whose
|
|
71
|
+
A LinkedIn step whose `actionId` is `linkedin.inmail.send` runs only when the LinkedIn account reports InMail available. Otherwise it is skipped without a card.
|
|
53
72
|
|
|
54
73
|
## Example request
|
|
55
74
|
|
|
@@ -64,7 +83,7 @@ Use the same UUID wherever the same placeholder letter appears.
|
|
|
64
83
|
"name": "Founder-led outbound",
|
|
65
84
|
"stages": [
|
|
66
85
|
{ "key": "<A>", "name": "Ready", "owner": "Agent", "goalDays": 3, "steps": [], "completion": { "kind": "next" } },
|
|
67
|
-
{ "key": "<B>", "name": "Warming", "owner": "Agent", "goalDays": 7, "
|
|
86
|
+
{ "key": "<B>", "name": "Warming", "owner": "Agent", "goalDays": 7, "steps": [
|
|
68
87
|
{ "key": "<uuid>", "channel": "LinkedIn", "action": "View profile", "condition": "Always", "instructions": "Read their recent activity for a signal to mention." },
|
|
69
88
|
{ "key": "<uuid>", "channel": "LinkedIn", "action": "Like a recent post", "condition": "Always", "instructions": "Only a post about their work.", "wait": { "until": "timeout", "days": 3 } }
|
|
70
89
|
], "completion": { "kind": "next" } },
|
|
@@ -83,7 +102,7 @@ Use the same UUID wherever the same placeholder letter appears.
|
|
|
83
102
|
"goalKey": "<F>",
|
|
84
103
|
"offRamps": [{ "key": "<R>", "name": "Unresponsive", "reason": "unresponsive" }],
|
|
85
104
|
"voice": "Write like a founder. Lead with their signal, offer something useful, and ask one short question.",
|
|
86
|
-
"rules": { "
|
|
105
|
+
"rules": { "allowBackward": false }
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
```
|