@audienti/cli 0.2.0-exora.2 → 0.2.0-exora.21

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 (91) hide show
  1. package/README.md +2 -2
  2. package/cli/account-reply-operations.mjs +27 -1
  3. package/cli/agents.mjs +74 -14
  4. package/cli/assignment-operations.mjs +12 -12
  5. package/cli/assignment.mjs +6 -6
  6. package/cli/attachment-operations.mjs +9 -0
  7. package/cli/attachments.mjs +28 -0
  8. package/cli/audience-operations.mjs +5 -0
  9. package/cli/audiences.mjs +7 -4
  10. package/cli/audienti.mjs +143 -23
  11. package/cli/audit-operations.mjs +17 -0
  12. package/cli/audit.mjs +39 -0
  13. package/cli/budget-operations.mjs +8 -0
  14. package/cli/budgets.mjs +34 -0
  15. package/cli/campaign-find.mjs +155 -0
  16. package/cli/campaign-operations.mjs +30 -0
  17. package/cli/campaigns.mjs +61 -0
  18. package/cli/cohort-operations.mjs +22 -0
  19. package/cli/cohorts.mjs +45 -0
  20. package/cli/connection-registry-operations.mjs +2 -1
  21. package/cli/content.mjs +2 -0
  22. package/cli/discovery-operations.mjs +4 -3
  23. package/cli/discovery.mjs +3 -2
  24. package/cli/file-operations.mjs +29 -0
  25. package/cli/files.mjs +14 -0
  26. package/cli/guardrails-operations.mjs +12 -0
  27. package/cli/guardrails.mjs +37 -0
  28. package/cli/hand-task-operations.mjs +11 -0
  29. package/cli/hand-tasks.mjs +18 -0
  30. package/cli/intent-operations.mjs +35 -0
  31. package/cli/intent.mjs +42 -0
  32. package/cli/json-file.mjs +1 -1
  33. package/cli/kv-operations.mjs +17 -0
  34. package/cli/kv.mjs +58 -0
  35. package/cli/mcp.mjs +5 -5
  36. package/cli/notification-operations.mjs +8 -0
  37. package/cli/offer-operations.mjs +1 -1
  38. package/cli/outbound-operations.mjs +16 -0
  39. package/cli/outbound.mjs +54 -0
  40. package/cli/outcome-operations.mjs +28 -0
  41. package/cli/outcomes.mjs +16 -0
  42. package/cli/owner-account-client.mjs +3 -3
  43. package/cli/playbook-help.mjs +83 -0
  44. package/cli/playbook-operations.mjs +38 -0
  45. package/cli/progress.mjs +29 -7
  46. package/cli/prospecting-settings-operations.mjs +1 -1
  47. package/cli/question-operations.mjs +12 -0
  48. package/cli/questions.mjs +32 -0
  49. package/cli/queue-operations.mjs +12 -0
  50. package/cli/queue.mjs +25 -0
  51. package/cli/recipient-operations.mjs +1 -1
  52. package/cli/recipients.mjs +6 -2
  53. package/cli/rooms-operations.mjs +48 -0
  54. package/cli/rooms.mjs +160 -0
  55. package/cli/search-operations.mjs +9 -0
  56. package/cli/secret-operations.mjs +7 -0
  57. package/cli/secrets.mjs +30 -0
  58. package/cli/sender-account-operations.mjs +7 -3
  59. package/cli/sender-accounts.mjs +18 -8
  60. package/cli/skills-mcp.mjs +13 -6
  61. package/cli/skills.mjs +73 -16
  62. package/cli/tools-operations.mjs +18 -10
  63. package/cli/tools.mjs +48 -20
  64. package/cli/usage-operations.mjs +8 -0
  65. package/cli/usage.mjs +41 -0
  66. package/cli/work-execution-operations.mjs +7 -0
  67. package/cli/work-execution.mjs +3 -3
  68. package/cli/work-operations.mjs +7 -5
  69. package/cli/work.mjs +11 -6
  70. package/cli/workspaces.mjs +132 -24
  71. package/package.json +2 -2
  72. package/skills/agent-rooms/SKILL.md +140 -0
  73. package/skills/agent-rooms/references/commands.md +78 -0
  74. package/skills/audienti/SKILL.md +112 -6
  75. package/skills/audienti/references/helpers.md +15 -2
  76. package/skills/audienti/references/profile-workflows.md +6 -4
  77. package/skills/audienti/scripts/contracts.mjs +1 -1
  78. package/skills/getting-started/SKILL.md +227 -0
  79. package/skills/getting-started/references/audience.md +143 -0
  80. package/skills/getting-started/references/campaign-draft.md +80 -0
  81. package/skills/getting-started/references/capabilities.md +238 -0
  82. package/skills/getting-started/references/mailbox-check.md +64 -0
  83. package/skills/getting-started/references/outcome-checks.md +125 -0
  84. package/skills/getting-started/references/replies.md +265 -0
  85. package/skills/getting-started/references/reply-checks.md +54 -0
  86. package/skills/playbook-design/SKILL.md +135 -0
  87. package/skills/playbook-design/references/fields.md +145 -0
  88. package/source-manifest.json +89 -41
  89. package/cli/approach-operations.mjs +0 -22
  90. package/cli/experiment-operations.mjs +0 -25
  91. package/cli/experiments.mjs +0 -26
