@audienti/cli 0.2.0-exora.23 → 0.2.0-exora.25

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.
@@ -14,7 +14,7 @@ export async function accountRepliesCli(args) {
14
14
  if (positions[0] !== 'prospects' || positions[1] !== 'replies' || !['set', 'get', 'list'].includes(command) || positions.length !== 4) throw Error('Use prospects replies set|get|list PERSON --workspace ID.');
15
15
  const allowed = ['workspace', 'profile', ...(command === 'set' ? ['file', 'evidence'] : command === 'get' ? ['evidence', 'history-cursor', 'history-limit'] : ['cursor', 'limit', 'status'])];
16
16
  if (Object.keys(flags).some(k => !allowed.includes(k))) throw Error('Option does not apply to this reply command.');
17
- if (command !== 'list' && !flags.evidence) throw Error('--evidence EVIDENCE is required.');
17
+ if (command === 'set' && !flags.evidence) throw Error('--evidence EVIDENCE is required.');
18
18
  let input = {};
19
19
  if (command === 'set') {
20
20
  if (!flags.file) throw Error('--file INPUT.json is required.');
@@ -31,5 +31,19 @@ export async function accountRepliesCli(args) {
31
31
  if (!/^\d+$/.test(flags[flag])) throw Error('invalid_fields');
32
32
  input[key] = Number(flags[flag]);
33
33
  }
34
- console.log(JSON.stringify(await callWorkspace(`workspace_reply_${command}`, input, configuration(flags.profile))));
34
+ const config = configuration(flags.profile);
35
+ if (command === 'get' && !input.evidenceId) {
36
+ // No --evidence: read the person's latest reply. The list runs oldest first, so walk to its last page.
37
+ let last = null, cursor;
38
+ for (;;) {
39
+ const page = await callWorkspace('workspace_reply_list', { workspaceId: input.workspaceId, prospectId: input.prospectId, limit: 100, ...(cursor ? { cursor } : {}) }, config);
40
+ const data = page.data ?? page;
41
+ last = data.items?.at(-1)?.evidenceId ?? last;
42
+ if (!data.nextCursor) break;
43
+ cursor = data.nextCursor;
44
+ }
45
+ if (!last) throw Error('This person has no stored reply yet. Run prospects timeline PERSON to see their messages.');
46
+ input.evidenceId = last;
47
+ }
48
+ console.log(JSON.stringify(await callWorkspace(`workspace_reply_${command}`, input, config)));
35
49
  }
@@ -5,7 +5,7 @@ const item = { ...scope, evidenceId: uuid };
5
5
  const status = z.enum(['needs_handling', 'handled', 'not_reply']);
6
6
  const outcome = z.enum(['response_recorded', 'closed_without_response', 'suppressed']);
7
7
  const limit = z.number().int().min(1).max(100);
8
- const classification = z.enum(['interested', 'question', 'later', 'not_interested', 'out_of_office', 'stop', 'bounce', 'referral', 'wrong_person']);
8
+ const classification = z.enum(['interested', 'question', 'later', 'not_interested', 'out_of_office', 'stop', 'bounce', 'referral', 'wrong_person', 'unclear']);
9
9
  const slot = z.object({ start: z.string().describe('UTC start, ending in Z.'), end: z.string().describe('UTC end, ending in Z.'), timeZone: z.string().describe("The user's IANA time zone, for display.") }).strict();
10
10
  const draft = z.object({
11
11
  type: z.enum(['email.send', 'linkedin.send']),
@@ -34,7 +34,7 @@ export const accountReplyOperations = {
34
34
  workspace_reply_set: {
35
35
  schema: z.object({ ...item, requestId: uuid, expectedVersion: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), status,
36
36
  outcome: outcome.nullable().describe("Null for needs_handling/not_reply; a handling outcome is required for handled."), reason: z.string().meta({ minLength: 1, maxLength: 2000 }).describe("Required trimmed NFC reason, 1–2000 codepoints. Interior control characters including newline and tab are rejected."), responseEvidenceId: uuid.nullable().describe("Required for response_recorded; null otherwise. Must identify matching outbound account evidence."),
37
- classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office. An out_of_office reply snoozes the person instead; a wrong_person reply with no name given stops the sends and asks who the right person is. Someone who names the right person is a referral."),
37
+ classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office. An out_of_office reply snoozes the person instead; an unclear reply holds the person for a decision; a wrong_person reply with no name given stops the sends and asks who the right person is. Someone who names the right person is a referral."),
38
38
  returnsOn: z.string().datetime().optional().describe("Only with out_of_office: the date the person is back (UTC, ending in Z). The snooze lasts until then; with no date it follows the playbook's out-of-office rule, 7 days by default."),
39
39
  invite: z.literal(true).optional().describe("The prospect sent a calendar invite you have not processed yet. Requires a classification; a meetings.confirm task follows."),
40
40
  draft: draft.optional().describe("Your drafted answer. Saved as a reply plan revision the user reviews in Operator; requires status needs_handling. Never sent until approved."),
@@ -46,6 +46,7 @@ export const accountReplyOperations = {
46
46
  body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
47
47
  },
48
48
  workspace_reply_get: {
49
+ description: 'Read one inbound message: its reply state and history, and the `labels` an agent or person recorded on it (label, title, quote, reason, confidence, who recorded it, when), newest first. Free CRM read. The CLI reads the latest reply of the person when `--evidence` is left out.',
49
50
  schema: z.object({ ...item, historyCursor: uuid.optional(), historyLimit: limit.optional() }).strict(),
50
51
  method: 'GET', scoped: true, path: a => query(detail(a), a, ['historyCursor', 'historyLimit']),
51
52
  },
package/cli/audienti.mjs CHANGED
@@ -6,7 +6,7 @@ const args = process.argv.slice(2);
6
6
  const usage = `Profiles: audienti profiles list|search|get ID | match evaluate|apply SOURCE CANDIDATE | history RESOLUTION (saved workspace records)
7
7
  Skills: audienti skills list [--compact]|show SKILL|read SKILL RESOURCE|helper NAME --file INPUT.json|- [--json]
8
8
  Tools: audienti call TOOL ['JSON'] | tools search "WORDS"|get TOOL|list|runs|balance [--help]
9
- Sender: audienti sender accounts list|get|alias|register|bind|unbind|state|automation|activity report [--profile PATH]
9
+ Sender: audienti sender accounts list|get|alias|register|bind|unbind|state|workspaces|automation|activity report [--profile PATH]
10
10
  Managed LinkedIn: audienti sender managed list|connect [--workspace ID] [--profile PATH] (connect: the user only; returns a sign-in link)
11
11
  audienti sender managed plan MANAGED_ACCOUNT_ID --plan free|premium_career|premium_business|premium_all_in_one|sales_navigator_core|sales_navigator_advanced|recruiter_lite|none [--workspace ID] (the user only; sets the connection note length; none clears it)
12
12
  Prospecting: audienti prospecting settings show|save|accounts|account-show|account-save|history [--profile PATH]
@@ -83,6 +83,11 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
83
83
  intent list [--workspace ID] (the closed set of message labels: name, direction, event, channels, typed detail; output is JSON)
84
84
  intent suggest (--text TEXT | --text-file PATH) --channel email|linkedin [--direction inbound|outbound] [--context TEXT] [--request-id UUID] [--workspace ID] (paid: what a message may mean, read by a model; prints the price per 1M input tokens first; suggests labels with a quote and reason, records nothing; keep one with intent record)
85
85
  intent record --person ID --evidence ID --label NAME --quote TEXT --reason TEXT [--confidence high|medium|low] [--request-id UUID] [--participation ID] [--workspace ID] (what a stored message means; get EVIDENCE from prospects conversation append|list; --file LABELS.json for several labels or a typed detail such as a stop result or a meeting time; every label is checked and a refusal says what to fix; a message you sent that asks for a meeting takes --label meeting_requested on its outbound evidence)
86
+ decision list [--state open|decided|all] [--person ID] [--participation ID] [--limit N] [--workspace ID] (what Operator asks the person when the rules do not decide; an open decision holds its person)
87
+ decision options ID [--workspace ID] (one decision with the options the playbook allows)
88
+ decision ask --participation ID [--reason TEXT] [--request-id UUID] [--workspace ID] (ask the person to decide about one running person)
89
+ decision prepare ID --recommended OPTION_ID --option OPTION_ID=ANALYSIS ... [--file PREPARE.json] [--request-id UUID] [--workspace ID] (an agent adds its view: two to five options, one line each)
90
+ decision answer ID --option OPTION_ID [--workspace ID] (a person picks; the move runs once)
86
91
  audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
87
92
  budgets set --workspace ID [--agent ID --cap N|none] [--new-agent-cap N|none] [--at-limit pause|ask] [--alert80 on|off] (alert80 covers Operator and email at 80%)
88
93
  Qualification: campaigns qualification submit --file INPUT.json
@@ -123,6 +128,7 @@ Tool names follow the commands: playbook_get for playbook show, playbook_draft_g
123
128
  Errors name the MCP tool to call next, not the command.`;
124
129
  // Example inputs for the commands that save prospects, so `--help` shows a working shape.
125
130
  const examples = {
131
+ 'decision prepare': { recommended: 'snooze_30', options: [{ id: 'snooze_30', analysis: 'They said ask again in March, so wait a month.' }, { id: 'continue', analysis: 'Keep the plan; a follow-up may go this week.' }] },
126
132
  'prospects conversation append': { requestId: 'UUID', item: { source: { provider: 'email', accountRef: 'you@yourcompany.com', itemId: 'PROVIDER_MESSAGE_ID', threadId: null }, payload: { direction: 'outbound', conversationType: 'direct', occurredAt: '2026-11-06T14:00:00Z', subject: 'Ten minutes on late invoices?', body: 'Hi Jordan, could we find 20 minutes next week?', participants: null } } },
127
133
  'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
128
134
  'prospects observation create': { requestId: 'UUID', kind: 'person', source: { provider: 'linkedin', url: 'https://www.linkedin.com/in/jane-doe' }, fields: { name: 'Jane Doe', title: 'VP Sales', imageUrl: 'https://media.licdn.com/dms/image/PHOTO_ID' }, provenance: { url: 'https://example.com/team/jane' }, observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
@@ -142,7 +148,7 @@ function commandHelp() {
142
148
  }
143
149
  console.log(shown.length ? shown.map(line => line.trim()).join('\n') : usage);
144
150
  const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command) || (words.length > 0 && command.startsWith(words.join(' '))));
145
- if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('intent') ? `\nA message you sent that asks for a meeting (store it first with prospects conversation append, direction outbound):\naudienti intent record --person PERSON --evidence EVIDENCE --label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."\nRules: the "Label a message with its words" and "Label a message you sent" sections of the getting-started replies reference.` : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
151
+ if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('decision') ? 'Choose option ids from `audienti decision options ID`. One plain line each, at most 140 characters, no IDs or error codes.' : example[0].startsWith('intent') ? `\nA message you sent that asks for a meeting (store it first with prospects conversation append, direction outbound):\naudienti intent record --person PERSON --evidence EVIDENCE --label meeting_requested --quote "find 20 minutes next week" --reason "We ask for a call next week."\nRules: the "Label a message with its words" and "Label a message you sent" sections of the getting-started replies reference.` : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
146
152
  }
147
153
 
148
154
  /** Says what is wrong with a command no handler took, instead of one message for every case. */
@@ -164,6 +170,7 @@ async function main() {
164
170
  if(args[0]==='prospects'&&args[1]==='replies'){const {accountRepliesCli}=await import('./account-replies.mjs');await accountRepliesCli(args);return;}
165
171
  if(args[0]==='prospects'&&args[1]==='contacts'){const {recipientsCli}=await import('./recipients.mjs');await recipientsCli(args);return;}
166
172
  if(args[0]==='usage'){const {usageCli}=await import('./usage.mjs');await usageCli(args);return;}
173
+ if(args[0]==='decision'){const {decisionCli}=await import('./decision.mjs');await decisionCli(args);return;}
167
174
  if(args[0]==='intent'){const {intentCli}=await import('./intent.mjs');await intentCli(args);return;}
168
175
  if(args[0]==='audit'){const {auditCli}=await import('./audit.mjs');await auditCli(args);return;}
169
176
  if(args[0]==='secret'||args[0]==='secrets'){const {secretsCli}=await import('./secrets.mjs');await secretsCli(args);return;}
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ const uuid = z.string().uuid(), scope = { workspaceId: uuid.optional() };
3
+ const base = a => `/workspaces/${a.workspaceId}/decisions`;
4
+ const hold = 'An open decision holds its person: nothing is sent to them until the decision is answered.';
5
+ export const decisionOperations = {
6
+ decision_list: {
7
+ schema: z.object({ ...scope, state: z.enum(['open', 'decided', 'all']).optional(), prospectId: uuid.optional(), participationId: uuid.optional(), limit: z.number().int().min(1).max(100).optional() }).strict(),
8
+ method: 'GET', path: a => { const q = new URLSearchParams(Object.entries({ state: a.state, prospectId: a.prospectId, participationId: a.participationId, limit: a.limit }).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)])).toString(); return `${base(a)}${q ? `?${q}` : ''}`; }, scoped: true,
9
+ description: `List the decisions Operator asks the person when the rules do not decide: an unclear or low-confidence reply, a reply nobody labeled for 24 hours, or an agent that asked. Open ones come first. Each has the options the playbook allows, the agent's view when it added one, and a stale flag after 7 days. ${hold}`,
10
+ },
11
+ decision_get: {
12
+ schema: z.object({ ...scope, decisionId: uuid }).strict(),
13
+ method: 'GET', path: a => `${base(a)}/${a.decisionId}`, scoped: true,
14
+ description: 'Read one decision with the options a person may pick from. Use these option ids with decision_prepare and decision_answer.',
15
+ },
16
+ decision_ask: {
17
+ schema: z.object({ ...scope, participationId: uuid, requestId: uuid, reason: z.string().trim().max(200).optional().describe('Optional. One plain sentence on why the rules cannot decide.') }).strict(),
18
+ method: 'POST', path: base, body: ({ workspaceId, ...rest }) => rest, scoped: true,
19
+ description: `Ask the person to decide about one running participation. If a decision is already open for the person, the reason is added to it. ${hold} The same requestId with the same fields returns the first answer.`,
20
+ },
21
+ decision_prepare: {
22
+ schema: z.object({
23
+ ...scope, decisionId: uuid, requestId: uuid,
24
+ recommended: z.string().min(1).max(80).describe('The id of the option you recommend. It must be one of the options you chose.'),
25
+ options: z.array(z.object({ id: z.string().min(1).max(80).describe('An option id from decision_get.'), analysis: z.string().describe('One plain line, at most 140 characters: what happens and why it may fit. No IDs or error codes.') }).strict()).min(2).max(5),
26
+ }).strict(),
27
+ method: 'POST', path: a => `${base(a)}/${a.decisionId}/prepare`, body: ({ workspaceId, decisionId, ...rest }) => rest, scoped: true,
28
+ description: 'Add your view to a decision: choose two to five of its options, write a one-line analysis for each and recommend one. Audienti refuses an option the playbook does not allow from the stage and lists the allowed ids. Take over and hold always stay open to the person. Your recommendation is never applied without the person\'s pick.',
29
+ },
30
+ decision_answer: {
31
+ schema: z.object({ ...scope, decisionId: uuid, option: z.string().min(1).max(80).describe('An option id from decision_get.') }).strict(),
32
+ method: 'POST', path: a => `${base(a)}/${a.decisionId}/answer`, body: ({ workspaceId, decisionId, ...rest }) => rest, scoped: true,
33
+ description: 'People only: an agent cannot pick. Pick an option of a decision. The pick is recorded and the option\'s move runs once; sending it again returns the first answer. Picking hold changes nothing and the person stays held.',
34
+ },
35
+ };
@@ -0,0 +1,51 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { callWorkspace, configuration } from './workspaces.mjs';
3
+ import { readJsonFile } from './json-file.mjs';
4
+
5
+ const FLAGS = ['workspace', 'profile', 'state', 'person', 'participation', 'limit', 'reason', 'request-id', 'recommended', 'option', 'file'];
6
+ const USAGE = 'Use decision list [--state open|decided|all] [--person ID] [--participation ID] [--limit N], decision options ID, decision ask --participation ID [--reason TEXT] [--request-id UUID], decision prepare ID (--recommended OPTION_ID --option OPTION_ID=ANALYSIS ... | --file PREPARE.json) [--request-id UUID], or decision answer ID --option OPTION_ID. Add --workspace ID to any of them.';
7
+
8
+ export async function decisionCli(args) {
9
+ const flags = {}, many = [], 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 (!FLAGS.includes(key) || (key !== 'option' && Object.hasOwn(flags, key)) || i + 1 >= args.length) throw Error(`Invalid flag --${key}. ${USAGE}`);
16
+ if (key === 'option') many.push(args[++i]); else flags[key] = args[++i];
17
+ }
18
+ const [, command, id] = positions;
19
+ const call = async (name, input) => console.log(JSON.stringify(await callWorkspace(name, { workspaceId: flags.workspace, ...input }, configuration(flags.profile))));
20
+ const only = (...allowed) => { const bad = Object.keys(flags).find(key => !['workspace', 'profile', ...allowed].includes(key)); if (bad || (many.length && !allowed.includes('option'))) throw Error(`Option does not apply to decision ${command}. ${USAGE}`); };
21
+ if (positions[0] !== 'decision' || !['list', 'options', 'ask', 'prepare', 'answer'].includes(command)) throw Error(USAGE);
22
+ if (command === 'list') {
23
+ only('state', 'person', 'participation', 'limit');
24
+ if (positions.length !== 2) throw Error(USAGE);
25
+ return call('decision_list', { ...(flags.state ? { state: flags.state } : {}), ...(flags.person ? { prospectId: flags.person } : {}), ...(flags.participation ? { participationId: flags.participation } : {}), ...(flags.limit ? { limit: Number(flags.limit) } : {}) });
26
+ }
27
+ if (command === 'ask') {
28
+ only('participation', 'reason', 'request-id');
29
+ if (positions.length !== 2 || !flags.participation) throw Error(`--participation ID is required. ${USAGE}`);
30
+ return call('decision_ask', { participationId: flags.participation, requestId: flags['request-id'] ?? randomUUID(), ...(flags.reason ? { reason: flags.reason } : {}) });
31
+ }
32
+ if (positions.length !== 3 || !id) throw Error(`Give the decision ID. ${USAGE}`);
33
+ if (command === 'options') { only(); return call('decision_get', { decisionId: id }); }
34
+ if (command === 'answer') {
35
+ only('option');
36
+ if (many.length !== 1) throw Error(`Give the option once: --option OPTION_ID. ${USAGE}`);
37
+ return call('decision_answer', { decisionId: id, option: many[0] });
38
+ }
39
+ only('recommended', 'option', 'file', 'request-id');
40
+ let body;
41
+ if (flags.file) {
42
+ if (many.length || flags.recommended) throw Error('Give the options in --file, or with --recommended and --option. Not both.');
43
+ body = readJsonFile(flags.file, 65536, 'Prepare file');
44
+ if (!body || typeof body !== 'object' || Array.isArray(body)) throw Error('The prepare file is { "recommended": "OPTION_ID", "options": [{ "id": "OPTION_ID", "analysis": "One plain line." }] }.');
45
+ } else {
46
+ if (!flags.recommended || !many.length) throw Error(`--recommended OPTION_ID and at least two --option OPTION_ID=ANALYSIS are required without --file. ${USAGE}`);
47
+ body = { recommended: flags.recommended, options: many.map(item => { const at = item.indexOf('='); if (at < 1) throw Error(`--option takes OPTION_ID=ANALYSIS, and "${item}" has no "=".`); return { id: item.slice(0, at), analysis: item.slice(at + 1) }; }) };
48
+ }
49
+ const { requestId, ...rest } = body;
50
+ return call('decision_prepare', { decisionId: id, requestId: flags['request-id'] ?? requestId ?? randomUUID(), ...rest });
51
+ }
@@ -20,7 +20,7 @@ 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
+ 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. A recorded message carries `labels` (label, title, quote, reason, confidence, who recorded it, when), newest first, and a label event carries its `label` once (its `detail` does not repeat it). 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},
24
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},
25
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},
26
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},
@@ -11,7 +11,7 @@ const label = z.object({
11
11
  label: z.string().describe('A name from intent_labels_list that fits the message direction and channel.'),
12
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
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.'),
14
+ confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Optional. It is stored with the label when given. A low-confidence label does not fire its rule: it holds the person and opens a decision, except stop and bounce, which always act.'),
15
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
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
17
  returnsOn: z.string().datetime().optional().describe('Only for out_of_office: the day the person is back (UTC, ending in Z).'),
@@ -1,5 +1,5 @@
1
1
  import { configuration,errorGuidance,validateUrl,WorkspaceRequestError } from './workspaces.mjs';
2
- export async function callOwnerAccountOperation(operation,args,profilePath=process.env.AUDIENTI_PROFILE,label='prospecting settings') {
2
+ export async function callOwnerAccountOperation(operation,args,profilePath=process.env.AUDIENTI_PROFILE,label='prospecting settings',showServerMessage=false) {
3
3
  const parsed=operation?.schema.safeParse(args);
4
4
  if(!parsed?.success)throw Error(`Invalid ${label} command or fields.`);
5
5
  const config=configuration(profilePath,'agents:manage'),profile=config.profile;
@@ -13,6 +13,7 @@ export async function callOwnerAccountOperation(operation,args,profilePath=proce
13
13
  const url=validateUrl(config.url);let response;
14
14
  try{response=await fetch(new URL('/api/v1'+operation.path(parsed.data),url),{method:operation.method,headers:{Authorization:`Bearer ${config.token}`,Accept:'application/json','Content-Type':'application/json'},...(operation.method==='POST'?{body:JSON.stringify(operation.body?operation.body(parsed.data):parsed.data)}:{}),redirect:'error',signal:AbortSignal.timeout(10000)});}catch{throw Error('Could not reach the API. Redirects are not followed.');}
15
15
  let result;try{result=await response.json();}catch{throw Error('The API returned an invalid response.');}
16
- if(!response.ok){const code=typeof result?.error?.code==='string'&&/^[a-z_]+$/.test(result.error.code)?result.error.code:'request_failed';throw new WorkspaceRequestError(response.status,code,`${label} request failed: ${code} (HTTP ${response.status}).${response.status===422&&code==='hours_not_narrower'?' An account can only narrow your working hours. Choose hours inside them, or change your own working hours first.':''}`,undefined,undefined,undefined,errorGuidance(result?.error,config.token));}
16
+ if(!response.ok){const code=typeof result?.error?.code==='string'&&/^[a-z_]+$/.test(result.error.code)?result.error.code:'request_failed';const said=showServerMessage&&typeof result?.error?.message==='string'?result.error.message.trim():'',plain=said&&said.length<=300&&!/[\u0000-\u001f\u007f]/.test(said)&&!said.includes(config.token)?said:'';
17
+ throw new WorkspaceRequestError(response.status,code,plain?`${plain} (${code})`:`${label} request failed: ${code} (HTTP ${response.status}).${response.status===422&&code==='hours_not_narrower'?' An account can only narrow your working hours. Choose hours inside them, or change your own working hours first.':''}`,undefined,undefined,undefined,errorGuidance(result?.error,config.token));}
17
18
  if(!result||!Object.hasOwn(result,'data'))throw Error('The API returned an invalid response.');return result;
18
19
  }
@@ -31,7 +31,7 @@ export const playbookOperations={
31
31
  playbook_draft_save:{schema:z.object({...scope,id:uuid,baseRevision:z.number().int().positive().describe('The version the edits start from: currentRevision from playbook_get, or the baseRevision of the draft you read. A value above the current version is refused with revision_conflict, which carries currentRevision. A lower value is kept; playbook_draft_publish checks it and refuses a draft that started from an older version.'),expectedUpdatedAt:z.string().datetime().nullable().describe('The updatedAt of the draft you read with playbook_draft_get, or null when it returned no draft. If the draft changed since (another tab, your agent or the person), the save is refused with draft_conflict and currentUpdatedAt, and nothing is replaced. Read the draft again, combine the changes, then save.'),fields}).strict(),method:'PUT',path:a=>`${base(a)}/playbooks/${a.id}/draft`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Keep your edits as the draft of a playbook. The draft is replaced as a whole, so send every field: read it with playbook_draft_get (or playbook_get when there is no draft) and change what you need. It never makes a version and uses no requestId. The person who owns the draft sees the change in the editor within a few seconds. A stale expectedUpdatedAt is refused with draft_conflict (HTTP 409). Same field rules as playbook_save, but problems do not stop a draft save: the answer carries check (the problems and warnings a publish would see). The draft is not fully checked until publish. Starter and archived playbooks are refused (starter_playbook, playbook_archived).'},
32
32
  playbook_draft_discard:{schema:z.object({...scope,id:uuid,expectedUpdatedAt:z.string().datetime().optional().describe('The updatedAt of the draft you read. When given and the draft changed since, the draft is kept and the call is refused with draft_conflict and currentUpdatedAt. Leave it out to discard whatever the draft holds.')}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}/draft${a.expectedUpdatedAt?`?expectedUpdatedAt=${encodeURIComponent(a.expectedUpdatedAt)}`:''}`,scoped:true,description:'Throw your draft of a playbook away. The saved versions stay. Safe to repeat: {id, discarded} says whether a draft existed. Pass expectedUpdatedAt to keep a draft that was saved since you read it (draft_conflict).'},
33
33
  playbook_draft_publish:{schema:z.object({...scope,id:uuid,requestId:uuid.describe('A fresh UUID. Reuse it only to retry the same call.')}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/draft/versions`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Save your draft of a playbook as the next version and remove the draft, in one step. Returns the new version, as playbook_save does, with the same checks (including 422 playbook_cannot_work). If a newer version was saved after the draft started, nothing is saved and the call is refused with 409 revision_conflict and currentRevision; read the draft and the new version, then save the draft again with the new baseRevision. No draft gives 404 no_draft. A retry with the same requestId returns the same version.'},
34
- workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Free CRM read.'},
34
+ workspace_pipeline_get:{schema:z.object({...scope,campaignId:uuid.optional(),pinId:uuid.optional()}).strict(),method:'GET',path:a=>`${base(a)}/pipeline?${query(a)}`,scoped:true,description:'Read the pipeline board: stages of one playbook version, the people currently in each stage, and counts of people closed out through each off-ramp. Each card carries the latest three `labels` on the person\'s messages (label, quote, reason, confidence, who recorded it) and `labelsTotal`. Free CRM read.'},
35
35
  playbook_progress_get:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.')}).strict(),method:'GET',path:a=>`${base(a)}/participations/${a.participationId}/progress`,scoped:true,description:'Read the participation’s admitted playbook pin and recorded stage/outcome history. Takes a participation ID (a person in a campaign; each person in the pipeline has one), not a playbook ID.'},
36
36
  playbook_progress_record:{schema:z.object({...scope,participationId:uuid,expectedVersion:z.number().int().nonnegative(),requestId:uuid,stageKey:uuid.optional(),offRampKey:uuid.optional(),finishStage:z.literal(true).optional(),reason:z.string().trim().min(1).max(2000),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20)}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/progress`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Report a stage, off-ramp, or finishStage:true after externally completing the current play and waits. Finish resolves next/stay/off-ramp from the admitted version. Stored evidence required for agents; human-review and backward-move rules apply. Assigned owner/Admin only. Goal ends prospecting; post-goal forward reporting never reactivates. No send or provider call.'},
37
37
  playbook_event_record:{schema:z.object({...scope,participationId:uuid.describe('A participation ID (one person in one campaign, from the pipeline), not a playbook ID.'),requestId:uuid,kind:z.enum(['meeting_requested','meeting_accepted','meeting_declined','meeting_booked','not_a_fit','steps_done']).describe('meeting_requested: a meeting was asked for. meeting_accepted: agreed to, no time yet. meeting_declined: turned down. meeting_booked: a meeting has a time. not_a_fit: the person is not a fit. steps_done: the steps of the current stage are done.'),reason:z.string().trim().min(1).max(300).describe('Why, in one or two sentences. It shows in the person’s history.'),evidence:z.array(z.object({kind:z.enum(['observation','conversation']),id:uuid}).strict()).max(20).optional(),at:z.string().datetime({offset:true}).optional().describe('When it happened, ISO 8601. Defaults to now and is never later than now.')}).strict(),method:'POST',path:a=>`${base(a)}/participations/${a.participationId}/events`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Record one event for a person: a meeting asked for, agreed to, turned down or booked, not a fit, or the steps of the current stage done. The playbook then settles: stages that list the event as an entry event take the person forward, and the rules decide the rest (not_a_fit ends the person in the not_fit off-ramp when the playbook has one). Safe to repeat: a second call writes nothing and answers recorded false. Ended participation is refused with participation_ended. Assigned owner/Admin only. No send or provider call.'},
@@ -21,6 +21,7 @@ export const senderAccountOperations={
21
21
  sender_alias_bind:{schema:z.object({...base,...alias,senderAccountId:uuid,agentId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases',body,ownerOnly:true},
22
22
  sender_alias_unbind:{schema:z.object({...base,...alias,senderAccountId:uuid}).strict(),method:'POST',path:a=>path(a)+'/aliases/unbind',body,ownerOnly:true},
23
23
  sender_account_state_set:{schema:z.object({...base,senderAccountId:uuid,expectedVersion:version,state:z.enum(['active','retired'])}).strict(),method:'POST',path:a=>path(a)+'/lifecycle',body,ownerOnly:true},
24
+ sender_account_workspaces_set:{schema:z.object({...base,senderAccountId:uuid,useIn:useIn.describe('Where the account is used from now on, replacing the earlier choice: allWorkspaces true with an empty workspaceIds for all of the user\'s workspaces, or allWorkspaces false with at least one workspace id the user belongs to.')}).strict(),method:'POST',path:a=>path(a)+'/workspaces',body,ownerOnly:true,description:'Changes where an approved sending account is used: all of the user\'s workspaces, or only the listed ones, each a workspace the user belongs to. Replaces the earlier choice. The answer carries useIn, the same shape sender_account_get gives.'},
24
25
  sender_automation_get:{schema:z.object({senderAccountId:uuid,actionType}).strict(),method:'GET',path:a=>path(a)+'/automation/'+encodeURIComponent(a.actionType)},
25
26
  sender_automation_set:{schema:z.object({...base,senderAccountId:uuid,actionType,expectedVersion:version,enabled:z.boolean()}).strict(),method:'POST',path:a=>path(a)+'/automation/'+encodeURIComponent(a.actionType),body,ownerOnly:true},
26
27
  sender_action_modes_get:{schema:z.object({senderAccountId:uuid}).strict(),method:'GET',path:a=>path(a)+'/action-modes'},
@@ -3,7 +3,7 @@ import {callOwnerAccountOperation} from './owner-account-client.mjs';
3
3
  import {readJsonFile} from './json-file.mjs';
4
4
  import {WorkspaceRequestError,callWorkspace,configuration} from './workspaces.mjs';
5
5
  export async function callSenderAccounts(name,args,profilePath){
6
- return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account');
6
+ return callOwnerAccountOperation(senderAccountOperations[name],args,profilePath,'sender account',true);
7
7
  }
8
8
  export async function senderAccountsCli(args){
9
9
  const flags={},positions=[];const known=['profile','file','cursor','limit','aliases-cursor','aliases-limit','history-cursor','history-limit','action-type','capability','provider','account-ref','workspace','plan'];
@@ -18,11 +18,11 @@ export async function senderAccountsCli(args){
18
18
  if(!names[positions[2]]||positions.length!==3||Object.keys(flags).some(key=>!['profile','workspace'].includes(key)))throw Error('Use sender managed list|connect|plan [--workspace ID] [--profile PATH].');
19
19
  console.log(JSON.stringify(await callWorkspace(names[positions[2]],{workspaceId:flags.workspace},configuration(flags.profile??process.env.AUDIENTI_PROFILE))));return;
20
20
  }
21
- if(positions[0]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|automation|modes.');
21
+ if(positions[0]!=='sender'||positions[1]!=='accounts')throw Error('Use sender accounts list|get|alias|register|bind|unbind|state|workspaces|automation|modes.');
22
22
  const automation=positions[2]==='automation',modes=positions[2]==='modes',activity=positions[2]==='activity',nested=automation||modes||activity,command=nested?`${positions[2]}-${positions[3]}`:positions[2];
23
- const names={'activity-report':'sender_activity_report',alias:'sender_alias_get',list:'sender_accounts_list',get:'sender_account_get',register:'sender_account_register',bind:'sender_alias_bind',unbind:'sender_alias_unbind',state:'sender_account_state_set','automation-get':'sender_automation_get','automation-set':'sender_automation_set','modes-get':'sender_action_modes_get','modes-set':'sender_action_mode_set'};
23
+ const names={'activity-report':'sender_activity_report',alias:'sender_alias_get',list:'sender_accounts_list',get:'sender_account_get',register:'sender_account_register',bind:'sender_alias_bind',unbind:'sender_alias_unbind',state:'sender_account_state_set',workspaces:'sender_account_workspaces_set','automation-get':'sender_automation_get','automation-set':'sender_automation_set','modes-get':'sender_action_modes_get','modes-set':'sender_action_mode_set'};
24
24
  const name=names[command];if(!name)throw Error('Unknown sender account command.');
25
- const operation=senderAccountOperations[name],hasId=!['list','register','alias'].includes(command),expected=nested?5:hasId?4:3;if(positions.length!==expected)throw Error('Unexpected sender arguments.');
25
+ const operation=senderAccountOperations[name],hasId=!['list','register','alias'].includes(command),expected=nested?5:hasId?4:3;if(positions.length!==expected)throw Error('Unexpected sender arguments.'+(command==='workspaces'?' Use sender accounts workspaces ACCOUNT_ID --file scope.json [--profile PATH].':''));
26
26
  const allowed=['profile',...(operation.method==='POST'?['file']:command==='alias'?['provider','account-ref']:command==='list'?['cursor','limit']:command==='get'?['aliases-cursor','aliases-limit','history-cursor','history-limit']:[]),...(automation?['action-type']:[]),...(command==='modes-set'?['capability']:[])];
27
27
  if(Object.keys(flags).some(key=>!allowed.includes(key)))throw Error('Option does not apply to this sender command.');
28
28
  let input={};if(operation.method==='POST'){if(!flags.file)throw Error('--file INPUT.json is required.');input=readJsonFile(flags.file,65536,'Sender input');if(!input||typeof input!=='object'||Array.isArray(input))throw Error('invalid_fields: input must be a JSON object.');if(Object.hasOwn(input,'senderAccountId')||Object.hasOwn(input,'actionType')||Object.hasOwn(input,'capability')||Object.hasOwn(input,'workspaceId'))throw Error('Path IDs and workspaceId are not allowed in the file.');}
@@ -23,6 +23,7 @@ import { suppressionOperations } from './suppression-operations.mjs';
23
23
  import { guardrailsOperations } from './guardrails-operations.mjs';
24
24
  import { auditOperations } from './audit-operations.mjs';
25
25
  import { intentOperations } from './intent-operations.mjs';
26
+ import { decisionOperations } from './decision-operations.mjs';
26
27
  import { usageOperations } from './usage-operations.mjs';
27
28
  import { outboundOperations } from './outbound-operations.mjs';
28
29
  import { cohortOperations } from './cohort-operations.mjs';
@@ -63,6 +64,7 @@ export const operations = {
63
64
  ...guardrailsOperations,
64
65
  ...auditOperations,
65
66
  ...intentOperations,
67
+ ...decisionOperations,
66
68
  ...usageOperations,
67
69
  ...outboundOperations,
68
70
  ...cohortOperations,
@@ -169,6 +171,8 @@ export function localGuidance(name, issues, args) {
169
171
  // The next step an error gives an agent (#483): one sentence, the path of the wrong value and the values that would have worked.
170
172
  // Checked here too, so a response never puts the token or control text into the agent's context.
171
173
  // #519: a refused label already says in plain words what is wrong; the code and the next step travel beside it.
174
+ // Refusals of the decision calls (#522). Their messages tell the person what to do next, so the CLI shows them.
175
+ const DECISION_REFUSALS = new Set(['agent_required', 'human_only', 'not_assigned_owner', 'participation_ended', 'decision_not_found', 'decision_closed', 'decision_superseded', 'already_decided', 'option_not_allowed', 'option_count', 'duplicate_option', 'analysis_required', 'analysis_too_long', 'analysis_not_plain', 'workspace_not_found']);
172
176
  const INTENT_REFUSALS = new Set(['prospect_not_found', 'conversation_evidence_not_found', 'participation_not_found', 'payload_too_large', 'idempotency_conflict', 'evidence_has_no_body', 'too_many_labels', 'unknown_label', 'already_recorded', 'label_not_available', 'label_direction', 'label_channel', 'duplicate_label', 'must_stand_alone', 'labels_conflict', 'result_required', 'result_mismatch', 'quote_required', 'quote_not_in_message', 'reason_required', 'reason_not_plain', 'detail_not_allowed']);
173
177
  export function errorGuidance(error, token, config) {
174
178
  const clean = text => typeof text === 'string' && !/[\u0000-\u001f\u007f]/.test(text) && !(token && text.includes(token));
@@ -202,7 +206,7 @@ export async function callWorkspace(name, args, config) {
202
206
  if(!response.ok) {
203
207
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
204
208
  const retryAt = typeof result?.error?.retryAt === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(result.error.retryAt) ? result.error.retryAt : undefined;
205
- const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && (code==='not_found' || code==='no_draft')) || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='draft_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000:1000) && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
209
+ const detail = (INTENT_REFUSALS.has(code) || (name.startsWith('decision_') && DECISION_REFUSALS.has(code)) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && (code==='not_found' || code==='no_draft')) || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_conflict' || code==='draft_conflict' || code==='playbook_in_use' || code==='definition_archived' || code==='playbook_archived' || code==='starter_playbook' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced' || code==='wrong_event_name' || code==='mark_task_done_first' || code==='no_event' || code==='prepare_first' || code==='task_closed' || code==='not_claimed'))) && typeof result?.error?.message==='string' && result.error.message.length<=(code==='playbook_cannot_work'?2000:1000) && !/[\u0000-\u001f\u007f]/.test(result.error.message) && !/^Workspace request failed/.test(result.error.message) && !(config.token && result.error.message.includes(config.token)) ? result.error.message.trim() : '';
206
210
  const blockers = (code==='execution_blocked' || code==='campaign_not_ready') && Array.isArray(result?.error?.blockers) && result.error.blockers.length<=28 && result.error.blockers.every(v=>typeof v==='string' && /^[a-z_]+$/.test(v)) ? result.error.blockers : undefined;
207
211
  const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? surfaceWording(detail, config) : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${surfaceWording(detail, config)}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token, config));
208
212
  if(code==='playbook_cannot_work'&&Array.isArray(result?.error?.errors)&&result.error.errors.length<=30)error.errors=result.error.errors.filter(item=>item&&typeof item.id==='string'&&typeof item.message==='string').map(item=>({id:item.id,level:item.level,message:item.message,...(typeof item.stageKey==='string'?{stageKey:item.stageKey}:{}),...(item.fix&&typeof item.fix.label==='string'?{fix:{id:item.fix.id,label:item.fix.label}}:{})}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.23",
3
+ "version": "0.2.0-exora.25",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,8 +7,8 @@ approved: inbound wins, and you record it. What the user approves is your answer
7
7
 
8
8
  1. Read the conversation: `audienti prospects conversation list PERSON --workspace ID`.
9
9
  2. Classify the reply as one of `interested`, `question`, `later`, `not_interested` (a clear no thanks),
10
- `out_of_office`, `stop`, `bounce`, `referral` or `wrong_person` (the prospect is the wrong person and
11
- names no one else). A stop, a bounce or a referral must carry its typed
10
+ `out_of_office`, `stop`, `bounce`, `referral`, `wrong_person` (the prospect is the wrong person and
11
+ names no one else) or `unclear` (you cannot tell what they mean; it holds the person for a decision, see below). A stop, a bounce or a referral must carry its typed
12
12
  `result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
13
13
  3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
14
14
  - Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
@@ -98,8 +98,9 @@ asks for everything to end looks like this:
98
98
  ```
99
99
 
100
100
  `audienti intent --help` prints this example. Give the same `--request-id` to repeat a call safely. Add
101
- `confidence` (`high`, `medium` or `low`) when you are unsure; it is stored only when you give it, and it
102
- does not change what the label does yet. A message keeps the labels it has: a second request that repeats
101
+ `confidence` (`high`, `medium` or `low`) when you are unsure. A low-confidence label does not fire its rule:
102
+ the person is held and a decision opens for your user to pick from (see below). `stop` and `bounce` always
103
+ act, whatever the confidence. A message keeps the labels it has: a second request that repeats
103
104
  one is refused with `already_recorded`, and one that would give the message a second reading is refused.
104
105
 
105
106
  Audienti checks every label and writes nothing if one fails. A refusal names the `field`, the `allowed`
@@ -113,6 +114,10 @@ values and the `next` step. The checks:
113
114
  reply and at most one meeting label.
114
115
  - The typed detail is present: a `stop`, `bounce` or `referral` needs its `result`; `meeting_booked` needs `meeting`.
115
116
 
117
+ A recorded label shows to the user as a chip with the quote, the reason, how sure you were and who recorded it, on the
118
+ reply card in Operator, the person's page and the Pipeline card. Write the `quote` and `reason` for a person to read. The same
119
+ labels come back as `labels` on `audienti prospects timeline PERSON`, `audienti prospects replies get PERSON` (the latest reply, or one message with `--evidence`) and `audienti pipeline show`. A label event on the timeline carries the label once, under `event.label`. Mark a guess with `confidence: low`: the chip says so.
120
+
116
121
  To get a second reading before you record, `audienti intent suggest --channel email --text "..."` asks a model
117
122
  to suggest labels. It costs credits: the call prints the price per million input tokens first, and a message
118
123
  uses a small part of that. It returns up to two suggestions, each with a `quote`, a `reason`, a `confidence` and
@@ -157,6 +162,26 @@ The same checks apply: a message with no stored text, a quote that is not in it,
157
162
  - The label goes on the person's running campaign. A person runs in one campaign per workspace, so you need no option; `--participation ID` names the run if you want it explicit. The answer to `work execution send` or `report` carries that `participationId` when the send started a playbook step and the person still runs in the campaign run the send started, also when you repeat the call. A person who left and rejoined has a new run: a repeat of the old send names none. The label works without it.
158
163
  - An answer to a reply that offers times is labeled the same way, with the evidence of the answer you sent.
159
164
 
165
+ ## When the rules do not decide: decisions
166
+
167
+ A reply the rules cannot act on holds the person, and a decision asks your user to pick. It opens when a
168
+ label is `unclear`, when a label has low confidence (not `stop` or `bounce`), when a reply has had no label
169
+ for 24 hours, or when you ask for one with `audienti decision ask`. While it is open nothing is sent to the
170
+ person. A `decisions.prepare` task tells you to add your view:
171
+
172
+ 1. Read the options the playbook allows: `audienti decision options DECISION_ID`. Only these ids are valid.
173
+ A stage option names its stage (`stageName`), a snooze option reads "Snooze 30 days, then continue", and each
174
+ trigger says who recorded the event behind it (`recordedBy`: kind and name). The clock raises a trigger with none.
175
+ 2. Choose two to five. Write one plain line for each (at most 140 characters, no IDs or error codes) and
176
+ recommend one: `audienti decision prepare DECISION_ID --recommended ID --option ID=ANALYSIS ...`.
177
+ Audienti refuses an option outside the list and names the allowed ids.
178
+ 3. Report the task done. Only your user picks, in Operator or with `audienti decision answer` under their own
179
+ sign-in. Audienti never applies your recommendation. After 24 hours without your view the user sees the
180
+ options without analyses. An open decision turns stale after 7 days; the person stays held.
181
+
182
+ A reply you can label clearly needs no decision: record the label with `audienti intent record` and the rules
183
+ act. A label recorded later ends the hold.
184
+
160
185
  ## Stop, bounce and referral
161
186
 
162
187
  These three need no decision from the user. Classify the reply, report the typed
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "5baa5f3af7b95f75d8e8f9d7467f85b98a1029f2",
3
+ "sourceCommit": "d6cf3b19751f65dfd1bef97a2323148fa1991b99",
4
4
  "files": {
5
5
  "LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
6
6
  "README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
7
7
  "bin/audienti-mcp.mjs": "6df1d6eb3a5bc65e7592e5b8f73cd4c98c159f2583723d042338633b356301df",
8
8
  "cli/account-intake-operations.mjs": "b43d764c628beabafad753d711ddb2af2c64276e4a6506ed0b854839091ea5db",
9
9
  "cli/account-intake.mjs": "aa77f2f96fd030f8830d6de278932186aea7c82cfda086a5a6cf569202feebc8",
10
- "cli/account-replies.mjs": "699e34e5c736ba316df8f164b4c50f82a7e4e2d5e51303e6b13944c3f2c854ab",
11
- "cli/account-reply-operations.mjs": "08cf9642f040d2ef577ad704b893bde4a5b97d4930d3fadac24f2aebcb87ae24",
10
+ "cli/account-replies.mjs": "80059e8cec767a3e33aa971acf467603ca83f05efe42c59cd57a19defc37c07e",
11
+ "cli/account-reply-operations.mjs": "12c3fa1533b09b7f5a376f24c9ef140cfe14211927d9780bf532503367b132da",
12
12
  "cli/agents.mjs": "e311dd08d1dc09648881a37eb9765bf6d155786f0b713204f51de29bc8134293",
13
13
  "cli/assignment-operations.mjs": "fc74e3fd79975cfcbac14891d5dec5f25e662777050fc46a8ad3aae5cf361c59",
14
14
  "cli/assignment.mjs": "90cb85e0df8aadca2ff8391c99e778b41261183b5f38e8c6c32afee2f98f871a",
@@ -16,7 +16,7 @@
16
16
  "cli/attachments.mjs": "2fee6b4abbf82da9847d4a927a3d4d3ca947a198a903a78927596a2ea858cbf4",
17
17
  "cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
18
18
  "cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
19
- "cli/audienti.mjs": "4e9e93e9a9ded80f5de70c9566f0699e8c16cf4887eb8614d13aff89aebe7b6c",
19
+ "cli/audienti.mjs": "fa7a7e479eb95d60678fa7b24dbbbbc84aacddcfdcd8edc7373f1859b86c8ba4",
20
20
  "cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
21
21
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
22
22
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
@@ -28,7 +28,9 @@
28
28
  "cli/cohorts.mjs": "c71940903430f3e6834608f113534910b75a98b3e410a5942f04051607488485",
29
29
  "cli/connection-registry-operations.mjs": "888ea440d00ffc9ed68d4cc084ff73a5a00d0e60bd6293baba8bb4539a25ae62",
30
30
  "cli/content.mjs": "139dee5fd37fd89669c39e2120ac70a2e0286b207b055ff666f54ba3512ab061",
31
- "cli/discovery-operations.mjs": "cc132abae230cc84439bcd24da813e2798ee1eb83a06a7d1141202ab9c68215a",
31
+ "cli/decision-operations.mjs": "0c4c2134c9615678def2b50f2a5254218b14f1174f1ce4e8ba7db78c751661af",
32
+ "cli/decision.mjs": "3d89d48b333e2620bce0191cf528ef25541ee841a7f8ae6d60249aead434b651",
33
+ "cli/discovery-operations.mjs": "9b6873d586d27bf67106a592db8ced75da1abf1a1330f192e69d34f81b2d080f",
32
34
  "cli/discovery.mjs": "34b3794ee6ca539dbbccef77ec2085fd5411023046bdfb9b83e2537414542a41",
33
35
  "cli/file-operations.mjs": "564f334dd9d52e9c56f44d58b5c3b0bda59ff72f54126b755bc472144069349e",
34
36
  "cli/files.mjs": "2221b067d6ade7ef5cc8965929aea2eb485781953d3f2b50600995b65fbdedae",
@@ -36,7 +38,7 @@
36
38
  "cli/guardrails.mjs": "e8b9c0a19956a9eab84475d3c56a4b6c77baf7ed9a55e8686d660ef1ad562aa8",
37
39
  "cli/hand-task-operations.mjs": "473473fe95324c8c8733e037feab5ac468c07b0658fabd739cfd7d400a7faa83",
38
40
  "cli/hand-tasks.mjs": "2c895cf6b30c594a7e27cc4ec4b7eddeb39f941cd195b9255c6e0d6b4a56371f",
39
- "cli/intent-operations.mjs": "97b08e7474cf0fd8201650f08fa5ca5a2be2b0ddf5ea2079eb644e21aa2a0153",
41
+ "cli/intent-operations.mjs": "63871dbc0262353a12663df2642fa0f7b4166d688f95f0b729ec01e9f1e10857",
40
42
  "cli/intent.mjs": "5aa62593f65f58f2714bb402365624005ae17f73f65da3952b06f45e3c109450",
41
43
  "cli/json-file.mjs": "96431b18215834c2531f5901e2526875c634dd2e85a9465fcffc937fbec52150",
42
44
  "cli/kv-operations.mjs": "505d79d0d535d3441883f8a37817c30c73367216a35dddb07181f0f0e799df79",
@@ -49,9 +51,9 @@
49
51
  "cli/outbound.mjs": "23903eb0fec4d7af3aa0031d63488ed9a8697e07ccf1314d194b48e109d15f82",
50
52
  "cli/outcome-operations.mjs": "cf133097799f240a006264a38a4c6bd4094078d6dec78a71679700ac3bdeacd5",
51
53
  "cli/outcomes.mjs": "765ccb112a90fc4d228b2a41e5e19c5e1f3e2fe14bc1d8128b0090fc95cdbf3f",
52
- "cli/owner-account-client.mjs": "91ab4d9b4b93c7feea668e5a3087cd9aa0766cf4cc657b7cac34e606d3a4b7a5",
54
+ "cli/owner-account-client.mjs": "aa2790fd3c16714b1a82922fe13012f261cced99025d692bca5d0c605a717cd1",
53
55
  "cli/playbook-help.mjs": "dad00cae19eb107f84c7934812800431535c6cf1dbcdcdd22c16522cfaddadf3",
54
- "cli/playbook-operations.mjs": "431738b47b56091e10f0466f03673c602ae00653f8b455a20d39a8242d845e21",
56
+ "cli/playbook-operations.mjs": "5b0680b69c2589c5e2b83aeb97f350b3c694de121a8307a76bd220a394088b76",
55
57
  "cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
56
58
  "cli/progress.mjs": "6908f2937adb87a83ebefc1f4324ed18adac65228547b0524a474ac3513b5c67",
57
59
  "cli/prospecting-settings-operations.mjs": "1829eb2fe7208f58601988143756072429adbfe00f8deb5a52613bf650a9eacc",
@@ -67,8 +69,8 @@
67
69
  "cli/search-operations.mjs": "903f12fffb6824872160c4136925ab5638142bb4e39236b37f927cd2c6a43776",
68
70
  "cli/secret-operations.mjs": "faefe9c9ece81087dc7aab32e88f331ed942d0757ce5a978b5f0722d514f377f",
69
71
  "cli/secrets.mjs": "cab4ea932e724ce3c807c7825869810b44fb8d75cdacec32f8101353f0e7c5f5",
70
- "cli/sender-account-operations.mjs": "6bded184368b765616cb6bd0ca86fe76e96c271540bc9f4f2312f8d1a1e138a6",
71
- "cli/sender-accounts.mjs": "406abd62a489894bec2cb3c9ebcc1b19572ab8f2d0fab4854deb78ea7dd89d0a",
72
+ "cli/sender-account-operations.mjs": "cf1c04e6289937611f5bd666953389c96887881f2b46203d79b9d8f57c55f3b2",
73
+ "cli/sender-accounts.mjs": "febe2a21f8cda4999e14eb78c01e9924dcc1914ff3c5cadcd845b7dbc4523491",
72
74
  "cli/skills-mcp.mjs": "6a91b16a789f50035ae0fe33f8ae2758bb9829949db7efb05c6d65dde335a9e0",
73
75
  "cli/skills.mjs": "ea8dfef32664714f9c63251d9a6bbdb44d77d41e272df46bb102ebfcd8df8b66",
74
76
  "cli/suppression-operations.mjs": "d1f827a785670fc6e44d87b18e4cfb6596b09e5be7c4f84c4b9a75ed13a962c6",
@@ -83,8 +85,8 @@
83
85
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
84
86
  "cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
85
87
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
86
- "cli/workspaces.mjs": "eebe2c4bfc6ddedb2fb0dd551d5fa348eeda2fdf2105babd514b29a97f4d6225",
87
- "package.json": "7083b89b9b44dd81b5d32685dd59728564d3c5193a1ef98bbbf0a5bd5ab9b606",
88
+ "cli/workspaces.mjs": "7ec6ec2a45a275d4425eb56731ccfc2d27abd979c5ae322294468e689c05c9ce",
89
+ "package.json": "e534913c63064467c1bce0c249dcc2ec2c7605b4c3d48bb39853f9236012b59b",
88
90
  "skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
89
91
  "skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
90
92
  "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
@@ -100,7 +102,7 @@
100
102
  "skills/getting-started/references/capabilities.md": "982e3583a47ba4ac832efae2f6af3a4d8c0c18cace159c76b2a4ad1a8d8e910e",
101
103
  "skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
102
104
  "skills/getting-started/references/outcome-checks.md": "094d09d231b38d4935aa9eb8ac089e46ba8e698489852fa97040e6b5eac77a9b",
103
- "skills/getting-started/references/replies.md": "acb5b77833cd9272ffeb05c27c24f9f2a8b1723da4e9a25cd6e8e6e666db2865",
105
+ "skills/getting-started/references/replies.md": "a372a01078f7c0a750b0fb98aff23ca9e7d9155c6b4ac549dd621444325e402b",
104
106
  "skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
105
107
  "skills/playbook-design/SKILL.md": "56aa5692c1bbe3843f11819f0f9270988ddd0424b16f38db72a854864155407f",
106
108
  "skills/playbook-design/references/fields.md": "7af0c8a2ad7b2ee83ff006fe4707a90c3edacdb8ccf83c09fa17f692b5fa2c81"