@audienti/cli 0.1.145 → 0.2.0-exora.2

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.
Files changed (66) hide show
  1. package/README.md +47 -895
  2. package/bin/audienti-mcp.mjs +2 -0
  3. package/cli/account-intake-operations.mjs +28 -0
  4. package/cli/account-intake.mjs +33 -0
  5. package/cli/account-replies.mjs +35 -0
  6. package/cli/account-reply-operations.mjs +30 -0
  7. package/cli/agents.mjs +107 -0
  8. package/cli/approach-operations.mjs +22 -0
  9. package/cli/assignment-operations.mjs +218 -0
  10. package/cli/assignment.mjs +116 -0
  11. package/cli/audience-operations.mjs +16 -0
  12. package/cli/audiences.mjs +26 -0
  13. package/cli/audienti.mjs +124 -0
  14. package/cli/connection-registry-operations.mjs +7 -0
  15. package/cli/content.mjs +29 -0
  16. package/cli/discovery-operations.mjs +27 -0
  17. package/cli/discovery.mjs +14 -0
  18. package/cli/experiment-operations.mjs +25 -0
  19. package/cli/experiments.mjs +26 -0
  20. package/cli/json-file.mjs +13 -0
  21. package/cli/mcp.mjs +47 -0
  22. package/cli/offer-operations.mjs +12 -0
  23. package/cli/offers.mjs +24 -0
  24. package/cli/owner-account-client.mjs +18 -0
  25. package/cli/profiles.mjs +29 -0
  26. package/cli/progress.mjs +12 -0
  27. package/cli/prospecting-settings-operations.mjs +16 -0
  28. package/cli/prospecting-settings.mjs +15 -0
  29. package/cli/recipient-operations.mjs +13 -0
  30. package/cli/recipients.mjs +30 -0
  31. package/cli/sender-account-operations.mjs +24 -0
  32. package/cli/sender-accounts.mjs +23 -0
  33. package/cli/skills-mcp.mjs +24 -0
  34. package/cli/skills.mjs +56 -0
  35. package/cli/suppression-operations.mjs +8 -0
  36. package/cli/suppression.mjs +12 -0
  37. package/cli/tools-operations.mjs +70 -0
  38. package/cli/tools.mjs +72 -0
  39. package/cli/work-execution-operations.mjs +35 -0
  40. package/cli/work-execution.mjs +29 -0
  41. package/cli/work-feedback-operations.mjs +17 -0
  42. package/cli/work-feedback.mjs +18 -0
  43. package/cli/work-operations.mjs +28 -0
  44. package/cli/work.mjs +27 -0
  45. package/cli/workspaces.mjs +158 -0
  46. package/package.json +15 -26
  47. package/skills/audienti/SKILL.md +61 -323
  48. package/skills/audienti/references/helpers.md +149 -0
  49. package/skills/audienti/references/profile-workflows.md +212 -0
  50. package/skills/audienti/references/workflow-receipt.md +49 -0
  51. package/skills/audienti/scripts/contracts.mjs +72 -0
  52. package/skills/audienti/scripts/profile-helpers.mjs +138 -0
  53. package/skills/audienti/scripts/run.mjs +40 -0
  54. package/source-manifest.json +60 -0
  55. package/.mcp.json +0 -13
  56. package/CHANGELOG.md +0 -1018
  57. package/bin/audienti-mcp.js +0 -5
  58. package/bin/audienti.js +0 -6
  59. package/install +0 -83
  60. package/skills/audienti/references/methodology.md +0 -111
  61. package/skills/audienti-gift-research/SKILL.md +0 -47
  62. package/src/agent-skills.js +0 -60
  63. package/src/api-client.js +0 -1639
  64. package/src/cli.js +0 -16992
  65. package/src/config.js +0 -130
  66. package/src/mcp.js +0 -103