@@ -0,0 +1,61 @@
1
+ import {callWorkspace,configuration} from './workspaces.mjs';
2
+ import {readJsonFile} from './json-file.mjs';
3
+ export async function campaignCli(args){
4
+ const position=[],flags={};for(let i=0;i<args.length;i++){
5
+ if(!args[i].startsWith('--')){position.push(args[i]);continue}
6
+ const key=args[i].slice(2);if(!['profile','workspace','file','request-id','expected-revision','revision','archived','limit','after','q','state','operation','expected-state','reason','per-day','count','max-credits','url','add','remove','set'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete campaign option.');
7
+ flags[key]=args[++i];
8
+ }
9
+ const command=position[1];
10
+ if(command==='find'){
11
+ if(position.length!==3)throw new Error('Use campaign find ID --count N --max-credits C.');
12
+ if(!/^\d{1,3}$/.test(flags.count??'')||Number(flags.count)<1||Number(flags.count)>500)throw new Error('Set --count to how many fits to find, from 1 to 500.');
13
+ if(flags['max-credits']===undefined)throw new Error('Set --max-credits to the most credits this run may spend.');
14
+ const {findCampaignProspects,parseCredits}=await import('./campaign-find.mjs');
15
+ const config=configuration(flags.profile);
16
+ const report=await findCampaignProspects((operation,input)=>callWorkspace(operation,input,config),{workspaceId:flags.workspace,campaignId:position[2],count:Number(flags.count),maxCredits:parseCredits(flags['max-credits'])},{progress:step=>console.error(JSON.stringify(step))});
17
+ console.log(JSON.stringify({data:report}));return;
18
+ }
19
+ if(command==='check'){
20
+ if(position.length!==3||!flags.url)throw new Error('Use campaign check ID --url LINKEDIN_URL [--request-id UUID].');
21
+ const {randomUUID}=await import('node:crypto');
22
+ console.log(JSON.stringify(await callWorkspace('campaign_prospect_check',{workspaceId:flags.workspace,campaignId:position[2],url:flags.url,requestId:flags['request-id']??randomUUID()},configuration(flags.profile))));return;
23
+ }
24
+ if(command==='targets'){
25
+ if(position.length!==3)throw new Error('Use campaign targets ID [--add a.com,b.com] [--remove a.com] [--set FILE|-] [--expected-revision N].');
26
+ const config=configuration(flags.profile),input={workspaceId:flags.workspace,campaignId:position[2]};
27
+ const change=['add','remove','set'].some(k=>flags[k]!==undefined);
28
+ if(!change){if(flags.limit!==undefined)input.limit=Number(flags.limit);if(flags.after)input.after=flags.after;console.log(JSON.stringify(await callWorkspace('campaign_targets_get',input,config)));return;}
29
+ if(flags.set!==undefined&&(flags.add!==undefined||flags.remove!==undefined))throw new Error('Use --set on its own, or --add and --remove.');
30
+ const split=v=>v.split(',').map(x=>x.trim()).filter(Boolean);
31
+ if(flags.add!==undefined)input.add=split(flags.add);
32
+ if(flags.remove!==undefined)input.remove=split(flags.remove);
33
+ if(flags.set!==undefined){const {readFileSync}=await import('node:fs');input.set=readFileSync(flags.set==='-'?0:flags.set,'utf8').split(/\r?\n/).map(x=>x.trim()).filter(x=>x&&!x.startsWith('#'));}
34
+ input.expectedRevision=flags['expected-revision']!==undefined?Number(flags['expected-revision']):(await callWorkspace('campaign_targets_get',{workspaceId:flags.workspace,campaignId:position[2],limit:1},config)).data.revision;
35
+ console.log(JSON.stringify(await callWorkspace('campaign_targets_change',input,config)));return;
36
+ }
37
+ if(command==='summary'){
38
+ if(position.length!==2)throw new Error('Use campaign summary [--workspace ID].');
39
+ console.log(JSON.stringify(await callWorkspace('campaign_summary',{workspaceId:flags.workspace},configuration(flags.profile))));return;
40
+ }
41
+ const name={list:'campaign_list',show:'campaign_get',create:'campaign_create',update:'campaign_update',transition:'campaign_transition',successor:'campaign_successor',events:'campaign_events',pace:flags['per-day']===undefined&&flags['max-credits']===undefined?'campaign_pace_get':'campaign_pace_set'}[command];
42
+ if(!name||position.length!==(['list','create'].includes(command)?2:3))throw new Error('Use campaign list|summary|show ID|create|update ID|transition ID|successor ID|events ID|pace ID [--per-day N|derived] [--max-credits C|none]|find ID --count N --max-credits C|check ID --url URL|targets ID [--add|--remove|--set].');
43
+ if(flags['per-day']!==undefined&&command!=='pace')throw new Error('Set the pace with campaign pace ID --per-day N|derived, after the campaign is saved.');
44
+ const input={workspaceId:flags.workspace};
45
+ if(position[2])input.campaignId=position[2];
46
+ if(['list','events'].includes(command)){
47
+ if(flags.archived!==undefined&&!['true','false'].includes(flags.archived))throw new Error('Archived must be true or false.');
48
+ if(flags.archived!==undefined)input.archived=flags.archived==='true';
49
+ if(flags.limit!==undefined)input.limit=Number(flags.limit);
50
+ if(flags.after)input.after=flags.after;
51
+ if(flags.q!==undefined)input.q=flags.q;if(flags.state!==undefined)input.state=flags.state;
52
+ }
53
+ if(command==='show'&&flags.revision!==undefined)input.revision=Number(flags.revision);
54
+ if(['create','update','successor'].includes(command)){if(!flags.file)throw new Error('Supply --file PATH containing Campaign fields.');input.fields=readJsonFile(flags.file,65536,'Campaign file');}
55
+ if(['create','successor'].includes(command))input.requestId=flags['request-id'];
56
+ if(['update','successor'].includes(command))input.expectedRevision=Number(flags['expected-revision']);
57
+ if(command==='transition'){input.operation=flags.operation;input.expectedState=flags['expected-state'];if(flags.reason!==undefined)input.reason=flags.reason;}
58
+ if(command==='pace'&&flags['per-day']!==undefined){const v=flags['per-day'];if(v!=='derived'&&!/^\d+$/.test(v))throw new Error('Pace must be a whole number of people a day, or derived.');input.perDay=v==='derived'?v:Number(v);}
59
+ if(command==='pace'&&flags['max-credits']!==undefined){const v=flags['max-credits'];if(v!=='none'&&!/^\d+$/.test(v))throw new Error('The credit cap must be a whole number of credits a day, or none.');input.maxCredits=v==='none'?null:Number(v);}
60
+ console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
61
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from 'zod';
2
+ const uuid = z.string().uuid();
3
+ const filters = { workspaceId: uuid.optional(), campaignId: uuid.optional(), ownerUserId: uuid.optional(), offerId: uuid.optional(), audienceId: uuid.optional(), tag: z.string().min(1).max(64).optional() };
4
+ const FILTER_TEXT = 'Filters (all optional, combined with AND): campaignId, ownerUserId (the campaign owner), offerId and audienceId (as pinned by the campaign), tag (a campaign tag). List the values with workspace_analytics_filters.';
5
+ const path = name => a => {
6
+ const query = new URLSearchParams(Object.entries(a).filter(([key, value]) => key !== 'workspaceId' && value !== undefined).map(([key, value]) => [key, String(value)]));
7
+ return `/workspaces/${a.workspaceId}/analytics/${name}${query.size ? `?${query}` : ''}`;
8
+ };
9
+ export const cohortOperations = {
10
+ workspace_analytics_cohorts: {
11
+ schema: z.object({ ...filters, stage: z.string().min(1).max(100).optional(), cohorts: z.number().int().min(1).max(52).optional(), horizon: z.number().int().min(1).max(26).optional() }).strict(), method: 'GET', path: path('cohorts'), scoped: true,
12
+ description: `Weekly entry cohorts: people grouped by the UTC week (Monday start) they were admitted to a campaign, and for each week since admission the share that reached a stage. stage is "goal" (default, each playbook's goal) or a stage key from the stages list in the response. cohorts is the number of weeks (default 12, at most 52); horizon is the number of weeks since entry shown after week 0 (default 8, at most 26). A cell is empty (null) until every member of the cohort has had that long. ${FILTER_TEXT} Same figures as Analytics > Cohorts.`,
13
+ },
14
+ workspace_stage_aging: {
15
+ schema: z.object({ ...filters, thresholdDays: z.number().int().min(1).max(365).optional() }).strict(), method: 'GET', path: path('stage-aging'), scoped: true,
16
+ description: `Stage aging: how long people still active in a campaign have been in their current stage, by stage: count, median days, 90th percentile days, oldest, and how many are over thresholdDays (default 7). ${FILTER_TEXT} Same figures as Analytics > Stage aging.`,
17
+ },
18
+ workspace_analytics_filters: {
19
+ schema: z.object({ workspaceId: uuid.optional() }).strict(), method: 'GET', path: path('filters'), scoped: true,
20
+ description: 'List the values the analytics filters can take in this workspace: campaigns, campaign owners, offers, audiences and campaign tags, with ids and names. Use the ids with workspace_analytics_get, workspace_analytics_cohorts and workspace_stage_aging.',
21
+ },
22
+ };
@@ -0,0 +1,45 @@
1
+ import { callWorkspace, configuration } from './workspaces.mjs';
2
+
3
+ const cell = value => { const text = String(value ?? ''); return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; };
4
+ const csv = rows => rows.map(row => row.map(cell).join(',')).join('\n') + '\n';
5
+ const share = value => value === null || value === undefined ? '' : `${(value * 100).toFixed(1)}%`;
6
+
7
+ /** One row per entry week: cohort size, then the share that reached the stage by each week since entry. */
8
+ export function cohortsCsv(data) {
9
+ const weeks = Array.from({ length: (data.horizon ?? 0) + 1 }, (_, i) => `week_${i}`);
10
+ return csv([['cohort_week', 'people', ...weeks], ...(data.weeks ?? []).map(row => [row.weekStart, row.size, ...row.share.map(share)])]);
11
+ }
12
+ export function agingCsv(data) {
13
+ return csv([['stage', 'people', 'median_days', 'p90_days', 'oldest_days', `over_${data.thresholdDays}_days`], ...(data.stages ?? []).map(row => [row.name, row.count, row.medianDays ?? '', row.p90Days ?? '', row.oldestDays ?? '', row.overThreshold])]);
14
+ }
15
+
16
+ const FLAGS = { workspace: null, profile: null, format: null, campaign: 'campaignId', owner: 'ownerUserId', offer: 'offerId', audience: 'audienceId', tag: 'tag', stage: 'stage', cohorts: 'cohorts', horizon: 'horizon', 'threshold-days': 'thresholdDays' };
17
+ const NUMBERS = new Set(['cohorts', 'horizon', 'threshold-days']);
18
+ const ALLOWED = { cohorts: ['campaign', 'owner', 'offer', 'audience', 'tag', 'stage', 'cohorts', 'horizon'], aging: ['campaign', 'owner', 'offer', 'audience', 'tag', 'threshold-days'], filters: [] };
19
+ const TOOLS = { cohorts: 'workspace_analytics_cohorts', aging: 'workspace_stage_aging', filters: 'workspace_analytics_filters' };
20
+ const USAGE = 'Use analytics cohorts|aging|filters [--workspace ID] [--campaign ID] [--owner ID] [--offer ID] [--audience ID] [--tag TAG] [cohorts: --stage goal|KEY --cohorts N --horizon N] [aging: --threshold-days N] [--format json|csv].';
21
+
22
+ export async function cohortCli(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 (!Object.hasOwn(FLAGS, key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}.`);
30
+ flags[key] = args[++i];
31
+ }
32
+ const kind = positions[1];
33
+ if (positions.length !== 2 || !TOOLS[kind]) throw Error(USAGE);
34
+ const input = { workspaceId: flags.workspace };
35
+ for (const [key, value] of Object.entries(flags)) {
36
+ if (['workspace', 'profile', 'format'].includes(key)) continue;
37
+ if (!ALLOWED[kind].includes(key)) throw Error(`--${key} does not apply to analytics ${kind}.`);
38
+ if (NUMBERS.has(key)) { if (!/^\d+$/.test(value)) throw Error(`--${key} must be a whole number.`); input[FLAGS[key]] = Number(value); } else input[FLAGS[key]] = value;
39
+ }
40
+ if (flags.format && !['json', 'csv'].includes(flags.format)) throw Error('--format must be json or csv.');
41
+ if (flags.format === 'csv' && kind === 'filters') throw Error('--format csv applies to analytics cohorts and aging.');
42
+ const result = await callWorkspace(TOOLS[kind], input, configuration(flags.profile));
43
+ if (flags.format === 'csv') { const data = result?.data ?? result; process.stdout.write(kind === 'cohorts' ? cohortsCsv(data) : agingCsv(data)); return; }
44
+ console.log(JSON.stringify(result));
45
+ }
@@ -3,5 +3,6 @@ const uuid=z.string().uuid(),scope={workspaceId:uuid.optional()},kind=z.enum(['c
3
3
  export const connectionRegistryOperations={
4
4
  workspace_connections_list:{schema:z.object({...scope,kind:kind.optional(),userId:uuid.optional(),cursor:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}?${new URLSearchParams(Object.entries(a).filter(([k,v])=>k!=='workspaceId'&&v!==undefined).map(([k,v])=>[k,String(v)]))}`,scoped:true,description:'Read agent-managed registrations, current authorization and report freshness. Does not inspect provider credentials or access remote accounts.'},
5
5
  workspace_connection_register:{schema:z.object({...scope,requestId:uuid,agentId:uuid,kind,provider:z.string(),name:z.string().trim().min(1).max(120)}).strict(),method:'POST',path:base,body:({workspaceId,...a})=>a,scoped:true,description:'Register your own agent’s existing workspace system. Current Admin required for CRM/data; members may register own calendars. Starts pending; no OAuth, credentials or external calls.'},
6
- workspace_connection_report:{schema:z.object({...scope,id:uuid,requestId:uuid,expectedVersion:z.number().int().positive(),kind,provider:z.string(),status:z.enum(['reported','unavailable']),capabilities:z.array(z.enum(['read_customers','read_deals','read_contacts','write_contacts','write_meetings','lookup_people','lookup_companies','read_booked_meetings'])).max(8)}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/reports`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Designated active agent only: report existing access, not a sync claim. Fixed 24h freshness. Calendar is booked-meeting access only; no send authorization. Same request replays historical response; list for current status.'}
6
+ workspace_connection_report:{schema:z.object({...scope,id:uuid,requestId:uuid,expectedVersion:z.number().int().positive(),kind,provider:z.string(),status:z.enum(['reported','unavailable']),capabilities:z.array(z.enum(['read_customers','read_deals','read_contacts','write_contacts','write_meetings','lookup_people','lookup_companies','read_booked_meetings'])).max(8)}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/reports`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Designated active agent only: report existing access, not a sync claim. Fixed 24h freshness. Calendar is booked-meeting access only; no send authorization. Same request replays historical response; list for current status.'},
7
+ workspace_connection_disconnect:{schema:z.object({...scope,id:uuid,requestId:uuid,expectedVersion:z.number().int().positive()}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/disconnect`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Stop sharing a connection with the workspace. People only. Current Admin required, except members may disconnect their own calendar. Send expectedVersion from the last list.'},
7
8
  };
package/cli/content.mjs CHANGED
@@ -10,6 +10,7 @@ const help = `Usage:
10
10
 
11
11
  Topic input: {"name":"Topic name","slug":"optional-slug","description":"optional"}.
12
12
  These commands read saved records or create a topic; they do not call a paid provider.
13
+ Each post names the tool run that last saved it in savedByOperationId; read the run with audienti tools runs get OPERATION_ID.
13
14
  Retrieve fresh data with audienti tools posts.fetch|posts.search|posts.comments|posts.reactions|profiles.posts.
14
15
  Use audienti tools describe NAME --json for the exact contract.
15
16
  All commands accept --profile PATH, --workspace UUID and --json.`;
@@ -21,6 +22,7 @@ export async function contentCli(argv) {
21
22
  let args = values.workspace ? { workspaceId: values.workspace } : {};
22
23
  if (verb === 'list' && positionals.length === 1) args = { ...args, cursor: values.cursor, ...(kind === 'posts' ? { topicId: values['topic-id'] } : {}) };
23
24
  else if (verb === 'get' && positionals.length === 2) args.id = id;
25
+ else if (verb === 'get') throw new Error(`Name the ${kind === 'posts' ? 'post' : 'topic'} to read: audienti ${kind} get ID.`);
24
26
  else if (kind === 'topics' && verb === 'create' && positionals.length === 1) {
25
27
  if (!values.input || !values['request-id']) throw new Error('Topic create requires --input FILE and --request-id UUID.');
26
28
  args = { ...readJsonFile(values.input, 32768, 'Topic input'), ...args, requestId: values['request-id'] };
@@ -5,7 +5,7 @@ const page=z.object({cursor:uuid.optional(),limit:z.number().int().min(1).max(10
5
5
  const endpoint=z.object({kind:z.enum(['prospect','profile']),id:uuid}).strict();
6
6
  const subject=z.object({kind:z.enum(['person','company']),prospectId:uuid}).strict();
7
7
  const text=z.string().nullable().optional();
8
- const fields=z.object({name:text,title:text,location:text,bio:text,domain:text,description:text}).strict();
8
+ const fields=z.object({name:text,title:text,companyName:text,location:text,bio:text,domain:text,description:text,imageUrl:text}).strict();
9
9
  const status=(values)=>z.object({status:z.enum(values),evidenceIds:z.array(uuid)}).strict();
10
10
  const root=a=>`/workspaces/${a.workspaceId}/prospects`;
11
11
  const body=a=>Object.fromEntries(Object.entries(a).filter(([k])=>k!=='workspaceId'));
@@ -20,8 +20,9 @@ export const discoveryOperations={
20
20
 
21
21
  prospect_list:{schema:z.object({...workspace,kind:z.enum(['person','company']).optional(),search:z.string().optional(),cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>root(a)+'?'+qs(a),scoped:true},
22
22
  prospect_get:{schema:z.object({...workspace,prospectId:uuid,profiles:page.optional(),observations:page.optional(),citations:page.optional(),qualifications:page.optional()}).strict(),method:'GET',path:a=>root(a)+`/${a.prospectId}?`+qs(a),scoped:true},
23
+ prospect_timeline:{description:'Read what happened to a person, newest first: profile reads with the fields each set, changed or confirmed; signal evidence; qualification checks; campaign stage events; your conversations with agents about the person. Free CRM read; no provider call.',schema:z.object({...workspace,prospectId:uuid,limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>root(a)+`/${a.prospectId}/timeline?`+qs(a),scoped:true},
23
24
  prospect_record_observation:{schema:z.object({...request,kind:z.enum(['person','company']),prospectId:uuid.optional(),source:z.object({provider:z.string(),providerId:z.string().optional(),url:z.string().optional()}).strict(),fields,provenance:z.object({url:z.string(),excerpt:z.string().optional()}).strict().optional(),observedAt:z.string().nullable().optional(),skill:skill.optional()}).strict(),method:'POST',path:a=>root(a)+'/observations',body,scoped:true},
24
25
  prospect_record_citation:{schema:z.object({...request,source:endpoint,target:endpoint,edgeType:z.enum(['works_for','works_at','worked_at','references']),roles:z.array(z.object({roleId:z.string(),title:z.string(),current:z.boolean(),startDate:text,endDate:text,employmentType:text,observedAt:text}).strict()).optional(),evidenceIds:z.array(uuid).optional(),employmentSelection:z.object({action:z.enum(['set_primary','clear']),roleId:z.string().optional(),evidenceHash:z.string().optional()}).strict().optional()}).strict(),method:'POST',path:a=>root(a)+'/citations',body,scoped:true},
25
- prospect_record_evidence:{schema:z.object({...request,subject,experimentId:uuid.optional(),experimentRevision:z.number().int().positive().optional(),kind:z.enum(['signal','interpretation']),data:z.record(z.string(),z.unknown()),sourceUrl:text,observedAt:text,skill}).strict(),method:'POST',path:a=>root(a)+'/evidence',body,scoped:true},
26
- experiment_submit_qualification:{schema:z.object({...request,experimentId:uuid,experimentRevision:z.number().int().positive(),prospectId:uuid,companyProspectId:uuid.optional(),companyIcp:status(['pass','fail','unverified']),personIcp:status(['pass','fail','unverified']),signal:status(['match','no_match','unverified','not_required']),fit:z.object({status:z.enum(['high','moderate','low','no_fit','unscored']),evidenceIds:z.array(uuid),reason:z.string().optional()}).strict(),skill}).strict(),method:'POST',path:a=>`/workspaces/${a.workspaceId}/experiments/${a.experimentId}/qualifications`,body:a=>Object.fromEntries(Object.entries(body(a)).filter(([k])=>k!=='experimentId')),scoped:true},
26
+ prospect_record_evidence:{schema:z.object({...request,subject,campaignId:uuid.optional(),campaignRevision:z.number().int().positive().optional(),kind:z.enum(['signal','interpretation']),data:z.record(z.string(),z.unknown()),sourceUrl:text,observedAt:text,skill}).strict(),method:'POST',path:a=>root(a)+'/evidence',body,scoped:true},
27
+ campaign_submit_qualification:{schema:z.object({...request,campaignId:uuid,campaignRevision:z.number().int().positive(),prospectId:uuid,companyProspectId:uuid.optional(),companyIcp:status(['pass','fail','unverified']),personIcp:status(['pass','fail','unverified']),signal:status(['match','no_match','unverified','not_required']),fit:z.object({status:z.enum(['high','moderate','low','no_fit','unscored']),evidenceIds:z.array(uuid),reason:z.string().optional()}).strict(),skill}).strict(),method:'POST',path:a=>`/workspaces/${a.workspaceId}/campaigns/${a.campaignId}/qualifications`,body:a=>Object.fromEntries(Object.entries(body(a)).filter(([k])=>k!=='campaignId')),scoped:true},
27
28
  };
package/cli/discovery.mjs CHANGED
@@ -4,11 +4,12 @@ export async function discoveryCli(args){
4
4
  const flags={},position=[];
5
5
  const children=['profiles','observations','citations','qualifications'];
6
6
  const allowed=['profile','workspace','file','request-id','kind','search','cursor','limit',...children.flatMap(k=>[k+'-cursor',k+'-limit'])];
7
- for(let i=0;i<args.length;i++){if(!args[i].startsWith('--')){position.push(args[i]);continue;}const key=args[i].slice(2);if(!allowed.includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete discovery option.');flags[key]=args[++i];}
7
+ for(let i=0;i<args.length;i++){if(args[i]==='--json')continue;if(!args[i].startsWith('--')){position.push(args[i]);continue;}const key=args[i].slice(2);if(!allowed.includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete discovery option.');flags[key]=args[++i];}
8
8
  const command=position.slice(0,3).join(' ');
9
9
  let name,input={workspaceId:flags.workspace};
10
10
  if(position[0]==='prospects'&&position[1]==='list'&&position.length===2){name='prospect_list';for(const k of ['kind','search','cursor'])if(flags[k]!==undefined)input[k]=flags[k];if(flags.limit!==undefined)input.limit=Number(flags.limit);}
11
11
  else if(position[0]==='prospects'&&position[1]==='get'&&position.length===3){name='prospect_get';input.prospectId=position[2];for(const k of children){const cursor=flags[k+'-cursor'],limit=flags[k+'-limit'];if(cursor!==undefined||limit!==undefined)input[k]={...(cursor===undefined?{}:{cursor}),...(limit===undefined?{}:{limit:Number(limit)})};}}
12
- else{ name={'prospects observation create':'prospect_record_observation','prospects citation create':'prospect_record_citation','prospects evidence create':'prospect_record_evidence','experiments qualification submit':'experiment_submit_qualification'}[command];if(!name||position.length!==3||!flags.file)throw new Error('Use prospects list|get ID|observation create|citation create|evidence create, or experiments qualification submit --file INPUT.json.');input={...readJsonFile(flags.file,65536,'Discovery file'),...(flags.workspace?{workspaceId:flags.workspace}:{}),...(flags['request-id']?{requestId:flags['request-id']}:{})}; }
12
+ else if(position[0]==='prospects'&&position[1]==='timeline'&&position.length===3){name='prospect_timeline';input.prospectId=position[2];if(flags.limit!==undefined)input.limit=Number(flags.limit);}
13
+ else{ name={'prospects observation create':'prospect_record_observation','prospects citation create':'prospect_record_citation','prospects evidence create':'prospect_record_evidence','campaigns qualification submit':'campaign_submit_qualification'}[command];if(!name||position.length!==3||!flags.file)throw new Error('Use prospects list|get ID|timeline ID|observation create|citation create|evidence create, or campaigns qualification submit --file INPUT.json.');input={...readJsonFile(flags.file,65536,'Discovery file'),...(flags.workspace?{workspaceId:flags.workspace}:{}),...(flags['request-id']?{requestId:flags['request-id']}:{})}; }
13
14
  console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
14
15
  }
@@ -0,0 +1,29 @@
1
+ import {z} from 'zod';
2
+ const fileType=z.string().max(127).refine(t=>['image/jpeg','image/png','application/pdf','video/quicktime','video/mp4'].includes(t)||/^text\/[a-z0-9][a-z0-9!#$&^_.+-]*$/i.test(t));
3
+ const fields={fileType,fileName:z.string().min(1).max(255)},key=z.string().min(1).max(1024);
4
+ const authority='Personal account files only. Verified human workspace:access credential required; agent credentials are rejected. No workspace selection.';
5
+ export const fileOperations={
6
+ file_list:{schema:z.object({}).strict(),method:'GET',path:()=>'/me/files',description:`List owned file records. ${authority}`},
7
+ file_upload_prepare:{schema:z.object(fields).strict(),method:'POST',path:()=>'/me/files/uploads',body:a=>a,description:`Prepare upload; POST returned fields and file bytes to s3UploadUrl, then register. Max5MiB, exact concrete MIME. Sensitive signed fields expire in one hour; do not persist. Signing does not upload. ${authority}`},
8
+ file_register:{schema:z.object({...fields,s3Key:key}).strict(),method:'POST',path:()=>'/me/files',body:a=>a,description:`Register an uploaded owner-prefix object. Reuse key and metadata after an unknown response; same input replays, changed metadata conflicts. ${authority}`},
9
+ file_download:{schema:z.object({s3Key:key}).strict(),method:'POST',path:()=>'/me/files/download',body:a=>a,description:`Get one-hour signed URL for owned file. Sensitive bearer URL: do not persist or share. ${authority}`},
10
+ file_delete:{schema:z.object({id:z.string().uuid()}).strict(),method:'POST',path:()=>'/me/files/delete',body:a=>a,description:`Remove owned record; storage cleanup is best effort. A success does not guarantee physical deletion or revoke outstanding signed URLs. ${authority}`},
11
+ };
12
+
13
+ const maxBytes=5*1024*1024;
14
+ fileOperations.file_upload={localUpload:true,schema:z.object({...fields,contentBase64:z.string().min(4).max(4*Math.ceil(maxBytes/3)).refine(value=>/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)&&Buffer.from(value,'base64').length<=maxBytes)}).strict(),description:`Upload small files and register them, composing the same signed POST protocol as the UI. Supply base64 content, at most5MiB; prefer the CLI for large files. Trust only your configured API server: its signed response chooses the storage host. On an uncertain response use the returned recovery key with file_register, never blindly repeat file_upload. ${authority}`};
15
+ export async function uploadFileContent(input,call){
16
+ const bytes=Buffer.from(input.contentBase64,'base64');
17
+ const prepared=(await call('file_upload_prepare',{fileName:input.fileName,fileType:input.fileType})).data;
18
+ const recovery={fileName:input.fileName,fileType:input.fileType,s3Key:prepared.s3Key};
19
+ try{
20
+ const target=new URL(prepared.s3UploadUrl);if(target.protocol!=='https:'&&!(target.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(target.hostname)))throw Error('Unsafe storage URL');
21
+ if(target.username||target.password)throw Error('Unsafe storage URL');
22
+ const form=new FormData();for(const [key,value] of Object.entries(prepared.s3UploadFields))form.append(key,String(value));form.append('file',new Blob([bytes],{type:input.fileType}),input.fileName);
23
+ const response=await fetch(target,{method:'POST',body:form,redirect:'error',signal:AbortSignal.timeout(60000)});if(!response.ok)throw Error('Storage rejected upload');
24
+ return await call('file_register',recovery);
25
+ }catch{
26
+ const error=new Error('Upload completion is unconfirmed. Inspect owned files or retry file_register with the recovery fields. Do not repeat file_upload blindly.');
27
+ error.toJSON=()=>({code:'file_upload_unconfirmed',message:error.message,recovery});throw error;
28
+ }
29
+ }
package/cli/files.mjs ADDED
@@ -0,0 +1,14 @@
1
+ import {readFileSync,statSync} from 'node:fs';
2
+ import {basename} from 'node:path';
3
+ import {callWorkspace,configuration} from './workspaces.mjs';
4
+ export async function fileCli(args){
5
+ const position=[],flags={};for(let i=0;i<args.length;i++){if(!args[i].startsWith('--')){position.push(args[i]);continue;}const key=args[i].slice(2);if(key==='json')continue;if(!['profile','name','type','key'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw Error('Unknown or incomplete file option.');flags[key]=args[++i];}
6
+ const command=position[1],name={list:'file_list',upload:'file_upload',prepare:'file_upload_prepare',register:'file_register',download:'file_download',delete:'file_delete'}[command];
7
+ if(!name||position.length!==(['delete','upload'].includes(command)?3:2))throw Error('Use file upload PATH --type MIME|list|prepare --name NAME --type MIME|register --key KEY --name NAME --type MIME|download --key KEY|delete ID.');
8
+ if(command==='upload'){
9
+ const path=position[2],stat=statSync(path);if(!stat.isFile()||stat.size===0||stat.size>5*1024*1024)throw Error('Select a non-empty file up to5MiB.');
10
+ const contentBase64=readFileSync(path).toString('base64');console.log(JSON.stringify(await callWorkspace(name,{fileName:flags.name??basename(path),fileType:flags.type,contentBase64},configuration(flags.profile))));return;
11
+ }
12
+ const input=command==='list'?{}:command==='delete'?{id:position[2]}:command==='download'?{s3Key:flags.key}:{fileName:flags.name,fileType:flags.type,...(command==='register'?{s3Key:flags.key}:{})};
13
+ console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
14
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ const workspace = { workspaceId: z.string().uuid().optional() };
3
+ const base = a => `/workspaces/${a.workspaceId}/guardrails`;
4
+ const values = z.array(z.string().trim().min(1).max(500)).max(200);
5
+ export const guardrailsOperations = {
6
+ workspace_guardrails_get: { schema: z.object(workspace).strict(), method: 'GET', path: base, scoped: true, description: 'Read workspace guardrails: whether automation is paused, who paused it, and the do-not-contact list.' },
7
+ workspace_guardrails_pause: { schema: z.object(workspace).strict(), method: 'POST', path: a => `${base(a)}/pause`, body: () => ({}), scoped: true, description: 'Pause all automation in the workspace. Sends, connects and hosted tools are refused with workspace_paused until a person resumes. Admin only.' },
8
+ workspace_guardrails_resume: { schema: z.object(workspace).strict(), method: 'POST', path: a => `${base(a)}/resume`, body: () => ({}), scoped: true, description: 'Resume automation. Only a person can resume; agent credentials are refused.' },
9
+ workspace_do_not_contact_update: { schema: z.object({ ...workspace, add: values.optional(), remove: values.optional() }).strict(), method: 'POST', path: a => `${base(a)}/do-not-contact`, body: a => ({ ...(a.add ? { add: a.add } : {}), ...(a.remove ? { remove: a.remove } : {}) }), scoped: true, description: 'Add or remove domains, email addresses and LinkedIn URLs on the do-not-contact list. Agents may add but not remove.' },
10
+ workspace_do_not_contact_import: { schema: z.object({ ...workspace, csv: z.string().min(1).max(400000) }).strict(), method: 'POST', path: a => `${base(a)}/do-not-contact/import`, body: a => ({ csv: a.csv }), scoped: true, description: 'Import do-not-contact entries from CSV text (domains, emails, LinkedIn URLs; at most 5,000 rows; header optional, value column named value/domain/email/linkedin/url or the first). Duplicates are skipped and invalid rows reported with row numbers. Matching people are blocked retroactively. Agents may import.' },
11
+ workspace_do_not_contact_export: { schema: z.object(workspace).strict(), method: 'GET', path: a => `${base(a)}/do-not-contact/export`, scoped: true, description: 'Export the do-not-contact list as CSV text (type, value, added_at, added_by, added_via).' },
12
+ };
@@ -0,0 +1,37 @@
1
+ import { readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { callWorkspace, configuration } from './workspaces.mjs';
3
+ export async function guardrailsCli(args) {
4
+ const flags = {}, positions = [];
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ if (arg === '--json') continue;
8
+ if (!arg.startsWith('--')) { positions.push(arg); continue; }
9
+ const key = arg.slice(2);
10
+ if (!['workspace', 'profile', 'file', 'output'].includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}.`);
11
+ flags[key] = args[++i];
12
+ }
13
+ const [group, verb, value] = positions;
14
+ const input = { workspaceId: flags.workspace };
15
+ let name, expected = 2;
16
+ if (group === 'guardrails' && verb === 'show') name = 'workspace_guardrails_get';
17
+ else if (group === 'guardrails' && verb === 'pause') name = 'workspace_guardrails_pause';
18
+ else if (group === 'guardrails' && verb === 'resume') name = 'workspace_guardrails_resume';
19
+ else if (group === 'dnc' && verb === 'list') name = 'workspace_guardrails_get';
20
+ else if (group === 'dnc' && (verb === 'add' || verb === 'remove')) { name = 'workspace_do_not_contact_update'; expected = 3; input[verb] = [value]; }
21
+ else if (group === 'dnc' && verb === 'import') {
22
+ name = 'workspace_do_not_contact_import'; expected = 2;
23
+ if (!flags.file) throw Error('dnc import needs --file LIST.csv.');
24
+ if (statSync(flags.file).size > 400000) throw Error('The CSV is larger than 400,000 characters. Split it into files of at most 5,000 rows.');
25
+ input.csv = readFileSync(flags.file, 'utf8');
26
+ }
27
+ else if (group === 'dnc' && verb === 'export') name = 'workspace_do_not_contact_export';
28
+ else throw Error('Use guardrails show|pause|resume or dnc list|add|remove VALUE|import --file LIST.csv|export [--output FILE.csv] --workspace ID.');
29
+ if (positions.length !== expected) throw Error('Wrong number of arguments. Use audienti help.');
30
+ const result = await callWorkspace(name, input, configuration(flags.profile));
31
+ if (group === 'dnc' && verb === 'export') {
32
+ const csv = (result?.data ?? result)?.csv ?? '';
33
+ if (flags.output) { writeFileSync(flags.output, csv); console.log(JSON.stringify({ data: { file: flags.output, count: (result?.data ?? result)?.count ?? 0 } })); } else process.stdout.write(csv);
34
+ return;
35
+ }
36
+ console.log(JSON.stringify(group === 'dnc' && verb === 'list' ? { data: result.data?.doNotContact ?? result.doNotContact ?? result } : result));
37
+ }
@@ -0,0 +1,11 @@
1
+ import {z} from 'zod';
2
+ const uuid=z.string().uuid(),scope={workspaceId:uuid.optional()};
3
+ const base=a=>`/workspaces/${a.workspaceId}/hand-tasks`;
4
+ const person='People only: an agent reads hand tasks but cannot do or record one.';
5
+ export const handTaskOperations={
6
+ hand_task_list:{schema:z.object({...scope,state:z.enum(['open','ready','done','skipped']).optional()}).strict(),method:'GET',path:a=>`${base(a)}${a.state?`?state=${a.state}`:''}`,scoped:true,description:'List the hand tasks of the workspace: Custom steps (a phone call, a gift, a handwritten note, a video) that an agent prepared and a person does by hand. open (the default) is what a person still has to do plus finished tasks whose event is awaited. Each item names the person, the step, the prepared items (script, note text, gift choice), the state and the event the playbook waits for.'},
7
+ hand_task_get:{schema:z.object({...scope,id:uuid}).strict(),method:'GET',path:a=>`${base(a)}/${a.id}`,scoped:true,description:'Read one hand task with its prepared items, outcome, notes and event.'},
8
+ hand_task_complete:{schema:z.object({...scope,id:uuid,outcome:z.string().trim().min(1).max(2000).describe('What happened, in a line: spoke for ten minutes, left a voicemail, gift handed over.'),notes:z.string().trim().max(8000).optional().describe('Call notes or anything the next step should know.'),result:z.enum(['replied','meeting_booked']).optional().describe('Set when the person replied or a meeting was booked; the playbook records the usual event.')}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/complete`,body:({workspaceId,id,...a})=>a,scoped:true,description:`Mark a hand task done and record what happened. The playbook continues: the step counts as run, and a wait for a named event starts. ${person}`},
9
+ hand_task_skip:{schema:z.object({...scope,id:uuid,reason:z.string().trim().min(1).max(2000)}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/skip`,body:({workspaceId,id,...a})=>a,scoped:true,description:`Close a hand task without doing it, with the reason. The playbook moves on without waiting. ${person}`},
10
+ hand_task_event:{schema:z.object({...scope,id:uuid,name:z.string().trim().min(1).max(120).optional(),notes:z.string().trim().max(8000).optional()}).strict(),method:'POST',path:a=>`${base(a)}/${a.id}/event`,body:({workspaceId,id,...a})=>a,scoped:true,description:`Record the event a finished hand task was waiting for, for example that the gift was delivered. It ends only that wait. ${person}`},
11
+ };
@@ -0,0 +1,18 @@
1
+ import {callWorkspace,configuration} from './workspaces.mjs';
2
+ const help='Use hand-tasks [list [--state open|ready|done|skipped]] | show ID | done ID --outcome TEXT [--notes TEXT] [--result replied|meeting_booked] | skip ID --reason TEXT | event ID [--name TEXT] [--notes TEXT]. Add --workspace ID to pick a workspace.';
3
+ export async function handTasksCli(args){
4
+ const position=[],flags={};for(let i=1;i<args.length;i++){
5
+ if(!args[i].startsWith('--')){position.push(args[i]);continue}
6
+ const key=args[i].slice(2);if(key==='json')continue;if(!['profile','workspace','state','outcome','notes','result','reason','name'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete hand-tasks option. ${help}`);
7
+ flags[key]=args[++i];
8
+ }
9
+ const command=position[0]??'list',id=position[1],config=configuration(flags.profile),workspaceId=flags.workspace;
10
+ const call=async(name,input)=>console.log(JSON.stringify((await callWorkspace(name,{workspaceId,...input},config)).data));
11
+ if(command==='list'){if(position.length>1)throw new Error(help);return call('hand_task_list',flags.state?{state:flags.state}:{});}
12
+ if(!id||position.length!==2)throw new Error(help);
13
+ if(command==='show')return call('hand_task_get',{id});
14
+ if(command==='done'){if(!flags.outcome)throw new Error('Say what happened: --outcome TEXT.');return call('hand_task_complete',{id,outcome:flags.outcome,...(flags.notes?{notes:flags.notes}:{}),...(flags.result?{result:flags.result}:{})});}
15
+ if(command==='skip'){if(!flags.reason)throw new Error('Say why you skip it: --reason TEXT.');return call('hand_task_skip',{id,reason:flags.reason});}
16
+ if(command==='event')return call('hand_task_event',{id,...(flags.name?{name:flags.name}:{}),...(flags.notes?{notes:flags.notes}:{})});
17
+ throw new Error(help);
18
+ }
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ const bounce = z.object({ type: z.literal('email.bounce'), address: z.string().meta({ minLength: 3, maxLength: 320 }).describe('The address that bounced.'), kind: z.enum(['hard', 'soft']).describe('hard: the address does not exist or refuses mail. soft: a full mailbox or a temporary failure.'), evidence: z.string().meta({ minLength: 1, maxLength: 2000 }).describe('The bounce message, its status code and diagnostic.') }).strict();
3
+ const own = z.string().meta({ minLength: 1, maxLength: 500 }).optional().describe("Optional. The label's own quote is used when this is left out.");
4
+ const resultInput = z.discriminatedUnion('type', [
5
+ bounce,
6
+ z.object({ type: z.literal('reply.stop'), scope: z.enum(['person', 'company']).describe('company only when the person asked to stop for their whole company.'), quote: own }).strict(),
7
+ z.object({ type: z.literal('reply.referral'), name: z.string().meta({ minLength: 1, maxLength: 200 }).describe('The person named.'), title: z.string().meta({ maxLength: 200 }).optional(), email: z.string().meta({ maxLength: 320 }).optional(), linkedinUrl: z.string().meta({ maxLength: 500 }).optional(), quote: own }).strict(),
8
+ ]);
9
+ const uuid = z.string().uuid();
10
+ const label = z.object({
11
+ label: z.string().describe('A name from intent_labels_list that fits the message direction and channel.'),
12
+ quote: z.string().meta({ minLength: 1, maxLength: 300 }).describe('The words in the message that support the label. Copy them from the message; case and spacing do not matter.'),
13
+ reason: z.string().meta({ minLength: 1, maxLength: 200 }).describe('One plain sentence: why the quote means this label. A person reads it, so no IDs, error codes or status numbers.'),
14
+ confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Optional. It is stored with the label when given and does not change what the label does yet.'),
15
+ result: resultInput.optional().describe('The typed detail a stop (reply.stop), bounce (email.bounce) or referral (reply.referral) needs. Same shape as replies set; a stop or referral takes the label quote as its own when it has none.'),
16
+ meeting: z.object({ at: z.string().datetime(), source: z.enum(['sent_invite', 'received_invite']) }).strict().optional().describe('Only for meeting_booked: when the meeting is (UTC, ending in Z) and who sent the invite.'),
17
+ returnsOn: z.string().datetime().optional().describe('Only for out_of_office: the day the person is back (UTC, ending in Z).'),
18
+ invite: z.literal(true).optional().describe('The person sent a calendar invite that is not processed yet.'),
19
+ }).strict();
20
+ export const intentOperations = {
21
+ intent_labels_list: {
22
+ schema: z.object({ workspaceId: uuid.optional() }).strict(),
23
+ method: 'GET', path: a => `/workspaces/${a.workspaceId}/intent-labels`, scoped: true,
24
+ description: 'List the closed set of message labels: each name with its direction (inbound or outbound), the event it starts, the channels it fits, the typed detail it needs, whether it is locked or must stand alone, and whether it is only planned. Read it to learn which names exist, then record one with intent_record.',
25
+ },
26
+ intent_record: {
27
+ schema: z.object({
28
+ workspaceId: uuid.optional(), prospectId: uuid, evidenceId: uuid, requestId: uuid,
29
+ participationId: uuid.optional().describe('Record on this one campaign participation only. Leave out to record on every running campaign of the person.'),
30
+ labels: z.array(label).min(1),
31
+ }).strict(),
32
+ method: 'POST', path: a => `/workspaces/${a.workspaceId}/intents`, scoped: true, body: ({ workspaceId, ...rest }) => rest,
33
+ description: 'Record what a message means: one to three labels, each with the quote that supports it and a plain reason. The message must already be stored as conversation evidence with its text. Every label is checked first and nothing is written if one fails: the name is known and fits the direction and channel, the quote is in the message, the reason is plain words, the labels fit together (stop, bounce and out_of_office stand alone), and the typed detail a stop, bounce, referral or booked meeting needs is present. A refusal names the field, the allowed values and the next step. The same requestId with the same body returns the first answer. The matching events land on the person\'s running campaigns, so the playbook rules act on them. Replies set records the same labels when you give it an intent block. A message you sent can be labeled too: store it first as outbound conversation evidence, then record meeting_requested when it asks for or offers a meeting. That puts the request on the meeting timeline and starts the meeting follow-ups without a reply.',
34
+ },
35
+ };
package/cli/intent.mjs ADDED
@@ -0,0 +1,42 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { callWorkspace, configuration } from './workspaces.mjs';
3
+ import { readJsonFile } from './json-file.mjs';
4
+
5
+ const RECORD_FLAGS = ['workspace', 'profile', 'person', 'evidence', 'label', 'quote', 'reason', 'confidence', 'request-id', 'participation', 'file'];
6
+ const USAGE = 'Use intent list [--workspace ID], or intent record --person ID --evidence ID (--label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] | --file LABELS.json) [--request-id UUID] [--participation ID] [--workspace ID].';
7
+
8
+ export async function intentCli(args) {
9
+ const flags = {}, positions = [];
10
+ for (let i = 0; i < args.length; i++) {
11
+ const arg = args[i];
12
+ if (arg === '--json') continue;
13
+ if (!arg.startsWith('--')) { positions.push(arg); continue; }
14
+ const key = arg.slice(2);
15
+ if (!RECORD_FLAGS.includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}. ${USAGE}`);
16
+ flags[key] = args[++i];
17
+ }
18
+ const command = positions[1];
19
+ if (positions.length !== 2 || positions[0] !== 'intent' || !['list', 'record'].includes(command)) throw Error(USAGE);
20
+ if (command === 'list') {
21
+ if (Object.keys(flags).some(key => !['workspace', 'profile'].includes(key))) throw Error('Option does not apply to intent list.');
22
+ console.log(JSON.stringify(await callWorkspace('intent_labels_list', { workspaceId: flags.workspace }, configuration(flags.profile))));
23
+ return;
24
+ }
25
+ if (!flags.person || !flags.evidence) throw Error(`--person ID and --evidence ID are required. ${USAGE}`);
26
+ const single = ['label', 'quote', 'reason', 'confidence'].filter(key => flags[key] !== undefined);
27
+ if (flags.file && single.length) throw Error('Give the labels in --file, or one label with --label, --quote and --reason. Not both.');
28
+ let labels, requestId = flags['request-id'];
29
+ if (flags.file) {
30
+ const body = readJsonFile(flags.file, 65536, 'Labels file');
31
+ if (Array.isArray(body)) labels = body;
32
+ else if (body && typeof body === 'object' && Array.isArray(body.labels) && Object.keys(body).every(key => ['labels', 'requestId'].includes(key))) { labels = body.labels; requestId ??= body.requestId; }
33
+ else throw Error('The labels file is a JSON array of labels, or { "labels": [...] }.');
34
+ } else {
35
+ if (!flags.label || !flags.quote || !flags.reason) throw Error(`--label, --quote and --reason are required without --file. ${USAGE}`);
36
+ labels = [{ label: flags.label, quote: flags.quote, reason: flags.reason, ...(flags.confidence ? { confidence: flags.confidence } : {}) }];
37
+ }
38
+ console.log(JSON.stringify(await callWorkspace('intent_record', {
39
+ workspaceId: flags.workspace, prospectId: flags.person, evidenceId: flags.evidence, requestId: requestId ?? randomUUID(),
40
+ ...(flags.participation ? { participationId: flags.participation } : {}), labels,
41
+ }, configuration(flags.profile))));
42
+ }
package/cli/json-file.mjs CHANGED
@@ -8,6 +8,6 @@ export function readJsonFile(path,limit,label){
8
8
  do{read=readSync(fd,buffer,length,buffer.length-length,null);length+=read;}while(read&&length<buffer.length);
9
9
  if(length>limit)throw new Error();
10
10
  return JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(buffer.subarray(0,length)));
11
- }catch{throw new Error(`${label} must be regular, non-symlink JSON no larger than ${limit/1024} KiB.`);}
11
+ }catch(error){if(error?.code==='ENOENT')throw new Error(`${label} not found: ${path}. Paths are relative to the current directory.`);throw new Error(`${label} must be regular, non-symlink JSON no larger than ${limit/1024} KiB.`);}
12
12
  finally{if(fd!==undefined)closeSync(fd)}
13
13
  }
@@ -0,0 +1,17 @@
1
+ import {z} from 'zod';
2
+ // Agent state store (ADR 017). One map feeds the CLI, the MCP server and the HTTP client alike.
3
+ const uuid=z.string().uuid(),workspaceId=uuid.optional();
4
+ const key=z.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/,'A key is 1 to 200 letters, digits, dots, colons, @, slashes, underscores or hyphens, and starts with a letter or digit.');
5
+ const text=z.string().min(1).max(200);
6
+ const paging={after:key.optional(),limit:z.number().int().min(1).max(50).optional()};
7
+ const base=a=>`/workspaces/${a.workspaceId}/agent-kv`;
8
+ const query=(a,keys)=>{const q=new URLSearchParams(keys.filter(k=>a[k]!==undefined).map(k=>[k,String(a[k])])).toString();return q?`?${q}`:''};
9
+ const body=keys=>a=>Object.fromEntries(keys.filter(k=>a[k]!==undefined).map(k=>[k,a[k]]));
10
+ const description="Agent state store: small values your user's agents share in this workspace between runs (cursors, change tokens, hashes, ids, timestamps). Every entry expires, by default 30 days after its last write. Never store credentials here; the Vault holds secrets and you store the Vault key name.";
11
+ export const kvOperations={
12
+ agent_kv_get:{schema:z.object({workspaceId,key}).strict(),method:'GET',path:a=>`${base(a)}/get${query(a,['key'])}`,scoped:true,description:`Read one entry: value, version, expiresAt and which agent wrote it. 404 not_found when missing or expired. ${description}`},
13
+ agent_kv_set:{schema:z.object({workspaceId,key,value:z.any(),ifVersion:z.number().int().min(1).optional(),nx:z.boolean().optional(),ttlSeconds:z.number().int().min(60).max(31536000).optional(),expiresAt:z.string().datetime({offset:true}).optional(),requestId:uuid.optional()}).strict(),method:'POST',path:a=>`${base(a)}/set`,body:body(['key','value','ifVersion','nx','ttlSeconds','expiresAt','requestId']),scoped:true,description:`Write one entry (a string, number, boolean, array or object up to 8 KB). A write refreshes expiry; ttlSeconds or expiresAt sets it, at most 365 days. ifVersion is compare-and-set: 409 version_conflict when the live version differs. nx writes only when no live entry exists. Reuse requestId on a retry: the same body replays for 30 days and answers replayed: true with the current entry; a different body under the same requestId is 409 request_conflict. Limits: 60 writes per 10 minutes per agent, 2,000 keys and 4 MB per user per workspace (429 with retryAt). Values shaped like credentials are refused (400 secret_rejected). ${description}`},
14
+ agent_kv_list:{schema:z.object({workspaceId,prefix:z.string().max(200).optional(),...paging}).strict(),method:'GET',path:a=>`${base(a)}/list${query(a,['prefix','after','limit'])}`,scoped:true,description:`List live entries in key order, 50 at a time, optionally under a key prefix; follow nextAfter for more. ${description}`},
15
+ agent_kv_search:{schema:z.object({workspaceId,key:text.optional(),value:text.optional(),...paging}).strict(),method:'GET',path:a=>`${base(a)}/search${query(a,['key','value','after','limit'])}`,scoped:true,description:`Find live entries whose key contains key text or whose value contains value text, case-insensitive, 50 at a time. Give at least one. ${description}`},
16
+ agent_kv_delete:{schema:z.object({workspaceId,key}).strict(),method:'POST',path:a=>`${base(a)}/delete`,body:body(['key']),scoped:true,description:'Delete one entry of the store. A missing key is not an error.'},
17
+ };
package/cli/kv.mjs ADDED
@@ -0,0 +1,58 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {callWorkspace,configuration} from './workspaces.mjs';
3
+ // Agent state store client (ADR 017): small values one user's agents share between runs. Never secrets.
4
+ const valued=['profile','workspace','if-version','ttl','expires-at','request-id','prefix','key','value','limit','after'];
5
+ const switches=['json','nx'];
6
+ export const kvUsage='KV: audienti kv get KEY | set KEY VALUE [--json] [--nx] [--if-version N] [--ttl 7d|--expires-at TIME] [--request-id ID] | list [--prefix P] [--after KEY] [--limit N] | search [--key TEXT] [--value TEXT] [--after KEY] [--limit N] | delete KEY (state between runs: cursors, hashes, ids, timestamps; never credentials)';
7
+ function parse(args){
8
+ const position=[],flags={};
9
+ for(let i=1;i<args.length;i++){
10
+ const arg=args[i];
11
+ if(!arg.startsWith('--')){position.push(arg);continue}
12
+ const key=arg.slice(2);
13
+ if(Object.hasOwn(flags,key))throw new Error(`Repeated kv option --${key}.`);
14
+ if(switches.includes(key)){flags[key]=true;continue}
15
+ if(!valued.includes(key)||args[i+1]===undefined)throw new Error(`Unknown or incomplete kv option --${key}.`);
16
+ flags[key]=args[++i];
17
+ }
18
+ return {command:position[0],rest:position.slice(1),flags};
19
+ }
20
+ const integer=(value,name)=>{if(value===undefined)return undefined;if(!/^\d+$/.test(value))throw new Error(`${name} must be a whole number.`);return Number(value)};
21
+ /** 90s, 15m, 6h, 7d or plain seconds. */
22
+ export function ttlSeconds(text){
23
+ const match=/^(\d+)(s|m|h|d)?$/.exec(text??'');
24
+ if(!match)throw new Error('Use --ttl like 90s, 15m, 6h or 7d.');
25
+ return Number(match[1])*{s:1,m:60,h:3600,d:86400}[match[2]??'s'];
26
+ }
27
+ const print=value=>console.log(JSON.stringify(value));
28
+ const call=(name,input,flags)=>callWorkspace(name,{...(flags.workspace?{workspaceId:flags.workspace}:{}),...input},configuration(flags.profile));
29
+ export async function kvCli(args){
30
+ const {command,rest,flags}=parse(args);
31
+ const need=(count,usage)=>{if(rest.length!==count)throw new Error(`Use audienti kv ${usage}.`)};
32
+ const page=()=>({...(flags.after!==undefined?{after:flags.after}:{}),...(flags.limit!==undefined?{limit:integer(flags.limit,'--limit')}:{})});
33
+ switch(command){
34
+ case 'get':need(1,'get KEY');return print(await call('agent_kv_get',{key:rest[0]},flags));
35
+ case 'set':{
36
+ need(2,'set KEY VALUE [--json] [--nx] [--if-version N] [--ttl 7d|--expires-at TIME]');
37
+ let value=rest[1];
38
+ if(flags.json){try{value=JSON.parse(rest[1])}catch{throw new Error('--json needs a valid JSON value, for example \'{"cursor":"abc"}\'.')}}
39
+ if(flags.ttl!==undefined&&flags['expires-at']!==undefined)throw new Error('Use --ttl or --expires-at, not both.');
40
+ if(flags['if-version']!==undefined&&flags.nx)throw new Error('Use --if-version or --nx, not both.');
41
+ const input={key:rest[0],value,requestId:flags['request-id']??randomUUID()};
42
+ if(flags['if-version']!==undefined)input.ifVersion=integer(flags['if-version'],'--if-version');
43
+ if(flags.nx)input.nx=true;
44
+ if(flags.ttl!==undefined)input.ttlSeconds=ttlSeconds(flags.ttl);
45
+ if(flags['expires-at']!==undefined)input.expiresAt=flags['expires-at'];
46
+ return print(await call('agent_kv_set',input,flags));
47
+ }
48
+ case 'list':need(0,'list [--prefix P] [--after KEY] [--limit N]');return print(await call('agent_kv_list',{...(flags.prefix!==undefined?{prefix:flags.prefix}:{}),...page()},flags));
49
+ case 'search':{
50
+ need(0,'search [--key TEXT] [--value TEXT]');
51
+ if(flags.key===undefined&&flags.value===undefined)throw new Error('Use audienti kv search --key TEXT and/or --value TEXT.');
52
+ return print(await call('agent_kv_search',{...(flags.key!==undefined?{key:flags.key}:{}),...(flags.value!==undefined?{value:flags.value}:{}),...page()},flags));
53
+ }
54
+ case 'delete':case 'del':need(1,'delete KEY');return print(await call('agent_kv_delete',{key:rest[0]},flags));
55
+ case undefined:case 'help':console.log(kvUsage);return;
56
+ default:throw new Error(`Unknown kv command "${command}". ${kvUsage}`);
57
+ }
58
+ }