@audienti/cli 0.2.0-exora.12 → 0.2.0-exora.15

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.
@@ -18,7 +18,7 @@ const draft = z.object({
18
18
  }).strict();
19
19
  // Brief 12: what a stop, bounce or referral means, as a typed detail next to the classification it matches.
20
20
  const quote = z.string().meta({ minLength: 1, maxLength: 500 }).describe("The prospect's words, verbatim.");
21
- const result = z.discriminatedUnion('type', [
21
+ export const replyResultInput = z.discriminatedUnion('type', [
22
22
  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().describe('Classification bounce. A hard bounce, or a second bounce, marks the address bounced and blocks it in every campaign.'),
23
23
  z.object({ type: z.literal('reply.stop'), scope: z.enum(['person', 'company']).describe('company only when the prospect asked to stop for their whole company.'), quote }).strict().describe('Classification stop, with status handled and outcome suppressed. The person, or everyone at the company, is suppressed for good.'),
24
24
  z.object({ type: z.literal('reply.referral'), name: z.string().meta({ minLength: 1, maxLength: 200 }).describe('The person the prospect named.'), title: z.string().meta({ maxLength: 200 }).optional(), email: z.string().meta({ maxLength: 320 }).optional(), linkedinUrl: z.string().meta({ maxLength: 500 }).optional(), quote }).strict().describe('Classification referral, with status needs_handling and the thank-you as the draft. The named person is checked against the audience before any contact.'),
@@ -38,8 +38,9 @@ export const accountReplyOperations = {
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."),
41
- result: result.optional().describe("Brief 12: the typed detail for a bounce, stop or referral. Its type must match the classification."),
41
+ result: replyResultInput.optional().describe("Brief 12: the typed detail for a bounce, stop or referral. Its type must match the classification."),
42
42
  meeting: z.object({ at: z.string().datetime(), source: z.enum(['sent_invite', 'received_invite']) }).strict().optional().describe("A booked meeting: when, and whether the user's calendar sent the invite or the prospect did. Records meeting_booked."),
43
+ intent: z.object({ quote: z.string().meta({ minLength: 1, maxLength: 300 }).describe('The words in the reply that support the classification or meeting. Copy them; case and spacing do not matter.'), reason: z.string().meta({ minLength: 1, maxLength: 200 }).describe('One plain sentence: why. A person reads it, so no IDs or error codes.'), confidence: z.enum(['high', 'medium', 'low']).optional().describe('How sure you are. Default high.') }).strict().optional().describe("The words behind your reading. With it, the label is checked like intent_record checks it (the quote is in the reply, the reason is plain) and the quote and reason are stored on the event. Requires a classification or a meeting."),
43
44
  meetingState: z.enum(['requested', 'accepted', 'declined']).optional().describe("Where a meeting that is not booked yet stands after this reply: requested (a time was asked for), accepted (they agreed, no time yet) or declined. Records the matching meeting event once. Ignored once a meeting is booked.") }).strict().describe("Explicit inbound classification. needs_handling/not_reply require null outcome and responseEvidenceId. handled requires an outcome; response_recorded requires matching outbound evidence, closed_without_response an explicit closure reason, and suppressed active DNC or Reject."),
44
45
  method: 'POST', scoped: true, path: detail,
45
46
  body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
package/cli/audienti.mjs CHANGED
@@ -81,6 +81,7 @@ Discovery: prospects list|get ID|timeline ID [--limit N]|observation create|cita
81
81
  usage breakdown [--workspace ID] [--since month|7d|30d|90d] [--by tool|provider|model|caller|day] [--format json|csv]
82
82
  workspace usage summary|callers|calls [--after ID] [--caller user:ID|agent:ID|key:ID] [--status all|succeeded|failed|pending]|call ID [--workspace ID] (paid tool use, last 30 days, and single calls)
83
83
  intent list [--workspace ID] (the closed set of message labels: name, direction, event, channels, typed detail; output is JSON)
84
+ 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)
84
85
  audit list [--workspace ID] [--since 7d] [--actor agent|person] [--category NAME] [--format json|csv]
85
86
  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%)
86
87
  Qualification: campaigns qualification submit --file INPUT.json
@@ -89,7 +90,7 @@ Find prospects: campaign find CAMPAIGN_ID --count N --max-credits C (searches w
89
90
  campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
90
91
  Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
91
92
  campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
92
- Playbooks: playbook list [--archived] [--limit N --cursor ID]|actions|show ID [--revision N]|save --file REQUEST.json|archive ID|restore ID|delete ID|progress PARTICIPATION_ID|record --file REQUEST.json|suggest --file REQUEST.json|suggestions|decide --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
93
+ Playbooks: playbook list [--archived] [--limit N --cursor ID]|actions|show ID [--revision N]|save --file REQUEST.json|archive ID|restore ID|delete ID|progress PARTICIPATION_ID|record --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
93
94
  Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
94
95
  analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
95
96
  analytics activity [--workspace ID] [--days N | --from ISO --to ISO] [--format json|csv]
@@ -114,6 +115,7 @@ Start local MCP stdio server: mcp
114
115
 
115
116
  // Example inputs for the commands that save prospects, so `--help` shows a working shape.
116
117
  const examples = {
118
+ 'intent record': [{ label: 'stop', quote: 'please stop writing to me', reason: 'They ask us to stop.', result: { type: 'reply.stop', scope: 'person' } }],
117
119
  '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' } },
118
120
  'prospects citation create': { requestId: 'UUID', source: { kind: 'prospect', id: 'PERSON_PROSPECT_ID' }, target: { kind: 'prospect', id: 'COMPANY_PROSPECT_ID' }, edgeType: 'works_at', roles: [{ roleId: 'role-1', title: 'VP Sales', current: true, observedAt: '2026-01-01T00:00:00Z' }] },
119
121
  'prospects evidence create': { requestId: 'UUID', subject: { kind: 'person', prospectId: 'PROSPECT_ID' }, campaignId: 'CAMPAIGN_ID', campaignRevision: 1, kind: 'interpretation', data: { conclusion: 'Leads a 40-person sales team', reason: 'Team page lists her as VP Sales' }, sourceUrl: 'https://example.com/team', observedAt: '2026-01-01T00:00:00Z', skill: { id: 'signal-prospect-research', version: '1' } },
@@ -130,8 +132,17 @@ function commandHelp() {
130
132
  shown = lines.filter(line => line.includes(prefix));
131
133
  }
132
134
  console.log(shown.length ? shown.map(line => line.trim()).join('\n') : usage);
133
- const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command));
134
- if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\nSteps and rules: the "Save prospects and contacts" section of llms.txt.`);
135
+ const example = Object.entries(examples).find(([command]) => words.join(' ').startsWith(command) || (words.length > 0 && command.startsWith(words.join(' '))));
136
+ if (example) console.log(`\nExample --file INPUT.json for ${example[0]}:\n${JSON.stringify(example[1], null, 1)}\n${example[0].startsWith('intent') ? 'Rules: the "Label a message with its words" section of the getting-started replies reference.' : 'Steps and rules: the "Save prospects and contacts" section of llms.txt.'}`);
137
+ }
138
+
139
+ /** Says what is wrong with a command no handler took, instead of one message for every case. */
140
+ function unknownCommandMessage(words) {
141
+ const first = words[0];
142
+ if (first === undefined) return 'Give a command. Use audienti --help.';
143
+ if (first.startsWith('-')) return `Put the command first, then its options: audienti COMMAND [--workspace ID] [--profile PATH]. "${first}" is an option, not a command. Use audienti --help.`;
144
+ if (first === 'mcp') return 'mcp starts the local MCP server and takes no options. Run: audienti mcp';
145
+ return `"${first}" is not an audienti command. Use audienti --help for the commands. Most commands also take --workspace ID, and --profile PATH when the profile is not set in AUDIENTI_PROFILE.`;
135
146
  }
136
147
 
137
148
  async function main() {
@@ -191,7 +202,8 @@ async function main() {
191
202
  profileConfig = configuration(undefined, 'account:read');
192
203
  if (!profileConfig.token) throw new Error('whoami needs a human account token, and AUDIENTI_PROFILE holds none. Agents confirm their connection with: audienti workspace list');
193
204
  }
194
- if (args[0] !== 'whoami' || args.length > 2 || (args[1] && args[1] !== '--json')) throw new Error('Unknown command or option. Use audienti --help.');
205
+ if (args[0] !== 'whoami') throw new Error(unknownCommandMessage(args));
206
+ if (args.length > 2 || (args[1] && args[1] !== '--json')) throw new Error('whoami takes only --json and --profile PATH. Use audienti --help.');
195
207
  const rawUrl = profileConfig ? profileConfig.url : process.env.AUDIENTI_API_URL;
196
208
  const token = profileConfig ? profileConfig.token : process.env.AUDIENTI_API_TOKEN;
197
209
  if (!rawUrl || !token) throw new Error('Set AUDIENTI_API_URL and AUDIENTI_API_TOKEN, or pass --profile PATH (or set AUDIENTI_PROFILE) to a profile with an account:read token. Create a token from your Account page.');
@@ -1,8 +1,35 @@
1
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();
2
20
  export const intentOperations = {
3
21
  intent_labels_list: {
4
- schema: z.object({ workspaceId: z.string().uuid().optional() }).strict(),
22
+ schema: z.object({ workspaceId: uuid.optional() }).strict(),
5
23
  method: 'GET', path: a => `/workspaces/${a.workspaceId}/intent-labels`, scoped: true,
6
- 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. Recording a label is not available yet; the next release adds it.',
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.',
7
34
  },
8
35
  };
package/cli/intent.mjs CHANGED
@@ -1,4 +1,9 @@
1
+ import { randomUUID } from 'node:crypto';
1
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].';
2
7
 
3
8
  export async function intentCli(args) {
4
9
  const flags = {}, positions = [];
@@ -7,9 +12,31 @@ export async function intentCli(args) {
7
12
  if (arg === '--json') continue;
8
13
  if (!arg.startsWith('--')) { positions.push(arg); continue; }
9
14
  const key = arg.slice(2);
10
- if (!['workspace', 'profile'].includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}.`);
15
+ if (!RECORD_FLAGS.includes(key) || Object.hasOwn(flags, key) || i + 1 >= args.length) throw Error(`Invalid flag --${key}. ${USAGE}`);
11
16
  flags[key] = args[++i];
