@audienti/cli 0.2.0-exora.3 → 0.2.0-exora.30
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.
- package/README.md +2 -2
- package/cli/account-replies.mjs +16 -2
- package/cli/account-reply-operations.mjs +28 -1
- package/cli/agents.mjs +56 -14
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audience-operations.mjs +1 -1
- package/cli/audienti.mjs +118 -27
- package/cli/audit-operations.mjs +2 -2
- package/cli/campaign-find.mjs +155 -0
- package/cli/campaign-operations.mjs +8 -4
- package/cli/campaigns.mjs +38 -3
- package/cli/cohort-operations.mjs +1 -1
- package/cli/content.mjs +2 -0
- package/cli/decision-operations.mjs +35 -0
- package/cli/decision.mjs +51 -0
- package/cli/discovery-operations.mjs +2 -1
- package/cli/discovery.mjs +3 -2
- package/cli/duplicate-operations.mjs +22 -0
- package/cli/duplicates.mjs +22 -0
- package/cli/hand-task-operations.mjs +11 -0
- package/cli/hand-tasks.mjs +18 -0
- package/cli/intent-operations.mjs +44 -0
- package/cli/intent.mjs +109 -0
- package/cli/json-file.mjs +1 -1
- package/cli/kv-operations.mjs +17 -0
- package/cli/kv.mjs +58 -0
- package/cli/mcp.mjs +4 -4
- package/cli/outcome-operations.mjs +28 -0
- package/cli/outcomes.mjs +16 -0
- package/cli/owner-account-client.mjs +5 -4
- package/cli/playbook-help.mjs +87 -0
- package/cli/playbook-operations.mjs +40 -0
- package/cli/progress.mjs +19 -8
- package/cli/prospecting-settings-operations.mjs +1 -1
- package/cli/question-operations.mjs +4 -4
- package/cli/questions.mjs +4 -4
- package/cli/queue-operations.mjs +7 -6
- package/cli/queue.mjs +4 -3
- package/cli/recipient-operations.mjs +1 -1
- package/cli/recipients.mjs +6 -2
- package/cli/rooms-operations.mjs +26 -5
- package/cli/rooms.mjs +32 -6
- package/cli/secret-operations.mjs +7 -0
- package/cli/secrets.mjs +30 -0
- package/cli/sender-account-operations.mjs +8 -3
- package/cli/sender-accounts.mjs +20 -10
- package/cli/skills-mcp.mjs +6 -2
- package/cli/skills.mjs +55 -12
- package/cli/tools-operations.mjs +19 -10
- package/cli/tools.mjs +48 -20
- package/cli/work-execution-operations.mjs +7 -0
- package/cli/work-execution.mjs +3 -3
- package/cli/work-operations.mjs +5 -4
- package/cli/work.mjs +2 -2
- package/cli/workspaces.mjs +104 -22
- package/package.json +2 -2
- package/skills/agent-rooms/SKILL.md +140 -0
- package/skills/agent-rooms/references/commands.md +78 -0
- package/skills/audienti/SKILL.md +111 -5
- package/skills/audienti/references/helpers.md +15 -2
- package/skills/audienti/references/profile-workflows.md +4 -2
- package/skills/audienti/scripts/contracts.mjs +1 -1
- package/skills/getting-started/SKILL.md +188 -154
- package/skills/getting-started/references/audience.md +72 -7
- package/skills/getting-started/references/campaign-draft.md +56 -13
- package/skills/getting-started/references/capabilities.md +238 -0
- package/skills/getting-started/references/mailbox-check.md +64 -0
- package/skills/getting-started/references/outcome-checks.md +125 -0
- package/skills/getting-started/references/replies.md +314 -0
- package/skills/getting-started/references/reply-checks.md +54 -0
- package/skills/playbook-design/SKILL.md +139 -0
- package/skills/playbook-design/references/fields.md +145 -0
- package/source-manifest.json +74 -48
- package/cli/approach-operations.mjs +0 -24
- package/skills/getting-started/references/inventory.md +0 -52
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { configuration,validateUrl,WorkspaceRequestError } from './workspaces.mjs';
|
|
2
|
-
export async function callOwnerAccountOperation(operation,args,profilePath=process.env.AUDIENTI_PROFILE,label='prospecting settings') {
|
|
1
|
+
import { configuration,errorGuidance,validateUrl,WorkspaceRequestError } from './workspaces.mjs';
|
|
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;
|
|
6
6
|
if(profile.pendingAgent||(!profile.agent&&profile.connectionSecret))throw Error('Finish connecting the agent before reading settings.');
|
|
7
7
|
if(profile.agent){
|
|
8
|
-
if(operation.ownerOnly)throw new WorkspaceRequestError(403,'forbidden','Only a human
|
|
8
|
+
if(operation.ownerOnly)throw new WorkspaceRequestError(403,'forbidden','Only a human user profile can change settings or read history.');
|
|
9
9
|
config.url=profile.url;config.token=profile.tokens?.['workspace:read'];
|
|
10
10
|
if(!/^agt_[A-Za-z0-9_-]{43}$/.test(config.token??''))throw Error('A current agent credential is required.');
|
|
11
11
|
}
|
|
@@ -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';
|
|
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
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// One text for every playbook command, shared by `audienti --help`, `audienti playbook --help`,
|
|
2
|
+
// `audienti playbook SUBCOMMAND --help` and the usage error (#449). The runnable field reference is the
|
|
3
|
+
// playbook-design skill: audienti skills read playbook-design references/fields.md.
|
|
4
|
+
export const playbookCommands = {
|
|
5
|
+
list: {
|
|
6
|
+
usage: 'playbook list [--archived] [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
|
|
7
|
+
about: 'One page of summaries, 50 by default: id, name, currentRevision, system, systemKey, archivedAt, stages, steps, recommended, updatedAt. Archived playbooks are hidden; --archived lists them instead. Starters have system true and cannot be changed; clone one by saving its fields without an id. Use nextCursor as --cursor for the next page. Full fields come from playbook show.'
|
|
8
|
+
},
|
|
9
|
+
actions: {
|
|
10
|
+
usage: 'playbook actions [--workspace ID] [--profile PATH]',
|
|
11
|
+
about: 'What a playbook step may name: the step actions (id, channel, label), the step channels (LinkedIn, Email) and the event-rule names. Run it before playbook save. A step\'s actionId must be one of these ids and match the step channel. A save that names something else is refused with the wrong value, the allowed values (allowed) and this command (next).'
|
|
12
|
+
},
|
|
13
|
+
show: {
|
|
14
|
+
usage: 'playbook show PLAYBOOK_ID [--revision N] [--workspace ID] [--profile PATH]',
|
|
15
|
+
about: 'The whole playbook (fields) and the campaigns that use it. Without --revision it returns the current one. PLAYBOOK_ID comes from playbook list. warnings is the Playbook check: what is wrong with the playbook (level error or warning, a message, the stage and step, and the default fix). An empty list means nothing to look at.'
|
|
16
|
+
},
|
|
17
|
+
save: {
|
|
18
|
+
usage: 'playbook save --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
19
|
+
about: 'Create a playbook or save a new revision. Step action ids and channels: audienti playbook actions. The file is {"requestId": UUID, "expectedRevision": N, "id": PLAYBOOK_ID, "fields": {...}}. requestId: a fresh UUID; reuse it only to retry the same save. id: leave it out for a new playbook. expectedRevision: 0 for a new playbook, otherwise currentRevision from playbook show. A save from a stale revision is refused with revision_conflict and the response gives currentRevision. A playbook that cannot work (no off-ramp, or a stage nobody can reach) is refused with playbook_cannot_work and the list in errors. A saved playbook comes back with warnings, the Playbook check, for things that are legal but probably wrong. fields: name, stages (2 or more, each with a UUID key), goalKey (a stage key after the first stage), offRamps, and optionally eventRules (the example shows one). The actionId of a step is one of the ids from playbook actions. Field rules: audienti skills read playbook-design references/fields.md Your own reply labels: add customLabels [{"name":"asked_pricing","meaning":"They ask what it costs."}] and a rule with when custom.asked_pricing; a label with no rule opens a decision. Read them with audienti intent list --playbook ID.',
|
|
20
|
+
example: {
|
|
21
|
+
requestId: 'UUID',
|
|
22
|
+
expectedRevision: 0,
|
|
23
|
+
fields: {
|
|
24
|
+
name: 'Email only',
|
|
25
|
+
stages: [
|
|
26
|
+
{ key: 'STAGE_A_UUID', name: 'Ready', owner: 'Agent', goalDays: 3, steps: [{ key: 'UUID', channel: 'Email', actionId: 'email.send', action: 'Email 1', condition: 'Always', instructions: 'Three sentences.', wait: { until: 'reply', days: 4 } }], completion: { kind: 'next' } },
|
|
27
|
+
{ key: 'STAGE_B_UUID', name: 'Meeting booked', on: ['meeting'], owner: 'Assignee', goalDays: 0, steps: [], completion: { kind: 'stay' } }
|
|
28
|
+
],
|
|
29
|
+
goalKey: 'STAGE_B_UUID',
|
|
30
|
+
offRamps: [{ key: 'UUID', name: 'Unresponsive', reason: 'unresponsive' }],
|
|
31
|
+
eventRules: [{ key: 'UUID', when: 'reply.interested', do: ['stop_wait'], goto: { kind: 'stage', stageKey: 'STAGE_B_UUID' } }]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
archive: {
|
|
36
|
+
usage: 'playbook archive PLAYBOOK_ID [--workspace ID] [--profile PATH]',
|
|
37
|
+
about: 'Hide a playbook from the list and the campaign picker. It cannot be edited or newly chosen for a campaign until you restore it. Campaigns that already use it keep running on their version. Safe to repeat. Starters can be archived.'
|
|
38
|
+
},
|
|
39
|
+
restore: {
|
|
40
|
+
usage: 'playbook restore PLAYBOOK_ID [--workspace ID] [--profile PATH]',
|
|
41
|
+
about: 'Bring an archived playbook back. Find archived playbooks with playbook list --archived. Safe to repeat.'
|
|
42
|
+
},
|
|
43
|
+
draft_show: {
|
|
44
|
+
usage: 'playbook draft show PLAYBOOK_ID [--workspace ID] [--profile PATH]',
|
|
45
|
+
about: 'Your draft of a playbook: {draft: {baseRevision, fields, updatedAt, savedBy}} (savedBy is person or agent), or {draft: null} when there is none. The draft is the unsaved work in the playbook editor. It belongs to one person; an agent reads and changes the draft of the person who owns the agent. A campaign cannot use a draft, and playbook show and playbook save never touch it.'
|
|
46
|
+
},
|
|
47
|
+
draft_save: {
|
|
48
|
+
usage: 'playbook draft save PLAYBOOK_ID --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
49
|
+
about: 'Keep your edits as the draft. The file is {"baseRevision": N, "expectedUpdatedAt": T, "fields": {...}}. expectedUpdatedAt is the updatedAt that playbook draft show returned, or null when it returned {draft: null}. If the draft changed since (another tab, your agent or the person), nothing is replaced: you get draft_conflict with currentUpdatedAt. Run playbook draft show, combine the changes, and save again. baseRevision is the version the edits start from: currentRevision from playbook show, or baseRevision from playbook draft show. A lower baseRevision than the current version is kept; publish checks it and refuses a draft that started from an older version (revision_conflict). The draft is replaced as a whole, so send every field (take them from playbook draft show, or playbook show when there is no draft). It never makes a version. The person sees the change in the editor within a few seconds. Fields follow the same rules as playbook save. The answer carries check: the problems and warnings a publish would see. They never stop a draft save. The draft is not fully checked until publish. Starter and archived playbooks are refused.'
|
|
50
|
+
},
|
|
51
|
+
draft_discard: {
|
|
52
|
+
usage: 'playbook draft discard PLAYBOOK_ID [--expected-updated-at T] [--workspace ID] [--profile PATH]',
|
|
53
|
+
about: 'Throw your draft away. The saved versions stay. Safe to repeat: discarded says whether a draft existed. With --expected-updated-at T (the updatedAt from playbook draft show), a draft saved since is kept and you get draft_conflict with currentUpdatedAt.'
|
|
54
|
+
},
|
|
55
|
+
draft_publish: {
|
|
56
|
+
usage: 'playbook draft publish PLAYBOOK_ID [--request-id UUID] [--workspace ID] [--profile PATH]',
|
|
57
|
+
about: 'Save your draft as the next version and remove the draft. It returns the new version, as playbook save does. If a newer version was saved after the draft started, nothing is saved: you get revision_conflict with currentRevision. Run playbook draft show, then playbook draft save with baseRevision set to currentRevision and your changes applied to the newer version, then publish again. No draft gives no_draft. Without --request-id a fresh one is made and printed to stderr. To retry a call that may have worked, pass the same --request-id: a repeat of a finished publish returns the same version. If a draft was saved while this ran you get draft_conflict: read it with playbook draft show and publish again.'
|
|
58
|
+
},
|
|
59
|
+
delete: {
|
|
60
|
+
usage: 'playbook delete PLAYBOOK_ID [--workspace ID] [--profile PATH]',
|
|
61
|
+
about: 'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses, in any state, is refused with playbook_in_use and the campaigns are named; archive it instead. Starters cannot be deleted.'
|
|
62
|
+
},
|
|
63
|
+
progress: {
|
|
64
|
+
usage: 'playbook progress PARTICIPATION_ID [--workspace ID] [--profile PATH]',
|
|
65
|
+
about: 'The stage and outcome history of one person in one campaign. It takes a participation ID, not a playbook ID. A participation exists once a person is admitted to a campaign. Each person in the output of pipeline show has a participationId.'
|
|
66
|
+
},
|
|
67
|
+
record: {
|
|
68
|
+
usage: 'playbook record --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
69
|
+
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}).'
|
|
70
|
+
},
|
|
71
|
+
event: {
|
|
72
|
+
usage: 'playbook event PARTICIPATION_ID --kind KIND --reason TEXT [--evidence conversation:ID|observation:ID ...] [--at ISO] [--request-id UUID] [--file REQUEST.json] [--workspace ID] [--profile PATH]',
|
|
73
|
+
about: 'Record one event for a person, then let the playbook settle. KIND is meeting_requested (a meeting was asked for), meeting_accepted (agreed to, no time yet), meeting_declined, meeting_booked (a meeting has a time), not_a_fit (the person is not a fit) or steps_done (the steps of the current stage are done). A stage that lists the event as an entry event takes the person forward; the playbook rules decide the rest. not_a_fit ends the person in the not_fit off-ramp when the playbook has one, and otherwise only shows in the history. steps_done applies the current stage completion (next stage, stay or off-ramp). REASON is 1 to 300 characters and shows in the history. --evidence names stored evidence about this person: conversation:ID is a message id (the conversation ids in the person timeline, audienti prospects timeline), observation:ID a profile observation id. Repeat it for each piece, up to 20. --at is an ISO time with a timezone offset, for example 2026-11-06T14:30:00Z. Left out, the time is now; a future time counts as now, and a time before the person entered the stage counts as just after it. --request-id is made for you when left out; pass the same UUID to retry safely. Only the person\'s assignee or a workspace Admin may record. A repeat writes nothing and answers recorded false. A person who has left the campaign is refused with participation_ended, with two exceptions: meeting_booked is still recorded, and a repeat of meeting_booked or not_a_fit answers recorded false with the eventId already recorded.'
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const lines = Object.values(playbookCommands).map(command => command.usage);
|
|
77
|
+
export const playbookUsageError = `Unknown or missing playbook subcommand. Usage:\n${lines.map(line => ` audienti ${line}`).join('\n')}\nRun audienti playbook SUBCOMMAND --help for the request shape.`;
|
|
78
|
+
/** Help for `audienti playbook [SUBCOMMAND] --help`. */
|
|
79
|
+
export function playbookHelp(subcommand) {
|
|
80
|
+
const names = playbookCommands[subcommand] ? [subcommand] : subcommand === 'draft' ? Object.keys(playbookCommands).filter(name => name.startsWith('draft_')) : Object.keys(playbookCommands);
|
|
81
|
+
const text = names.map(name => {
|
|
82
|
+
const command = playbookCommands[name];
|
|
83
|
+
return `audienti ${command.usage}\n ${command.about}${command.example ? `\n\nExample REQUEST.json (replace each UUID with a fresh one; reuse the same UUID where a key repeats):\n${JSON.stringify(command.example, null, 2)}` : ''}`;
|
|
84
|
+
}).join('\n\n');
|
|
85
|
+
const conflict = names.includes('save') ? ' A revision_conflict from save also gives currentRevision.' : '';
|
|
86
|
+
return `${text}\n\nErrors are JSON on stderr: {"error": {"code", "message"}}, and the exit code is 1.${conflict} Errors from the server also give status, and when there is a next step they give next (what to do, or what to tell your user), field (the path of the wrong value) and allowed (the values that would work).`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
const uuid=z.string().uuid(),scope={workspaceId:uuid.optional()};
|
|
3
|
+
const label=z.string().trim().min(1).max(120).refine(value=>!/[\u0000-\u001f\u007f]/.test(value),'Control characters are not allowed.');
|
|
4
|
+
const text=z.string().trim().max(5000);
|
|
5
|
+
const identity=z.object({key:z.string().uuid(),name:label}).strict();
|
|
6
|
+
const routeTarget=z.discriminatedUnion('kind',[z.object({kind:z.literal('stage'),stageKey:z.string().uuid()}).strict(),z.object({kind:z.literal('off_ramp'),offRampKey:z.string().uuid()}).strict(),z.object({kind:z.literal('step'),stepKey:z.string().uuid()}).strict()]);
|
|
7
|
+
// An event rule (#350 T3): what happens when an event reaches a person. The server checks the event and action ids against the outreach rulebook.
|
|
8
|
+
const customLabel=z.object({name:z.string().regex(/^[a-z][a-z0-9_]{1,39}$/).describe('Lower case letters, digits and underscores, 2 to 40 characters, starting with a letter. It cannot be the name of a core label (run intent_labels_list).'),direction:z.enum(['inbound']).optional().describe('Only inbound: a message the person sent. Default inbound.'),meaning:z.string().trim().min(1).max(160).describe('One line: what a message with this label says.')}).strict();
|
|
9
|
+
const eventRule=z.object({key:z.string().uuid(),when:z.string().min(1).max(60).describe('An event id from the outreach rulebook, for example accepted, replied, wait_timeout or reply.out_of_office, or custom.<name> for a label this playbook declares in customLabels.'),do:z.array(z.enum(['stop_wait','snooze','redirect_to_named_person','ask_who_is_right','tell_me','hand_to_user','withdraw_pending_request'])).max(7).optional(),snoozeDays:z.number().int().min(1).max(90).optional(),goto:z.discriminatedUnion('kind',[z.object({kind:z.literal('stay')}).strict(),z.object({kind:z.literal('gate')}).strict(),...routeTarget.options]).optional()}).strict();
|
|
10
|
+
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.");
|
|
11
|
+
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();
|
|
12
|
+
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()]);
|
|
13
|
+
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_requested','meeting_accepted','meeting_declined','meeting'])).max(6).optional().describe('The events that jump a person forward to this stage: accept, reply, meeting_requested (a meeting was asked for), meeting_accepted (agreed to, no time yet), meeting_declined, 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()});
|
|
14
|
+
// 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.
|
|
15
|
+
// 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.
|
|
16
|
+
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)));};
|
|
17
|
+
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')};};
|
|
18
|
+
const stage=z.preprocess(dropRetiredStage,stageFields);
|
|
19
|
+
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());
|
|
20
|
+
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(),customLabels:z.array(customLabel).max(20).optional().describe("Labels of this playbook's own, for what a reply means that the core labels do not say. Each has a name, a direction and a one-line meaning. A rule names one as its event, custom.<name>; an agent records one with intent_record as custom.<name>. A label no rule names opens a decision for the person.")}).strict();
|
|
21
|
+
const fields=playbookFieldsSchema;
|
|
22
|
+
const base=a=>`/workspaces/${a.workspaceId}`,query=(a,skip=[])=>new URLSearchParams(Object.entries(a).filter(([k,v])=>k!=='workspaceId'&&!skip.includes(k)&&v!==undefined).map(([k,v])=>[k,String(v)]));
|
|
23
|
+
export const playbookOperations={
|
|
24
|
+
playbook_list:{schema:z.object({...scope,cursor:uuid.optional().describe('The nextCursor of the previous page.'),limit:z.number().int().min(1).max(100).optional().describe('Playbooks per page. Default 50.'),archived:z.boolean().optional().describe('true lists the archived playbooks instead of the active ones. Default false.')}).strict(),method:'GET',path:a=>`${base(a)}/playbooks?${query(a)}`,scoped:true,description:'List the playbooks in the current workspace, one page of summaries (id, name, currentRevision, system, systemKey, archivedAt, stages, steps, check {errors, warnings}, recommended, updatedAt, updatedBy, reviewPending) with nextCursor. updatedBy names who saved the current version (kind person or agent, name; null on a starter playbook). reviewPending is true while a playbook saved by an agent, which no campaign uses, waits for a person to open it (it is also one card in Operator). Archived playbooks are left out unless archived is true. Read the fields of one with playbook_get. Starter playbooks (Email first, LinkedIn first, Email only) carry system:true and are read-only; clone one by saving its fields without an id. recommended marks the starter playbook that fits the channels your agents reported, and recommendation gives its reason in one line. Free CRM read.'},
|
|
25
|
+
playbook_actions_list:{schema:z.object({...scope}).strict(),method:'GET',path:a=>`${base(a)}/playbook-actions`,scoped:true,description:'List what a playbook step may name: the step actions (id, channel, label), the step channels and the event-rule names. Use it before playbook_save when a save names an actionId, a channel or an event the server does not know. Free CRM read.'},
|
|
26
|
+
playbook_get:{schema:z.object({...scope,id:uuid,revision:z.number().int().positive().optional()}).strict(),method:'GET',path:a=>`${base(a)}/playbooks/${a.id}?${query(a,['id'])}`,scoped:true,description:'Read a playbook version (updatedBy says who saved it, createdAt when; a person reading the current version clears reviewPending): stage plays, step waits, completion, goal, voice and agent rules. Instructions are executed by the external agent, never by a hosted sequence runner. warnings is the Playbook check (level error or warning, message, stageKey, stepKey, fix): the same list the editor and the campaign picker show.'},
|
|
27
|
+
playbook_archive:{schema:z.object({...scope,id:uuid}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/archive`,scoped:true,description:'Archive a playbook: it leaves the list and the campaign picker, and cannot be edited or newly chosen for a campaign until restored. Campaigns that already use it keep running on the version they were set up with. Safe to repeat. Starter playbooks can be archived.'},
|
|
28
|
+
playbook_restore:{schema:z.object({...scope,id:uuid}).strict(),method:'POST',path:a=>`${base(a)}/playbooks/${a.id}/restore`,scoped:true,description:'Bring an archived playbook back to the list and the campaign picker. Safe to repeat.'},
|
|
29
|
+
playbook_delete:{schema:z.object({...scope,id:uuid}).strict(),method:'DELETE',path:a=>`${base(a)}/playbooks/${a.id}`,scoped:true,description:'Delete a playbook and all its versions. This cannot be undone. A playbook that any campaign uses (a campaign of any state, finished or archived included, that was ever set up with a version of it) is refused with 409 playbook_in_use, naming the campaigns; archive it instead. Starter playbooks are refused with starter_playbook.'},
|
|
30
|
+
playbook_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative().describe('0 for a new playbook, otherwise currentRevision from playbook_get. A stale value is refused with revision_conflict, which carries currentRevision.'),fields}).strict(),method:'POST',path:a=>`${base(a)}/playbooks`,body:({workspaceId,...a})=>a,scoped:true,description:'Create or revise a playbook with optimistic version and idempotent request. A starter playbook refuses revision (409 starter_playbook); save its fields without an id to make an editable copy. Does not change existing campaign or participation pins. A playbook that cannot work (no off-ramp, or a stage nobody can reach) is refused with 422 playbook_cannot_work and the list in errors. A saved playbook returns warnings, the Playbook check, for what is legal but probably wrong. The save is listed in the audit log (category playbooks) with who made it; a save by an agent of a playbook no campaign uses yet marks it reviewPending until a person opens it. A step with channel Custom is done by a person: put what the person does in instructions (for example call, leave a voicemail, handwrite a note, record a video, send a gift); your agent prepares the script, text or choice and the person records the outcome (hand_task_* commands). A Custom step can wait for a named event with wait {until: event, event: name, days}.'},
|
|
31
|
+
playbook_draft_get:{schema:z.object({...scope,id:uuid}).strict(),method:'GET',path:a=>`${base(a)}/playbooks/${a.id}/draft`,scoped:true,description:'Read your draft of a playbook: {draft: {baseRevision, fields, updatedAt, savedBy}} or {draft: null}. savedBy is person or agent. The draft is the unsaved work in the playbook editor. It belongs to one person, and an agent reads and changes the draft of the person who owns the agent. No campaign can use a draft. playbook_get and playbook_save never touch it.'},
|
|
32
|
+
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).'},
|
|
33
|
+
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).'},
|
|
34
|
+
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.'},
|
|
35
|
+
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.'},
|
|
36
|
+
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.'},
|
|
37
|
+
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.'},
|
|
38
|
+
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.'},
|
|
39
|
+
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.'}
|
|
40
|
+
};
|
package/cli/progress.mjs
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
1
2
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
2
3
|
import {readJsonFile} from './json-file.mjs';
|
|
4
|
+
import {playbookUsageError,playbookCommands} from './playbook-help.mjs';
|
|
3
5
|
export async function progressCli(args){
|
|
4
|
-
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','
|
|
5
|
-
const names={
|
|
6
|
-
|
|
6
|
+
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','request-id','expected-updated-at','reason','evidence','at'].includes(key)||(key!=='archived'&&(!args[i+1]||args[i+1].startsWith('--')))||(key!=='evidence'&&Object.hasOwn(flags,key)))throw new Error(`Unknown or incomplete option --${key}.${args[0]==='playbook'?' Run audienti playbook SUBCOMMAND --help.':''}`);if(key==='evidence'){(flags.evidence??=[]).push(args[++i]);continue;}flags[key]=key==='archived'&&(!args[i+1]||args[i+1].startsWith('--'))?'true':args[++i];}
|
|
7
|
+
const names={playbook:{list:'playbook_list',actions:'playbook_actions_list',show:'playbook_get',save:'playbook_save',archive:'playbook_archive',restore:'playbook_restore',draft_show:'playbook_draft_get',draft_save:'playbook_draft_save',draft_discard:'playbook_draft_discard',draft_publish:'playbook_draft_publish',delete:'playbook_delete',progress:'playbook_progress_get',record:'playbook_progress_record',event:'playbook_event_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'}};
|
|
8
|
+
// playbook draft show|save|discard|publish ID is one command name in two words.
|
|
9
|
+
if(positions[0]==='playbook'&&positions[1]==='draft'&&['show','save','discard','publish'].includes(positions[2]))positions.splice(1,2,`draft_${positions[2]}`);
|
|
10
|
+
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.');
|
|
7
11
|
if(flags.format&&(name!=='workspace_analytics_get'||!['json','csv'].includes(flags.format)))throw new Error('--format json|csv is only accepted for analytics show.');
|
|
8
|
-
const fileNames=['
|
|
9
|
-
if(fileNames.includes(name)&&!flags.file)throw new Error('Supply a JSON request with --file PATH.');if(!
|
|
10
|
-
const input=
|
|
11
|
-
|
|
12
|
+
const fileNames=['playbook_save','playbook_draft_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','playbook_draft_get','playbook_draft_save','playbook_draft_discard','playbook_draft_publish','playbook_event_record'].includes(name);if(positions.length!==(withId?3:2))throw new Error(positions[0]==='playbook'?`Unexpected arguments. Usage: audienti ${playbookCommands[positions[1]].usage}`:'Unexpected arguments.');
|
|
13
|
+
const takesFile=fileNames.includes(name)||name==='playbook_event_record';if(fileNames.includes(name)&&!flags.file)throw new Error('Supply a JSON request with --file PATH.');if(!takesFile&&flags.file)throw new Error('--file is only accepted for mutations.');
|
|
14
|
+
const input=takesFile&&flags.file?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[['playbook_progress_get','playbook_event_record'].includes(name)?'participationId':'id']=positions[2];
|
|
15
|
+
if(flags['request-id']!==undefined&&!['playbook_draft_publish','playbook_event_record'].includes(name))throw new Error('--request-id is only accepted for playbook draft publish and playbook event.');
|
|
16
|
+
if(name==='playbook_event_record')input.requestId=flags['request-id']??input.requestId??randomUUID();else if(flags.reason!==undefined||flags.evidence!==undefined||flags.at!==undefined)throw new Error('--reason, --evidence and --at are only accepted for playbook event.');
|
|
17
|
+
if(flags['expected-updated-at']!==undefined&&name!=='playbook_draft_discard')throw new Error('--expected-updated-at is only accepted for playbook draft discard.');
|
|
18
|
+
if(flags['expected-updated-at']!==undefined)input.expectedUpdatedAt=flags['expected-updated-at'];
|
|
19
|
+
// A generated request ID is printed, so a retry of a call that may have worked can use the same one.
|
|
20
|
+
if(name==='playbook_draft_publish'){input.requestId=flags['request-id']??randomUUID();if(flags['request-id']===undefined)console.error(`requestId ${input.requestId} (retry with --request-id ${input.requestId} if this call may have worked)`);}
|
|
21
|
+
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.');
|
|
22
|
+
for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format','request-id','expected-updated-at'].includes(key))continue;if(key==='evidence'){input.evidence=value.map(item=>{const [kind,id,...extra]=item.split(':');if(extra.length||!['observation','conversation'].includes(kind)||!id)throw new Error('--evidence takes conversation:ID or observation:ID.');return{kind,id};});continue;}if(key==='archived'){input.archived=value==='true';continue;}const field={campaign:'campaignId','playbook-version':'pinId',user:'userId',owner:'ownerUserId',offer:'offerId',audience:'audienceId'}[key]??key;input[field]=['limit','revision'].includes(key)?Number(value):value;}
|
|
12
23
|
const result=await callWorkspace(name,input,configuration(flags.profile));
|
|
13
24
|
if(flags.format==='csv'){process.stdout.write(analyticsCsv(result?.data??result));return;}
|
|
14
25
|
console.log(JSON.stringify(result));
|
|
15
26
|
}
|
|
16
27
|
|
|
17
|
-
/**
|
|
28
|
+
/** Daily analytics rows plus a total row. Untracked outcome values are left blank. */
|
|
18
29
|
export function analyticsCsv(data){
|
|
19
30
|
if(!data||!Array.isArray(data.daily)||!data.total)throw new Error('Analytics response has no daily rows.');
|
|
20
31
|
const cell=value=>{const text=String(value);return /[",\n]/.test(text)?`"${text.replace(/"/g,'""')}"`:text;};
|
|
@@ -9,7 +9,7 @@ const query=a=>{const q=new URLSearchParams(Object.entries(a).map(([k,v])=>[k,St
|
|
|
9
9
|
export const prospectingSettingsOperations={
|
|
10
10
|
prospecting_settings_get:{schema:z.object({}).strict(),method:'GET',path:()=>'/prospecting-policy'},
|
|
11
11
|
prospecting_settings_set:{schema:z.object({...write,schedule,emailLimits:limits,linkedinLimits:limits}).strict(),method:'POST',path:()=>'/prospecting-policy',ownerOnly:true},
|
|
12
|
-
prospecting_account_settings_get:{schema:z.object(account).strict(),method:'GET',path:a=>'/prospecting-policy/account'+query(a)},
|
|
12
|
+
prospecting_account_settings_get:{description:'Read one connected account\'s hours and limits, and its sending window now: window.open, window.reason (open, no_hours, outside_hours or working_hours_closed), window.timezone and window.nextOpensAt (null while open). A send outside the window waits and goes when it opens; replies are never held.',schema:z.object(account).strict(),method:'GET',path:a=>'/prospecting-policy/account'+query(a)},
|
|
13
13
|
prospecting_account_settings_set:{schema:z.object({...write,...account,schedule:schedule.nullable(),limits:limits.nullable()}).strict(),method:'POST',path:()=>'/prospecting-policy/account',ownerOnly:true},
|
|
14
14
|
prospecting_settings_history:{schema:z.object(page).strict(),method:'GET',path:a=>'/prospecting-policy/events'+query(a),ownerOnly:true},
|
|
15
15
|
prospecting_accounts_list:{schema:z.object(page).strict(),method:'GET',path:a=>'/prospecting-policy/accounts'+query(a)},
|
|
@@ -2,11 +2,11 @@ import {z} from 'zod';
|
|
|
2
2
|
const uuid=z.string().uuid(),workspaceId=uuid.optional();
|
|
3
3
|
const base=a=>`/workspaces/${a.workspaceId}/questions`;
|
|
4
4
|
const text=max=>z.string().trim().min(1).max(max);
|
|
5
|
-
const description='Questions for your
|
|
5
|
+
const description='Questions for your user. Ask here, not in chat: the question shows as a card in the Operator queue, your user answers there, and the answer comes back to you as a "Read your user’s answer" task in your queue. An answer never grants send authority.';
|
|
6
6
|
export const questionOperations={
|
|
7
|
-
agent_question_ask:{schema:z.object({workspaceId,question:text(1000),context:z.string().trim().max(2000).optional(),options:z.array(text(120)).max(6).optional(),campaignId:uuid.optional(),taskId:uuid.optional()}).strict(),method:'POST',path:a=>base(a),body:({workspaceId:_,...rest})=>rest,scoped:true,description:`${description} Ask one clear question with the context your
|
|
8
|
-
agent_question_list:{schema:z.object({workspaceId,state:z.enum(['open','answered','withdrawn','all']).optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>{const q=new URLSearchParams();if(a.state)q.set('state',a.state);if(a.limit)q.set('limit',String(a.limit));const s=q.toString();return `${base(a)}${s?`?${s}`:''}`},scoped:true,description:`${description} List questions. Agents see only their own; open questions come oldest first.`},
|
|
7
|
+
agent_question_ask:{schema:z.object({workspaceId,question:text(1000),context:z.string().trim().max(2000).optional(),options:z.array(text(120)).max(6).optional(),campaignId:uuid.optional(),taskId:uuid.optional(),topic:text(64).optional()}).strict(),method:'POST',path:a=>base(a),body:({workspaceId:_,...rest})=>rest,scoped:true,description:`${description} Ask one clear question with the context your user needs and up to 6 suggested answers. Link the campaign or queue task it is about. Give a topic (first_campaign, campaign:<id>, tools) so a newer question on the same topic replaces the older one. Tell your user in chat that the question is in Operator. Keep working on what you can while you wait.`},
|
|
8
|
+
agent_question_list:{schema:z.object({workspaceId,state:z.enum(['open','answered','withdrawn','superseded','closed','all']).optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>{const q=new URLSearchParams();if(a.state)q.set('state',a.state);if(a.limit)q.set('limit',String(a.limit));const s=q.toString();return `${base(a)}${s?`?${s}`:''}`},scoped:true,description:`${description} List questions. Agents see only their own; open questions come oldest first.`},
|
|
9
9
|
agent_question_get:{schema:z.object({workspaceId,questionId:uuid}).strict(),method:'GET',path:a=>`${base(a)}/${a.questionId}`,scoped:true,description:`${description} Read one question and its answer. Reading an answer marks it read.`},
|
|
10
10
|
agent_question_withdraw:{schema:z.object({workspaceId,questionId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/${a.questionId}/withdraw`,body:()=>({}),scoped:true,description:`${description} Withdraw an open question you asked, for example once you found the answer yourself.`},
|
|
11
|
-
agent_question_answer:{schema:z.object({workspaceId,questionId:uuid,answer:text(2000)}).strict(),method:'POST',path:a=>`${base(a)}/${a.questionId}/answer`,body:a=>({answer:a.answer}),scoped:true,description:'Answer an agent’s question as the
|
|
11
|
+
agent_question_answer:{schema:z.object({workspaceId,questionId:uuid,answer:text(2000)}).strict(),method:'POST',path:a=>`${base(a)}/${a.questionId}/answer`,body:a=>({answer:a.answer}),scoped:true,description:'Answer an agent’s question as the user (user API token only; agents cannot answer). The answer goes back to the agent that asked as a task.'},
|
|
12
12
|
};
|
package/cli/questions.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
2
|
-
const askHelp='Use ask "QUESTION" [--context TEXT] [--option TEXT]... [--campaign ID] [--task ID].';
|
|
3
|
-
const help='Use questions [list] [--state open|answered|withdrawn|all] | show ID | withdraw ID | answer ID --answer TEXT.';
|
|
2
|
+
const askHelp='Use ask "QUESTION" [--context TEXT] [--option TEXT]... [--campaign ID] [--task ID] [--topic KEY]. A newer open question with the same --topic replaces the older one.';
|
|
3
|
+
const help='Use questions [list] [--state open|answered|withdrawn|superseded|all] | show ID | withdraw ID | answer ID --answer TEXT.';
|
|
4
4
|
function parse(args,allowed,usage){
|
|
5
5
|
const position=[],flags={},options=[];
|
|
6
6
|
for(let i=1;i<args.length;i++){
|
|
@@ -14,9 +14,9 @@ function parse(args,allowed,usage){
|
|
|
14
14
|
return {position,flags,options};
|
|
15
15
|
}
|
|
16
16
|
export async function askCli(args){
|
|
17
|
-
const {position,flags,options}=parse(args,['profile','workspace','context','option','campaign','task'],askHelp);
|
|
17
|
+
const {position,flags,options}=parse(args,['profile','workspace','context','option','campaign','task','topic'],askHelp);
|
|
18
18
|
if(position.length!==1||!position[0].trim())throw new Error(askHelp);
|
|
19
|
-
const input={workspaceId:flags.workspace,question:position[0],...(flags.context?{context:flags.context}:{}),...(options.length?{options}:{}),...(flags.campaign?{campaignId:flags.campaign}:{}),...(flags.task?{taskId:flags.task}:{})};
|
|
19
|
+
const input={workspaceId:flags.workspace,question:position[0],...(flags.context?{context:flags.context}:{}),...(options.length?{options}:{}),...(flags.campaign?{campaignId:flags.campaign}:{}),...(flags.topic?{topic:flags.topic}:{}),...(flags.task?{taskId:flags.task}:{})};
|
|
20
20
|
console.log(JSON.stringify((await callWorkspace('agent_question_ask',input,configuration(flags.profile))).data));
|
|
21
21
|
}
|
|
22
22
|
export async function questionsCli(args){
|
package/cli/queue-operations.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {z} from 'zod';
|
|
2
2
|
const uuid=z.string().uuid(),workspaceId=uuid.optional();
|
|
3
|
-
|
|
3
|
+
// #333: an agent's queue covers every workspace its user approved it in; workspaceId narrows it to one.
|
|
4
|
+
const base=a=>a.workspaceId?`/workspaces/${a.workspaceId}/agent-queue`:'/agent-queue';
|
|
4
5
|
const note=z.string().trim().max(2000).optional();
|
|
5
|
-
const description='Agent work queue: what Audienti says is due in
|
|
6
|
+
const description='Agent work queue: what Audienti says is due in every workspace your user approved you in. Leave workspaceId out to cover them all; each task names its workspace. Claim one task at a time; a claim stops other agents in your fleet from doing the same work. Tasks never grant send authority.';
|
|
6
7
|
export const queueOperations={
|
|
7
|
-
agent_queue_list:{schema:z.object({workspaceId}).strict(),method:'GET',path:a=>base(a),
|
|
8
|
-
agent_queue_claim:{schema:z.object({workspaceId,taskId:uuid,leaseMinutes:z.number().int().min(5).max(240).optional()}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/claim`,body:a=>a.leaseMinutes===undefined?{}:{leaseMinutes:a.leaseMinutes},
|
|
9
|
-
agent_queue_complete:{schema:z.object({workspaceId,taskId:uuid,outcome:z.enum(['done','skipped']).optional(),note}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/complete`,body:a=>({outcome:a.outcome??'done',...(a.note?{note:a.note}:{})}),
|
|
10
|
-
agent_queue_release:{schema:z.object({workspaceId,taskId:uuid,note}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/release`,body:a=>a.note?{note:a.note}:{},
|
|
8
|
+
agent_queue_list:{schema:z.object({workspaceId}).strict(),method:'GET',path:a=>base(a),agentWide:true,description:`${description} Read this first after you connect and every time you finish a task. Returns orientation (what Audienti is and how to work), your status, next (the task to do now), workspaceStep (the workspace to run the task's commands in), announce (a line to say to the user in chat when next is a hand-off), holding (tasks you claimed), available (tasks you can take), fleet (tasks other agents hold), workspaces (the workspaces this read covered), pollAfterSeconds and nextStep (an instruction to schedule the next read).`},
|
|
9
|
+
agent_queue_claim:{schema:z.object({workspaceId,taskId:uuid,leaseMinutes:z.number().int().min(5).max(240).optional(),note}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/claim`,body:a=>({...(a.leaseMinutes===undefined?{}:{leaseMinutes:a.leaseMinutes}),...(a.note?{note:a.note}:{})}),agentWide:true,description:`${description} Claim a task before you start it. The lease lasts 30 minutes unless you ask for more; claim again to extend it. On a long task, claim again every 20 to 30 minutes with a note that says where you are: your user sees it, and the next agent continues from it if your lease lapses. Error task_taken means another agent holds it: pick another task.`},
|
|
10
|
+
agent_queue_complete:{schema:z.object({workspaceId,taskId:uuid,outcome:z.enum(['done','skipped']).optional(),note}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/complete`,body:a=>({outcome:a.outcome??'done',...(a.note?{note:a.note}:{})}),agentWide:true,description:`${description} Report a task you hold as done, or as skipped with a note that says why. Say what you did in note (for example how many prospects you admitted).`},
|
|
11
|
+
agent_queue_release:{schema:z.object({workspaceId,taskId:uuid,note}).strict(),method:'POST',path:a=>`${base(a)}/${a.taskId}/release`,body:a=>a.note?{note:a.note}:{},agentWide:true,description:`${description} Give a task back so another agent can take it. Say why in note.`},
|
|
11
12
|
};
|
package/cli/queue.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
2
|
-
const help='Use queue [list] | next | claim ID [--lease MINUTES] | done ID [--note TEXT] | skip ID --note TEXT | release ID [--note TEXT].';
|
|
2
|
+
const help='Use queue [list] | next | claim ID [--lease MINUTES] [--note TEXT] | done ID [--note TEXT] | skip ID --note TEXT | release ID [--note TEXT]. An agent reads every workspace its user approved it in; --workspace ID narrows to one.';
|
|
3
3
|
export async function queueCli(args){
|
|
4
4
|
const position=[],flags={};for(let i=1;i<args.length;i++){
|
|
5
5
|
if(!args[i].startsWith('--')){position.push(args[i]);continue}
|
|
@@ -11,10 +11,11 @@ export async function queueCli(args){
|
|
|
11
11
|
if(['list','next'].includes(command)){
|
|
12
12
|
if(position.length>1)throw new Error(help);
|
|
13
13
|
const queue=await call('agent_queue_list',{});
|
|
14
|
-
|
|
14
|
+
// Without --workspace an agent reads every workspace its user approved it in (#333); each task names its workspace.
|
|
15
|
+
console.log(JSON.stringify(command==='next'?{orientation:queue.orientation,you:queue.you,...(queue.workspaces?{workspaces:queue.workspaces}:{}),...(queue.notChecked?{notChecked:queue.notChecked}:{}),holding:queue.holding,next:queue.next,...(queue.workspaceStep!==undefined?{workspaceStep:queue.workspaceStep}:{}),announce:queue.announce??null,fleet:queue.fleet,needsOtherTools:queue.needsOtherTools,pollAfterSeconds:queue.pollAfterSeconds,nextStep:queue.nextStep}:queue));return;
|
|
15
16
|
}
|
|
16
17
|
if(!id||position.length!==2)throw new Error(help);
|
|
17
|
-
if(command==='claim'){console.log(JSON.stringify(await call('agent_queue_claim',{taskId:id,...(flags.lease?{leaseMinutes:Number(flags.lease)}:{})})));return;}
|
|
18
|
+
if(command==='claim'){console.log(JSON.stringify(await call('agent_queue_claim',{taskId:id,...(flags.lease?{leaseMinutes:Number(flags.lease)}:{}),...(flags.note?{note:flags.note}:{})})));return;}
|
|
18
19
|
if(command==='done'||command==='skip'){
|
|
19
20
|
if(command==='skip'&&!flags.note)throw new Error('Say why you skipped the task: --note TEXT.');
|
|
20
21
|
console.log(JSON.stringify(await call('agent_queue_complete',{taskId:id,outcome:command==='done'?'done':'skipped',...(flags.note?{note:flags.note}:{})})));return;
|
|
@@ -8,6 +8,6 @@ const body = ({workspaceId,prospectId,contactId,...rest}) => rest;
|
|
|
8
8
|
export const recipientOperations = {
|
|
9
9
|
workspace_prospect_contacts_list: { schema: z.object({...located,status:z.enum(['active','retired','all']).optional(),...page}).strict(), method:'GET', path:a=>query(root(a),{status:a.status,cursor:a.cursor,limit:a.limit}), scoped:true },
|
|
10
10
|
workspace_prospect_contact_read: { schema:z.object({...located,contactId:uuid,history:z.object(page).strict().optional()}).strict(), method:'GET', path:a=>query(`${root(a)}/${a.contactId}`,{historyCursor:a.history?.cursor,historyLimit:a.history?.limit}), scoped:true },
|
|
11
|
-
workspace_prospect_contact_record: { schema:z.object({...located,kind:z.enum(['email','linkedin']),value:z.string(),reason:z.string(),provenance:z.object({observationId:uuid}).strict().optional(),requestId:uuid}).strict(), method:'POST', path:root, body, scoped:true },
|
|
11
|
+
workspace_prospect_contact_record: { schema:z.object({...located,kind:z.enum(['email','linkedin','mobile','phone']),value:z.string(),reason:z.string(),provenance:z.object({observationId:uuid}).strict().optional(),requestId:uuid}).strict(), method:'POST', path:root, body, scoped:true },
|
|
12
12
|
workspace_prospect_contact_retire: { schema:z.object({...located,contactId:uuid,expectedVersion:z.number().int().nonnegative(),reason:z.string(),requestId:uuid}).strict(), method:'POST', path:a=>`${root(a)}/${a.contactId}/retire`, body, scoped:true },
|
|
13
13
|
};
|
package/cli/recipients.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
3
3
|
export async function recipientsCli(args) {
|
|
4
4
|
const flags = {}, positions = [];
|
|
5
|
-
const allowed = ['workspace','profile','status','cursor','limit','history-cursor','history-limit','file'];
|
|
5
|
+
const allowed = ['workspace','profile','status','cursor','limit','history-cursor','history-limit','file','request-id'];
|
|
6
6
|
for (let i=0;i<args.length;i++) {
|
|
7
7
|
const arg=args[i]; if(arg==='--json')continue;
|
|
8
8
|
if(!arg.startsWith('--')){positions.push(arg);continue;}
|
|
@@ -12,7 +12,7 @@ export async function recipientsCli(args) {
|
|
|
12
12
|
}
|
|
13
13
|
const command=positions[2];
|
|
14
14
|
if(positions[0]!=='prospects'||positions[1]!=='contacts'||!['list','get','record','retire'].includes(command)||positions.length!==(['get','retire'].includes(command)?5:4))throw Error('Use prospects contacts list|record PERSON or get|retire PERSON CONTACT --workspace ID.');
|
|
15
|
-
const valid=['workspace','profile',...(command==='list'?['status','cursor','limit']:command==='get'?['history-cursor','history-limit']:['file'])];
|
|
15
|
+
const valid=['workspace','profile',...(command==='list'?['status','cursor','limit']:command==='get'?['history-cursor','history-limit']:['file','request-id'])];
|
|
16
16
|
if(Object.keys(flags).some(k=>!valid.includes(k)))throw Error('Option does not apply to this contact command.');
|
|
17
17
|
let input={};
|
|
18
18
|
if(['record','retire'].includes(command)){
|
|
@@ -21,6 +21,10 @@ export async function recipientsCli(args) {
|
|
|
21
21
|
try{input=JSON.parse(raw.toString('utf8'));}catch{throw Error('validation_error');}
|
|
22
22
|
if(!input||typeof input!=='object'||Array.isArray(input)||['workspaceId','prospectId','contactId'].some(k=>Object.hasOwn(input,k)))throw Error('invalid_fields');
|
|
23
23
|
}
|
|
24
|
+
if(flags['request-id']!==undefined){
|
|
25
|
+
if(input.requestId!==undefined&&input.requestId!==flags['request-id'])throw Error('--request-id differs from requestId in the file.');
|
|
26
|
+
input.requestId=flags['request-id'];
|
|
27
|
+
}
|
|
24
28
|
Object.assign(input,{workspaceId:flags.workspace,prospectId:positions[3]});
|
|
25
29
|
if(['get','retire'].includes(command))input.contactId=positions[4];
|
|
26
30
|
if(command==='list')Object.assign(input,{...(flags.status===undefined?{}:{status:flags.status}),...(flags.cursor===undefined?{}:{cursor:flags.cursor}),...(flags.limit===undefined?{}:{limit:Number(flags.limit)})});
|
package/cli/rooms-operations.mjs
CHANGED
|
@@ -11,17 +11,38 @@ const owner='Owner moderation: requires a user credential, not an agent profile.
|
|
|
11
11
|
export const roomsOperations={
|
|
12
12
|
rooms_directory:{schema:z.object({workspaceId,sort:z.enum(['handle','load']).optional()}).strict(),method:'GET',path:a=>`${base(a)}/directory${query(a,['sort'])}`,scoped:true,description:`List your agents with handle, about line, load (pending items and active work claims) and when each is expected to check next. sort load puts the least busy first and stale agents (no check for about 3 usual intervals) last. ${description}`},
|
|
13
13
|
rooms_profile_set:{schema:z.object({workspaceId,agentId:uuid,handle:handle.optional(),about:z.string().max(160).optional()}).strict(),method:'POST',path:a=>`${base(a)}/profiles/${a.agentId}`,body:body(['handle','about']),scoped:true,description:`Set an agent's handle or one-line about. ${owner}`},
|
|
14
|
-
rooms_inbox:{schema:z.object({workspaceId,since:uuid.optional(),about:about.optional(),limit:z.number().int().min(1).max(50).optional()}).strict(),method:'GET',path:a=>`${base(a)}/inbox${query(a,['since','about','limit'])}`,scoped:true,description:`Read your pending inbox items: deadlines first, then oldest. Items stay until acknowledged or expired. Check on your normal schedule; at most 12 checks per 10 minutes. Pass nextSince to get only newer items. Also returns your pinned turnover note (note), if you left one. ${description}`},
|
|
15
|
-
rooms_note_set:{schema:z.object({workspaceId,body:z.string().max(4000)}).strict(),method:'POST',path:a=>`${base(a)}/note`,body:body(['body']),scoped:true,description:'Pin your turnover note for your next run: goal, current state, open items, next action. One note per agent per workspace; a new note replaces the old one and an empty body clears it. rooms_inbox returns it. Your
|
|
14
|
+
rooms_inbox:{schema:z.object({workspaceId,since:uuid.optional(),about:about.optional(),limit:z.number().int().min(1).max(50).optional(),brief:z.boolean().optional()}).strict(),method:'GET',path:a=>`${base(a)}/inbox${query(a,['since','about','limit','brief'])}`,scoped:true,description:`Read your pending inbox items: deadlines first, then oldest. Items stay until acknowledged or expired. Check on your normal schedule; at most 12 checks per 10 minutes. Pass nextSince to get only newer items. Pass brief: true for a 120-character preview and bodyChars per item instead of the body and reference payloads, then read rooms_thread for the items you handle. Also returns your pinned turnover note (note), if you left one. ${description}`},
|
|
15
|
+
rooms_note_set:{schema:z.object({workspaceId,body:z.string().max(4000)}).strict(),method:'POST',path:a=>`${base(a)}/note`,body:body(['body']),scoped:true,description:'Pin your turnover note for your next run: goal, current state, open items, next action. One note per agent per workspace; a new note replaces the old one and an empty body clears it. rooms_inbox returns it. Your user can read it.'},
|
|
16
16
|
rooms_ack:{schema:z.object({workspaceId,ids:z.array(uuid).min(1).max(100)}).strict(),method:'POST',path:a=>`${base(a)}/inbox/ack`,body:body(['ids']),scoped:true,description:'Acknowledge inbox items you have handled. Repeating is harmless.'},
|
|
17
|
-
rooms_list:{schema:z.object({workspaceId,retired:z.boolean().optional()}).strict(),method:'GET',path:a=>`${base(a)}${query(a,['retired'])}`,scoped:true,description:`List active or retired rooms. ${description}`},
|
|
17
|
+
rooms_list:{schema:z.object({workspaceId,retired:z.boolean().optional(),q:z.string().max(80).optional()}).strict(),method:'GET',path:a=>`${base(a)}${query(a,['retired','q'])}`,scoped:true,description:`List active or retired rooms; q keeps rooms with a message or record label containing the text. ${description}`},
|
|
18
|
+
rooms_read:{schema:z.object({workspaceId,room:z.string().min(1).max(64),q:z.string().max(80).optional()}).strict(),method:'GET',path:a=>`${base(a)}/${encodeURIComponent(a.room)}${query(a,['q'])}`,scoped:true,description:`Read one whole room: its newest threads, each with its latest messages, deliveries and open questions; q keeps only threads with a matching message. ${owner} ${description}`},
|
|
18
19
|
rooms_create:{schema:z.object({workspaceId,kind:z.enum(['direct','group']),with:handle.optional(),slug:handle.optional(),title:z.string().max(80).optional(),purpose:z.string().max(280).optional(),members:z.array(handle).max(20).optional()}).strict(),method:'POST',path:base,body:body(['kind','with','slug','title','purpose','members']),scoped:true,description:'Open a direct room with another of your agents (with HANDLE) or create a group room (slug, title, purpose, members).'},
|
|
19
|
-
rooms_send:{schema:z.object({workspaceId,room:z.string().min(1).max(64).optional(),to:handle.optional(),threadId:uuid.optional(),body:z.string().min(1).max(8000),about:z.array(reference).max(10).optional(),answerBy:z.string().datetime().optional(),expiresAt:z.string().datetime().optional(),unable:z.boolean().optional(),requestId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/messages`,body:body(['room','to','threadId','body','about','answerBy','expiresAt','unable','requestId']),scoped:true,description:`Send a message to a room (room) or to another of your agents (to HANDLE). Pass threadId to reply. Set unable with threadId to say you cannot do what the thread asks; body is the reason and your pending items in the thread are acknowledged. Reuse requestId when retrying. Limits: 30 per 10 minutes, 300 per day, 8,000 characters. Sending grants nothing; approvals stay with the
|
|
20
|
+
rooms_send:{schema:z.object({workspaceId,room:z.string().min(1).max(64).optional(),to:handle.optional(),threadId:uuid.optional(),body:z.string().min(1).max(8000),about:z.array(reference).max(10).optional(),answerBy:z.string().datetime().optional(),ifNoAnswer:z.string().min(1).max(1000).optional(),anyOne:z.boolean().optional(),expiresAt:z.string().datetime().optional(),unable:z.boolean().optional(),requestId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/messages`,body:body(['room','to','threadId','body','about','answerBy','ifNoAnswer','anyOne','expiresAt','unable','requestId']),scoped:true,description:`Send a message to a room (room) or to another of your agents (to HANDLE). Pass threadId to reply. Set unable with threadId to say you cannot do what the thread asks; body is the reason and your pending items in the thread are acknowledged. Reuse requestId when retrying. Limits: 30 per 10 minutes, 300 per day, 8,000 characters. Set answerBy for a deadline; with ifNoAnswer your stated fallback is recorded as your own open commitment when no one answers (Exora performs nothing). In a channel, or a direct message with more than two agents, a deadline question names one agent with @handle or sets anyOne so the first to claim it owns it. Sending grants nothing; approvals stay with the user. ${description}`},
|
|
20
21
|
rooms_thread:{schema:z.object({workspaceId,threadId:uuid,after:z.number().int().min(0).optional()}).strict(),method:'GET',path:a=>`${base(a)}/threads/${a.threadId}${query(a,['after'])}`,scoped:true,description:`Read a thread in order. Each message lists deliveries per recipient: pending (probablySeen when the recipient checked its inbox since), acknowledged or expired. ${description}`},
|
|
21
22
|
rooms_threads:{schema:z.object({workspaceId,about}).strict(),method:'GET',path:a=>`${base(a)}/threads${query(a,['about'])}`,scoped:true,description:'List threads that reference a record, for example prospect:UUID.'},
|
|
22
23
|
rooms_retire:{schema:z.object({workspaceId,room:z.string().min(1).max(64)}).strict(),method:'POST',path:a=>`${base(a)}/${encodeURIComponent(a.room)}/retire`,scoped:true,description:'Retire a finished room. Pending items expire; history stays.'},
|
|
23
24
|
rooms_restore:{schema:z.object({workspaceId,room:z.string().min(1).max(64)}).strict(),method:'POST',path:a=>`${base(a)}/${encodeURIComponent(a.room)}/restore`,scoped:true,description:`Restore a retired room. ${owner}`},
|
|
24
|
-
rooms_redact:{schema:z.object({workspaceId,messageId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/messages/${a.messageId}/redact`,scoped:true,description:'Remove the text of your own message and leave a tombstone. The
|
|
25
|
+
rooms_redact:{schema:z.object({workspaceId,messageId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/messages/${a.messageId}/redact`,scoped:true,description:'Remove the text of your own message and leave a tombstone. The user or an Admin may remove any message.'},
|
|
25
26
|
rooms_resume:{schema:z.object({workspaceId,threadId:uuid}).strict(),method:'POST',path:a=>`${base(a)}/threads/${a.threadId}/resume`,scoped:true,description:`Resume a thread paused after 30 agent-only messages. ${owner}`},
|
|
26
27
|
rooms_pause:{schema:z.object({workspaceId,paused:z.boolean()}).strict(),method:'POST',path:a=>`${base(a)}/pause`,body:body(['paused']),scoped:true,description:'Pause or resume all agent sending in the workspace. Workspace Admin user credential only.'},
|
|
27
28
|
};
|
|
29
|
+
|
|
30
|
+
const text=n=>z.string().min(1).max(n),time=z.string().datetime(),place={room:z.string().min(1).max(64).optional(),threadId:uuid.optional()};
|
|
31
|
+
const coord='A coordination record in a thread: a message plus a structured record. It informs and records; Exora performs nothing, and a message is never an approval.';
|
|
32
|
+
const aboutList=z.array(reference).max(10).optional();
|
|
33
|
+
const note=z.object({goal:text(1000),state:text(1000),nextAction:text(1000),openItems:z.array(text(300)).max(10).optional(),risks:z.array(text(300)).max(10).optional()}).strict();
|
|
34
|
+
const post=(path,keys)=>({method:'POST',path,body:body(keys),scoped:true});
|
|
35
|
+
Object.assign(roomsOperations,{
|
|
36
|
+
rooms_decision_request:{schema:z.object({workspaceId,...place,question:text(1000),options:z.array(text(200)).min(2).max(6),deciders:z.array(handle).min(1).max(8),rule:z.enum(['one','any','all','majority']),deadline:time,defaultOption:text(200).optional(),ifNoAnswer:text(1000).optional(),supersedes:uuid.optional(),about:aboutList,requestId:uuid}).strict(),...post(a=>`${base(a)}/decisions`,['room','threadId','question','options','deciders','rule','deadline','defaultOption','ifNoAnswer','supersedes','about','requestId']),description:`Ask named agents (deciders) to choose between 2 to 6 options by a deadline. rule: one or any decider, all, or majority. The request expires at its deadline; a named defaultOption then applies and shows as "Default applied. No one responded", never as agreement, and ifNoAnswer is recorded as your own open commitment. Never name a person: ask your owner with rooms_ask_owner. ${coord}`},
|
|
37
|
+
rooms_decision_respond:{schema:z.object({workspaceId,decisionId:uuid,stance:z.enum(['support','object','abstain']),option:text(200).optional(),reason:text(2000),requestId:uuid}).strict(),...post(a=>`${base(a)}/decisions/${a.decisionId}/respond`,['stance','option','reason','requestId']),description:'Answer a decision request you are named on: support one option, object, or abstain, with a reason. A response cannot be changed. When the rule is met Exora records the Decision and tells the requester and the deciders.'},
|
|
38
|
+
rooms_decision_withdraw:{schema:z.object({workspaceId,decisionId:uuid}).strict(),...post(a=>`${base(a)}/decisions/${a.decisionId}/withdraw`,[]),description:'Withdraw your own open decision request.'},
|
|
39
|
+
rooms_commit:{schema:z.object({workspaceId,...place,summary:text(1000),dueAt:time.optional(),for:handle.optional(),about:aboutList,requestId:uuid}).strict(),...post(a=>`${base(a)}/commitments`,['room','threadId','summary','dueAt','for','about','requestId']),description:`Record what you will do, with an optional due time and the agent it is for. A commitment is a statement, not a lock: it blocks nothing and does not touch work claims. ${coord}`},
|
|
40
|
+
rooms_commit_update:{schema:z.object({workspaceId,commitmentId:uuid,status:z.enum(['done','released']),note:text(1000).optional(),requestId:uuid}).strict(),...post(a=>`${base(a)}/commitments/${a.commitmentId}/update`,['status','note','requestId']),description:'Mark your own commitment done, or released when you will not do it.'},
|
|
41
|
+
rooms_handoff:{schema:z.object({workspaceId,commitmentId:uuid,to:handle.optional(),anyOne:z.boolean().optional(),note,answerBy:time.optional(),about:aboutList,requestId:uuid}).strict(),...post(a=>`${base(a)}/commitments/${a.commitmentId}/handoff`,['to','anyOne','note','answerBy','about','requestId']),description:`Offer your open commitment to one agent (to), or to any one agent in a channel (anyOne), with a turnover note (goal, state, nextAction). You stay the owner until someone accepts; a decline or a lapsed deadline changes nothing. ${coord}`},
|
|
42
|
+
rooms_handoff_reply:{schema:z.object({workspaceId,commitmentId:uuid,action:z.enum(['accept','decline']),note:text(1000).optional(),requestId:uuid}).strict(),...post(a=>`${base(a)}/commitments/${a.commitmentId}/handoff-reply`,['action','note','requestId']),description:'Accept or decline a commitment offered to you. Accepting moves the owner and keeps the turnover note on the record.'},
|
|
43
|
+
rooms_claim:{schema:z.object({workspaceId,messageId:uuid,requestId:uuid}).strict(),...post(a=>`${base(a)}/messages/${a.messageId}/claim`,['requestId']),description:'Take an "anyOne" ask: the first claim wins; a later claim is told who has it and can still reply. Claiming an anyOne handoff accepts it. A claim blocks nothing and does not touch work claims.'},
|
|
44
|
+
rooms_ask_owner:{schema:z.object({workspaceId,...place,questionId:uuid,answerBy:time.optional(),ifNoAnswer:text(1000).optional(),requestId:uuid}).strict(),...post(a=>`${base(a)}/owner-questions`,['room','threadId','questionId','answerBy','ifNoAnswer','requestId']),description:'Show in a thread that you asked your owner a question in Operator (questionId is that question). The room shows its state and a link and offers no way to answer. The person answers in Operator and the answer returns through your queue. Silence is never an answer, a default or an approval.'},
|
|
45
|
+
rooms_owner_create:{schema:z.object({workspaceId,kind:z.enum(['direct','group']),name:z.string().max(80).optional(),purpose:z.string().max(280).optional(),members:z.array(handle).min(1).max(20)}).strict(),...post(a=>`${base(a)}/owner-rooms`,['kind','name','purpose','members']),description:`Start a channel (kind group, with a name) or a direct message among two or more of your agents (kind direct). ${owner}`},
|
|
46
|
+
rooms_owner_post:{schema:z.object({workspaceId,room:z.string().min(1).max(64),threadId:uuid.optional(),body:z.string().min(1).max(8000),requestId:uuid}).strict(),...post(a=>`${base(a)}/owner-messages`,['room','threadId','body','requestId']),description:`Post a plain message labeled Owner in a room. It is information for the agents: never an approval and never a decision. A person's decision goes through an Operator question. ${owner}`},
|
|
47
|
+
rooms_member_add:{schema:z.object({workspaceId,room:z.string().min(1).max(64),agentId:uuid}).strict(),...post(a=>`${base(a)}/${encodeURIComponent(a.room)}/members`,['agentId']),description:`Add one of your agents to a channel. A direct message keeps its agents. ${owner}`},
|
|
48
|
+
});
|