@audienti/cli 0.1.145 → 0.2.0-exora.10

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 (110) 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 +55 -0
  7. package/cli/agents.mjs +160 -0
  8. package/cli/assignment-operations.mjs +218 -0
  9. package/cli/assignment.mjs +116 -0
  10. package/cli/attachment-operations.mjs +9 -0
  11. package/cli/attachments.mjs +28 -0
  12. package/cli/audience-operations.mjs +21 -0
  13. package/cli/audiences.mjs +29 -0
  14. package/cli/audienti.mjs +217 -0
  15. package/cli/audit-operations.mjs +17 -0
  16. package/cli/audit.mjs +39 -0
  17. package/cli/budget-operations.mjs +8 -0
  18. package/cli/budgets.mjs +34 -0
  19. package/cli/campaign-find.mjs +155 -0
  20. package/cli/campaign-operations.mjs +30 -0
  21. package/cli/campaigns.mjs +61 -0
  22. package/cli/cohort-operations.mjs +22 -0
  23. package/cli/cohorts.mjs +45 -0
  24. package/cli/connection-registry-operations.mjs +8 -0
  25. package/cli/content.mjs +29 -0
  26. package/cli/discovery-operations.mjs +28 -0
  27. package/cli/discovery.mjs +15 -0
  28. package/cli/file-operations.mjs +29 -0
  29. package/cli/files.mjs +14 -0
  30. package/cli/guardrails-operations.mjs +12 -0
  31. package/cli/guardrails.mjs +37 -0
  32. package/cli/json-file.mjs +13 -0
  33. package/cli/kv-operations.mjs +17 -0
  34. package/cli/kv.mjs +58 -0
  35. package/cli/mcp.mjs +47 -0
  36. package/cli/notification-operations.mjs +8 -0
  37. package/cli/offer-operations.mjs +12 -0
  38. package/cli/offers.mjs +24 -0
  39. package/cli/outbound-operations.mjs +16 -0
  40. package/cli/outbound.mjs +54 -0
  41. package/cli/outcome-operations.mjs +26 -0
  42. package/cli/outcomes.mjs +16 -0
  43. package/cli/owner-account-client.mjs +18 -0
  44. package/cli/playbook-help.mjs +79 -0
  45. package/cli/playbook-operations.mjs +36 -0
  46. package/cli/profiles.mjs +29 -0
  47. package/cli/progress.mjs +26 -0
  48. package/cli/prospecting-settings-operations.mjs +16 -0
  49. package/cli/prospecting-settings.mjs +15 -0
  50. package/cli/question-operations.mjs +12 -0
  51. package/cli/questions.mjs +32 -0
  52. package/cli/queue-operations.mjs +12 -0
  53. package/cli/queue.mjs +25 -0
  54. package/cli/recipient-operations.mjs +13 -0
  55. package/cli/recipients.mjs +34 -0
  56. package/cli/rooms-operations.mjs +48 -0
  57. package/cli/rooms.mjs +160 -0
  58. package/cli/search-operations.mjs +9 -0
  59. package/cli/secret-operations.mjs +7 -0
  60. package/cli/secrets.mjs +30 -0
  61. package/cli/sender-account-operations.mjs +28 -0
  62. package/cli/sender-accounts.mjs +33 -0
  63. package/cli/skills-mcp.mjs +31 -0
  64. package/cli/skills.mjs +113 -0
  65. package/cli/suppression-operations.mjs +8 -0
  66. package/cli/suppression.mjs +12 -0
  67. package/cli/tools-operations.mjs +78 -0
  68. package/cli/tools.mjs +100 -0
  69. package/cli/usage-operations.mjs +8 -0
  70. package/cli/usage.mjs +41 -0
  71. package/cli/work-execution-operations.mjs +42 -0
  72. package/cli/work-execution.mjs +29 -0
  73. package/cli/work-feedback-operations.mjs +17 -0
  74. package/cli/work-feedback.mjs +18 -0
  75. package/cli/work-operations.mjs +30 -0
  76. package/cli/work.mjs +32 -0
  77. package/cli/workspaces.mjs +255 -0
  78. package/package.json +15 -26
  79. package/skills/agent-rooms/SKILL.md +140 -0
  80. package/skills/agent-rooms/references/commands.md +78 -0
  81. package/skills/audienti/SKILL.md +161 -317
  82. package/skills/audienti/references/helpers.md +162 -0
  83. package/skills/audienti/references/profile-workflows.md +214 -0
  84. package/skills/audienti/references/workflow-receipt.md +49 -0
  85. package/skills/audienti/scripts/contracts.mjs +72 -0
  86. package/skills/audienti/scripts/profile-helpers.mjs +138 -0
  87. package/skills/audienti/scripts/run.mjs +40 -0
  88. package/skills/getting-started/SKILL.md +227 -0
  89. package/skills/getting-started/references/audience.md +143 -0
  90. package/skills/getting-started/references/campaign-draft.md +80 -0
  91. package/skills/getting-started/references/capabilities.md +237 -0
  92. package/skills/getting-started/references/mailbox-check.md +64 -0
  93. package/skills/getting-started/references/outcome-checks.md +114 -0
  94. package/skills/getting-started/references/replies.md +186 -0
  95. package/skills/getting-started/references/reply-checks.md +54 -0
  96. package/skills/playbook-design/SKILL.md +136 -0
  97. package/skills/playbook-design/references/fields.md +130 -0
  98. package/source-manifest.json +104 -0
  99. package/.mcp.json +0 -13
  100. package/CHANGELOG.md +0 -1018
  101. package/bin/audienti-mcp.js +0 -5
  102. package/bin/audienti.js +0 -6
  103. package/install +0 -83
  104. package/skills/audienti/references/methodology.md +0 -111
  105. package/skills/audienti-gift-research/SKILL.md +0 -47
  106. package/src/agent-skills.js +0 -60
  107. package/src/api-client.js +0 -1639
  108. package/src/cli.js +0 -16992
  109. package/src/config.js +0 -130
  110. package/src/mcp.js +0 -103
