@audienti/cli 0.2.0-exora.3 → 0.2.0-exora.30
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/README.md +2 -2
- package/cli/account-replies.mjs +16 -2
- package/cli/account-reply-operations.mjs +28 -1
- package/cli/agents.mjs +56 -14
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audience-operations.mjs +1 -1
- package/cli/audienti.mjs +118 -27
- package/cli/audit-operations.mjs +2 -2
- package/cli/campaign-find.mjs +155 -0
- package/cli/campaign-operations.mjs +8 -4
- package/cli/campaigns.mjs +38 -3
- package/cli/cohort-operations.mjs +1 -1
- package/cli/content.mjs +2 -0
- package/cli/decision-operations.mjs +35 -0
- package/cli/decision.mjs +51 -0
- package/cli/discovery-operations.mjs +2 -1
- package/cli/discovery.mjs +3 -2
- package/cli/duplicate-operations.mjs +22 -0
- package/cli/duplicates.mjs +22 -0
- package/cli/hand-task-operations.mjs +11 -0
- package/cli/hand-tasks.mjs +18 -0
- package/cli/intent-operations.mjs +44 -0
- package/cli/intent.mjs +109 -0
- package/cli/json-file.mjs +1 -1
- package/cli/kv-operations.mjs +17 -0
- package/cli/kv.mjs +58 -0
- package/cli/mcp.mjs +4 -4
- package/cli/outcome-operations.mjs +28 -0
- package/cli/outcomes.mjs +16 -0
- package/cli/owner-account-client.mjs +5 -4
- package/cli/playbook-help.mjs +87 -0
- package/cli/playbook-operations.mjs +40 -0
- package/cli/progress.mjs +19 -8
- package/cli/prospecting-settings-operations.mjs +1 -1
- package/cli/question-operations.mjs +4 -4
- package/cli/questions.mjs +4 -4
- package/cli/queue-operations.mjs +7 -6
- package/cli/queue.mjs +4 -3
- package/cli/recipient-operations.mjs +1 -1
- package/cli/recipients.mjs +6 -2
- package/cli/rooms-operations.mjs +26 -5
- package/cli/rooms.mjs +32 -6
- package/cli/secret-operations.mjs +7 -0
- package/cli/secrets.mjs +30 -0
- package/cli/sender-account-operations.mjs +8 -3
- package/cli/sender-accounts.mjs +20 -10
- package/cli/skills-mcp.mjs +6 -2
- package/cli/skills.mjs +55 -12
- package/cli/tools-operations.mjs +19 -10
- package/cli/tools.mjs +48 -20
- package/cli/work-execution-operations.mjs +7 -0
- package/cli/work-execution.mjs +3 -3
- package/cli/work-operations.mjs +5 -4
- package/cli/work.mjs +2 -2
- package/cli/workspaces.mjs +104 -22
- package/package.json +2 -2
- package/skills/agent-rooms/SKILL.md +140 -0
- package/skills/agent-rooms/references/commands.md +78 -0
- package/skills/audienti/SKILL.md +111 -5
- package/skills/audienti/references/helpers.md +15 -2
- package/skills/audienti/references/profile-workflows.md +4 -2
- package/skills/audienti/scripts/contracts.mjs +1 -1
- package/skills/getting-started/SKILL.md +188 -154
- package/skills/getting-started/references/audience.md +72 -7
- package/skills/getting-started/references/campaign-draft.md +56 -13
- package/skills/getting-started/references/capabilities.md +238 -0
- package/skills/getting-started/references/mailbox-check.md +64 -0
- package/skills/getting-started/references/outcome-checks.md +125 -0
- package/skills/getting-started/references/replies.md +314 -0
- package/skills/getting-started/references/reply-checks.md +54 -0
- package/skills/playbook-design/SKILL.md +139 -0
- package/skills/playbook-design/references/fields.md +145 -0
- package/source-manifest.json +74 -48
- package/cli/approach-operations.mjs +0 -24
- package/skills/getting-started/references/inventory.md +0 -52
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','handle','exec','interval','timeout'];
|
|
10
|
-
const switches=['retired','stop','once','json','unable','clear'];
|
|
11
|
-
export const roomsUsage='Rooms: audienti rooms directory [--sort load] | inbox [--since ITEM] [--about TYPE:ID] [--limit N] | 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] | 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.');
|
|
@@ -51,7 +55,7 @@ async function hook(flags,now=Date.now()){
|
|
|
51
55
|
const path=statePath(flags),state=readState(path);
|
|
52
56
|
let items=state.items;
|
|
53
57
|
if(!items||!state.checkedAt||now-state.checkedAt>=HOOK_CACHE*1000){
|
|
54
|
-
try{items=(await call('rooms_inbox',{limit:20},flags)).data.items.map(i=>({id:i.id,reason:i.reason,overdue:i.overdue,answerBy:i.answerBy,message:{threadId:i.message.threadId,from:{handle:i.message.from.handle}}}))}catch{return}
|
|
58
|
+
try{items=(await call('rooms_inbox',{limit:20,brief:true},flags)).data.items.map(i=>({id:i.id,reason:i.reason,overdue:i.overdue,answerBy:i.answerBy,message:{threadId:i.message.threadId,from:{handle:i.message.from.handle}}}))}catch{return}
|
|
55
59
|
Object.assign(state,{checkedAt:now,items});
|
|
56
60
|
}
|
|
57
61
|
if(flags.stop){
|
|
@@ -102,7 +106,7 @@ export async function roomsCli(args){
|
|
|
102
106
|
const need=(count,text)=>{if(rest.length!==count)throw new Error(`Use audienti rooms ${text}.`)};
|
|
103
107
|
switch(command){
|
|
104
108
|
case 'directory':need(0,'directory [--sort load]');return print(await call('rooms_directory',{...(flags.sort?{sort:flags.sort}:{})},flags));
|
|
105
|
-
case 'inbox':need(0,'inbox');return print(await call('rooms_inbox',{...(flags.since?{since:flags.since}:{}),...(flags.about?{about:flags.about}:{}),...(flags.limit?{limit:integer(flags.limit,'Limit')}:{})},flags));
|
|
109
|
+
case 'inbox':need(0,'inbox');return print(await call('rooms_inbox',{...(flags.since?{since:flags.since}:{}),...(flags.about?{about:flags.about}:{}),...(flags.limit?{limit:integer(flags.limit,'Limit')}:{}),...(flags.brief?{brief:true}:{})},flags));
|
|
106
110
|
case 'ack':if(!rest.length)throw new Error('Use audienti rooms ack ITEM....');return print(await call('rooms_ack',{ids:rest},flags));
|
|
107
111
|
case 'send':{
|
|
108
112
|
if(rest.length>1||(rest.length===1&&rest[0]!=='-'))throw new Error('Use audienti rooms send (--to HANDLE|--room ROOM) --body TEXT.');
|
|
@@ -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':{
|
|
@@ -120,12 +126,32 @@ export async function roomsCli(args){
|
|
|
120
126
|
case 'thread':need(1,'thread ID [--after N]');return print(await call('rooms_thread',{threadId:rest[0],...(flags.after?{after:integer(flags.after,'After')}:{})},flags));
|
|
121
127
|
case 'threads':need(0,'threads --about TYPE:ID');return print(await call('rooms_threads',{about:flags.about},flags));
|
|
122
128
|
case 'create':need(0,'create (--with HANDLE | --slug S --title T)');return print(await call('rooms_create',flags.with?{kind:'direct',with:flags.with}:{kind:'group',slug:flags.slug,title:flags.title,...(flags.purpose?{purpose:flags.purpose}:{}),...(flags.members?{members:flags.members.split(',')}:{})},flags));
|
|
123
|
-
case 'list':need(0,'list [--retired]');return print(await call('rooms_list',{...(flags.retired?{retired:true}:{})},flags));
|
|
129
|
+
case 'list':need(0,'list [--retired] [--q TEXT]');return print(await call('rooms_list',{...(flags.retired?{retired:true}:{}),...(flags.q!==undefined?{q:flags.q}:{})},flags));
|
|
130
|
+
case 'read':need(1,'read ROOM [--q TEXT]');return print(await call('rooms_read',{room:rest[0],...(flags.q!==undefined?{q:flags.q}:{})},flags));
|
|
124
131
|
case 'retire':case 'restore':need(1,`${command} ROOM`);return print(await call(`rooms_${command}`,{room:rest[0]},flags));
|
|
125
132
|
case 'redact':need(1,'redact MESSAGE');return print(await call('rooms_redact',{messageId:rest[0]},flags));
|
|
126
133
|
case 'resume':need(1,'resume THREAD');return print(await call('rooms_resume',{threadId:rest[0]},flags));
|
|
127
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));
|
|
128
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));
|
|
129
155
|
case 'hook':need(0,'hook [--stop]');return hook(flags);
|
|
130
156
|
case 'watch':need(0,'watch --exec CMD');return watch(flags);
|
|
131
157
|
case 'wait':need(0,'wait --thread ID');return wait(flags);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const workspace = { workspaceId: z.string().uuid().optional() };
|
|
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
|
+
export const secretOperations = {
|
|
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 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
|
+
};
|
package/cli/secrets.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
2
|
+
|
|
3
|
+
/** secret get NAME prints the value alone (so $(audienti secret get NAME) works); secret list prints names. --json prints the API answer. */
|
|
4
|
+
export async function secretsCli(args) {
|
|
5
|
+
const flags = {}, positions = [];
|
|
6
|
+
let json = false;
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i];
|
|
9
|
+
if (arg === '--json') { json = true; continue; }
|
|
10
|
+
if (!arg.startsWith('--')) { positions.push(arg); continue; }
|
|
11
|
+
const key = arg.slice(2);
|
|
12
|
+
if (!['workspace', 'profile'].includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}.`);
|
|
13
|
+
flags[key] = args[++i];
|
|
14
|
+
}
|
|
15
|
+
const usage = 'Use secret list [--workspace ID] or secret get NAME [--workspace ID] [--json].';
|
|
16
|
+
if (positions[0] !== 'secret' && positions[0] !== 'secrets') throw Error(usage);
|
|
17
|
+
const input = { workspaceId: flags.workspace }, config = configuration(flags.profile);
|
|
18
|
+
if (positions[1] === 'list' && positions.length === 2) {
|
|
19
|
+
const result = await callWorkspace('workspace_secret_list', input, config);
|
|
20
|
+
if (json) { console.log(JSON.stringify(result)); return; }
|
|
21
|
+
for (const secret of result.data.secrets) console.log(secret.name);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (positions[1] === 'get' && positions.length === 3) {
|
|
25
|
+
const result = await callWorkspace('workspace_secret_get', { ...input, name: positions[2] }, config);
|
|
26
|
+
if (json) console.log(JSON.stringify(result)); else console.log(result.data.value);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
throw Error(usage);
|
|
30
|
+
}
|
|
@@ -5,20 +5,25 @@ 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.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
|
+
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, a new account is used in all of them and an account approved before keeps its workspaces. Approving an unbound account again starts over: the workspaces you choose replace the old ones. An account still approved elsewhere only gains workspaces.');
|
|
8
10
|
const pageLimit=z.number().int().min(1).max(100).optional();
|
|
9
11
|
const query=a=>{const q=new URLSearchParams(Object.entries(a).map(([k,v])=>[k,String(v)]));return q.size?'?'+q:'';};
|
|
10
12
|
const path=a=>`/sender-accounts/${encodeURIComponent(a.senderAccountId)}`;
|
|
11
|
-
const body=a=>Object.fromEntries(Object.entries(a).filter(([key])=>!['senderAccountId','actionType'].includes(key)));
|
|
13
|
+
const body=a=>Object.fromEntries(Object.entries(a).filter(([key])=>!['senderAccountId','actionType','capability'].includes(key)));
|
|
12
14
|
const opaque=z.string().trim().min(1).max(256).regex(/^[ -~]+$/);
|
|
13
15
|
export const senderAccountOperations={
|
|
14
16
|
sender_activity_report:{schema:z.object({...base,senderAccountId:uuid,sourceEventId:opaque,namespace:identity.shape.namespace,occurredAt:z.iso.datetime({offset:true}),providerMessageId:opaque.nullable(),relatedAttemptId:uuid.nullable()}).strict(),method:'POST',path:a=>path(a)+'/activity',body},
|
|
15
17
|
sender_alias_get:{schema:z.object({provider:alias.provider,accountRef:alias.accountRef}).strict(),method:'GET',path:a=>'/sender-accounts/alias'+query(a),ownerOnly:true},
|
|
16
18
|
sender_accounts_list:{schema:z.object({cursor:uuid.optional(),limit:pageLimit}).strict(),method:'GET',path:a=>'/sender-accounts'+query(a)},
|
|
17
|
-
sender_account_get:{schema:z.object({senderAccountId:uuid,aliasesCursor:text(1,1024).optional(),aliasesLimit:pageLimit,historyCursor:uuid.optional(),historyLimit:pageLimit}).strict(),method:'GET',path:a=>path(a)+query(body(a))},
|
|
18
|
-
sender_account_register:{schema:z.object({...base,...alias,agentId:uuid,accountIdentity:identity}).strict(),method:'POST',path:()=>'/sender-accounts',ownerOnly:true},
|
|
19
|
+
sender_account_get:{schema:z.object({senderAccountId:uuid,aliasesCursor:text(1,1024).optional(),aliasesLimit:pageLimit,historyCursor:uuid.optional(),historyLimit:pageLimit}).strict(),method:'GET',path:a=>path(a)+query(body(a)),description:'Reads one sending account: its bound names and useIn (allWorkspaces and the workspaces, id and name, it is used in; only workspaces its user belongs to).'},
|
|
20
|
+
sender_account_register:{schema:z.object({...base,...alias,agentId:uuid,accountIdentity:identity,useIn:useIn.optional()}).strict(),method:'POST',path:()=>'/sender-accounts',ownerOnly:true},
|
|
19
21
|
sender_alias_bind:{schema:z.object({...base,...alias,senderAccountId:uuid,agentId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases',body,ownerOnly:true},
|
|
20
22
|
sender_alias_unbind:{schema:z.object({...base,...alias,senderAccountId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases/unbind',body,ownerOnly:true},
|
|
21
23
|
sender_account_state_set:{schema:z.object({...base,senderAccountId:uuid,expectedVersion:version,state:z.enum(['active','retired'])}).strict(),method:'POST',path:a=>path(a)+'/lifecycle',body,ownerOnly:true},
|
|
24
|
+
sender_account_workspaces_set:{schema:z.object({...base,senderAccountId:uuid,useIn:useIn.describe('Where the account is used from now on, replacing the earlier choice: allWorkspaces true with an empty workspaceIds for all of the user\'s workspaces, or allWorkspaces false with at least one workspace id the user belongs to.')}).strict(),method:'POST',path:a=>path(a)+'/workspaces',body,ownerOnly:true,description:'Changes where an approved sending account is used: all of the user\'s workspaces, or only the listed ones, each a workspace the user belongs to. Replaces the earlier choice. The answer carries useIn, the same shape sender_account_get gives.'},
|
|
22
25
|
sender_automation_get:{schema:z.object({senderAccountId:uuid,actionType}).strict(),method:'GET',path:a=>path(a)+'/automation/'+encodeURIComponent(a.actionType)},
|
|
23
26
|
sender_automation_set:{schema:z.object({...base,senderAccountId:uuid,actionType,expectedVersion:version,enabled:z.boolean()}).strict(),method:'POST',path:a=>path(a)+'/automation/'+encodeURIComponent(a.actionType),body,ownerOnly:true},
|
|
27
|
+
sender_action_modes_get:{schema:z.object({senderAccountId:uuid}).strict(),method:'GET',path:a=>path(a)+'/action-modes'},
|
|
28
|
+
sender_action_mode_set:{schema:z.object({...base,senderAccountId:uuid,capability,expectedVersion:version,mode:z.enum(['not_allowed','approval','automatic']).describe('not_allowed is Off: the agent may not do this, and the action is blocked even when approved. approval is Draft for approval: the agent does the prep and writes it; a person approves, edits or rejects it, and it needs an exact approval before it is sent. automatic is Automatic: the agent does it and sends it, so only the approval card is skipped.')}).strict(),method:'POST',path:a=>path(a)+'/action-modes/'+encodeURIComponent(a.capability),body,ownerOnly:true},
|
|
24
29
|
};
|
package/cli/sender-accounts.mjs
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import {senderAccountOperations} from './sender-account-operations.mjs';
|
|
2
2
|
import {callOwnerAccountOperation} from './owner-account-client.mjs';
|
|
3
3
|
import {readJsonFile} from './json-file.mjs';
|
|
4
|
-
import {WorkspaceRequestError} from './workspaces.mjs';
|
|
4
|
+
import {WorkspaceRequestError,callWorkspace,configuration} from './workspaces.mjs';
|
|
5
5
|
export async function callSenderAccounts(name,args,profilePath){
|
|
6
|
-
return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account');
|
|
6
|
+
return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account',true);
|
|
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','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
|
-
if(positions[0]
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
17
|
+
const names={list:'workspace_managed_accounts_list',connect:'workspace_managed_account_connect'};
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
if(positions[0]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|workspaces|automation|modes.');
|
|
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];
|
|
23
|
+
const names={'activity-report':'sender_activity_report',alias:'sender_alias_get',list:'sender_accounts_list',get:'sender_account_get',register:'sender_account_register',bind:'sender_alias_bind',unbind:'sender_alias_unbind',state:'sender_account_state_set',workspaces:'sender_account_workspaces_set','automation-get':'sender_automation_get','automation-set':'sender_automation_set','modes-get':'sender_action_modes_get','modes-set':'sender_action_mode_set'};
|
|
14
24
|
const name=names[command];if(!name)throw Error('Unknown sender account command.');
|
|
15
|
-
const operation=senderAccountOperations[name],hasId=!['list','register','alias'].includes(command),expected=nested?5:hasId?4:3;if(positions.length!==expected)throw Error('Unexpected sender arguments.');
|
|
16
|
-
const allowed=['profile',...(operation.method==='POST'?['file']:command==='alias'?['provider','account-ref']:command==='list'?['cursor','limit']:command==='get'?['aliases-cursor','aliases-limit','history-cursor','history-limit']:[]),...(automation?['action-type']:[])];
|
|
25
|
+
const operation=senderAccountOperations[name],hasId=!['list','register','alias'].includes(command),expected=nested?5:hasId?4:3;if(positions.length!==expected)throw Error('Unexpected sender arguments.'+(command==='workspaces'?' Use sender accounts workspaces ACCOUNT_ID --file scope.json [--profile PATH].':''));
|
|
26
|
+
const allowed=['profile',...(operation.method==='POST'?['file']:command==='alias'?['provider','account-ref']:command==='list'?['cursor','limit']:command==='get'?['aliases-cursor','aliases-limit','history-cursor','history-limit']:[]),...(automation?['action-type']:[]),...(command==='modes-set'?['capability']:[])];
|
|
17
27
|
if(Object.keys(flags).some(key=>!allowed.includes(key)))throw Error('Option does not apply to this sender command.');
|
|
18
|
-
let input={};if(operation.method==='POST'){if(!flags.file)throw Error('--file INPUT.json is required.');input=readJsonFile(flags.file,65536,'Sender input');if(!input||typeof input!=='object'||Array.isArray(input))throw Error('invalid_fields: input must be a JSON object.');if(Object.hasOwn(input,'senderAccountId')||Object.hasOwn(input,'actionType')||Object.hasOwn(input,'workspaceId'))throw Error('Path IDs and workspaceId are not allowed in the file.');}
|
|
28
|
+
let input={};if(operation.method==='POST'){if(!flags.file)throw Error('--file INPUT.json is required.');input=readJsonFile(flags.file,65536,'Sender input');if(!input||typeof input!=='object'||Array.isArray(input))throw Error('invalid_fields: input must be a JSON object.');if(Object.hasOwn(input,'senderAccountId')||Object.hasOwn(input,'actionType')||Object.hasOwn(input,'capability')||Object.hasOwn(input,'workspaceId'))throw Error('Path IDs and workspaceId are not allowed in the file.');}
|
|
19
29
|
if(command==='alias'){input.provider=flags.provider;input.accountRef=flags['account-ref'];}
|
|
20
|
-
if(hasId)input.senderAccountId=positions[nested?4:3];if(automation)input.actionType=flags['action-type'];
|
|
30
|
+
if(hasId)input.senderAccountId=positions[nested?4:3];if(automation)input.actionType=flags['action-type'];if(command==='modes-set')input.capability=flags.capability;
|
|
21
31
|
for(const key of ['cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit'])if(flags[key]!==undefined){const field=key.replace(/-([a-z])/g,(_,c)=>c.toUpperCase());if(key.endsWith('limit')){if(!/^\d+$/.test(flags[key]))throw new WorkspaceRequestError(400,'invalid_fields','Page limit must be 1–100.');input[field]=Number(flags[key]);}else input[field]=flags[key];}
|
|
22
32
|
console.log(JSON.stringify(await callSenderAccounts(name,input,flags.profile??process.env.AUDIENTI_PROFILE)));
|
|
23
33
|
}
|
package/cli/skills-mcp.mjs
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { helperContracts, SKILL_VERSION } from '../skills/audienti/scripts/contracts.mjs';
|
|
3
3
|
import { runHelper } from '../skills/audienti/scripts/profile-helpers.mjs';
|
|
4
|
-
import { skillArtifact, skillCatalog, skillDetails, SKILL_IDS } from './skills.mjs';
|
|
4
|
+
import { skillArtifact, skillCatalog, skillDetails, skillResource, SKILL_IDS } from './skills.mjs';
|
|
5
5
|
|
|
6
6
|
const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
7
7
|
const result = value => ({ content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value });
|
|
8
8
|
|
|
9
9
|
export function registerSkills(server) {
|
|
10
|
-
server.registerTool('skills_list', { description: 'List bundled agent skills and deterministic helpers. Local, no authentication or provider calls.', inputSchema: z.object({}).strict(), annotations }, async () => result(skillCatalog()));
|
|
10
|
+
server.registerTool('skills_list', { description: 'List bundled agent skills and deterministic helpers. Pass compact:true for descriptions and resource names without instructions or schemas. Local, no authentication or provider calls.', inputSchema: z.object({ compact: z.boolean().optional() }).strict(), annotations }, async ({ compact }) => result(skillCatalog({ compact })));
|
|
11
|
+
server.registerTool('skills_read', { description: 'Read one bundled skill resource: SKILL.md first, then a reference or helpers/NAME.json named by skills_list with compact:true. Reading never installs or runs anything.', inputSchema: z.object({ id: z.enum(SKILL_IDS), resource: z.string().min(1).max(160) }).strict(), annotations }, async ({ id, resource }) => {
|
|
12
|
+
try { return result(skillResource(id, resource)); }
|
|
13
|
+
catch { return { isError: true, content: [{ type: 'text', text: 'Unknown bundled skill resource. Use skills_list with compact:true.' }] }; }
|
|
14
|
+
});
|
|
11
15
|
server.registerTool('skills_show', { description: 'Read bundled skill instructions, references, version, digest and helper schemas. Run getting-started once right after connecting.', inputSchema: z.object({ id: z.enum(SKILL_IDS) }).strict(), annotations }, async ({ id }) => result(skillDetails(id)));
|
|
12
16
|
for (const [name, contract] of Object.entries(helperContracts)) {
|
|
13
17
|
server.registerTool(`skills_${name.replaceAll('-', '_')}`, {
|
package/cli/skills.mjs
CHANGED
|
@@ -6,14 +6,23 @@ import { executeHelperFile } from '../skills/audienti/scripts/run.mjs';
|
|
|
6
6
|
const bundles = {
|
|
7
7
|
audienti: {
|
|
8
8
|
version: SKILL_VERSION,
|
|
9
|
-
description: 'Agent-owned profile research with deterministic local helpers and guarded CRM writes.',
|
|
10
9
|
files: ['SKILL.md', 'references/profile-workflows.md', 'references/helpers.md', 'references/workflow-receipt.md', 'scripts/contracts.mjs', 'scripts/profile-helpers.mjs', 'scripts/run.mjs'],
|
|
11
10
|
helpers: true,
|
|
12
11
|
},
|
|
13
12
|
'getting-started': {
|
|
13
|
+
version: '1.5.1',
|
|
14
|
+
files: ['SKILL.md', 'references/capabilities.md', 'references/campaign-draft.md', 'references/audience.md', 'references/outcome-checks.md', 'references/replies.md', 'references/reply-checks.md', 'references/mailbox-check.md'],
|
|
15
|
+
helpers: false,
|
|
16
|
+
},
|
|
17
|
+
'playbook-design': {
|
|
18
|
+
version: '1.1.0',
|
|
19
|
+
files: ['SKILL.md', 'references/fields.md'],
|
|
20
|
+
helpers: false,
|
|
21
|
+
},
|
|
22
|
+
'agent-rooms': {
|
|
14
23
|
version: '1.0.0',
|
|
15
|
-
description: '
|
|
16
|
-
files: ['SKILL.md', 'references/
|
|
24
|
+
description: "Talk with your user's other registered agents in rooms: check your inbox each run, ask and answer, reply unable, and leave a turnover note.",
|
|
25
|
+
files: ['SKILL.md', 'references/commands.md'],
|
|
17
26
|
helpers: false,
|
|
18
27
|
},
|
|
19
28
|
};
|
|
@@ -24,11 +33,14 @@ function readSkill(id = 'audienti') {
|
|
|
24
33
|
if (!bundle) throw new Error('Unknown bundled skill. Use skills list.');
|
|
25
34
|
const root = new URL(`../skills/${id}/`, import.meta.url);
|
|
26
35
|
const contents = bundle.files.map(path => ({ path, text: readFileSync(new URL(path, root), 'utf8') }));
|
|
36
|
+
const front = contents[0].text.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
|
|
37
|
+
const name = front.match(/^name: (.+)$/m)?.[1].trim(), description = front.match(/^description: (.+)$/m)?.[1].trim();
|
|
38
|
+
if (name !== id || !description) throw new Error('Bundled skill requires a matching name and description in SKILL.md frontmatter.');
|
|
27
39
|
const hash = createHash('sha256');
|
|
28
40
|
for (const file of contents) hash.update(`${file.path}\0${Buffer.byteLength(file.text)}\0`).update(file.text);
|
|
29
41
|
return { contents, details: {
|
|
30
42
|
id, version: bundle.version, digest: hash.digest('hex'),
|
|
31
|
-
description
|
|
43
|
+
description,
|
|
32
44
|
instructions: contents[0].text,
|
|
33
45
|
references: contents.filter(file => file.path.startsWith('references/')),
|
|
34
46
|
helpers: bundle.helpers ? Object.entries(helperContracts).map(([name, contract]) => ({ name, ...contract })) : [],
|
|
@@ -37,34 +49,65 @@ function readSkill(id = 'audienti') {
|
|
|
37
49
|
|
|
38
50
|
export function skillDetails(id = 'audienti') { return readSkill(id).details; }
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
const helperPath = name => `helpers/${name}.json`;
|
|
53
|
+
|
|
54
|
+
// Compact discovery omits instructions and schemas so an agent can pick a skill,
|
|
55
|
+
// then read only the files it needs with skillResource.
|
|
56
|
+
export function skillCatalog({ compact = false } = {}) {
|
|
57
|
+
if (!compact) return { skills: SKILL_IDS.map(id => { const { instructions, references, ...entry } = skillDetails(id); return entry; }) };
|
|
58
|
+
return { skills: SKILL_IDS.map(id => {
|
|
59
|
+
const { version, digest, description, helpers } = skillDetails(id);
|
|
60
|
+
return {
|
|
61
|
+
id, version, digest, description,
|
|
62
|
+
resources: [...bundles[id].files, ...helpers.map(({ name }) => helperPath(name))],
|
|
63
|
+
helpers: helpers.map(({ name, description: about }) => ({ name, description: about, resource: helperPath(name) })),
|
|
64
|
+
};
|
|
65
|
+
}) };
|
|
42
66
|
}
|
|
43
67
|
|
|
68
|
+
export function skillResource(id, path) {
|
|
69
|
+
const artifact = skillArtifact(id);
|
|
70
|
+
// Match shipped resource names exactly; never turn input into a disk path.
|
|
71
|
+
const file = artifact.files.find(entry => entry.path === path);
|
|
72
|
+
const helper = artifact.helpers.find(entry => helperPath(entry.name) === path);
|
|
73
|
+
if (!file && !helper) throw new Error('Unknown bundled skill resource. Use skills list --compact.');
|
|
74
|
+
return { id: artifact.id, version: artifact.version, digest: artifact.digest, path, content: file ? file.content : JSON.stringify(helper) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const usage = `Usage: audienti skills list [--compact] | show ${SKILL_IDS.join('|')} | read SKILL RESOURCE | helper NAME --file INPUT.json|- [--json]`;
|
|
78
|
+
|
|
44
79
|
export function skillArtifact(id = 'audienti') {
|
|
45
80
|
const { details, contents } = readSkill(id);
|
|
46
81
|
return { ...details, files: contents.map(({ path, text }) => ({ path, content: text })) };
|
|
47
82
|
}
|
|
48
83
|
|
|
49
84
|
export async function skillsCli(argv) {
|
|
50
|
-
|
|
85
|
+
// Skills are public; ignore --profile and --workspace so agents can pass them everywhere.
|
|
86
|
+
const raw = argv.slice(1), drop = new Set();
|
|
87
|
+
raw.forEach((value, i) => { if (value === '--profile' || value === '--workspace') { drop.add(i); drop.add(i + 1); } });
|
|
88
|
+
const args = raw.filter((_, i) => !drop.has(i)), json = args.includes('--json');
|
|
51
89
|
const filtered = args.filter(value => value !== '--json');
|
|
52
90
|
if (args.filter(value => value === '--json').length > 1) throw new Error('Invalid skills options.');
|
|
53
91
|
if (filtered.length === 1 && ['--help', 'help'].includes(filtered[0])) {
|
|
54
|
-
process.stdout.write(
|
|
92
|
+
process.stdout.write(`${usage}\nLocal skill instructions/helpers; no login, installation or provider calls.\n`); return;
|
|
55
93
|
}
|
|
56
|
-
if (filtered[0] === 'list' && filtered.length === 1) {
|
|
57
|
-
const catalog = skillCatalog();
|
|
94
|
+
if (filtered[0] === 'list' && (filtered.length === 1 || (filtered.length === 2 && filtered[1] === '--compact'))) {
|
|
95
|
+
const compact = filtered.length === 2, catalog = skillCatalog({ compact });
|
|
96
|
+
if (compact && !json) { process.stdout.write(catalog.skills.map(skill => `${skill.id}@${skill.version} — ${skill.description}\n${skill.resources.map(path => ` ${path}\n`).join('')}`).join('')); return; }
|
|
58
97
|
process.stdout.write(json ? `${JSON.stringify(catalog)}\n` : catalog.skills.map(skill => `${skill.id}@${skill.version} — ${skill.description}\n`).join('')); return;
|
|
59
98
|
}
|
|
60
99
|
if (filtered[0] === 'show' && filtered.length === 2) {
|
|
61
100
|
const details = skillDetails(filtered[1]);
|
|
62
|
-
process.stdout.write(json ? `${JSON.stringify(details)}\n` : `${details.instructions}\n`); return;
|
|
101
|
+
process.stdout.write(json ? `${JSON.stringify(details)}\n` : `${details.instructions}\n${details.references.map(file => `\n--- ${file.path} ---\n${file.text}\n`).join('')}`); return;
|
|
102
|
+
}
|
|
103
|
+
if (filtered[0] === 'read' && filtered.length === 3) {
|
|
104
|
+
const resource = skillResource(filtered[1], filtered[2]);
|
|
105
|
+
process.stdout.write(json ? `${JSON.stringify(resource)}\n` : `${resource.content}\n`); return;
|
|
63
106
|
}
|
|
64
107
|
if (filtered[0] === 'helper' && filtered.length === 4 && filtered[2] === '--file') {
|
|
65
108
|
try { process.stdout.write(`${JSON.stringify(executeHelperFile(filtered[1], filtered[3]))}\n`); }
|
|
66
109
|
catch { throw new Error('Skill helper failed. Inspect its schema and provide valid JSON of at most 1 MiB.'); }
|
|
67
110
|
return;
|
|
68
111
|
}
|
|
69
|
-
throw new Error(
|
|
112
|
+
throw new Error(usage);
|
|
70
113
|
}
|
package/cli/tools-operations.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const uuid = z.string().uuid(), workspace = { workspaceId: uuid.optional() };
|
|
3
3
|
const root = a => `/workspaces/${a.workspaceId}/tools`;
|
|
4
|
+
const toolId = z.string().regex(/^[a-z][a-z0-9._]{0,63}$/);
|
|
4
5
|
const key = z.string().regex(/^[a-z][a-z0-9_]{0,47}$/).refine(v => !['constructor','prototype'].includes(v));
|
|
5
6
|
const instruction = z.string().trim().min(1).max(4000);
|
|
6
7
|
const question = z.discriminatedUnion('type', [
|
|
@@ -15,18 +16,18 @@ const contentInputs = {
|
|
|
15
16
|
'posts.search':z.object({query:z.string().trim().min(1).max(500),topicId:uuid,postedLimit,...page,sortBy:z.enum(['relevance','date']).optional()}).strict(),
|
|
16
17
|
'posts.comments':z.object({postId:uuid,...page}).strict(),
|
|
17
18
|
'posts.reactions':z.object({postId:uuid,page:page.page}).strict(),
|
|
18
|
-
'profiles.search':z.object({kind:z.enum(['person','company']).optional(),query:z.string().trim().min(1).max(500).optional(),title:z.string().trim().min(1).max(200).optional(),location:z.string().trim().min(1).max(200).optional(),geoId:z.string().regex(/^\d{1,20}$/).optional(),industryIds:z.array(z.string().regex(/^\d{1,20}$/)).max(20).optional(),keywordsCompany:z.string().trim().min(1).max(200).optional(),currentCompany:z.string().trim().min(1).max(1024).optional(),companySizes:z.array(z.enum(['1-10','11-50','51-200','201-500','501-1000','1001-5000','5001-10000','10001+'])).max(8).optional(),page:page.page}).strict(),
|
|
19
|
+
'profiles.search':z.object({kind:z.enum(['person','company']).optional(),query:z.string().trim().min(1).max(500).optional(),title:z.string().trim().min(1).max(200).optional(),location:z.string().trim().min(1).max(200).optional(),geoId:z.string().regex(/^\d{1,20}$/).optional(),industryIds:z.array(z.string().regex(/^\d{1,20}$/)).max(20).optional(),keywordsCompany:z.string().trim().min(1).max(200).optional(),currentCompany:z.string().trim().min(1).max(1024).optional(),companySizes:z.array(z.enum(['1-10','11-50','51-200','201-500','501-1000','1001-5000','5001-10000','10001+'])).max(8).optional(),page:page.page,saveProfiles:z.boolean().optional()}).strict(),
|
|
19
20
|
'profiles.posts':z.object({profileId:uuid,topicId:uuid,postedLimit,...page}).strict(),
|
|
20
21
|
};
|
|
21
22
|
const contentOperations = Object.fromEntries(Object.entries(contentInputs).map(([tool,input])=>[`tools_${tool.replaceAll('.','_')}`,{tool,paid:true,schema:z.object({...workspace,requestId:uuid,input}).strict(),method:'POST',path:a=>root(a)+'/'+tool,body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000}]));
|
|
22
23
|
const recordsRoot = (a,kind) => `/workspaces/${a.workspaceId}/${kind}`;
|
|
23
24
|
const contactInputs = {
|
|
24
|
-
'email.find':z.object({profileId:uuid,domain:z.string().trim().min(1).max(253),refresh:z.boolean().optional()}).strict(),
|
|
25
|
-
'mobile.find':z.object({profileId:uuid,email:z.string().min(1).max(320).optional(),refresh:z.boolean().optional()}).strict(),
|
|
25
|
+
'email.find':z.object({profileId:uuid,domain:z.string().trim().min(1).max(253),refresh:z.boolean().optional(),cached:z.boolean().optional()}).strict(),
|
|
26
|
+
'mobile.find':z.object({profileId:uuid,email:z.string().min(1).max(320).optional(),refresh:z.boolean().optional(),cached:z.boolean().optional()}).strict(),
|
|
26
27
|
};
|
|
27
28
|
export const hostedToolOperations = {
|
|
28
29
|
...Object.fromEntries(Object.entries(contactInputs).map(([tool,input])=>[`tools_${tool.replaceAll('.','_')}`,{tool,paid:true,limits:{cacheDays:7,maximumAttempts:tool==='email.find'?130:3},schema:z.object({...workspace,requestId:uuid,input}).strict(),method:'POST',path:a=>root(a)+'/'+tool,body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:60000}])),
|
|
29
|
-
tools_email_verify:{tool:'email.verify',paid:true,limits:{providerCalls:1,cacheDays:7},schema:z.object({...workspace,requestId:uuid,input:z.object({email:z.string().max(320),refresh:z.boolean().optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/email.verify',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
30
|
+
tools_email_verify:{tool:'email.verify',paid:true,limits:{providerCalls:1,cacheDays:7},schema:z.object({...workspace,requestId:uuid,input:z.object({email:z.string().max(320),refresh:z.boolean().optional(),cached:z.boolean().optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/email.verify',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
30
31
|
...contentOperations,
|
|
31
32
|
topics_create:{traced:true,schema:z.object({...workspace,requestId:uuid,name:z.string().trim().min(1).max(200),slug:z.string().max(200).optional(),description:z.string().max(4000).optional()}).strict(),method:'POST',path:a=>recordsRoot(a,'topics'),body:({workspaceId,...a})=>a,scoped:true},
|
|
32
33
|
...Object.fromEntries(['topics','posts'].flatMap(kind=>[
|
|
@@ -34,12 +35,16 @@ export const hostedToolOperations = {
|
|
|
34
35
|
[`${kind}_get`,{traced:true,schema:z.object({...workspace,id:uuid}).strict(),method:'GET',path:a=>recordsRoot(a,kind)+'/'+a.id,scoped:true}],
|
|
35
36
|
])),
|
|
36
37
|
tools_list:{schema:z.object(workspace).strict(),method:'GET',path:root,scoped:true},
|
|
37
|
-
|
|
38
|
+
tools_search:{schema:z.object({...workspace,query:z.string().trim().min(1).max(200),limit:z.number().int().min(1).max(50).optional()}).strict(),method:'GET',path:a=>root(a)+'/search?'+new URLSearchParams({q:a.query,...a.limit!==undefined?{limit:String(a.limit)}:{}}),scoped:true,description:'One search across hosted tools, Audienti CLI commands and skills. Product words work (ICP finds audience tools, commands and skills; enrich finds profile tools) and so do provider names. Returns compact rows only: id, kind (tool, command or skill), one-line description and, for paid tools, the price per call or the metered unit. Then tools_get for a tool\'s inputs and tools_call to run it.'},
|
|
39
|
+
tools_get:{schema:z.object({...workspace,tool:toolId}).strict(),method:'GET',path:a=>root(a)+'/'+encodeURIComponent(a.tool),scoped:true,description:'One catalog tool: input JSON schema, limits, price per call, whether it is configured and available now, and this workspace\'s own 30-day call count, success rate and median speed.'},
|
|
40
|
+
tools_call:{paid:true,generic:true,schema:z.object({...workspace,tool:toolId,requestId:uuid,input:z.record(z.string(),z.unknown())}).strict(),method:'POST',path:a=>root(a)+'/call',body:a=>({tool:a.tool,requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:90000,description:'Run any catalog tool by id (audienti.<object>.<verb>) or old name (find ids with tools_search, inputs with tools_get). Record tools accept input cached:false to skip the saved record and make a fresh paid call. Charged through workspace credits like the per-tool operations. Generate requestId once and reuse it with the same input after any interruption or error: a replay is free and returns the saved result, a new ID is a new purchase. A timeout may have consumed paid work; inspect tools_runs_get rather than redispatching with a new ID. Positive credit balance is required for paid dispatch; completion may overdraw. Rejected means no provider dispatch.'},
|
|
41
|
+
tools_web_search:{tool:'web.search',paid:true,limits:{providerCalls:1,cacheHours:24},schema:z.object({...workspace,requestId:uuid,input:z.object({query:z.string().trim().min(1).max(1000),country:z.string().regex(/^[A-Za-z]{2}$/).optional(),language:z.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/).optional(),page:z.number().int().min(1).max(100).optional(),limit:z.number().int().min(1).max(10).optional(),timeRange:z.enum(['day','week','month','year']).optional(),refresh:z.boolean().optional(),cached:z.boolean().optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/web.search',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
38
42
|
tools_web_fetch:{tool:'web.fetch',paid:true,limits:{providerCalls:3,resultCache:false},schema:z.object({...workspace,requestId:uuid,input:z.object({url:z.string().min(1).max(2048)}).strict()}).strict(),method:'POST',path:a=>root(a)+'/web.fetch',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:90000},
|
|
39
43
|
tools_proxy_sessions_create:{tool:'proxy.sessions.create',paid:true,limits:{maxSeconds:3600,concurrentWorkspaceConnections:32,meter:'upstream_tcp_payload_bytes'},schema:z.object({...workspace,requestId:uuid,input:z.object({ttlSeconds:z.number().int().min(60).max(3600).optional(),parentOperationId:uuid.optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/proxy.sessions.create',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
40
|
-
tools_proxy_sessions_close:{schema:z.object({...workspace,operationId:uuid}).strict(),method:'POST',path:a=>root(a)+'/proxy.sessions.close',body:a=>({operationId:a.operationId}),scoped:true},
|
|
44
|
+
tools_proxy_sessions_close:{tool:'proxy.sessions.close',schema:z.object({...workspace,operationId:uuid}).strict(),method:'POST',path:a=>root(a)+'/proxy.sessions.close',body:a=>({operationId:a.operationId}),scoped:true},
|
|
41
45
|
tools_decide:{tool:'decide',paid:true,schema:z.object({...workspace,requestId:uuid,input:z.object({state:z.union([z.string(),z.record(z.string(),z.unknown()),z.array(z.unknown())]),questions:z.record(key,question).refine(v=>Object.keys(v).length>=1&&Object.keys(v).length<=16)}).strict()}).strict(),method:'POST',path:a=>root(a)+'/decide',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
42
|
-
|
|
46
|
+
tools_intent_label:{tool:'intent.label',paid:true,limits:{textCharacters:4000,contextCharacters:2000,suggestions:2},description:'Suggest what a message means. Paid: built on decide, billed on the input tokens the provider measures at the decide price (read it first with tools_get audienti.intent.label: tokenPrice). Give the message text, direction (inbound or outbound) and channel, and optionally context. Returns up to two suggestions from the intent label list (one reading, one meeting label), each with a quote that is words of the message, a plain reason, a confidence and any typed detail still to add (needs). Records nothing and sends nothing: record a suggestion you keep with intent_record, which runs every check again. Reuse requestId after any interruption; a replay is free.',schema:z.object({...workspace,requestId:uuid,input:z.object({text:z.string().min(1).max(4000),direction:z.enum(['inbound','outbound']),channel:z.enum(['email','linkedin']),context:z.string().trim().min(1).max(2000).optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/intent.label',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
47
|
+
tools_profiles_fetch:{tool:'profiles.fetch',paid:true,schema:z.object({...workspace,requestId:uuid,input:z.object({url:z.string().min(1).max(1024),refresh:z.boolean().optional(),cached:z.boolean().optional(),fields:z.array(z.enum(['name','headline','about','location','experience','currentPositions','education','skills','websites','photo','connections','followers','certifications','languages','industries','employeeCount','employeeCountRange','founded','specialities','locations','companyType'])).min(1).max(20).optional()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/profiles.fetch',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
|
|
43
48
|
tools_runs_get:{schema:z.object({...workspace,operationId:uuid,attemptAfter:z.number().int().min(0).max(2147483647).optional(),entryAfter:uuid.optional()}).strict(),method:'GET',path:a=>root(a)+`/runs/${a.operationId}`+((a.attemptAfter!==undefined||a.entryAfter)?`?${new URLSearchParams({...a.attemptAfter!==undefined?{attemptAfter:String(a.attemptAfter)}:{},...a.entryAfter?{entryAfter:a.entryAfter}:{}})}`:''),scoped:true},
|
|
44
49
|
tools_runs_list:{schema:z.object({...workspace,cursor:uuid.optional()}).strict(),method:'GET',path:a=>root(a)+'/runs'+(a.cursor?`?cursor=${encodeURIComponent(a.cursor)}`:''),scoped:true},
|
|
45
50
|
tools_balance:{schema:z.object(workspace).strict(),method:'GET',path:a=>root(a)+'/balance',scoped:true},
|
|
@@ -59,12 +64,16 @@ hostedToolOperations.tools_profiles_discover = {
|
|
|
59
64
|
path: a => root(a) + '/profiles.discover',
|
|
60
65
|
};
|
|
61
66
|
export const paidToolOperations = Object.fromEntries(Object.entries(hostedToolOperations).filter(([,operation])=>operation.paid));
|
|
62
|
-
export const paidToolByName = Object.fromEntries(Object.entries(paidToolOperations).map(([name,operation])=>[operation.tool,{name,...operation}]));
|
|
63
|
-
|
|
67
|
+
export const paidToolByName = Object.fromEntries(Object.entries(paidToolOperations).filter(([,operation])=>operation.tool).map(([name,operation])=>[operation.tool,{name,...operation}]));
|
|
68
|
+
// tools_call is paid except for the free proxy.sessions.close control call, which has no requestId or fee.
|
|
69
|
+
export const isPaidToolOperation = (name, args) => name === 'tools_call' ? !['proxy.sessions.close', 'audienti.proxy.session.close'].includes(args?.tool) : Object.hasOwn(paidToolOperations, name);
|
|
64
70
|
|
|
65
71
|
// Only known descriptor shape decides where a paid operation lives. Never infer
|
|
66
72
|
// a nested operation for unrelated responses or let projection state mask billing.
|
|
67
|
-
|
|
73
|
+
// Through tools_call the shape follows the resolved tool: profiles.enrich nests its operation.
|
|
74
|
+
export const paidToolResult = (name, data, args) => {
|
|
75
|
+
if (name === 'tools_call') return ['profiles.enrich', 'audienti.people.profile.enrich'].includes(args?.tool) ? data?.operation : data;
|
|
68
76
|
const descriptor = paidToolOperations[name];
|
|
69
77
|
return descriptor?.resultKey ? data?.[descriptor.resultKey] : data;
|
|
70
78
|
};
|
|
79
|
+
|
package/cli/tools.mjs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { hostedToolOperations, paidToolByName, isPaidToolOperation, paidToolResult } from './tools-operations.mjs';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
import { readJsonFile } from './json-file.mjs';
|
|
5
5
|
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
6
6
|
const help = `Usage:
|
|
7
|
-
audienti
|
|
8
|
-
audienti tools
|
|
9
|
-
audienti tools
|
|
10
|
-
audienti tools
|
|
7
|
+
audienti call TOOL ['JSON_INPUT'] [--input FILE|-] [--request-id UUID] [--workspace UUID] [--json]
|
|
8
|
+
audienti tools search "WORDS" [--limit N] [--workspace UUID] one server search over tools, audienti commands and skills; rows: id, kind, description, price
|
|
9
|
+
audienti tools get TOOL [--workspace UUID] one tool: inputs, price, availability, your 30-day success rate and speed
|
|
10
|
+
audienti tools list [--workspace UUID] [--json] the whole catalog
|
|
11
11
|
audienti tools runs get UUID [--attempt-after ORDINAL] [--entry-after UUID] | list [--cursor UUID]
|
|
12
12
|
audienti tools balance
|
|
13
|
+
audienti tools NAME --input FILE|- --request-id UUID [--workspace UUID] [--json] same as call NAME; kept for existing scripts
|
|
14
|
+
audienti tools proxy.sessions.close UUID [--workspace UUID]
|
|
15
|
+
|
|
16
|
+
The server catalog is the only source of tool names, inputs and prices: run tools search or tools get, then call.
|
|
17
|
+
call sends {tool,requestId,input} to one route for every tool. Published ids look like audienti.email.verify;
|
|
18
|
+
an old name (for example profiles.enrich) resolves on the server. A tool with no configured provider is not listed or callable.
|
|
19
|
+
Record tools (profile read and enrich, email and mobile find, email.verify, web.search) reuse a saved record when one is
|
|
20
|
+
current; put "cached":false in the input to skip it, make a fresh paid call and save the result.
|
|
21
|
+
Without --request-id, call generates one and prints it to stderr. Save it: replaying the same ID is free and
|
|
22
|
+
returns the saved result; a new ID is a new purchase.
|
|
13
23
|
|
|
14
24
|
email.find and mobile.find may return running: poll tools runs get OPERATION_ID.
|
|
15
25
|
Keep the same request ID after interruptions; never submit a new one to retry a purchase.
|
|
@@ -23,12 +33,12 @@ profiles.fetch accepts a person /in/ or company /company/ URL as {url,fields?,re
|
|
|
23
33
|
Explicit refresh requires paid admission; the result includes saved profile/prospect IDs.
|
|
24
34
|
Fields: name, headline, about, location, experience, currentPositions, education, skills,
|
|
25
35
|
websites, photo, connections, followers, certifications, languages. Company-specific fields:
|
|
26
|
-
industries, employeeCount, employeeCountRange, founded, specialities, locations.
|
|
36
|
+
industries, employeeCount, employeeCountRange, founded, specialities, locations, companyType.
|
|
27
37
|
profiles.enrich accepts the same fields as profiles.fetch, using url or saved profileId.
|
|
28
38
|
Its operation has the same admission identity as profiles.fetch; the Prospect projection is a current free read.
|
|
29
39
|
profiles.discover is the clear external-search alias of profiles.search; profiles list searches saved workspace records.
|
|
30
40
|
profiles.search accepts person filters or {kind:"company",query,companySizes?}; results are partial profiles.
|
|
31
|
-
Use
|
|
41
|
+
Use tools get TOOL for exact fields. No implicit email lookup.
|
|
32
42
|
decide accepts {state,questions}; question types: boolean, choice, score.
|
|
33
43
|
Questions have instructions; choice requires named criteria; score criteria are ordered
|
|
34
44
|
from level zero. State is text, an object or an array. At most 32 KiB, 16 questions,
|
|
@@ -43,13 +53,41 @@ async function readStdin() {
|
|
|
43
53
|
for await(const chunk of process.stdin){size+=chunk.length;if(size>32768)throw new Error('Tool input exceeds 32 KiB.');chunks.push(chunk);}
|
|
44
54
|
try{return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(Buffer.concat(chunks)));}catch{throw new Error('Tool input must be valid UTF-8 JSON.');}
|
|
45
55
|
}
|
|
56
|
+
const asJson=value=>{try{return JSON.parse(value);}catch{throw new Error('Tool input must be valid JSON.');}};
|
|
57
|
+
export async function printPaid(name,args,values){
|
|
58
|
+
try{
|
|
59
|
+
const result=await callWorkspace(name,args,configuration(values.profile));
|
|
60
|
+
const paidResult=paidToolResult(name,result?.data,args);
|
|
61
|
+
if(['failed','uncertain','rejected'].includes(paidResult?.status)){
|
|
62
|
+
process.stderr.write(JSON.stringify({error:{code:paidResult.error?.code??'tool_incomplete'},operationId:paidResult.operationId,requestId:args.requestId,billing:paidResult.billing,...(paidResult.result?.persistence?{result:paidResult.result}:{}),requestTraceId:result.requestTraceId,recovery:paidResult.status==='rejected'?'resolve_rejection_then_new_request_id':paidResult.result?.persistence?.recovery??'retry_same_request_id'})+'\n');if(values.explain)process.stderr.write(values.explain(paidResult)+'\n');process.exitCode=1;return;
|
|
63
|
+
}
|
|
64
|
+
console.log(JSON.stringify(result));
|
|
65
|
+
}catch(error){
|
|
66
|
+
if(isPaidToolOperation(name,args))process.stderr.write(`Retain request ID ${args.requestId}; retry with the same ID to inspect/recover this operation.\n`);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parseOptions={help:{type:'boolean',short:'h'},json:{type:'boolean'},input:{type:'string'},'request-id':{type:'string'},workspace:{type:'string'},profile:{type:'string'},cursor:{type:'string'},limit:{type:'string'},'attempt-after':{type:'string'},'entry-after':{type:'string'}};
|
|
71
|
+
/** audienti call TOOL ['JSON'] : one generic route for every catalog tool. */
|
|
72
|
+
export async function callCli(argv) {
|
|
73
|
+
const {positionals,values}=parseArgs({args:argv.slice(1),allowPositionals:true,strict:true,options:parseOptions});
|
|
74
|
+
if(values.help||positionals.length===0){console.log(help);return;}
|
|
75
|
+
const [tool,inline]=positionals;
|
|
76
|
+
if(positionals.length>2||(inline!==undefined&&values.input))throw new Error('Give the input once: a JSON argument or --input FILE|-.');
|
|
77
|
+
const input=inline!==undefined?asJson(inline):values.input?(values.input==='-'?await readStdin():readJsonFile(values.input,32768,'Tool input')):{};
|
|
78
|
+
if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('Tool input must be a JSON object.');
|
|
79
|
+
const requestId=values['request-id']??randomUUID();
|
|
80
|
+
if(!values['request-id'])process.stderr.write(`request-id: ${requestId}\n`);
|
|
81
|
+
await printPaid('tools_call',{workspaceId:values.workspace,tool,requestId,input},values);
|
|
82
|
+
}
|
|
46
83
|
export async function toolsCli(argv) {
|
|
47
|
-
const {positionals,values}=parseArgs({args:argv.slice(1),allowPositionals:true,strict:true,options:
|
|
84
|
+
const {positionals,values}=parseArgs({args:argv.slice(1),allowPositionals:true,strict:true,options:parseOptions});
|
|
48
85
|
if(values.help||positionals.length===0){console.log(help);return;}
|
|
49
86
|
const [verb,sub,id]=positionals;
|
|
50
|
-
if(verb==='describe'&&Object.hasOwn(paidToolByName,sub)&&positionals.length===2){console.log(JSON.stringify({name:sub,version:'1',canonicalTool:paidToolByName[sub].canonicalTool??sub,responseOperationPath:paidToolByName[sub].resultKey??null,inputSchema:z.toJSONSchema(paidToolByName[sub].schema.shape.input),limits:paidToolByName[sub].limits??(sub==='decide'?{bytes:32768,depth:32,questions:16,choices:32,scoreLevels:10}:sub==='profiles.fetch'?{providerCalls:1,normalizedBytes:65536,cacheDays:30}:{providerCalls:1,resultCache:false}),retry:'Reuse the same requestId and authenticated principal after interruptions.'},null,values.json?undefined:2));return;}
|
|
51
87
|
let name,args={workspaceId:values.workspace};
|
|
52
88
|
if(verb==='list'&&positionals.length===1)name='tools_list';
|
|
89
|
+
else if(verb==='search'&&positionals.length>=2){name='tools_search';args.query=positionals.slice(1).join(' ');if(values.limit!==undefined)args.limit=Number(values.limit);}
|
|
90
|
+
else if((verb==='get'||verb==='describe')&&positionals.length===2){name='tools_get';args.tool=sub;}
|
|
53
91
|
else if(verb==='balance'&&positionals.length===1)name='tools_balance';
|
|
54
92
|
else if(verb==='runs'&&sub==='get'&&positionals.length===3){name='tools_runs_get';args.operationId=id;args.entryAfter=values['entry-after'];if(values['attempt-after']!==undefined)args.attemptAfter=Number(values['attempt-after']);}
|
|
55
93
|
else if(verb==='runs'&&sub==='list'&&positionals.length===2){name='tools_runs_list';args.cursor=values.cursor;}
|
|
@@ -58,15 +96,5 @@ export async function toolsCli(argv) {
|
|
|
58
96
|
if(!values.input||!values['request-id'])throw new Error(`${verb} requires --input FILE|- and --request-id UUID. Keep the request ID for retries.`);
|
|
59
97
|
name=paidToolByName[verb].name;args.requestId=values['request-id'];args.input=values.input==='-'?await readStdin():readJsonFile(values.input,32768,'Tool input');
|
|
60
98
|
}else throw new Error('Unknown tools command. Use audienti tools --help.');
|
|
61
|
-
|
|
62
|
-
const result=await callWorkspace(name,args,configuration(values.profile));
|
|
63
|
-
const paidResult = paidToolResult(name,result?.data);
|
|
64
|
-
if(['failed','uncertain','rejected'].includes(paidResult?.status)){
|
|
65
|
-
process.stderr.write(JSON.stringify({error:{code:paidResult.error?.code??'tool_incomplete'},operationId:paidResult.operationId,requestId:args.requestId,billing:paidResult.billing,...(paidResult.result?.persistence?{result:paidResult.result}:{}),requestTraceId:result.requestTraceId,recovery:paidResult.status==='rejected'?'resolve_rejection_then_new_request_id':paidResult.result?.persistence?.recovery??'retry_same_request_id'})+'\n');process.exitCode=1;return;
|
|
66
|
-
}
|
|
67
|
-
console.log(JSON.stringify(result));
|
|
68
|
-
}catch(error){
|
|
69
|
-
if(isPaidToolOperation(name))process.stderr.write(`Retain request ID ${args.requestId}; retry with the same ID to inspect/recover this operation.\n`);
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
99
|
+
await printPaid(name,args,values);
|
|
72
100
|
}
|
|
@@ -25,8 +25,15 @@ const body=({workspaceId,planId,clientActionId,attemptId,...rest})=>rest;
|
|
|
25
25
|
export const workspaceExecutionOperations={
|
|
26
26
|
workspace_work_claim:{schema:z.object({...location,expectedRevision:z.number().int().positive(),requestId:uuid}).strict(),scoped:true,method:'POST',path:a=>`${path(a)}/claims`,body},
|
|
27
27
|
workspace_work_start:{schema:z.object({...location,claimId:uuid,requestId:uuid}).strict(),scoped:true,method:'POST',path:a=>`${path(a)}/start`,body},
|
|
28
|
+
workspace_work_send:{schema:z.object({...location,claimId:uuid,requestId:uuid}).strict(),scoped:true,method:'POST',path:a=>`${path(a)}/send`,body,description:'LinkedIn accounts connected through Audienti only. Starts the claimed, approved action and sends it once through the managed account, then records the result. Replaying the same claim returns the recorded send; it never sends again.'},
|
|
28
29
|
workspace_work_attempt_get:{schema:z.object({...location,historyCursor:uuid.optional(),historyLimit:z.number().int().min(1).max(100).optional()}).strict(),scoped:true,method:'GET',path:a=>{const q=new URLSearchParams();for(const key of ['historyCursor','historyLimit'])if(a[key]!==undefined)q.set(key,String(a[key]));return `${path(a)}/attempt?${q}`;}},
|
|
29
30
|
};
|
|
31
|
+
// LinkedIn accounts connected through Audienti. Only the user connects one; agents list them and send through work send.
|
|
32
|
+
export const managedAccountOperations={
|
|
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.'},
|
|
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.'},
|
|
36
|
+
};
|
|
30
37
|
export const narrowExecutionOperations={
|
|
31
38
|
work_claim_release:{schema:z.object({claimId:uuid,requestId:uuid,reason}).strict(),method:'POST',path:a=>`/work-execution/claims/${a.claimId}/release`,body:({claimId,...rest})=>rest},
|
|
32
39
|
work_attempt_lookup:{schema:z.object({claimId:uuid}).strict(),method:'GET',path:a=>`/work-execution/claims/${a.claimId}/attempt`},
|