@audienti/cli 0.2.0-exora.5 → 0.2.0-exora.7
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 +21 -10
- package/cli/campaign-operations.mjs +3 -2
- package/cli/campaigns.mjs +15 -2
- package/cli/discovery-operations.mjs +1 -1
- package/cli/outcome-operations.mjs +7 -0
- package/cli/playbook-help.mjs +62 -0
- package/cli/playbook-operations.mjs +13 -7
- package/cli/progress.mjs +5 -4
- package/cli/recipients.mjs +6 -2
- package/cli/rooms-operations.mjs +21 -1
- package/cli/rooms.mjs +28 -3
- package/cli/secret-operations.mjs +1 -1
- package/cli/sender-account-operations.mjs +1 -1
- package/cli/work-operations.mjs +1 -1
- package/cli/workspaces.mjs +18 -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/audienti/SKILL.md +10 -5
- package/skills/getting-started/SKILL.md +9 -1
- package/skills/getting-started/references/campaign-draft.md +4 -2
- package/skills/getting-started/references/capabilities.md +30 -7
- package/skills/getting-started/references/outcome-checks.md +25 -0
- package/skills/getting-started/references/replies.md +26 -1
- package/skills/playbook-design/SKILL.md +5 -4
- package/skills/playbook-design/references/fields.md +6 -4
- package/source-manifest.json +31 -28
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);
|
|
@@ -3,5 +3,5 @@ const workspace = { workspaceId: z.string().uuid().optional() };
|
|
|
3
3
|
export const secretName = z.string().regex(/^[A-Z][A-Z0-9_]{0,63}$/, 'A secret name uses capital letters, digits and underscores, for example HUBSPOT_TOKEN.');
|
|
4
4
|
export const secretOperations = {
|
|
5
5
|
workspace_secret_list: { schema: z.object(workspace).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets`, scoped: true, description: 'List the names of the secrets in the workspace Vault. Names only, never values. Use workspace_secret_get for one value.' },
|
|
6
|
-
workspace_secret_get: { schema: z.object({ ...workspace, name: secretName }).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets/${encodeURIComponent(a.name)}`, scoped: true, description: 'Get one secret by name from the workspace Vault (for example HUBSPOT_TOKEN). Every agent in the workspace can read every secret and reading is free. Each read
|
|
6
|
+
workspace_secret_get: { schema: z.object({ ...workspace, name: secretName }).strict(), method: 'GET', path: a => `/workspaces/${a.workspaceId}/secrets/${encodeURIComponent(a.name)}`, scoped: true, description: 'Get one secret by name from the workspace Vault (for example HUBSPOT_TOKEN). Every agent in the workspace can read every secret and reading is free. Each read records who read it last and when. Use the value for the task only: never print it, paste it into a message or save it to a file.' },
|
|
7
7
|
};
|
|
@@ -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/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.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(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,10 @@ 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'))) && 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===409 && (code==='revision_conflict' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=1000 && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
|
|
155
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==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
|
|
157
168
|
// What a failed paid check already spent (pico USD), so a caller counting its spend never loses it.
|
|
158
169
|
if(typeof result?.error?.charged==='string'&&/^\d{1,20}$/.test(result.error.charged))error.charged=result.error.charged;
|
|
159
170
|
if(isPaidToolOperation(name,input)){error.requestId=input.requestId;error.recovery=response.status>=500?'retry_same_request_id':'inspect_rejection';}
|
|
@@ -175,7 +186,7 @@ export async function workspaceCli(args) {
|
|
|
175
186
|
if(position[0]==='profile' && position[1]==='save') {
|
|
176
187
|
if(!['account:read','workspace:access','agents:manage'].includes(flags.scope)) throw new Error('Specify --scope account:read, workspace:access or agents:manage.');
|
|
177
188
|
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;
|
|
189
|
+
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
190
|
}
|
|
180
191
|
const config=configuration(flags.profile);const command=position.slice(1).join(' ');
|
|
181
192
|
let name, input={};
|
|
@@ -205,6 +216,6 @@ export async function workspaceCli(args) {
|
|
|
205
216
|
if(!name) throw new Error('Unknown workspace command. Use audienti --help.');
|
|
206
217
|
const result=await callWorkspace(name,input,config);
|
|
207
218
|
|
|
208
|
-
if(name==='workspace_select') writeProfile(flags.profile,{...config.profile,workspaceId:config.workspaceId});
|
|
219
|
+
if(name==='workspace_select') writeProfile(profileFrom(flags.profile),{...config.profile,workspaceId:config.workspaceId});
|
|
209
220
|
console.log(JSON.stringify(result));
|
|
210
221
|
}
|
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.
|
package/skills/audienti/SKILL.md
CHANGED
|
@@ -71,9 +71,12 @@ takes `--file INPUT.json`; the request fields are in the API schema at
|
|
|
71
71
|
`/api/v1/openapi.json`.
|
|
72
72
|
|
|
73
73
|
1. `audienti prospects observation create --file person.json` saves a person or company
|
|
74
|
-
profile with its source URL and returns the prospect ID.
|
|
74
|
+
profile with its source URL and returns the prospect ID. A person's source is their
|
|
75
|
+
own LinkedIn profile URL, email, phone or provider ID (or pass the existing person's
|
|
76
|
+
`prospectId`); a web page that names them goes in `provenance.url`, because a plain
|
|
77
|
+
`web` source never identifies a person. Add `fields.imageUrl` with
|
|
75
78
|
the LinkedIn profile photo or company logo URL when you have it; People and
|
|
76
|
-
Companies show it instead of initials.
|
|
79
|
+
Companies show it instead of initials. A company's source can be its own site.
|
|
77
80
|
2. `audienti prospects citation create --file employment.json` links a person to their
|
|
78
81
|
company (`edgeType` `works_at`, one current role). The first current employer
|
|
79
82
|
becomes the person's company in People.
|
|
@@ -101,9 +104,11 @@ value may follow in brackets, never on its own. The command:
|
|
|
101
104
|
- searches LinkedIn with the audience's titles, locations, industries and keyword, plus
|
|
102
105
|
the campaign's topic signals. With no person location, it searches the company
|
|
103
106
|
locations as where the person is;
|
|
104
|
-
- checks each new person against the campaign's audience rules,
|
|
105
|
-
|
|
106
|
-
company
|
|
107
|
+
- checks each new person against the campaign's audience rules, cheapest first: it reads
|
|
108
|
+
what the workspace already holds, fetches the person only if that is missing or old,
|
|
109
|
+
and fetches the company only when the audience has company rules or the campaign
|
|
110
|
+
targets companies. A person who fails a person rule is not a fit, and their company is
|
|
111
|
+
not paid for. The check also records where the person works as their primary employer;
|
|
107
112
|
- stops at N fits or before the next paid call would pass the cap.
|
|
108
113
|
|
|
109
114
|
The report lists the fits, the people held for your judgment, the not-fit count and the
|
|
@@ -101,6 +101,9 @@ capability report and ask your user to approve it in **Settings → Sending**; t
|
|
|
101
101
|
Audienti gives you a queue task to send one test email from the mailbox to itself, read it
|
|
102
102
|
back and report its code; the mailbox counts once you do
|
|
103
103
|
([the mailbox test email](references/mailbox-check.md)). Never ask your user for the code.
|
|
104
|
+
A campaign can run without a mailbox: its people with an email step wait at that step, and their
|
|
105
|
+
LinkedIn steps go on. Nothing is skipped. They go on once a mailbox is confirmed, so the fix is the
|
|
106
|
+
email connector.
|
|
104
107
|
|
|
105
108
|
## 2. Introduce yourself and propose a first campaign
|
|
106
109
|
|
|
@@ -209,7 +212,12 @@ Audienti hosted tool and the workspace has no credits left: tell the user once t
|
|
|
209
212
|
waits on credits and runs when they add credits in Billing, and do not ask again. When
|
|
210
213
|
credits are added or run out, or the user adds or removes one of their own provider keys
|
|
211
214
|
in the Vault, every agent gets a notice task: run `audienti agent capabilities` again,
|
|
212
|
-
then do what its steps say.
|
|
215
|
+
then do what its steps say. If you reported an `owner.notify` capability, Audienti puts one
|
|
216
|
+
**Tell your user about an alert** task in your queue for each alert it records. Its steps
|
|
217
|
+
give the message and a link: tell your user that message on whatever channel you use with
|
|
218
|
+
them (chat, push, Slack, SMS or email), include the link, and mark the task done once it
|
|
219
|
+
is delivered. Done means delivered: a done task is not created again for the same alert.
|
|
220
|
+
An agent that did not report `owner.notify` gets no such task. When the queue is empty, your timer runs it
|
|
213
221
|
again after `pollAfterSeconds`; when `ask` or `queue next` returns an `announce` line,
|
|
214
222
|
say it in your chat and point to Operator. Keep machine state in Audienti, not in your
|
|
215
223
|
own memory: `audienti kv set KEY VALUE` stores cursors, change tokens, hashes, ids and
|
|
@@ -23,8 +23,10 @@ Fill this before you ask the user anything. Mark each value with where it came f
|
|
|
23
23
|
## Signals: the through line
|
|
24
24
|
|
|
25
25
|
Prospect finding starts from signals: they find the companies that need the offer
|
|
26
|
-
today. A campaign with no signal
|
|
27
|
-
draft has at least one
|
|
26
|
+
today. A campaign with no signal, no target companies and no finding instructions cannot
|
|
27
|
+
start, so every signal campaign you draft has at least one signal. When the user already
|
|
28
|
+
has the people, or a source Audienti does not have, draft a custom campaign instead:
|
|
29
|
+
`method: "custom"`, finding instructions in the user's words and no signals. A signal is not "they might want to buy". It is an
|
|
28
30
|
observable event that shows a company has the problem the offer solves, now. Build the
|
|
29
31
|
line in this order:
|
|
30
32
|
|
|
@@ -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,11 +189,16 @@ 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 |
|
|
185
196
|
|
|
197
|
+
`owner.notify` means you can reach your user outside Audienti: chat, push, Slack, SMS or email. Report it
|
|
198
|
+
and Audienti puts a **Tell your user about an alert** task in your queue for each alert it records in
|
|
199
|
+
the workspace; the task carries the message and a link, and you mark it done once you delivered it. An
|
|
200
|
+
agent that has not reported `owner.notify` gets no such task.
|
|
201
|
+
|
|
186
202
|
### Tools you use for yourself
|
|
187
203
|
|
|
188
204
|
| Capability | Providers | Access | What it does | Launch |
|
|
@@ -212,3 +228,10 @@ Translated inventory reports match fewer queue tasks than before:
|
|
|
212
228
|
- `people_data` alone no longer matches verify tasks.
|
|
213
229
|
- `email` and `linkedin` match send tasks only for their own provider and only with
|
|
214
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
|
|
@@ -58,6 +59,12 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
58
59
|
|
|
59
60
|
The classification records a `replied` event on the person's active campaigns, except
|
|
60
61
|
for `bounce` and `out_of_office`, which are not replies from the person.
|
|
62
|
+
An `out_of_office` reply records an auto-reply instead. The playbook's out-of-office rule
|
|
63
|
+
snoozes the person: the wait and the next step hold for 7 days, or until the return date
|
|
64
|
+
when you pass `returnsOn`. Nothing is sent meanwhile and the person does not move. A
|
|
65
|
+
`wrong_person` reply that names no one stops the sends and leaves the reply for the user to
|
|
66
|
+
settle: ask who the right person is. When the reply names the right person, classify it as a
|
|
67
|
+
`referral` with its typed `result`.
|
|
61
68
|
|
|
62
69
|
## Stop, bounce and referral
|
|
63
70
|
|
|
@@ -145,6 +152,24 @@ the thread the prospect wrote in, report the result, then close the reply with s
|
|
|
145
152
|
such agent, no task appears.
|
|
146
153
|
- The meeting moves the person to the playbook's goal stage. Record it once.
|
|
147
154
|
|
|
155
|
+
## Before a meeting is booked
|
|
156
|
+
|
|
157
|
+
Record where an unbooked meeting stands with `"meetingState"` in a `replies set` result:
|
|
158
|
+
|
|
159
|
+
- `requested`: a time was asked for or offered.
|
|
160
|
+
- `accepted`: they agreed, and no time is fixed yet.
|
|
161
|
+
- `declined`: they said no to the meeting. A plain no is not a request to stop; classify the
|
|
162
|
+
reply on its own.
|
|
163
|
+
|
|
164
|
+
Each state is written once, and only when it differs from the last one. Nothing is written after
|
|
165
|
+
a booking. If a playbook step waits for a meeting and the wait runs out while the request is
|
|
166
|
+
`requested` or `accepted`, Audienti first asks you for a follow-up, up to 3 times (the playbook
|
|
167
|
+
can change the count and the wait; the default wait is 5 days). Write each follow-up yourself
|
|
168
|
+
after you read the whole conversation, and send it on the thread's channel. Do not use a fixed
|
|
169
|
+
template unless the user put one in your instructions. Save it as a plan action with the
|
|
170
|
+
`playbookStepKey` the task names. After the last wait runs out, Audienti records
|
|
171
|
+
`meeting_no_answer` itself and tells the owner. Do not record it.
|
|
172
|
+
|
|
148
173
|
## Taken over
|
|
149
174
|
|
|
150
175
|
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
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| `goalKey` | The `key` of the goal stage. Not the first stage. |
|
|
19
19
|
| `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive`, `taken_over`, `bounced`, `skipped` or `other`. "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: `flagOverdue`, `allowBackward` (booleans), `emailWindow` (`weekdays_morning`, `weekdays`, `any_day`), `nurtureDays` (60, 90, 180 or null). Approval of a move is a setting on the account, not a playbook rule. An `autoMove` in an older playbook is ignored. |
|
|
22
22
|
|
|
23
23
|
## Stage
|
|
24
24
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
| Field | Rule |
|
|
41
41
|
| --- | --- |
|
|
42
42
|
| `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
|
|
43
|
+
| `channel` | `LinkedIn` or `Email`. |
|
|
44
44
|
| `action` | A short label, for example "Send connection request". |
|
|
45
45
|
| `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
46
|
| `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 +48,10 @@
|
|
|
48
48
|
| `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
49
|
| `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
50
|
| `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. |
|
|
51
|
+
| `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`. |
|
|
52
|
+
| `actionId` | Optional: an action id from the outreach rulebook, for example `linkedin.message.send`. The `action` text stays the label the person reads. |
|
|
51
53
|
|
|
52
|
-
A LinkedIn step whose
|
|
54
|
+
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
55
|
|
|
54
56
|
## Example request
|
|
55
57
|
|
|
@@ -83,7 +85,7 @@ Use the same UUID wherever the same placeholder letter appears.
|
|
|
83
85
|
"goalKey": "<F>",
|
|
84
86
|
"offRamps": [{ "key": "<R>", "name": "Unresponsive", "reason": "unresponsive" }],
|
|
85
87
|
"voice": "Write like a founder. Lead with their signal, offer something useful, and ask one short question.",
|
|
86
|
-
"rules": { "
|
|
88
|
+
"rules": { "flagOverdue": true, "allowBackward": false, "emailWindow": "weekdays_morning", "nurtureDays": 90 }
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
```
|