@@ -0,0 +1,24 @@
1
+ import {z} from 'zod';
2
+ const text=(min,max)=>z.string().meta({minLength:min,maxLength:max});
3
+ const uuid=z.string().uuid(),version=z.number().int().min(0).max(2147483646);
4
+ const base={requestId:uuid,reason:text(1,2000).describe('Trimmed NFC; no controls including interior newlines or tabs. Does not authorize sending.')};
5
+ const alias={provider:z.enum(['email','linkedin']),accountRef:text(1,200).regex(/^[A-Za-z0-9@._:+\/-]+$/),expectedAliasVersion:version};
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
+ const actionType=z.enum(['email.send','linkedin.send']);
8
+ const pageLimit=z.number().int().min(1).max(100).optional();
9
+ const query=a=>{const q=new URLSearchParams(Object.entries(a).map(([k,v])=>[k,String(v)]));return q.size?'?'+q:'';};
10
+ const path=a=>`/sender-accounts/${encodeURIComponent(a.senderAccountId)}`;
11
+ const body=a=>Object.fromEntries(Object.entries(a).filter(([key])=>!['senderAccountId','actionType'].includes(key)));
12
+ const opaque=z.string().trim().min(1).max(256).regex(/^[ -~]+$/);
13
+ export const senderAccountOperations={
14
+ 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
+ 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
+ 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_alias_bind:{schema:z.object({...base,...alias,senderAccountId:uuid,agentId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases',body,ownerOnly:true},
20
+ sender_alias_unbind:{schema:z.object({...base,...alias,senderAccountId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases/unbind',body,ownerOnly:true},
21
+ 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},
22
+ sender_automation_get:{schema:z.object({senderAccountId:uuid,actionType}).strict(),method:'GET',path:a=>path(a)+'/automation/'+encodeURIComponent(a.actionType)},
23
+ 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},
24
+ };
@@ -0,0 +1,23 @@
1
+ import {senderAccountOperations} from './sender-account-operations.mjs';
2
+ import {callOwnerAccountOperation} from './owner-account-client.mjs';
3
+ import {readJsonFile} from './json-file.mjs';
4
+ import {WorkspaceRequestError} from './workspaces.mjs';
5
+ export async function callSenderAccounts(name,args,profilePath){
6
+ return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account');
7
+ }
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'];
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]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|automation.');
12
+ const automation=positions[2]==='automation',activity=positions[2]==='activity',nested=automation||activity,command=nested?`${positions[2]}-${positions[3]}`:positions[2];
13
+ 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','automation-get':'sender_automation_get','automation-set':'sender_automation_set'};
14
+ 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']:[])];
17
+ 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.');}
19
+ 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'];
21
+ 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
+ console.log(JSON.stringify(await callSenderAccounts(name,input,flags.profile??process.env.AUDIENTI_PROFILE)));
23
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod';
2
+ import { helperContracts, SKILL_VERSION } from '../skills/audienti/scripts/contracts.mjs';
3
+ import { runHelper } from '../skills/audienti/scripts/profile-helpers.mjs';
4
+ import { skillArtifact, skillCatalog, skillDetails } from './skills.mjs';
5
+
6
+ const annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
7
+ const result = value => ({ content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value });
8
+
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()));
11
+ server.registerTool('skills_show', { description: 'Read bundled skill instructions, references, version, digest and helper schemas.', inputSchema: z.object({ id: z.literal('audienti') }).strict(), annotations }, async ({ id }) => result(skillDetails(id)));
12
+ for (const [name, contract] of Object.entries(helperContracts)) {
13
+ server.registerTool(`skills_${name.replaceAll('-', '_')}`, {
14
+ description: contract.description,
15
+ inputSchema: z.fromJSONSchema(contract.inputSchema), outputSchema: z.fromJSONSchema(contract.outputSchema), annotations,
16
+ }, async input => {
17
+ try { return result(runHelper(name, input)); }
18
+ catch { return { isError: true, content: [{ type: 'text', text: 'Invalid helper input; inspect its bundled schema and the 1 MiB limit.' }] }; }
19
+ });
20
+ }
21
+ server.registerResource('audienti-skill', `audienti://skills/audienti/${SKILL_VERSION}`, {
22
+ description: 'Versioned public skill artifact: instructions, references, scripts and schemas. Reading it does not install or execute anything.', mimeType: 'application/json',
23
+ }, async uri => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(skillArtifact()) }] }));
24
+ }
package/cli/skills.mjs ADDED
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { SKILL_VERSION, helperContracts } from '../skills/audienti/scripts/contracts.mjs';
4
+ import { executeHelperFile } from '../skills/audienti/scripts/run.mjs';
5
+
6
+ const root = new URL('../skills/audienti/', import.meta.url);
7
+ const files = ['SKILL.md', 'references/profile-workflows.md', 'references/helpers.md', 'references/workflow-receipt.md', 'scripts/contracts.mjs', 'scripts/profile-helpers.mjs', 'scripts/run.mjs'];
8
+
9
+ function readSkill(id = 'audienti') {
10
+ if (id !== 'audienti') throw new Error('Unknown bundled skill. Use skills list.');
11
+ const contents = files.map(path => ({ path, text: readFileSync(new URL(path, root), 'utf8') }));
12
+ const hash = createHash('sha256');
13
+ for (const file of contents) hash.update(`${file.path}\0${Buffer.byteLength(file.text)}\0`).update(file.text);
14
+ return { contents, details: {
15
+ id, version: SKILL_VERSION, digest: hash.digest('hex'),
16
+ description: 'Agent-owned profile research with deterministic local helpers and guarded CRM writes.',
17
+ instructions: contents[0].text,
18
+ references: contents.filter(file => file.path.startsWith('references/')),
19
+ helpers: Object.entries(helperContracts).map(([name, contract]) => ({ name, ...contract })),
20
+ } };
21
+ }
22
+
23
+ export function skillDetails(id = 'audienti') { return readSkill(id).details; }
24
+
25
+ export function skillCatalog() {
26
+ const { instructions, references, ...entry } = skillDetails();
27
+ return { skills: [entry] };
28
+ }
29
+
30
+ export function skillArtifact(id = 'audienti') {
31
+ const { details, contents } = readSkill(id);
32
+ return { ...details, files: contents.map(({ path, text }) => ({ path, content: text })) };
33
+ }
34
+
35
+ export async function skillsCli(argv) {
36
+ const args = argv.slice(1), json = args.includes('--json');
37
+ const filtered = args.filter(value => value !== '--json');
38
+ if (args.filter(value => value === '--json').length > 1) throw new Error('Invalid skills options.');
39
+ if (filtered.length === 1 && ['--help', 'help'].includes(filtered[0])) {
40
+ process.stdout.write('Usage: audienti skills list | show audienti | helper NAME --file INPUT.json|- [--json]\nLocal skill instructions/helpers; no login, installation or provider calls.\n'); return;
41
+ }
42
+ if (filtered[0] === 'list' && filtered.length === 1) {
43
+ const catalog = skillCatalog();
44
+ process.stdout.write(json ? `${JSON.stringify(catalog)}\n` : catalog.skills.map(skill => `${skill.id}@${skill.version} — ${skill.description}\n`).join('')); return;
45
+ }
46
+ if (filtered[0] === 'show' && filtered.length === 2) {
47
+ const details = skillDetails(filtered[1]);
48
+ process.stdout.write(json ? `${JSON.stringify(details)}\n` : `${details.instructions}\n`); return;
49
+ }
50
+ if (filtered[0] === 'helper' && filtered.length === 4 && filtered[2] === '--file') {
51
+ try { process.stdout.write(`${JSON.stringify(executeHelperFile(filtered[1], filtered[3]))}\n`); }
52
+ catch { throw new Error('Skill helper failed. Inspect its schema and provide valid JSON of at most 1 MiB.'); }
53
+ return;
54
+ }
55
+ throw new Error('Usage: audienti skills list | show audienti | helper NAME --file INPUT.json|- [--json]');
56
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ const uuid=z.string().uuid();
3
+ const located={workspaceId:uuid.optional(),prospectId:uuid};
4
+ const path=a=>`/workspaces/${a.workspaceId}/prospects/${a.prospectId}/suppression`;
5
+ export const suppressionOperations={
6
+ workspace_prospect_suppression:{schema:z.object({...located,history:z.object({cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict().optional()}).strict(),method:'GET',path:a=>{const q=new URLSearchParams();if(a.history?.cursor)q.set('historyCursor',a.history.cursor);if(a.history?.limit!==undefined)q.set('historyLimit',String(a.history.limit));return path(a)+(q.size?'?'+q:'');},scoped:true},
7
+ workspace_prospect_suppression_set:{schema:z.object({...located,kind:z.enum(['dnc','reject']),action:z.enum(['set','clear']),expectedVersion:z.number().int().nonnegative(),requestId:uuid,reason:z.string().optional()}).strict(),method:'POST',path,body:({workspaceId,prospectId,...body})=>body,scoped:true},
8
+ };
@@ -0,0 +1,12 @@
1
+ import { callWorkspace, configuration } from './workspaces.mjs';
2
+ export async function suppressionCli(args){
3
+ const flags={},positions=[];
4
+ const allowed=['workspace','profile','history-cursor','history-limit','kind','action','expected-version','request-id','reason'];
5
+ 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(!allowed.includes(key)||Object.hasOwn(flags,key)||args[i+1]===undefined||args[i+1].startsWith('--'))throw Error('Unknown, repeated or incomplete suppression option.');flags[key]=args[++i];}
6
+ if(positions.length!==4||positions[0]!=='prospects'||positions[1]!=='suppression'||!['get','set'].includes(positions[2]))throw Error('Use prospects suppression get|set ID --workspace ID.');
7
+ const read=positions[2]==='get';const valid=['workspace','profile',...(read?['history-cursor','history-limit']:['kind','action','expected-version','request-id','reason'])];if(Object.keys(flags).some(k=>!valid.includes(k)))throw Error('Option does not apply to this command.');
8
+ const input={workspaceId:flags.workspace,prospectId:positions[3]};
9
+ if(read)input.history={...(flags['history-cursor']===undefined?{}:{cursor:flags['history-cursor']}),...(flags['history-limit']===undefined?{}:{limit:Number(flags['history-limit'])})};
10
+ else Object.assign(input,{kind:flags.kind,action:flags.action,expectedVersion:flags['expected-version']===undefined?undefined:Number(flags['expected-version']),requestId:flags['request-id'],...(flags.reason===undefined?{}:{reason:flags.reason})});
11
+ console.log(JSON.stringify(await callWorkspace(read?'workspace_prospect_suppression':'workspace_prospect_suppression_set',input,configuration(flags.profile))));
12
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ const uuid = z.string().uuid(), workspace = { workspaceId: uuid.optional() };
3
+ const root = a => `/workspaces/${a.workspaceId}/tools`;
4
+ const key = z.string().regex(/^[a-z][a-z0-9_]{0,47}$/).refine(v => !['constructor','prototype'].includes(v));
5
+ const instruction = z.string().trim().min(1).max(4000);
6
+ const question = z.discriminatedUnion('type', [
7
+ z.object({type:z.literal('boolean'),instructions:instruction,criteria:z.object({true:instruction.optional(),false:instruction.optional()}).strict().optional()}).strict(),
8
+ z.object({type:z.literal('choice'),instructions:instruction,criteria:z.record(key,z.string().max(2000)).refine(v=>Object.keys(v).length>=1&&Object.keys(v).length<=32)}).strict(),
9
+ z.object({type:z.literal('score'),instructions:instruction,criteria:z.array(instruction).min(1).max(10)}).strict(),
10
+ ]);
11
+ const page = { page:z.number().int().min(1).max(10000).optional(), paginationToken:z.string().min(1).max(4096).optional() };
12
+ const postedLimit = z.enum(['24h','week','month']).optional();
13
+ const contentInputs = {
14
+ 'posts.fetch':z.object({url:z.string().min(1).max(2048),topicId:uuid}).strict(),
15
+ 'posts.search':z.object({query:z.string().trim().min(1).max(500),topicId:uuid,postedLimit,...page,sortBy:z.enum(['relevance','date']).optional()}).strict(),
16
+ 'posts.comments':z.object({postId:uuid,...page}).strict(),
17
+ '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.posts':z.object({profileId:uuid,topicId:uuid,postedLimit,...page}).strict(),
20
+ };
21
+ 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
+ const recordsRoot = (a,kind) => `/workspaces/${a.workspaceId}/${kind}`;
23
+ 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(),
26
+ };
27
+ export const hostedToolOperations = {
28
+ ...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
+ ...contentOperations,
31
+ 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
+ ...Object.fromEntries(['topics','posts'].flatMap(kind=>[
33
+ [`${kind}_list`,{traced:true,schema:z.object({...workspace,cursor:uuid.optional(),...(kind==='posts'?{topicId:uuid.optional()}:{})}).strict(),method:'GET',path:a=>recordsRoot(a,kind)+'?'+new URLSearchParams(Object.fromEntries(Object.entries({cursor:a.cursor,topicId:a.topicId}).filter(([,v])=>v))),scoped:true}],
34
+ [`${kind}_get`,{traced:true,schema:z.object({...workspace,id:uuid}).strict(),method:'GET',path:a=>recordsRoot(a,kind)+'/'+a.id,scoped:true}],
35
+ ])),
36
+ tools_list:{schema:z.object(workspace).strict(),method:'GET',path:root,scoped:true},
37
+ 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()}).strict()}).strict(),method:'POST',path:a=>root(a)+'/web.search',body:a=>({requestId:a.requestId,input:a.input}),scoped:true,timeoutMs:30000},
38
+ 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
+ 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},
41
+ 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
+ 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(),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'])).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
+ 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
+ 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
+ tools_balance:{schema:z.object(workspace).strict(),method:'GET',path:a=>root(a)+'/balance',scoped:true},
46
+ };
47
+
48
+ hostedToolOperations.tools_profiles_search.description = 'Legacy external paid Profile search, not saved database search. Retrieves one fresh provider page. Reuse requestId after interruptions; a timeout may have consumed paid work. Inspect tools_runs_get rather than redispatching with a new ID.';
49
+ // Compatibility aliases share server admission identity; response shape is explicit.
50
+ const fetchDescriptor = hostedToolOperations.tools_profiles_fetch;
51
+ const fetchInput = fetchDescriptor.schema.shape.input;
52
+ hostedToolOperations.tools_profiles_enrich = {
53
+ ...fetchDescriptor, tool: 'profiles.enrich', description: 'Explicit paid Profile enrichment by URL or authorized saved Profile ID; eligible 30-day cache reuse is free and refresh forces paid admission. Returns operation plus current workspace Prospect/profile projection. Shares admission identity with profiles.fetch. Reuse requestId after interruptions; a timeout may have consumed paid work. Inspect tools_runs_get rather than redispatching with a new ID.', limits: { providerCalls: 1, normalizedBytes: 65536, cacheDays: 30 }, resultKey: 'operation', canonicalTool: 'profiles.fetch',
54
+ schema: fetchDescriptor.schema.extend({ input: z.union([fetchInput, fetchInput.omit({url:true}).extend({profileId:uuid}).strict()]) }),
55
+ path: a => root(a) + '/profiles.enrich',
56
+ };
57
+ hostedToolOperations.tools_profiles_discover = {
58
+ ...hostedToolOperations.tools_profiles_search, tool: 'profiles.discover', description: 'Explicit paid external Profile discovery. Same admission identity and result as legacy profiles.search. For saved workspace profiles use profile_list. Reuse requestId after interruptions; a timeout may have consumed paid work. Inspect tools_runs_get rather than redispatching with a new ID.', canonicalTool: 'profiles.search',
59
+ path: a => root(a) + '/profiles.discover',
60
+ };
61
+ 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
+ export const isPaidToolOperation = name => Object.hasOwn(paidToolOperations,name);
64
+
65
+ // Only known descriptor shape decides where a paid operation lives. Never infer
66
+ // a nested operation for unrelated responses or let projection state mask billing.
67
+ export const paidToolResult = (name, data) => {
68
+ const descriptor = paidToolOperations[name];
69
+ return descriptor?.resultKey ? data?.[descriptor.resultKey] : data;
70
+ };
package/cli/tools.mjs ADDED
@@ -0,0 +1,72 @@
1
+ import { z } from 'zod';
2
+ import { hostedToolOperations, paidToolByName, isPaidToolOperation, paidToolResult } from './tools-operations.mjs';
3
+ import { parseArgs } from 'node:util';
4
+ import { readJsonFile } from './json-file.mjs';
5
+ import { callWorkspace, configuration } from './workspaces.mjs';
6
+ const help = `Usage:
7
+ audienti tools list [--workspace UUID] [--json]
8
+ audienti tools proxy.sessions.close UUID [--workspace UUID]
9
+ audienti tools describe decide|email.verify|email.find|mobile.find|profiles.fetch|posts.fetch|posts.search|posts.comments|posts.reactions|profiles.posts|profiles.search|proxy.sessions.create|web.search|web.fetch
10
+ audienti tools decide|email.verify|email.find|mobile.find|profiles.fetch|posts.fetch|posts.search|posts.comments|posts.reactions|profiles.posts|profiles.search|proxy.sessions.create|web.search|web.fetch --input FILE|- --request-id UUID [--workspace UUID] [--json]
11
+ audienti tools runs get UUID [--attempt-after ORDINAL] [--entry-after UUID] | list [--cursor UUID]
12
+ audienti tools balance
13
+
14
+ email.find and mobile.find may return running: poll tools runs get OPERATION_ID.
15
+ Keep the same request ID after interruptions; never submit a new one to retry a purchase.
16
+
17
+ web.search returns one selected result page; workspace cache lasts 24 hours (refresh=true bypasses it).
18
+ web.fetch returns inert text/links through a bounded HTML waterfall. Each completed logical
19
+ web result costs 0.1 credit ($1 per 1000); failed requests, cache hits and request replays are free.
20
+ Uses the selected private profile/workspace. Supply --profile PATH when needed.
21
+ email.verify accepts {email,refresh?}; it records provider deliverability evidence without establishing ownership or authorizing sending. It reuses workspace validation for seven days; refresh makes a configured paid validation. Cache and same-request replay are free.
22
+ profiles.fetch accepts a person /in/ or company /company/ URL as {url,fields?,refresh?}; a complete saved profile is free for 30 days.
23
+ Explicit refresh requires paid admission; the result includes saved profile/prospect IDs.
24
+ Fields: name, headline, about, location, experience, currentPositions, education, skills,
25
+ websites, photo, connections, followers, certifications, languages. Company-specific fields:
26
+ industries, employeeCount, employeeCountRange, founded, specialities, locations.
27
+ profiles.enrich accepts the same fields as profiles.fetch, using url or saved profileId.
28
+ Its operation has the same admission identity as profiles.fetch; the Prospect projection is a current free read.
29
+ profiles.discover is the clear external-search alias of profiles.search; profiles list searches saved workspace records.
30
+ profiles.search accepts person filters or {kind:"company",query,companySizes?}; results are partial profiles.
31
+ Use describe for exact fields. No implicit email lookup.
32
+ decide accepts {state,questions}; question types: boolean, choice, score.
33
+ Questions have instructions; choice requires named criteria; score criteria are ordered
34
+ from level zero. State is text, an object or an array. At most 32 KiB, 16 questions,
35
+ 32 choices per question and 10 score levels. No provider credentials belong in input.
36
+ The request ID must be saved and reused after a timeout or interruption; never create a
37
+ new ID just to retry an uncertain operation. A rejected operation made no provider call;
38
+ after fixing its cause (for example adding credits), use a new ID for new work. Results and diagnostics contain trace IDs.
39
+ Example input: {"state":"Can we talk?","questions":{"interest":{"type":"boolean","instructions":"Does this express interest?"}}}
40
+ JSON results go to stdout; failures go to stderr and exit nonzero.`;
41
+ async function readStdin() {
42
+ const chunks=[];let size=0;
43
+ 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
+ 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
+ }
46
+ export async function toolsCli(argv) {
47
+ const {positionals,values}=parseArgs({args:argv.slice(1),allowPositionals:true,strict:true,options:{help:{type:'boolean',short:'h'},json:{type:'boolean'},input:{type:'string'},'request-id':{type:'string'},workspace:{type:'string'},profile:{type:'string'},cursor:{type:'string'},'attempt-after':{type:'string'},'entry-after':{type:'string'}}});
48
+ if(values.help||positionals.length===0){console.log(help);return;}
49
+ 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
+ let name,args={workspaceId:values.workspace};
52
+ if(verb==='list'&&positionals.length===1)name='tools_list';
53
+ else if(verb==='balance'&&positionals.length===1)name='tools_balance';
54
+ 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
+ else if(verb==='runs'&&sub==='list'&&positionals.length===2){name='tools_runs_list';args.cursor=values.cursor;}
56
+ else if(verb==='proxy.sessions.close'&&positionals.length===2){name='tools_proxy_sessions_close';args.operationId=sub;}
57
+ else if(Object.hasOwn(paidToolByName,verb)&&positionals.length===1){
58
+ if(!values.input||!values['request-id'])throw new Error(`${verb} requires --input FILE|- and --request-id UUID. Keep the request ID for retries.`);
59
+ name=paidToolByName[verb].name;args.requestId=values['request-id'];args.input=values.input==='-'?await readStdin():readJsonFile(values.input,32768,'Tool input');
60
+ }else throw new Error('Unknown tools command. Use audienti tools --help.');
61
+ try{
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
+ }
72
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ const uuid=z.string().uuid(), time=z.iso.datetime({offset:true}), version=z.number().int().min(0).max(2147483646);
3
+ const reason=z.string().meta({minLength:1,maxLength:2000,description:'Trimmed NFC reason, 1–2000 Unicode code points, no controls or unpaired surrogates.'});
4
+ const opaque=z.string().trim().min(1).max(256).regex(/^[ -~]+$/);
5
+ const unavailable=z.object({availability:z.literal('unavailable'),reason}).strict();
6
+ const evidence=z.discriminatedUnion('kind',[
7
+ z.object({kind:z.literal('provider_report'),reference:opaque,observedAt:time}).strict(),
8
+ z.object({kind:z.literal('account_evidence'),evidenceId:uuid,observedAt:time}).strict(),
9
+ z.object({kind:z.literal('owner_confirmation'),observedAt:time}).strict(),
10
+ z.object({kind:z.literal('no_dispatch'),journalRef:opaque,sealed:z.literal(true)}).strict(),
11
+ z.object({kind:z.literal('transport_uncertain'),reference:opaque,observedAt:time}).strict(),
12
+ ]);
13
+ const content=z.union([
14
+ z.object({subject:z.string().meta({minLength:1,maxLength:256}),body:z.string().meta({minLength:1,maxLength:20000})}).strict(),
15
+ z.object({body:z.string().meta({minLength:1,maxLength:8000})}).strict(),
16
+ ]);
17
+ export const workExecutionResultSchema=z.discriminatedUnion('outcome',[
18
+ z.object({outcome:z.literal('succeeded'),actualContent:z.union([unavailable,z.object({availability:z.literal('available'),content}).strict()]),providerMessageId:z.union([unavailable,z.object({availability:z.literal('available'),value:opaque}).strict()]),effectAt:time.nullable(),evidence}).strict(),
19
+ z.object({outcome:z.literal('failed'),effect:z.literal('none'),evidence}).strict(),
20
+ z.object({outcome:z.literal('unknown'),effect:z.literal('possible'),evidence}).strict(),
21
+ ]);
22
+ const location={workspaceId:uuid.optional(),planId:uuid,clientActionId:z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/)};
23
+ const path=a=>`/workspaces/${a.workspaceId}/work/${a.planId}/actions/${encodeURIComponent(a.clientActionId)}`;
24
+ const body=({workspaceId,planId,clientActionId,attemptId,...rest})=>rest;
25
+ export const workspaceExecutionOperations={
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
+ 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_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
+ export const narrowExecutionOperations={
31
+ 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
+ work_attempt_lookup:{schema:z.object({claimId:uuid}).strict(),method:'GET',path:a=>`/work-execution/claims/${a.claimId}/attempt`},
33
+ work_result_report:{schema:z.object({attemptId:uuid,requestId:uuid,mode:z.enum(['report','correct']),expectedResultVersion:version,reason,result:workExecutionResultSchema}).strict(),method:'POST',path:a=>`/work-execution/attempts/${a.attemptId}/results`,body},
34
+ };
35
+ export const workExecutionOperations={...workspaceExecutionOperations,...narrowExecutionOperations};
@@ -0,0 +1,29 @@
1
+ import { workExecutionOperations } from './work-execution-operations.mjs';
2
+ import { callWorkspace, configuration } from './workspaces.mjs';
3
+ import { callOwnerAccountOperation } from './owner-account-client.mjs';
4
+ import { readJsonFile } from './json-file.mjs';
5
+ export function callWorkExecution(name,args,profile=process.env.AUDIENTI_PROFILE) {
6
+ const operation=workExecutionOperations[name];if(!operation)throw Error('Unknown work execution operation.');
7
+ return operation.scoped?callWorkspace(name,args,configuration(profile)):callOwnerAccountOperation(operation,args,profile,'work execution');
8
+ }
9
+ export async function workExecutionCli(args) {
10
+ const flags={},positions=[];
11
+ for(let i=0;i<args.length;i++){
12
+ const arg=args[i];if(arg==='--json')continue;if(!arg.startsWith('--')){positions.push(arg);continue;}
13
+ const key=arg.slice(2);if(!['profile','workspace','action','claim','attempt','file','cursor','limit'].includes(key)||Object.hasOwn(flags,key)||!args[i+1]||args[i+1].startsWith('--'))throw Error('Unknown, repeated or incomplete execution option.');flags[key]=args[++i];
14
+ }
15
+ const command=positions[2],workspace=['claim','start','get'].includes(command),read=['get','lookup'].includes(command);
16
+ const names={claim:'workspace_work_claim',start:'workspace_work_start',get:'workspace_work_attempt_get',release:'work_claim_release',lookup:'work_attempt_lookup',report:'work_result_report'};
17
+ if(!names[command]||positions.length!==(workspace?4:3))throw Error('Use work execution claim|start|get PLAN, release|lookup --claim ID, or report --attempt ID.');
18
+ const allowed=['profile',...(workspace?['workspace','action']:command==='report'?['attempt']:['claim']),...(read?command==='get'?['cursor','limit']:[]:['file'])];
19
+ if(Object.keys(flags).some(key=>!allowed.includes(key)))throw Error('Option does not apply to this execution command.');
20
+ if(workspace&&!flags.action)throw Error('This command requires --action.');
21
+ if(!workspace&&!(command==='report'?flags.attempt:flags.claim))throw Error('Supply the claim or attempt ID.');
22
+ const input=read?{}:readJsonFile(flags.file,65536,'Work execution input');
23
+ if(!input||typeof input!=='object'||Array.isArray(input))throw Error('invalid_fields: input must be a JSON object.');
24
+ const paths=workspace?['workspaceId','planId','clientActionId']:command==='report'?['attemptId']:['claimId'];
25
+ if(paths.some(key=>Object.hasOwn(input,key)))throw Error('invalid_fields: path IDs come from positions and flags only.');
26
+ Object.assign(input,workspace?{planId:positions[3],clientActionId:flags.action,...(flags.workspace?{workspaceId:flags.workspace}:{})}:command==='report'?{attemptId:flags.attempt}:{claimId:flags.claim});
27
+ if(command==='get'){if(flags.cursor)input.historyCursor=flags.cursor;if(flags.limit)input.historyLimit=Number(flags.limit);}
28
+ console.log(JSON.stringify(await callWorkExecution(names[command],input,flags.profile)));
29
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ const uuid = z.string().uuid(), version = z.number().int().positive();
3
+ const actionId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
4
+ const reason = z.string().meta({ minLength: 1, maxLength: 2000, description: 'Trimmed NFC reason, 1–2000 Unicode code points; interior controls (including tabs/newlines) and unpaired surrogates are rejected. Visible to workspace members.' });
5
+ const location = { workspaceId: uuid.optional(), planId: uuid };
6
+ const item = { ...location, reviewRequestId: uuid };
7
+ const mutation = { ...item, expectedVersion: version, requestId: uuid, reason };
8
+ const root = a => `/workspaces/${a.workspaceId}/work/${a.planId}/change-requests`;
9
+ const detail = a => `${root(a)}/${a.reviewRequestId}`;
10
+ const body = ({ workspaceId, planId, clientActionId, reviewRequestId, ...input }) => input;
11
+ export const workFeedbackOperations = {
12
+ workspace_work_change_request: { schema: z.object({ ...location, clientActionId: actionId, expectedRevision: version, expectedApprovalVersion: z.number().int().nonnegative(), kind: z.enum(['revise','alternative']), reason, requestId: uuid }).strict(), method: 'POST', scoped: true, path: a => `/workspaces/${a.workspaceId}/work/${a.planId}/actions/${a.clientActionId}/change-requests`, body },
13
+ workspace_work_change_respond: { schema: z.object({ ...mutation, responseRevision: version, responseClientActionId: actionId }).strict().describe('Respond with the changed original for Revise, or a new replacement after removing the original. Skip always requires a new action ID. A response never approves the proposal.'), method: 'POST', scoped: true, path: a => `${detail(a)}/respond`, body },
14
+ workspace_work_change_withdraw: { schema: z.object(mutation).strict(), method: 'POST', scoped: true, path: a => `${detail(a)}/withdraw`, body },
15
+ workspace_work_change_get: { schema: z.object(item).strict(), method: 'GET', scoped: true, path: detail },
16
+ workspace_work_change_list: { schema: z.object({ ...location, page: z.object({ cursor: uuid.optional(), limit: z.number().int().min(1).max(100).optional() }).strict().optional(), status: z.enum(['open','responded','withdrawn','cancelled']).optional() }).strict(), method: 'GET', scoped: true, path: a => { const q = new URLSearchParams(); for (const [key,value] of Object.entries({ ...a.page, status:a.status })) if(value!==undefined)q.set(key,String(value)); return `${root(a)}?${q}`; } },
17
+ };
@@ -0,0 +1,18 @@
1
+ import { callWorkspace, configuration } from './workspaces.mjs';
2
+ import { readJsonFile } from './json-file.mjs';
3
+ export async function workFeedbackCli(args) {
4
+ const flags={}, positions=[];
5
+ 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(!['profile','workspace','file','action','change','cursor','limit','status'].includes(key)||Object.hasOwn(flags,key)||!args[i+1]||args[i+1].startsWith('--'))throw new Error('Unknown, repeated, or incomplete work changes option.');flags[key]=args[++i]; }
6
+ const [, , command, planId]=positions;
7
+ if(positions.length!==4||!['request','respond','withdraw','get','list'].includes(command))throw new Error('Use work changes request|respond|withdraw|get|list PLAN.');
8
+ const read=['get','list'].includes(command),specific=command==='request'?['action']:command==='list'?['cursor','limit','status']:['change'];
9
+ const allowed=['profile','workspace',...specific,...(read?[]:['file'])];
10
+ if(Object.keys(flags).some(key=>!allowed.includes(key)))throw new Error('Option does not apply to this work changes command.');
11
+ if(command==='request'&&!flags.action)throw new Error('Request requires --action.');
12
+ if(!['request','list'].includes(command)&&!flags.change)throw new Error('This command requires --change.');
13
+ const input=read?{}:readJsonFile(flags.file,65536,'Work change input');
14
+ if(['workspaceId','planId','clientActionId','reviewRequestId'].some(key=>Object.hasOwn(input,key)))throw new Error('invalid_fields: location IDs come from flags and position only.');
15
+ Object.assign(input,{planId,...(flags.workspace?{workspaceId:flags.workspace}:{}),...(flags.action?{clientActionId:flags.action}:{}),...(flags.change?{reviewRequestId:flags.change}:{})});
16
+ if(command==='list'){input.page={...(flags.cursor?{cursor:flags.cursor}:{}),...(flags.limit?{limit:Number(flags.limit)}:{})};if(flags.status!==undefined)input.status=flags.status;}
17
+ console.log(JSON.stringify(await callWorkspace(`workspace_work_change_${command}`,input,configuration(flags.profile))));
18
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ const uuid = z.string().uuid(), version = z.number().int().nonnegative();
3
+ const workspace = { workspaceId: uuid.optional() };
4
+ const page = z.object({cursor: uuid.optional(), limit: z.number().int().min(1).max(100).optional()}).strict();
5
+ const clientId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
6
+ const accountRef = z.string().min(1).max(200).regex(/^[A-Za-z0-9@._:+\/-]+$/);
7
+ const evidence = z.array(uuid).max(100);
8
+ const commonAction = {clientActionId: clientId, reason: z.string().min(1), evidenceIds: evidence, replyToEvidenceId: uuid.optional()};
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();
11
+ const cadence = z.object({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
+ const located = {...workspace,planId:uuid,clientActionId:clientId};
13
+ const mutation = {...located,expectedRevision:z.number().int().positive(),requestId:uuid};
14
+ const root = a => `/workspaces/${a.workspaceId}`;
15
+ const plan = a => `${root(a)}/work/${a.planId}`;
16
+ const action = a => `${plan(a)}/actions/${encodeURIComponent(a.clientActionId)}`;
17
+ const omit = (a, keys) => Object.fromEntries(Object.entries(a).filter(([k])=>!keys.includes(k)));
18
+ const body = a => omit(a,['workspaceId','planId','clientActionId']);
19
+ const qs = values => { const p=new URLSearchParams(); for(const [k,v] of Object.entries(values)) if(v!==undefined)p.set(k,String(v));return p.toString(); };
20
+ export const workOperations = {
21
+ workspace_work_action_plan_save: {schema:z.object({...workspace,planId:uuid.nullable(),prospectId:uuid,experimentId:uuid.nullable(),experimentRevision:z.number().int().positive().nullable(),expectedRevision:version,requestId:uuid,origin:z.discriminatedUnion('kind',[z.object({kind:z.literal('agent'),skill:z.object({id:z.string(),version:z.string()}).strict()}).strict(),z.object({kind:z.literal('human'),editReason:z.string()}).strict()]),rationale:z.string(),evidenceIds:evidence,actions:z.array(z.discriminatedUnion('type',[email,linkedin])).min(1).max(50),cadence:z.array(cadence).max(50)}).strict(), method:'POST',path:a=>`${root(a)}/action-plans`,body:a=>omit(a,['workspaceId']),scoped:true},
22
+ workspace_work_operator_list: {schema:z.object({...workspace,page:page.optional(),filter:z.object({state:z.enum(['open','cancelled','terminal','invalidated']).optional(),reviewStatus:z.enum(['awaiting_revision','invalidated','pending_review','approved','in_progress','reconciliation_required','completed','cancelled']).optional(),search:z.string().optional(),queue:z.enum(['review','blocked','activity']).optional()}).strict().optional()}).strict(),method:'GET',path:a=>`${root(a)}/operator/work?${qs({...a.page,...a.filter})}`,scoped:true},
23
+ workspace_work_operator_get: {schema:z.object({...workspace,planId:uuid,revision:z.number().int().positive().optional(),history:page.optional()}).strict(),method:'GET',path:a=>`${plan(a)}?${qs({revision:a.revision,historyCursor:a.history?.cursor,historyLimit:a.history?.limit})}`,scoped:true},
24
+ workspace_work_action_approve: {schema:z.object({...mutation,expectedApprovalVersion:version}).strict(),method:'POST',path:a=>`${action(a)}/approve`,body,scoped:true},
25
+ workspace_work_action_withdraw: {schema:z.object({...mutation,expectedApprovalVersion:z.number().int().positive(),reason:z.string().min(1)}).strict(),method:'POST',path:a=>`${action(a)}/withdraw`,body,scoped:true},
26
+ workspace_work_action_cancel: {schema:z.object({...mutation,reason:z.string().min(1)}).strict(),method:'POST',path:a=>`${action(a)}/cancel`,body,scoped:true},
27
+ workspace_work_eligibility_get: {schema:z.object(located).strict(),method:'GET',path:a=>`${action(a)}/eligibility`,scoped:true},
28
+ };
package/cli/work.mjs ADDED
@@ -0,0 +1,27 @@
1
+ import { callWorkspace, configuration } from './workspaces.mjs';
2
+ import { readJsonFile } from './json-file.mjs';
3
+ export async function workCli(args) {
4
+ const flags={}, position=[];
5
+ const allowed=['profile','workspace','file','request-id','plan-id','client-action-id','cursor','limit','history-cursor','history-limit','revision','state','review-status','queue','search'];
6
+ for(let i=0;i<args.length;i++) {
7
+ if(args[i]==='--json')continue;
8
+ if(!args[i].startsWith('--')){position.push(args[i]);continue;}
9
+ const key=args[i].slice(2);if(!allowed.includes(key)||Object.hasOwn(flags,key)||args[i+1]===undefined||args[i+1].startsWith('--'))throw new Error('Unknown, repeated, or incomplete work option.');flags[key]=args[++i];
10
+ }
11
+ const names={'work plan save':'workspace_work_action_plan_save','work list':'workspace_work_operator_list','work show':'workspace_work_operator_get','work approve':'workspace_work_action_approve','work withdraw-approval':'workspace_work_action_withdraw','work cancel':'workspace_work_action_cancel','work eligibility':'workspace_work_eligibility_get'};
12
+ const name=names[position.join(' ')];if(!name)throw new Error('Use work plan save, list, show, approve, withdraw-approval, cancel, or eligibility. Writes require --file INPUT.json.');
13
+ const read=['workspace_work_operator_list','workspace_work_operator_get','workspace_work_eligibility_get'].includes(name);
14
+ const input=read?{}:readJsonFile(flags.file,65536,'Work input');
15
+ for(const [flag,key] of [['workspace','workspaceId'],['request-id','requestId'],['plan-id','planId'],['client-action-id','clientActionId']])if(flags[flag]!==undefined)input[key]=flags[flag];
16
+ if(name==='workspace_work_operator_list'){
17
+ input.page={...(flags.cursor===undefined?{}:{cursor:flags.cursor}),...(flags.limit===undefined?{}:{limit:Number(flags.limit)})};
18
+ input.filter={};for(const [flag,key] of [['state','state'],['review-status','reviewStatus'],['search','search'],['queue','queue']])if(flags[flag]!==undefined)input.filter[key]=flags[flag];
19
+ } else if(name==='workspace_work_operator_get'){
20
+ if(flags.revision!==undefined)input.revision=Number(flags.revision);
21
+ input.history={...(flags['history-cursor']===undefined?{}:{cursor:flags['history-cursor']}),...(flags['history-limit']===undefined?{}:{limit:Number(flags['history-limit'])})};
22
+ }
23
+ const applicable=new Set(['profile','workspace','file','request-id','plan-id','client-action-id',...(name==='workspace_work_operator_list'?['cursor','limit','state','review-status','search','queue']:[]),...(name==='workspace_work_operator_get'?['revision','history-cursor','history-limit']:[])]);
24
+ if(Object.keys(flags).some(k=>!applicable.has(k)))throw new Error('Option does not apply to this work command.');
25
+ if(read&&flags.file!==undefined)throw new Error('Read commands do not accept --file.');
26
+ console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
27
+ }