12
17
  }
13
- if (positions.length !== 2 || positions[1] !== 'list') throw Error('Use intent list [--workspace ID].');
14
- console.log(JSON.stringify(await callWorkspace('intent_labels_list', { workspaceId: flags.workspace }, configuration(flags.profile))));
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))));
15
42
  }
@@ -51,18 +51,6 @@ export const playbookCommands = {
51
51
  record: {
52
52
  usage: 'playbook record --file REQUEST.json [--workspace ID] [--profile PATH]',
53
53
  about: 'Report that a person moved to a stage or off-ramp, or finished the current stage play. The file has participationId, expectedVersion (the version from playbook progress), requestId (a fresh UUID), exactly one of stageKey, offRampKey or finishStage true, reason, and evidence (a list of {kind: observation|conversation, id}).'
54
- },
55
- suggest: {
56
- usage: 'playbook suggest --file REQUEST.json [--workspace ID] [--profile PATH]',
57
- about: 'The same request as record, but it asks a person to approve the move in Operator. Nothing changes until they approve.'
58
- },
59
- suggestions: {
60
- usage: 'playbook suggestions [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
61
- about: 'Pending suggestions that wait for a person to approve or dismiss them.'
62
- },
63
- decide: {
64
- usage: 'playbook decide --file REQUEST.json [--workspace ID] [--profile PATH]',
65
- about: 'Approve or dismiss one pending suggestion: {"id": SUGGESTION_ID, "requestId": UUID, "decision": "approve" or "dismiss"}. People only; an agent cannot approve.'
66
54
  }
67
55
  };