package/cli/skills.mjs ADDED
@@ -0,0 +1,113 @@
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 bundles = {
7
+ audienti: {
8
+ version: SKILL_VERSION,
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'],
10
+ helpers: true,
11
+ },
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': {
23
+ version: '1.0.0',
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'],
26
+ helpers: false,
27
+ },
28
+ };
29
+ export const SKILL_IDS = Object.keys(bundles);
30
+
31
+ function readSkill(id = 'audienti') {
32
+ const bundle = Object.hasOwn(bundles, id) ? bundles[id] : null;
33
+ if (!bundle) throw new Error('Unknown bundled skill. Use skills list.');
34
+ const root = new URL(`../skills/${id}/`, import.meta.url);
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.');
39
+ const hash = createHash('sha256');
40
+ for (const file of contents) hash.update(`${file.path}\0${Buffer.byteLength(file.text)}\0`).update(file.text);
41
+ return { contents, details: {
42
+ id, version: bundle.version, digest: hash.digest('hex'),
43
+ description,
44
+ instructions: contents[0].text,
45
+ references: contents.filter(file => file.path.startsWith('references/')),
46
+ helpers: bundle.helpers ? Object.entries(helperContracts).map(([name, contract]) => ({ name, ...contract })) : [],
47
+ } };
48
+ }
49
+
50
+ export function skillDetails(id = 'audienti') { return readSkill(id).details; }
51
+
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
+ }) };
66
+ }
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
+
79
+ export function skillArtifact(id = 'audienti') {
80
+ const { details, contents } = readSkill(id);
81
+ return { ...details, files: contents.map(({ path, text }) => ({ path, content: text })) };
82
+ }
83
+
84
+ export async function skillsCli(argv) {
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');
89
+ const filtered = args.filter(value => value !== '--json');
90
+ if (args.filter(value => value === '--json').length > 1) throw new Error('Invalid skills options.');
91
+ if (filtered.length === 1 && ['--help', 'help'].includes(filtered[0])) {
92
+ process.stdout.write(`${usage}\nLocal skill instructions/helpers; no login, installation or provider calls.\n`); return;
93
+ }
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; }
97
+ process.stdout.write(json ? `${JSON.stringify(catalog)}\n` : catalog.skills.map(skill => `${skill.id}@${skill.version} — ${skill.description}\n`).join('')); return;
98
+ }
99
+ if (filtered[0] === 'show' && filtered.length === 2) {
100
+ const details = skillDetails(filtered[1]);
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;
106
+ }
107
+ if (filtered[0] === 'helper' && filtered.length === 4 && filtered[2] === '--file') {
108
+ try { process.stdout.write(`${JSON.stringify(executeHelperFile(filtered[1], filtered[3]))}\n`); }
109
+ catch { throw new Error('Skill helper failed. Inspect its schema and provide valid JSON of at most 1 MiB.'); }
110
+ return;
111
+ }
112
+ throw new Error(usage);
113
+ }
@@ -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,78 @@
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 toolId = z.string().regex(/^[a-z][a-z0-9._]{0,63}$/);
5
+ const key = z.string().regex(/^[a-z][a-z0-9_]{0,47}$/).refine(v => !['constructor','prototype'].includes(v));
6
+ const instruction = z.string().trim().min(1).max(4000);
7
+ const question = z.discriminatedUnion('type', [
8
+ z.object({type:z.literal('boolean'),instructions:instruction,criteria:z.object({true:instruction.optional(),false:instruction.optional()}).strict().optional()}).strict(),
9
+ 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(),
10
+ z.object({type:z.literal('score'),instructions:instruction,criteria:z.array(instruction).min(1).max(10)}).strict(),
11
+ ]);
12
+ const page = { page:z.number().int().min(1).max(10000).optional(), paginationToken:z.string().min(1).max(4096).optional() };
13
+ const postedLimit = z.enum(['24h','week','month']).optional();
14
+ const contentInputs = {
15
+ 'posts.fetch':z.object({url:z.string().min(1).max(2048),topicId:uuid}).strict(),
16
+ 'posts.search':z.object({query:z.string().trim().min(1).max(500),topicId:uuid,postedLimit,...page,sortBy:z.enum(['relevance','date']).optional()}).strict(),
17
+ 'posts.comments':z.object({postId:uuid,...page}).strict(),
18
+ 'posts.reactions':z.object({postId:uuid,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(),
20
+ 'profiles.posts':z.object({profileId:uuid,topicId:uuid,postedLimit,...page}).strict(),
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}]));
23
+ const recordsRoot = (a,kind) => `/workspaces/${a.workspaceId}/${kind}`;
24
+ const contactInputs = {
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(),
27
+ };
28
+ export const hostedToolOperations = {
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}])),
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},
31
+ ...contentOperations,
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},
33
+ ...Object.fromEntries(['topics','posts'].flatMap(kind=>[
34
+ [`${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}],
35
+ [`${kind}_get`,{traced:true,schema:z.object({...workspace,id:uuid}).strict(),method:'GET',path:a=>recordsRoot(a,kind)+'/'+a.id,scoped:true}],
36
+ ])),
37
+ tools_list:{schema:z.object(workspace).strict(),method:'GET',path:root,scoped:true},
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},
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},
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},
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},
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},
46
+ 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},
47
+ 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},
48
+ 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},
49
+ tools_balance:{schema:z.object(workspace).strict(),method:'GET',path:a=>root(a)+'/balance',scoped:true},
50
+ };
51
+
52
+ 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.';
53
+ // Compatibility aliases share server admission identity; response shape is explicit.
54
+ const fetchDescriptor = hostedToolOperations.tools_profiles_fetch;
55
+ const fetchInput = fetchDescriptor.schema.shape.input;
56
+ hostedToolOperations.tools_profiles_enrich = {
57
+ ...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',
58
+ schema: fetchDescriptor.schema.extend({ input: z.union([fetchInput, fetchInput.omit({url:true}).extend({profileId:uuid}).strict()]) }),
59
+ path: a => root(a) + '/profiles.enrich',
60
+ };
61
+ hostedToolOperations.tools_profiles_discover = {
62
+ ...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',
63
+ path: a => root(a) + '/profiles.discover',
64
+ };
65
+ export const paidToolOperations = Object.fromEntries(Object.entries(hostedToolOperations).filter(([,operation])=>operation.paid));
66
+ export const paidToolByName = Object.fromEntries(Object.entries(paidToolOperations).filter(([,operation])=>operation.tool).map(([name,operation])=>[operation.tool,{name,...operation}]));
67
+ // tools_call is paid except for the free proxy.sessions.close control call, which has no requestId or fee.
68
+ export const isPaidToolOperation = (name, args) => name === 'tools_call' ? !['proxy.sessions.close', 'audienti.proxy.session.close'].includes(args?.tool) : Object.hasOwn(paidToolOperations, name);
69
+
70
+ // Only known descriptor shape decides where a paid operation lives. Never infer
71
+ // a nested operation for unrelated responses or let projection state mask billing.
72
+ // Through tools_call the shape follows the resolved tool: profiles.enrich nests its operation.
73
+ export const paidToolResult = (name, data, args) => {
74
+ if (name === 'tools_call') return ['profiles.enrich', 'audienti.people.profile.enrich'].includes(args?.tool) ? data?.operation : data;
75
+ const descriptor = paidToolOperations[name];
76
+ return descriptor?.resultKey ? data?.[descriptor.resultKey] : data;
77
+ };
78
+
package/cli/tools.mjs ADDED
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from 'node:crypto';
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 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
+ audienti tools runs get UUID [--attempt-after ORDINAL] [--entry-after UUID] | list [--cursor UUID]
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.
23
+
24
+ email.find and mobile.find may return running: poll tools runs get OPERATION_ID.
25
+ Keep the same request ID after interruptions; never submit a new one to retry a purchase.
26
+
27
+ web.search returns one selected result page; workspace cache lasts 24 hours (refresh=true bypasses it).
28
+ web.fetch returns inert text/links through a bounded HTML waterfall. Each completed logical
29
+ web result costs 0.1 credit ($1 per 1000); failed requests, cache hits and request replays are free.
30
+ Uses the selected private profile/workspace. Supply --profile PATH when needed.
31
+ 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.
32
+ profiles.fetch accepts a person /in/ or company /company/ URL as {url,fields?,refresh?}; a complete saved profile is free for 30 days.
33
+ Explicit refresh requires paid admission; the result includes saved profile/prospect IDs.
34
+ Fields: name, headline, about, location, experience, currentPositions, education, skills,
35
+ websites, photo, connections, followers, certifications, languages. Company-specific fields:
36
+ industries, employeeCount, employeeCountRange, founded, specialities, locations, companyType.
37
+ profiles.enrich accepts the same fields as profiles.fetch, using url or saved profileId.
38
+ Its operation has the same admission identity as profiles.fetch; the Prospect projection is a current free read.
39
+ profiles.discover is the clear external-search alias of profiles.search; profiles list searches saved workspace records.
40
+ profiles.search accepts person filters or {kind:"company",query,companySizes?}; results are partial profiles.
41
+ Use tools get TOOL for exact fields. No implicit email lookup.
42
+ decide accepts {state,questions}; question types: boolean, choice, score.
43
+ Questions have instructions; choice requires named criteria; score criteria are ordered
44
+ from level zero. State is text, an object or an array. At most 32 KiB, 16 questions,
45
+ 32 choices per question and 10 score levels. No provider credentials belong in input.
46
+ The request ID must be saved and reused after a timeout or interruption; never create a
47
+ new ID just to retry an uncertain operation. A rejected operation made no provider call;
48
+ after fixing its cause (for example adding credits), use a new ID for new work. Results and diagnostics contain trace IDs.
49
+ Example input: {"state":"Can we talk?","questions":{"interest":{"type":"boolean","instructions":"Does this express interest?"}}}
50
+ JSON results go to stdout; failures go to stderr and exit nonzero.`;
51
+ async function readStdin() {
52
+ const chunks=[];let size=0;
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);}
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.');}
55
+ }
56
+ const asJson=value=>{try{return JSON.parse(value);}catch{throw new Error('Tool input must be valid JSON.');}};
57
+ 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');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
+ }
83
+ export async function toolsCli(argv) {
84
+ const {positionals,values}=parseArgs({args:argv.slice(1),allowPositionals:true,strict:true,options:parseOptions});
85
+ if(values.help||positionals.length===0){console.log(help);return;}
86
+ const [verb,sub,id]=positionals;
87
+ let name,args={workspaceId:values.workspace};
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;}
91
+ else if(verb==='balance'&&positionals.length===1)name='tools_balance';
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']);}
93
+ else if(verb==='runs'&&sub==='list'&&positionals.length===2){name='tools_runs_list';args.cursor=values.cursor;}
94
+ else if(verb==='proxy.sessions.close'&&positionals.length===2){name='tools_proxy_sessions_close';args.operationId=sub;}
95
+ else if(Object.hasOwn(paidToolByName,verb)&&positionals.length===1){
96
+ if(!values.input||!values['request-id'])throw new Error(`${verb} requires --input FILE|- and --request-id UUID. Keep the request ID for retries.`);
97
+ name=paidToolByName[verb].name;args.requestId=values['request-id'];args.input=values.input==='-'?await readStdin():readJsonFile(values.input,32768,'Tool input');
98
+ }else throw new Error('Unknown tools command. Use audienti tools --help.');
99
+ await printPaid(name,args,values);
100
+ }
@@ -0,0 +1,8 @@
1
+ import { z } from 'zod';
2
+ export const usageOperations = {
3
+ workspace_usage_breakdown: {
4
+ schema: z.object({ workspaceId: z.string().uuid().optional(), period: z.enum(['month', '7d', '30d', '90d']).optional() }).strict(),
5
+ method: 'GET', path: a => `/workspaces/${a.workspaceId}/usage/breakdown${a.period ? `?period=${a.period}` : ''}`, scoped: true,
6
+ description: 'Credit cost for a period (default this month) by tool, provider, model and caller, with a zero-filled daily series and share of total. Amounts are pico USD (1 credit = 10,000,000,000). Model is recorded only for AI decision calls. Same figures as Settings > Usage.',
7
+ },
8
+ };
package/cli/usage.mjs ADDED
@@ -0,0 +1,41 @@
1
+ import { callWorkspace, configuration } from './workspaces.mjs';
2
+
3
+ const PERIODS = { month: 'month', '7d': '7d', '30d': '30d', '90d': '90d' };
4
+ const DIMENSIONS = ['tool', 'provider', 'model', 'caller', 'day'];
5
+ const PICO_PER_CREDIT = 10_000_000_000n;
6
+ const cell = value => { const text = String(value); return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; };
7
+ export function credits(amount) {
8
+ const value = BigInt(amount), whole = value / PICO_PER_CREDIT, fraction = (value % PICO_PER_CREDIT).toString().padStart(10, '0').replace(/0+$/, '');
9
+ return fraction ? `${whole}.${fraction}` : String(whole);
10
+ }
11
+ const percent = share => `${(share * 100).toFixed(1)}%`;
12
+ /** Rows for one dimension: a label, calls, credits and share of the period total. */
13
+ export function breakdownRows(data, by) {
14
+ if (by === 'day') return (data.daily ?? []).map(row => [row.day, row.calls, credits(row.charged), '']);
15
+ const items = { tool: data.byTool, provider: data.byProvider, model: data.byModel, caller: data.byCaller }[by] ?? [];
16
+ const name = row => by === 'tool' ? row.tool : by === 'provider' ? row.provider : by === 'model' ? row.model ?? 'Not recorded' : [row.owner, row.keyName, row.agentName].filter(Boolean).join(' / ');
17
+ return items.map(row => [name(row), row.calls, credits(row.charged), percent(row.share)]);
18
+ }
19
+ export function breakdownCsv(data, by) {
20
+ return [[by, 'calls', 'credits', 'share'], ...breakdownRows(data, by)].map(row => row.map(cell).join(',')).join('\n') + '\n';
21
+ }
22
+ export async function usageCli(args) {
23
+ const flags = {}, positions = [];
24
+ for (let i = 0; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg === '--json') continue;
27
+ if (!arg.startsWith('--')) { positions.push(arg); continue; }
28
+ const key = arg.slice(2);
29
+ if (!['workspace', 'profile', 'since', 'by', 'format'].includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}.`);
30
+ flags[key] = args[++i];
31
+ }
32
+ if (positions.length !== 2 || positions[1] !== 'breakdown') throw Error('Use usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv].');
33
+ if (flags.since && !PERIODS[flags.since]) throw Error('--since must be month, 7d, 30d or 90d.');
34
+ if (flags.by && !DIMENSIONS.includes(flags.by)) throw Error('--by must be tool, provider, model, caller or day.');
35
+ if (flags.format && !['json', 'csv'].includes(flags.format)) throw Error('--format must be json or csv.');
36
+ const input = { workspaceId: flags.workspace };
37
+ if (flags.since) input.period = PERIODS[flags.since];
38
+ const result = await callWorkspace('workspace_usage_breakdown', input, configuration(flags.profile));
39
+ if (flags.format === 'csv') { process.stdout.write(breakdownCsv(result?.data ?? result, flags.by ?? 'tool')); return; }
40
+ console.log(JSON.stringify(result));
41
+ }
@@ -0,0 +1,42 @@
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_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.'},
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}`;}},
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
+ };
37
+ export const narrowExecutionOperations={
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},
39
+ work_attempt_lookup:{schema:z.object({claimId:uuid}).strict(),method:'GET',path:a=>`/work-execution/claims/${a.claimId}/attempt`},
40
+ 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},
41
+ };
42
+ 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','send','get'].includes(command),read=['get','lookup'].includes(command);
16
+ const names={claim:'workspace_work_claim',start:'workspace_work_start',send:'workspace_work_send',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|send|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,30 @@
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, capability: z.enum(['profile.connection.request','profile.message.send']).optional().describe('The owner action: profile.connection.request or profile.message.send on linkedin.send, profile.message.send on email.send. The sending account\'s mode for it decides whether approval is needed or the action is not allowed. Without it the old per-channel automation switch applies, unless a mode restricts it.'), reason: z.string().min(1), evidenceIds: evidence, replyToEvidenceId: uuid.optional(), playbookStepKey: uuid.optional().describe("The playbook step this action runs; a succeeded result starts that step's wait. Saving the plan checks it: the key must name a step of the playbook pinned to the person's active participation in the plan's Campaign, and the step's channel must match the action type (email.send only with an Email step, linkedin.send only with a LinkedIn step); otherwise the save is refused with invalid_fields.")};
9
+ 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(),attachments:z.array(z.object({id:uuid}).strict()).min(1).max(1).optional()}).strict(),recipient:z.object({prospectId:uuid,profileUrl:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('linkedin'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
11
+ const cadence = z.object({notBefore:z.string().datetime().optional(),clientActionId:clientId,afterClientActionId:clientId.nullable(),trigger:z.literal('reported_success').nullable(),at:z.string().nullable(),waitSeconds:z.number().int().min(0).max(7776000).nullable(),condition:z.literal('no_pending_reply').nullable()}).strict();
12
+ 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_activity_list: {description:'Read recorded workspace Activity, newest first: actor, fact, person, time and recorded status. Human tokens require workspace:access; connected agents require workspace:read and workspace policy access, acting as their user. Stage decisions follow Admin/current-assignee visibility. Fixed pages of 50; reuse search and both cursor fields. Read only, no decision or inferred send success.',schema:z.object({...workspace,search:z.string().trim().max(120).optional(),cursor:z.object({at:z.string().datetime(),id:z.string().max(160)}).strict().optional()}).strict(),method:'GET',path:a=>`${root(a)}/operator/activity?${qs({search:a.search,cursorAt:a.cursor?.at,cursorId:a.cursor?.id})}`,scoped:true},
22
+ workspace_work_action_plan_save: {schema:z.object({...workspace,planId:uuid.nullable(),prospectId:uuid,campaignId:uuid.nullable(),campaignRevision: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},
23
+ workspace_work_operator_list: {description:'Read workspace proposals. order=priority matches the Operator queue: open replies first, then oldest proposals, with head-page totals. Omit order for legacy oldest-first order. Keep order and filters unchanged while paging. Read only; grants no execution authority.',schema:z.object({...workspace,order:z.enum(['oldest','priority']).optional(),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','skipped','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,order:a.order})}`,scoped:true},
24
+ 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},
25
+ workspace_work_action_approve: {schema:z.object({...mutation,expectedApprovalVersion:version}).strict(),method:'POST',path:a=>`${action(a)}/approve`,body,scoped:true},
26
+ 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},
27
+ workspace_work_action_cancel: {schema:z.object({...mutation,reason:z.string().min(1)}).strict(),method:'POST',path:a=>`${action(a)}/cancel`,body,scoped:true},
28
+ workspace_work_action_skip: {description:'Human only. Skip one pending touch without sending it; the plan continues with its next step. When no touch is left the plan closes and the person stays in the campaign for the next playbook step. Only skipping the playbook\'s last step for the person ends their campaign, with the note skipped by owner; a skipped reply proposal never does, and the reply stays unanswered.',schema:z.object(mutation).strict(),method:'POST',path:a=>`${action(a)}/skip`,body,scoped:true},
29
+ workspace_work_eligibility_get: {schema:z.object(located).strict(),method:'GET',path:a=>`${action(a)}/eligibility`,scoped:true},
30
+ };
package/cli/work.mjs ADDED
@@ -0,0 +1,32 @@
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','order','cursor-at','cursor-id'];
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 activity':'workspace_work_activity_list','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 skip':'workspace_work_action_skip','work eligibility':'workspace_work_eligibility_get'};
12
+ const name=names[position.join(' ')];if(!name)throw new Error('Use work plan save, list, activity, show, approve, withdraw-approval, cancel, skip, or eligibility. Writes require --file INPUT.json.');
13
+ const read=['workspace_work_activity_list','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_activity_list'){
17
+ if(flags.search!==undefined)input.search=flags.search;
18
+ if((flags['cursor-at']===undefined)!==(flags['cursor-id']===undefined))throw new Error('Activity requires both --cursor-at and --cursor-id.');
19
+ if(flags['cursor-at']!==undefined)input.cursor={at:flags['cursor-at'],id:flags['cursor-id']};
20
+ } else if(name==='workspace_work_operator_list'){
21
+ if(flags.order!==undefined)input.order=flags.order;
22
+ input.page={...(flags.cursor===undefined?{}:{cursor:flags.cursor}),...(flags.limit===undefined?{}:{limit:Number(flags.limit)})};
23
+ 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];
24
+ } else if(name==='workspace_work_operator_get'){
25
+ if(flags.revision!==undefined)input.revision=Number(flags.revision);
26
+ input.history={...(flags['history-cursor']===undefined?{}:{cursor:flags['history-cursor']}),...(flags['history-limit']===undefined?{}:{limit:Number(flags['history-limit'])})};
27
+ }
28
+ 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','order']:[]),...(name==='workspace_work_activity_list'?['search','cursor-at','cursor-id']:[]),...(name==='workspace_work_operator_get'?['revision','history-cursor','history-limit']:[])]);
29
+ if(Object.keys(flags).some(k=>!applicable.has(k)))throw new Error('Option does not apply to this work command.');
30
+ if(read&&flags.file!==undefined)throw new Error('Read commands do not accept --file.');
31
+ console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
32
+ }