68
56
  const lines = Object.values(playbookCommands).map(command => command.usage);
@@ -9,11 +9,12 @@ const eventRule=z.object({key:z.string().uuid(),when:z.string().min(1).max(60).d
9
9
  const stepRoutes=z.object({accept:routeTarget.optional(),reply:routeTarget.optional(),meeting:routeTarget.optional(),timeout:routeTarget.optional()}).strict().describe("Where an event that reaches the person after this step ran sends them. A route wins over the stage's jump list; an event with no route follows the stage. Going to an earlier stage needs rules.allowBackward.");
10
10
  const playbookStepSchema=z.object({key:z.string().uuid(),channel:z.enum(['LinkedIn','Email','Custom']),action:label,condition:z.enum(['Always','Only if viable','Only if connected']).optional().describe('Left out: Only if connected for a LinkedIn message (it can only reach a connection), Always for everything else. Only if connected skips the step for a person who was asked to connect and has not accepted. Only if viable needs viability.'),viability:text.optional(),instructions:text.optional().describe('What the agent should write or do. Left out, one coaching sentence for the kind of action.'),wait:z.object({until:z.enum(['accept','reply','meeting','timeout','event']),days:z.number().int().min(1).max(60),event:label.optional().describe('Custom steps only, with until event: the name of the event a person records to end the wait, for example Gift delivered.'),followUps:z.number().int().min(0).max(10).optional().describe('Meeting waits only: how many follow-ups your agent writes, one after each wait, before the meeting is marked No answer. Default 3.')}).strict().nullable().optional().describe('How long the step waits and for what. Left out on a new step that sends: a reply wait of 3 days (a connection request waits 7 days for the accept). null means no wait. A step the playbook already has keeps what it has.'),waitDays:z.number().int().min(1).max(60).optional().describe('Older form of wait: the same as wait {until: timeout, days}.'),side:z.boolean().optional(),onTimeout:z.enum(['next','off_ramp']).optional(),humanize:z.boolean().optional().describe('Email only. Rewrite the email text before approval so it reads as written by a person. On by default; set false to send the text as written.'),actionId:z.string().max(80).optional().describe('An action id from the outreach rulebook, for example linkedin.message.send.'),routes:stepRoutes.optional(),eventRules:z.array(eventRule).max(10).optional()}).strict();
11
11
  const completion=z.discriminatedUnion('kind',[z.object({kind:z.literal('next')}).strict(),z.object({kind:z.literal('stay')}).strict(),z.object({kind:z.literal('off_ramp'),offRampKey:z.string().uuid()}).strict()]);
12
- const stageFields=identity.extend({movesInWhen:text.optional(),owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional().describe('Days a person should take in this stage. Left out: the waits of its steps plus 2 days, at least 3.'),enabled:z.boolean().optional().describe('false skips the stage: people are never moved into it. The first stage and the goal stage cannot be skipped.'),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional().describe('What happens when the last step finishes. Left out: a stage with steps that an agent works, other than the goal, closes people out as Unresponsive; otherwise they stay.'),on:z.array(z.enum(['accept','reply','meeting','manual'])).max(4).optional(),eventRules:z.array(eventRule).max(10).optional()});
12
+ const stageFields=identity.extend({owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional().describe('Days a person should take in this stage. Left out: the waits of its steps plus 2 days, at least 3.'),enabled:z.boolean().optional().describe('false skips the stage: people are never moved into it. The first stage and the goal stage cannot be skipped.'),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting'])).max(3).optional().describe('The events that jump a person forward to this stage: accept, reply, meeting (a booked meeting). A person who has one of these events moves to the furthest later stage that lists it, and the stages passed over are recorded as skipped. Leave it out for a stage people reach when the stage before it finishes, or when a person moves them. Not for the first stage.'),eventRules:z.array(eventRule).max(10).optional()});
13
13
  // Settings the product no longer asks for (#457). An older request may still carry them: they are accepted, ignored and left out of the new version.
14
- // autoMove: conflict 20, approval lives on the account. flagOverdue, emailWindow, nurtureDays: no code read them. optional (stage): use enabled.
14
+ // autoMove: conflict 20, approval lives on the account. flagOverdue, emailWindow, nurtureDays: no code read them. optional (stage): use enabled. movesInWhen (words) and 'manual' in on: the stage's events say when a person enters it.
15
15
  const dropKeys=keys=>value=>{if(!value||typeof value!=='object'||Array.isArray(value)||!keys.some(key=>key in value))return value;return Object.fromEntries(Object.entries(value).filter(([key])=>!keys.includes(key)));};
16
- const stage=z.preprocess(dropKeys(['optional']),stageFields);
16
+ const dropRetiredStage=value=>{const rest=dropKeys(['optional','movesInWhen'])(value);if(!rest||typeof rest!=='object'||Array.isArray(rest)||!Array.isArray(rest.on))return rest;return {...rest,on:rest.on.filter(event=>event!=='manual')};};
17
+ const stage=z.preprocess(dropRetiredStage,stageFields);
17
18
  const playbookRulesSchema=z.preprocess(dropKeys(['autoMove','flagOverdue','emailWindow','nurtureDays']),z.object({allowBackward:z.boolean().optional().describe('Let a move go to an earlier stage, for example when a meeting is cancelled. Default false.')}).strict());
18
19
  const playbookFieldsSchema=z.object({name:label,stages:z.array(stage).min(2).max(30).optional().describe('The stages, in order. Leave it out for the default skeleton: Ready, Outreach underway (no steps; people who finish it leave as Unresponsive), In conversation and Meeting booked as the goal. Then add steps with a later save.'),goalKey:z.string().uuid().optional().describe('The key of the goal stage, after the first stage. Leave it out with stages to use the last stage; it is ignored when you leave stages out.'),offRamps:z.array(identity.extend({reason:z.enum(['rejected','not_fit','nurture','unresponsive','other','taken_over','bounced','skipped']).optional().describe('How Analytics counts people who leave here. Left out, it is guessed from the name (Not interested is rejected, Not a fit is not_fit, Unresponsive is unresponsive), else other.')})).max(30).optional().describe('Where people leave the playbook. Left out, the three standard off-ramps: Unresponsive, Not interested and Not a fit.'),voice:text.optional().describe('One sentence of how the agent writes. Left out, a plain, short default.'),rules:playbookRulesSchema.optional(),eventRules:z.array(eventRule).max(30).optional()}).strict();
19
20
  const fields=playbookFieldsSchema;
@@ -29,8 +30,5 @@ export const playbookOperations={
29
30
  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.'},
30
31
  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.'},
31
32
  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.'},
32
- playbook_progress_suggest:{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}/suggestions`,body:({workspaceId,participationId,...a})=>a,scoped:true,description:'Suggest a pinned stage move, off-ramp or play completion for human review in Operator. Stored evidence required. Does not change progress or send anything. Assigned owner/Admin; current human approval revalidates progress.'},
33
- playbook_suggestion_decide:{schema:z.object({...scope,id:uuid,requestId:uuid,decision:z.enum(['approve','dismiss'])}).strict(),method:'POST',path:a=>`${base(a)}/playbook-suggestions/${a.id}/decision`,body:({workspaceId,id,...a})=>a,scoped:true,description:'Approve or dismiss a pending stage suggestion. People only: the current owner or an Admin. Approving moves the participation to the suggested stage.'},
34
- playbook_suggestions_list:{schema:z.object({...scope,cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>`${base(a)}/playbook-suggestions?${query(a)}`,scoped:true,description:'List pending stage suggestions visible to the current owner or Admin. Humans approve or dismiss in Operator; agents cannot approve.'},
35
33
  workspace_analytics_get:{schema:z.object({...scope,campaignId:uuid.optional(),ownerUserId:uuid.optional(),offerId:uuid.optional(),audienceId:uuid.optional(),tag:z.string().min(1).max(64).optional(),pinId:uuid.optional(),from:z.string().datetime(),to:z.string().datetime()}).strict(),method:'GET',path:a=>`${base(a)}/analytics?${query(a)}`,scoped:true,description:'Read UTC-period distinct outcomes and explicit stage events. Missing activity or cost attribution is not tracked, never fabricated zero. Optional filters: campaignId, ownerUserId (campaign owner), offerId, audienceId, tag (campaign tag); list the values with workspace_analytics_filters. Maximum 366 days; narrow large scopes.'}
36
34
  };
package/cli/progress.mjs CHANGED
@@ -3,10 +3,10 @@ import {readJsonFile} from './json-file.mjs';
3
3
  import {playbookUsageError,playbookCommands} from './playbook-help.mjs';
4
4
  export async function progressCli(args){
5
5
  const positions=[],flags={};for(let i=0;i<args.length;i++){const arg=args[i];if(!arg.startsWith('--')){positions.push(arg);continue;}const key=arg.slice(2);if(!['profile','workspace','file','cursor','limit','revision','from','to','campaign','playbook-version','kind','user','format','owner','offer','audience','tag','archived'].includes(key)||(key!=='archived'&&(!args[i+1]||args[i+1].startsWith('--')))||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);flags[key]=key==='archived'&&(!args[i+1]||args[i+1].startsWith('--'))?'true':args[++i];}
6
- const names={playbook:{list:'playbook_list',actions:'playbook_actions_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record',suggest:'playbook_progress_suggest',suggestions:'playbook_suggestions_list',decide:'playbook_suggestion_decide'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
6
+ const names={playbook:{list:'playbook_list',actions:'playbook_actions_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record'},analytics:{show:'workspace_analytics_get'},pipeline:{show:'workspace_pipeline_get'},connections:{list:'workspace_connections_list',register:'workspace_connection_register',report:'workspace_connection_report',disconnect:'workspace_connection_disconnect'}};
7
7
  const name=names[positions[0]]?.[positions[1]];if(!name)throw new Error(positions[0]==='playbook'?playbookUsageError:'Use analytics show --from ISO --to ISO; pipeline show; connections list|register --file PATH|report --file PATH|disconnect --file PATH.');
8
8
  if(flags.format&&(name!=='workspace_analytics_get'||!['json','csv'].includes(flags.format)))throw new Error('--format json|csv is only accepted for analytics show.');
9
- const fileNames=['playbook_save','playbook_progress_record','playbook_progress_suggest','workspace_connection_register','workspace_connection_report','playbook_suggestion_decide','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get','playbook_archive','playbook_restore','playbook_delete'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
9
+ const fileNames=['playbook_save','playbook_progress_record','workspace_connection_register','workspace_connection_report','workspace_connection_disconnect'];const withId=['playbook_get','playbook_progress_get','playbook_archive','playbook_restore','playbook_delete'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
10
10
  if(fileNames.includes(name)&&!flags.file)throw new Error('Supply a JSON request with --file PATH.');if(!fileNames.includes(name)&&flags.file)throw new Error('--file is only accepted for mutations.');
11
11
  const input=fileNames.includes(name)?readJsonFile(flags.file,65536,'Request file'):{};if(!input||typeof input!=='object'||Array.isArray(input))throw new Error('Request must be an object.');if(flags.workspace)input.workspaceId=flags.workspace;if(withId)input[name==='playbook_progress_get'?'participationId':'id']=positions[2];
12
12
  if(flags.archived!==undefined&&(name!=='playbook_list'||!['true','false'].includes(flags.archived)))throw new Error(name==='playbook_list'?'--archived takes true or false, or no value.':'--archived is only accepted for playbook list.');
@@ -164,6 +164,8 @@ export function localGuidance(name, issues, args) {
164
164
  }
165
165
  // The next step an error gives an agent (#483): one sentence, the path of the wrong value and the values that would have worked.
166
166
  // Checked here too, so a response never puts the token or control text into the agent's context.
167
+ // #519: a refused label already says in plain words what is wrong; the code and the next step travel beside it.
168
+ 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']);
167
169
  export function errorGuidance(error, token) {
168
170
  const clean = text => typeof text === 'string' && !/[\u0000-\u001f\u007f]/.test(text) && !(token && text.includes(token));
169
171
  return {
@@ -196,9 +198,9 @@ export async function callWorkspace(name, args, config) {
196
198
  if(!response.ok) {
197
199
  const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
198
200
  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;
199
- const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_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() : '';
201
+ const detail = (INTENT_REFUSALS.has(code) || (response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===422 && code==='playbook_cannot_work') || (response.status===409 && (code==='revision_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() : '';
200
202
  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;
201
- const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token));
203
+ const error = new WorkspaceRequestError(response.status, code, name==='intent_record' && (INTENT_REFUSALS.has(code) || code==='invalid_fields') && detail ? detail : `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId, errorGuidance(result?.error, config.token));
202
204
  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}}:{})}));
203
205
  if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
204
206
  // A playbook that is in use names the campaigns that use it (#459).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@audienti/cli",
3
- "version": "0.2.0-exora.12",
3
+ "version": "0.2.0-exora.15",
4
4
  "description": "Audienti CLI and MCP client for app.audienti.com",
5
5
  "type": "module",
6
6
  "bin": {
@@ -75,6 +75,47 @@ when you pass `returnsOn`. Nothing is sent meanwhile and the person does not mov
75
75
  settle: ask who the right person is. When the reply names the right person, classify it as a
76
76
  `referral` with its typed `result`.
77
77
 
78
+ ## Label a message with its words
79
+
80
+ A label says what a message means. Read the names with `audienti intent list`. A label goes on a message
81
+ that is stored as evidence. Get its `EVIDENCE` ID from `audienti prospects conversation append` (when you
82
+ store the message yourself) or `audienti prospects conversation list PERSON` (for one already stored); intake
83
+ returns it too. Record one to three labels, each with the `quote` that supports it (at most 300 characters,
84
+ copied from the message) and a plain `reason` (at most 200 characters, no IDs or error codes):
85
+
86
+ ```
87
+ audienti intent record --person PERSON --evidence EVIDENCE \
88
+ --label interested --quote "we are interested in a demo" --reason "They ask for a demo next week."
89
+ ```
90
+
91
+ Flags carry one label. Use `--file LABELS.json` for several labels or a typed detail (the `result` of a
92
+ stop, bounce or referral; `meeting` for `meeting_booked`; `returnsOn` for `out_of_office`). A stop that
93
+ asks for everything to end looks like this:
94
+
95
+ ```
96
+ [{"label": "stop", "quote": "please stop writing to me", "reason": "They ask us to stop.",
97
+ "result": {"type": "reply.stop", "scope": "person"}}]
98
+ ```
99
+
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
103
+ one is refused with `already_recorded`, and one that would give the message a second reading is refused.
104
+
105
+ Audienti checks every label and writes nothing if one fails. A refusal names the `field`, the `allowed`
106
+ values and the `next` step. The checks:
107
+
108
+ - The message is stored with its text. A message stored without a body cannot carry a label: submit the text as evidence.
109
+ - The name is on the list, and fits the message direction and channel (`bounce` is email only).
110
+ - The quote is in the message (subject and body), ignoring case and spacing.
111
+ - The reason is plain words a person can read.
112
+ - The labels fit together: `stop`, `bounce` and `out_of_office` stand alone; a message has one reading of the
113
+ reply and at most one meeting label.
114
+ - The typed detail is present: a `stop`, `bounce` or `referral` needs its `result`; `meeting_booked` needs `meeting`.
115
+
116
+ `replies set` records the same labels. Add `"intent": {"quote": "...", "reason": "..."}` next to `classification`
117
+ or `meetingState` and it runs the same checks and stores the words on the event. Without `intent` it works as before.
118
+
78
119
  ## Stop, bounce and referral
79
120
 
80
121
  These three need no decision from the user. Classify the reply, report the typed
@@ -113,7 +113,7 @@ When you or the user have a new idea for how to reach people:
113
113
  the old and new campaigns can be compared. Name what changed in the campaign notes.
114
114
  5. Ask the user before the successor starts.
115
115
 
116
- ## Record progress and ask for review
116
+ ## Record progress and ask when the rules cannot decide
117
117
 
118
118
  Read the playbook before doing work for a campaign pinned to it: `audienti playbook
119
119
  show PLAYBOOK_ID`. Stage plays are instructions for you, the external agent; they are
@@ -123,10 +123,9 @@ not a hosted sequence that runs by itself.
123
123
  `offRampKey` or `finishStage: true`, the participation's pinned playbook version and
124
124
  its current expected progress version, and stored evidence. Do not invent outcomes.
125
125
  `audienti playbook` prints the command usage; the API schema has the required fields.
126
- - Human review: `audienti playbook suggest --file PATH`. The user approves or dismisses
127
- it in **Operator → Queue**. A suggestion does not move a stage, and agents cannot
128
- approve one. A stay completion records work without moving the stage.
129
- `audienti playbook suggestions` lists the suggestions you can see.
126
+ - When you see something that moves a person (a reply, an accept, a booked meeting), record
127
+ that event; the playbook's rules decide the move. If the rules cannot decide, ask the
128
+ user with `audienti ask "QUESTION"`. The question appears in Operator.
130
129
  - None of these commands sends a message to a provider.
131
130
 
132
131
  ## Done
@@ -47,7 +47,7 @@ The other items are warnings; read them after each save and clear them:
47
47
  | Item `id` | Meaning and default fix |
48
48
  | --- | --- |
49
49
  | `stage_never_ends` | A stage with steps and `completion` `stay`. Use `off_ramp` with the Unresponsive off-ramp. |
50
- | `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event or finish the stage before it with `next`. |
50
+ | `stage_by_hand_only` | Nothing opens a stage but a person or agent. Give it an `on` event ("Jump forward to here when") or finish the stage before it with `next`. The first stage after Ready is not flagged. |
51
51
  | `send_without_wait` | A send step with no `wait`, followed by another send on the same channel. Add a wait. |
52
52
  | `goal_shorter_than_waits` | `goalDays` is below the waits added up. Use the waits plus 2 days. |
53
53
  | `send_in_person_stage` | A send step in the goal stage or an `Assignee` stage writes to someone who already answered. Remove it. |
@@ -61,8 +61,7 @@ The other items are warnings; read them after each save and clear them:
61
61
  | --- | --- |
62
62
  | `key` | A UUID, unique in the playbook. Keep it across revisions. |
63
63
  | `name` | Unique in the playbook. |
64
- | `movesInWhen` | What moves a prospect into this stage, for example "They replied". Shown to people; `on` decides. |
65
- | `on` | The events that open this stage: `accept`, `reply`, `meeting` or `manual`. Defaults to `["manual"]`. An event moves the prospect to the furthest later stage that opens on it; the stages passed over are recorded as skipped, never as done. |
64
+ | `on` | The events that jump a prospect forward to this stage: `accept` (they accept your LinkedIn connection request, not a meeting invite), `reply` or `meeting` (a booked meeting). Do not list `accept` or `reply` on the goal stage unless a reply or an accept should count as a win. An event moves the prospect to the furthest later stage that lists it; the stages passed over are recorded as skipped, never as done. Leave it out for a stage people reach when the stage before it finishes, or when a person moves them. Do not set it on the first stage. People and the pipeline read "Starts when they reply" from it. The older `movesInWhen` text and the value `manual` are accepted and ignored. |
66
65
  | `owner` | `Agent` or `Assignee` (the person assigned to the prospect). |
67
66
  | `goalDays` | 0 to 365. The days after which a prospect in this stage is overdue (the pipeline card turns orange). 0 means never overdue. |
68
67
  | `enabled` | Defaults to true. `false` skips the stage: no one is moved into it. The first stage and the goal stage cannot be skipped. |
@@ -131,7 +130,7 @@ Use the same UUID wherever the same placeholder letter appears.
131
130
  { "key": "<uuid>", "channel": "LinkedIn", "action": "Send message", "condition": "Only if connected", "instructions": "Lead with their signal and offer something useful.", "wait": { "until": "reply", "days": 5 } },
132
131
  { "key": "<uuid>", "channel": "Email", "action": "Email 1", "condition": "Always", "instructions": "Three sentences: signal, offer, one short question.", "wait": { "until": "reply", "days": 4 }, "onTimeout": "off_ramp" }
133
132
  ], "completion": { "kind": "off_ramp", "offRampKey": "<R>" } },
134
- { "key": "<D>", "name": "In conversation", "movesInWhen": "They replied", "on": ["reply"], "owner": "Assignee", "goalDays": 3, "steps": [
133
+ { "key": "<D>", "name": "In conversation", "on": ["reply"], "owner": "Assignee", "goalDays": 3, "steps": [
135
134
  { "key": "<uuid>", "channel": "Email", "action": "Draft a reply", "condition": "Always", "instructions": "Answer their question first. Propose a time only if they asked.", "wait": { "until": "reply", "days": 2 } }
136
135
  ], "completion": { "kind": "stay" } },
137
136
  { "key": "<E>", "name": "Meeting requested", "owner": "Assignee", "goalDays": 3, "steps": [], "completion": { "kind": "next" } },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "repository": "https://github.com/omalab/exora",
3
- "sourceCommit": "9673ca0c2e8c98a117672f0ba0181e66a1a5c556",
3
+ "sourceCommit": "abb0afcaf30cbc60c862e53e5b52e26da909f3b0",
4
4
  "files": {
5
5
  "LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
6
6
  "README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
@@ -8,7 +8,7 @@
8
8
  "cli/account-intake-operations.mjs": "b43d764c628beabafad753d711ddb2af2c64276e4a6506ed0b854839091ea5db",
9
9
  "cli/account-intake.mjs": "aa77f2f96fd030f8830d6de278932186aea7c82cfda086a5a6cf569202feebc8",
10
10
  "cli/account-replies.mjs": "699e34e5c736ba316df8f164b4c50f82a7e4e2d5e51303e6b13944c3f2c854ab",
11
- "cli/account-reply-operations.mjs": "4359096dbf75d8c3602266588d2aea2b6cdecd903e2a094dbf25d26e2fab22b2",
11
+ "cli/account-reply-operations.mjs": "21f1441e708d9de8af83ec8f7444a5ca5d686fab90be669b904dbe1454eb7a13",
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": "d78b513ea58026f8f2f2eb6c70f4cd00c589b6b6520be53ebd1332377a8d6a78",
19
+ "cli/audienti.mjs": "6cf78f0359a450bfc456599aa047efcf93f0f9353e810449957f932ea0ce1a43",
20
20
  "cli/audit-operations.mjs": "afaf87d592a1e5f6bff2a8649a6a64a8f471950b7d99e98995d9205aa5910398",
21
21
  "cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
22
22
  "cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
@@ -36,8 +36,8 @@
36
36
  "cli/guardrails.mjs": "e8b9c0a19956a9eab84475d3c56a4b6c77baf7ed9a55e8686d660ef1ad562aa8",
37
37
  "cli/hand-task-operations.mjs": "473473fe95324c8c8733e037feab5ac468c07b0658fabd739cfd7d400a7faa83",
38
38
  "cli/hand-tasks.mjs": "2c895cf6b30c594a7e27cc4ec4b7eddeb39f941cd195b9255c6e0d6b4a56371f",
39
- "cli/intent-operations.mjs": "e3151e71e9064c8de0953e666e23e4d2c4681b1f3900ef79569b6a074d48ab8c",
40
- "cli/intent.mjs": "3f31297a09d144204fb04b5d54f67172fc39d3d3833bf4e8ad34469295b22fea",
39
+ "cli/intent-operations.mjs": "104e883319b4e9c2fb43de373fcf9d5df537871995a0b81b4ee86fa206ba6707",
40
+ "cli/intent.mjs": "db0ccb85977fca26f865970dacd0538e81b070d7c0ec078f38358f33c458a96a",
41
41
  "cli/json-file.mjs": "96431b18215834c2531f5901e2526875c634dd2e85a9465fcffc937fbec52150",
42
42
  "cli/kv-operations.mjs": "505d79d0d535d3441883f8a37817c30c73367216a35dddb07181f0f0e799df79",
43
43
  "cli/kv.mjs": "05783610001dd002af07a4bb8f26e64d74aa4981bbe38a2100552b810cb3bc72",
@@ -50,10 +50,10 @@
50
50
  "cli/outcome-operations.mjs": "cf133097799f240a006264a38a4c6bd4094078d6dec78a71679700ac3bdeacd5",
51
51
  "cli/outcomes.mjs": "765ccb112a90fc4d228b2a41e5e19c5e1f3e2fe14bc1d8128b0090fc95cdbf3f",
52
52
  "cli/owner-account-client.mjs": "e7912f7c67571676cb94bcc9424b7d4a3bf9ef1461c9a5d6c31a2651e546a9ea",
53
- "cli/playbook-help.mjs": "56a5b6ba4c94022931f9c751c954133239404048aba99d1ff28cd290eac22f41",
54
- "cli/playbook-operations.mjs": "f5a90688c266d2445fc69500ed39aa0eb0548c8894a710130190bf8177dcbfb1",
53
+ "cli/playbook-help.mjs": "541bba97695e9761cf73d182f62900e8117bddb23a39c77f1d18258cd7e6f5a5",
54
+ "cli/playbook-operations.mjs": "3994739d63dbd6658d5e14eea889c56ce1dc19a9c639f178335a56387793717a",
55
55
  "cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
56
- "cli/progress.mjs": "aeca47f4e59be306dac4592cf2e4209a30e322fc13e4e103e64e09faa370f5a8",
56
+ "cli/progress.mjs": "c394eae609596accba0d170d376fe35b58cddff2cf7b106f670da7eaed76dd5f",
57
57
  "cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
58
58
  "cli/prospecting-settings.mjs": "b09f47c84416d9892d7d5b17dcf684cbb560e1cacb25513826ff04ef9307626e",
59
59
  "cli/question-operations.mjs": "c7070dd1b5ec9c5b0df93012ef08d0d6b4d6e5c8da267a37beb1acc5c3cf4ea1",
@@ -83,8 +83,8 @@
83
83
  "cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
84
84
  "cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
85
85
  "cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
86
- "cli/workspaces.mjs": "0f75d41387e0dd1aa9e20a70fba5298e0d9ac6c86c04f050723c31e04479ad83",
87
- "package.json": "b5e0afb3718de66a76c5106f1a9bef5bec9447d33f26b7d1def3e7936b6e8e6d",
86
+ "cli/workspaces.mjs": "5694825a763008bf0dcfa4e17bfe79a1d593207099f500bfa21184f899e83971",
87
+ "package.json": "7aabd2b008aa0b7645e3e5ba52088b1ac90e312c183a7bfa7f7563510cb461bb",
88
88
  "skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
89
89
  "skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
90
90
  "skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
@@ -100,9 +100,9 @@
100
100
  "skills/getting-started/references/capabilities.md": "982e3583a47ba4ac832efae2f6af3a4d8c0c18cace159c76b2a4ad1a8d8e910e",
101
101
  "skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
102
102
  "skills/getting-started/references/outcome-checks.md": "094d09d231b38d4935aa9eb8ac089e46ba8e698489852fa97040e6b5eac77a9b",
103
- "skills/getting-started/references/replies.md": "865c97bea9c4bbecc2aba15be933356d11c37f3239be2cd436442230b5e0501e",
103
+ "skills/getting-started/references/replies.md": "d085b7353f13d70a1b0333d851fe573fd2c99e6bb57923cf8717982d77a0504b",
104
104
  "skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
105
- "skills/playbook-design/SKILL.md": "86d77069a73d5c21c43623d2faa84a3957cfb1f30f7fae45f239b32e249f892a",
106
- "skills/playbook-design/references/fields.md": "41b20ab4a82501def62f8138fc218f48180a46c37fcf68d47f6a5802c5a747bf"
105
+ "skills/playbook-design/SKILL.md": "eec38bb7cd1586e11c6c27cccf328c1731d404f0767bd1df0c3ac8273495cb50",
106
+ "skills/playbook-design/references/fields.md": "7d4a22451c3b77bb21a76d211d25a44b84a1bc46cc6524472ed121e77d2866aa"
107
107
  }
108
108
  }