@audienti/cli 0.2.0-exora.6 → 0.2.0-exora.7
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/cli/account-reply-operations.mjs +5 -3
- package/cli/agents.mjs +8 -6
- package/cli/attachment-operations.mjs +9 -0
- package/cli/attachments.mjs +28 -0
- package/cli/audienti.mjs +15 -6
- package/cli/outcome-operations.mjs +7 -0
- package/cli/playbook-help.mjs +62 -0
- package/cli/playbook-operations.mjs +13 -7
- package/cli/progress.mjs +4 -3
- package/cli/rooms-operations.mjs +21 -1
- package/cli/rooms.mjs +28 -3
- package/cli/sender-account-operations.mjs +1 -1
- package/cli/work-operations.mjs +1 -1
- package/cli/workspaces.mjs +18 -7
- package/package.json +1 -1
- package/skills/agent-rooms/SKILL.md +36 -0
- package/skills/agent-rooms/references/commands.md +22 -2
- package/skills/getting-started/references/capabilities.md +25 -7
- package/skills/getting-started/references/outcome-checks.md +25 -0
- package/skills/getting-started/references/replies.md +26 -1
- package/skills/playbook-design/SKILL.md +5 -4
- package/skills/playbook-design/references/fields.md +6 -4
- package/source-manifest.json +23 -20
|
@@ -5,7 +5,7 @@ const item = { ...scope, evidenceId: uuid };
|
|
|
5
5
|
const status = z.enum(['needs_handling', 'handled', 'not_reply']);
|
|
6
6
|
const outcome = z.enum(['response_recorded', 'closed_without_response', 'suppressed']);
|
|
7
7
|
const limit = z.number().int().min(1).max(100);
|
|
8
|
-
const classification = z.enum(['interested', 'question', 'later', 'out_of_office', 'stop', 'bounce', 'referral']);
|
|
8
|
+
const classification = z.enum(['interested', 'question', 'later', 'out_of_office', 'stop', 'bounce', 'referral', 'wrong_person']);
|
|
9
9
|
const slot = z.object({ start: z.string().describe('UTC start, ending in Z.'), end: z.string().describe('UTC end, ending in Z.'), timeZone: z.string().describe("The user's IANA time zone, for display.") }).strict();
|
|
10
10
|
const draft = z.object({
|
|
11
11
|
type: z.enum(['email.send', 'linkedin.send']),
|
|
@@ -34,11 +34,13 @@ export const accountReplyOperations = {
|
|
|
34
34
|
workspace_reply_set: {
|
|
35
35
|
schema: z.object({ ...item, requestId: uuid, expectedVersion: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), status,
|
|
36
36
|
outcome: outcome.nullable().describe("Null for needs_handling/not_reply; a handling outcome is required for handled."), reason: z.string().meta({ minLength: 1, maxLength: 2000 }).describe("Required trimmed NFC reason, 1–2000 codepoints. Interior control characters including newline and tab are rejected."), responseEvidenceId: uuid.nullable().describe("Required for response_recorded; null otherwise. Must identify matching outbound account evidence."),
|
|
37
|
-
classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office."),
|
|
37
|
+
classification: classification.optional().describe("What the reply is. Records a replied event on the person's active campaigns, except for bounce and out_of_office. An out_of_office reply snoozes the person instead; a wrong_person reply with no name given stops the sends and asks who the right person is. Someone who names the right person is a referral."),
|
|
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."),
|
|
38
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."),
|
|
39
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."),
|
|
40
41
|
result: result.optional().describe("Brief 12: the typed detail for a bounce, stop or referral. Its type must match the classification."),
|
|
41
|
-
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.")
|
|
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
|
+
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."),
|
|
42
44
|
method: 'POST', scoped: true, path: detail,
|
|
43
45
|
body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
|
|
44
46
|
},
|
package/cli/agents.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { readProfile, writeProfile, validateUrl, configuration, WorkspaceRequestError } from './workspaces.mjs';
|
|
1
|
+
import { readProfile, writeProfile, validateUrl, configuration, profileFrom, WorkspaceRequestError } from './workspaces.mjs';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import {readJsonFile} from './json-file.mjs';
|
|
4
4
|
|
|
5
5
|
export const agentManagementOperations = ['agent_list','agent_revoke','agent_permission','agent_capabilities_set','agent_capabilities_get','agent_inventory_get','agent_capability_sharing_get','agent_capability_sharing_set','agent_account_authorizations_get','agent_account_authorization_set'];
|
|
6
|
+
// The seven capability groups and the 'approve' guardrail (#120). 'listen' is the retired name of 'research'; the server still accepts it.
|
|
7
|
+
const capabilityKeys=['find','enrich','expand','research','engage','sync','notify','approve','listen'];
|
|
6
8
|
const inventoryLabel=z.string().trim().min(1).max(80);
|
|
7
9
|
// Capability rows (ADR 015). The server checks ids, providers and access against the catalog; the client checks the shape.
|
|
8
10
|
const capabilityRow=z.object({capability:z.string().min(1).max(64),provider:z.string().min(1).max(32).optional(),via:z.string().min(1).max(140),account:z.string().min(1).max(200).optional(),access:z.enum(['read','write','send']),cost:z.enum(['owner','credits']).optional(),status:z.enum(['connected','needs_sign_in','unavailable']),limits:z.object({daily:z.number().int().min(0).max(100000).optional(),hourly:z.number().int().min(0).max(100000).optional()}).strict().optional(),identity:z.object({namespace:z.string().min(1).max(64),accountId:z.string().min(1).max(256)}).strict().optional()}).strict();
|
|
9
11
|
// A LinkedIn seat's cadence, InMail and safe limits (brief 09); the server checks the bounds.
|
|
10
12
|
const seatLimit=z.number().int().min(0).max(100000);
|
|
11
|
-
const seatFacts={checkEverySeconds:z.number().int().min(3600).max(604800).optional(),inmail:z.object({available:z.boolean(),quotaPerMonth:seatLimit.optional()}).strict().optional(),limits:z.object({connectionRequestsPerDay:seatLimit.optional(),connectionRequestsPerWeek:seatLimit.optional(),messagesPerDay:seatLimit.optional(),inmailPerMonth:seatLimit.optional()}).strict().optional()};
|
|
13
|
+
const seatFacts={checkEverySeconds:z.number().int().min(3600).max(604800).optional(),inmail:z.object({available:z.boolean(),quotaPerMonth:seatLimit.optional(),creditsLeft:seatLimit.optional()}).strict().optional(),plan:z.enum(['free','premium_career','premium_business','premium_all_in_one','sales_navigator_core','sales_navigator_advanced','recruiter_lite']).optional(),limits:z.object({connectionRequestsPerDay:seatLimit.optional(),connectionRequestsPerWeek:seatLimit.optional(),messagesPerDay:seatLimit.optional(),inmailPerMonth:seatLimit.optional()}).strict().optional()};
|
|
12
14
|
const accountEntry=z.object({capability:z.string(),tools:z.array(z.string()).min(1).max(8)}).strict();
|
|
13
15
|
const accountReport=z.object({provider:z.enum(['email','linkedin']),accountRef:z.string().min(1).max(200),status:z.enum(['healthy','unavailable','disconnected']),accountIdentity:z.object({namespace:z.string(),accountId:z.string()}).strict().optional(),capabilities:z.array(accountEntry).max(16),...seatFacts}).strict();
|
|
14
16
|
const capabilityReport=z.object({runtime:z.object({name:z.string().trim().min(1).max(80),model:z.string().trim().min(1).max(80).optional()}).strict(),capabilities:z.array(capabilityRow).max(64),seats:z.array(z.object({provider:z.literal('linkedin'),account:z.string().min(1).max(200),...seatFacts}).strict()).max(50).optional()}).strict();
|
|
@@ -24,13 +26,13 @@ export const agentOperations = {
|
|
|
24
26
|
agent_capability_sharing_get: {schema:z.object({id:z.string().uuid()}).strict()},
|
|
25
27
|
agent_capability_sharing_set: {schema:z.object({id:z.string().uuid(),provider:z.enum(['email','linkedin']),accountRef:z.string().min(1).max(200),workspaceId:z.string().uuid(),shared:z.boolean()}).strict()},
|
|
26
28
|
// One object schema for MCP clients; callAgent then requires exactly one of the two report shapes.
|
|
27
|
-
agent_capabilities_report: {description:'Getting started step 2: report what this agent can do as capability rows ({ runtime, capabilities: [{ capability, provider?, via, account?, access, cost?, status, limits?, identity? }], seats?: [{ provider: "linkedin", account, checkEverySeconds?, inmail?, limits? }] }), ids from the capability catalog. A LinkedIn account sends only when its rows carry identity { namespace: "linkedin", accountId: the member id }; a mailbox without one is matched by its address. Replaces the previous report. The old per-account shape (provider, accountRef, status, capabilities) is still accepted. Never use via "audienti" and never include credentials. Requires an active agent profile.',schema:z.object({runtime:capabilityReport.shape.runtime.optional(),provider:accountReport.shape.provider.optional(),accountRef:accountReport.shape.accountRef.optional(),status:accountReport.shape.status.optional(),accountIdentity:accountReport.shape.accountIdentity,capabilities:z.array(z.union([capabilityRow,accountEntry])).max(64),seats:capabilityReport.shape.seats,...seatFacts}).strict(),shape:eitherReport},
|
|
29
|
+
agent_capabilities_report: {description:'Getting started step 2: report what this agent can do as capability rows ({ runtime, capabilities: [{ capability, provider?, via, account?, access, cost?, status, limits?, identity? }], seats?: [{ provider: "linkedin", account, checkEverySeconds?, plan?, inmail?, limits? }] }), ids from the capability catalog. A LinkedIn account sends only when its rows carry identity { namespace: "linkedin", accountId: the member id }; a mailbox without one is matched by its address. Replaces the previous report. The old per-account shape (provider, accountRef, status, capabilities) is still accepted. Never use via "audienti" and never include credentials. Requires an active agent profile.',schema:z.object({runtime:capabilityReport.shape.runtime.optional(),provider:accountReport.shape.provider.optional(),accountRef:accountReport.shape.accountRef.optional(),status:accountReport.shape.status.optional(),accountIdentity:accountReport.shape.accountIdentity,capabilities:z.array(z.union([capabilityRow,accountEntry])).max(64),seats:capabilityReport.shape.seats,...seatFacts}).strict(),shape:eitherReport},
|
|
28
30
|
agent_capabilities_list: {description:'Read what this agent can do now: its own capability rows plus Audienti hosted rows for the workspace (when the workspace has credits and the user approved the group). Uses the profile workspace unless workspaceId is given. Requires an active agent profile.',schema:z.object({workspaceId:z.string().uuid().optional()}).strict()},
|
|
29
31
|
agent_capabilities_show: {schema:z.object({}).strict()},
|
|
30
32
|
agent_capabilities_get: {schema:z.object({id:z.string().uuid()}).strict()},
|
|
31
33
|
agent_permission: {schema:z.object({id:z.string().uuid(),scope:z.enum(['workspace:read','workspace:access'])}).strict()},
|
|
32
|
-
agent_capabilities_set: {schema:z.object({id:z.string().uuid(),capabilities:z.array(z.enum(
|
|
33
|
-
agent_connect: { schema: z.object({ name: z.string().trim().min(1).max(80), capabilities: z.array(z.enum(
|
|
34
|
+
agent_capabilities_set: {schema:z.object({id:z.string().uuid(),capabilities:z.array(z.enum(capabilityKeys)).min(1).max(9)}).strict()},
|
|
35
|
+
agent_connect: { schema: z.object({ name: z.string().trim().min(1).max(80), capabilities: z.array(z.enum(capabilityKeys)).max(9).optional() }).strict() },
|
|
34
36
|
agent_poll: { schema: z.object({}).strict() },
|
|
35
37
|
agent_cancel: { schema: z.object({}).strict() },
|
|
36
38
|
agent_list: { schema: z.object({}).strict() },
|
|
@@ -139,7 +141,7 @@ export async function agentCli(args) {
|
|
|
139
141
|
else if (positions[2] === undefined && !flags.file && !flags.id) {name='agent_capabilities_list';input=flags.workspace ? {workspaceId:flags.workspace} : {};}
|
|
140
142
|
else throw new Error('Use agent capabilities [--workspace ID] [--json], report --file PATH or show [--id ID].');
|
|
141
143
|
}
|
|
142
|
-
const result = await callAgent(name, input, configuration(flags.profile, agentManagementOperations.includes(name) ? 'agents:manage' : 'workspace:access'), flags.profile);
|
|
144
|
+
const result = await callAgent(name, input, configuration(flags.profile, agentManagementOperations.includes(name) ? 'agents:manage' : 'workspace:access'), profileFrom(flags.profile));
|
|
143
145
|
console.log(name === 'agent_capabilities_list' && !json ? capabilityText(result.data) : JSON.stringify(result));
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
// #413: a file is uploaded first and named by id in a LinkedIn message (content.attachments). An image is at most 5 MB;
|
|
3
|
+
// a voice note is at most 2 minutes and 5 MB.
|
|
4
|
+
const uuid=z.string().uuid(),maxBytes=5*1024*1024;
|
|
5
|
+
const base64=z.string().min(4).max(4*Math.ceil(maxBytes/3)+8).refine(value=>value.length%4===0&&/^[A-Za-z0-9+/]+={0,2}$/.test(value));
|
|
6
|
+
export const attachmentOperations={
|
|
7
|
+
attachment_upload:{schema:z.object({workspaceId:uuid.optional(),name:z.string().trim().min(1).max(255),contentType:z.string().max(127),contentBase64:base64,durationSeconds:z.number().int().positive().max(120).optional()}).strict(),method:'POST',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments`,body:a=>({name:a.name,contentType:a.contentType,contentBase64:a.contentBase64,...(a.durationSeconds?{durationSeconds:a.durationSeconds}:{})}),agentWide:true,description:'Store an image (jpeg, png, gif, webp; at most 5 MB) or a voice note (mpeg, mp4, ogg, wav, webm; at most 5 MB and 2 minutes) and return its id. Name the id in content.attachments of a linkedin.send action tagged profile.message.send. A voice note needs durationSeconds and goes only to a connection. Needs workspace:access.'},
|
|
8
|
+
attachment_get:{schema:z.object({workspaceId:uuid.optional(),attachmentId:uuid}).strict(),method:'GET',path:a=>`${a.workspaceId?`/workspaces/${a.workspaceId}`:''}/attachments/${a.attachmentId}`,agentWide:true,description:'One uploaded file with its content as base64. An agent that sends from its own LinkedIn account fetches the file here, then attaches it in LinkedIn.'},
|
|
9
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {readFileSync,writeFileSync,statSync} from 'node:fs';
|
|
2
|
+
import {basename,extname} from 'node:path';
|
|
3
|
+
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
4
|
+
const help='Use attachments upload FILE [--type MIME] [--name NAME] [--duration SECONDS] [--workspace ID] [--profile PATH], or attachments get ATTACHMENT_ID --out PATH [--workspace ID] [--profile PATH]. An image is at most 5 MB; a voice note is at most 2 minutes and 5 MB, and its length goes in --duration.';
|
|
5
|
+
const TYPES={'.jpg':'image/jpeg','.jpeg':'image/jpeg','.png':'image/png','.gif':'image/gif','.webp':'image/webp','.mp3':'audio/mpeg','.m4a':'audio/mp4','.ogg':'audio/ogg','.wav':'audio/wav'};
|
|
6
|
+
export async function attachmentsCli(args){
|
|
7
|
+
const position=[],flags={};for(let i=1;i<args.length;i++){
|
|
8
|
+
if(!args[i].startsWith('--')){position.push(args[i]);continue}
|
|
9
|
+
const key=args[i].slice(2);if(key==='json')continue;if(!['profile','workspace','type','name','duration','out'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error(`Unknown or incomplete attachments option. ${help}`);
|
|
10
|
+
flags[key]=args[++i];
|
|
11
|
+
}
|
|
12
|
+
const config=configuration(flags.profile);
|
|
13
|
+
if(position[0]==='upload'&&position.length===2){
|
|
14
|
+
const file=position[1],type=flags.type??TYPES[extname(file).toLowerCase()];
|
|
15
|
+
if(!type)throw new Error(`Give --type for this file. ${help}`);
|
|
16
|
+
const stat=statSync(file);if(!stat.isFile()||stat.size>5*1024*1024)throw new Error('The file must be a regular file of at most 5 MB.');
|
|
17
|
+
const duration=flags.duration===undefined?undefined:Number(flags.duration);
|
|
18
|
+
if(duration!==undefined&&!Number.isInteger(duration))throw new Error('--duration is a whole number of seconds.');
|
|
19
|
+
const result=await callWorkspace('attachment_upload',{workspaceId:flags.workspace,name:flags.name??basename(file),contentType:type,contentBase64:readFileSync(file).toString('base64'),...(duration===undefined?{}:{durationSeconds:duration})},config);
|
|
20
|
+
console.log(JSON.stringify(result.data));return;
|
|
21
|
+
}
|
|
22
|
+
if(position[0]==='get'&&position.length===2&&flags.out){
|
|
23
|
+
const result=await callWorkspace('attachment_get',{workspaceId:flags.workspace,attachmentId:position[1]},config);
|
|
24
|
+
const {contentBase64,...rest}=result.data;writeFileSync(flags.out,Buffer.from(contentBase64,'base64'));
|
|
25
|
+
console.log(JSON.stringify({...rest,savedTo:flags.out}));return;
|
|
26
|
+
}
|
|
27
|
+
throw new Error(help);
|
|
28
|
+
}
|
package/cli/audienti.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { isAssignmentCommand, assignmentCli } from './assignment.mjs';
|
|
3
|
+
import { playbookHelp } from './playbook-help.mjs';
|
|
3
4
|
// The CLI is an HTTP client. It has no database access or separate business rules.
|
|
4
5
|
const args = process.argv.slice(2);
|
|
5
6
|
const usage = `Profiles: audienti profiles list|search|get ID | match evaluate|apply SOURCE CANDIDATE | history RESOLUTION (saved workspace records)
|
|
@@ -24,9 +25,9 @@ Required environment:
|
|
|
24
25
|
AUDIENTI_API_TOKEN Account API token (create it from your Account page)
|
|
25
26
|
|
|
26
27
|
Use HTTPS for remote hosts. Local HTTP is supported on loopback only.
|
|
27
|
-
Agent commands: agent connect --name NAME [--capabilities find,enrich,
|
|
28
|
+
Agent commands: agent connect --name NAME [--capabilities find,enrich,expand,research,engage,sync,notify,approve]|poll|cancel|list|revoke ID --profile PATH
|
|
28
29
|
Owner permission change: agent permission ID --scope workspace:read|workspace:access --profile USER_PROFILE
|
|
29
|
-
Owner capability change: agent capabilities ID --set find,enrich,
|
|
30
|
+
Owner capability change: agent capabilities ID --set find,enrich,expand,research,engage,sync,notify,approve --profile USER_PROFILE (agents are refused outside this set)
|
|
30
31
|
Agent capabilities: agent capabilities report --file CAPABILITIES.json --profile AGENT_PROFILE (capability rows; the old per-account shape still works)
|
|
31
32
|
What the agent can do: agent capabilities [--workspace ID] [--json] --profile AGENT_PROFILE (works now, and works with Audienti tools)
|
|
32
33
|
Read capabilities: agent capabilities show [--id AGENT_ID] --profile PROFILE
|
|
@@ -50,6 +51,7 @@ Audiences: audience options --kind industries|locations|sizes|types|seniorities|
|
|
|
50
51
|
audience test ID --url LINKEDIN_URL --request-id UUID (spends hosted credits unless the profile is saved)
|
|
51
52
|
Files: file upload PATH --type MIME|list|prepare|register|download|delete ID (personal files, human token only)
|
|
52
53
|
Queue: audienti queue [list]|next|claim ID [--note TEXT]|done ID [--note TEXT]|skip ID --note TEXT|release ID [--workspace ID] (start here after you connect; without --workspace an agent reads every workspace its user approved it in; done or skip again with a new --note corrects the note of a finished task)
|
|
54
|
+
Attachments: audienti attachments upload FILE [--duration SECONDS] (store an image or voice note, get its id for content.attachments)
|
|
53
55
|
Outcome checks: audienti outcomes report TASK_ID --file RESULTS.json (results for the people an outcomes.check task lists)
|
|
54
56
|
Questions: audienti ask "QUESTION" [--context TEXT] [--option TEXT]... [--campaign ID] [--task ID] (questions for your user go to Operator, not chat)
|
|
55
57
|
audienti questions [list] [--state open|answered|withdrawn|all] | show ID | withdraw ID | answer ID --answer TEXT
|
|
@@ -80,7 +82,7 @@ Find prospects: campaign find CAMPAIGN_ID --count N --max-credits C (searches w
|
|
|
80
82
|
campaign check CAMPAIGN_ID --url LINKEDIN_URL [--request-id UUID] (checks one person the same way; reuse the request ID after an interruption)
|
|
81
83
|
Target companies: campaign targets CAMPAIGN_ID [--limit N --after CURSOR] (reads the companies people must work at to join)
|
|
82
84
|
campaign targets CAMPAIGN_ID --add acme.com,linkedin.com/company/globex [--remove old.com] | --set FILE|- (one company per line; saves a new revision)
|
|
83
|
-
Playbooks: playbook list|show ID|save --file REQUEST.json|progress
|
|
85
|
+
Playbooks: playbook list [--limit N --cursor ID]|show ID [--revision N]|save --file REQUEST.json|progress PARTICIPATION_ID|record --file REQUEST.json|suggest --file REQUEST.json|suggestions|decide --file REQUEST.json (audienti playbook SUBCOMMAND --help for the request shape)
|
|
84
86
|
Analytics: analytics show --from ISO --to ISO [--campaign ID] [--playbook-version ID] [--format json|csv]
|
|
85
87
|
analytics outbound [--workspace ID] [--days N | --from ISO --to ISO] [--by sender|action|day] [--format json|csv]
|
|
86
88
|
analytics activity [--workspace ID] [--days N | --from ISO --to ISO] [--format json|csv]
|
|
@@ -97,7 +99,7 @@ Read state: workspace notification-read --workspace ID --key KEY --read true|fal
|
|
|
97
99
|
Search: workspace search --workspace ID --query TEXT [--people-only true] [--recent FILE.json]
|
|
98
100
|
Invitation commands: workspace invitation show|accept|decline|retry|cancel ID
|
|
99
101
|
Options: --workspace ID --name NAME --request-id UUID --email EMAIL --generation N
|
|
100
|
-
Optional private profile: --profile PATH (outside this repository)
|
|
102
|
+
Optional private profile: --profile PATH (outside this repository), or the AUDIENTI_PROFILE variable. --profile wins.
|
|
101
103
|
Save an environment token: profile save --scope account:read|workspace:access|agents:manage --profile PATH
|
|
102
104
|
Start local MCP stdio server: mcp
|
|
103
105
|
--json writes the API response envelope.
|
|
@@ -126,6 +128,7 @@ function commandHelp() {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
async function main() {
|
|
131
|
+
if (args[0] === 'playbook' && args.slice(1).some(value => value === '--help' || value === '-h')) { console.log(playbookHelp(args[1]?.startsWith('-') ? undefined : args[1])); return; }
|
|
129
132
|
if (args.length > 1 && args.slice(1).some(value => value === '--help' || value === '-h') && !['tools', 'call', 'skills'].includes(args[0])) { commandHelp(); return; }
|
|
130
133
|
if(args[0]==='prospects'&&['intake','conversation'].includes(args[1])){const {accountIntakeCli}=await import('./account-intake.mjs');await accountIntakeCli(args);return;}
|
|
131
134
|
if(args[0]==='sender'){const {senderAccountsCli}=await import('./sender-accounts.mjs');await senderAccountsCli(args);return;}
|
|
@@ -154,6 +157,7 @@ async function main() {
|
|
|
154
157
|
if(args[0]==='ask'){const {askCli}=await import('./questions.mjs');await askCli(args);return;}
|
|
155
158
|
if(args[0]==='questions'){const {questionsCli}=await import('./questions.mjs');await questionsCli(args);return;}
|
|
156
159
|
if(args[0]==='queue'){const {queueCli}=await import('./queue.mjs');await queueCli(args);return;}
|
|
160
|
+
if(args[0]==='attachments'){const {attachmentsCli}=await import('./attachments.mjs');await attachmentsCli(args);return;}
|
|
157
161
|
if(args[0]==='outcomes'){const {outcomesCli}=await import('./outcomes.mjs');await outcomesCli(args);return;}
|
|
158
162
|
if(args[0]==='rooms'){const {roomsCli}=await import('./rooms.mjs');await roomsCli(args);return;}
|
|
159
163
|
if(args[0]==='kv'){const {kvCli}=await import('./kv.mjs');await kvCli(args);return;}
|
|
@@ -172,11 +176,16 @@ async function main() {
|
|
|
172
176
|
profileConfig = configuration(args[profileIndex + 1], 'account:read');
|
|
173
177
|
if (!profileConfig.token) throw new Error('whoami needs a human account token. Agents confirm their connection with: audienti workspace list --profile PATH');
|
|
174
178
|
args.splice(profileIndex, 2);
|
|
179
|
+
} else if (args[0] === 'whoami' && profileIndex < 0 && process.env.AUDIENTI_PROFILE && !(process.env.AUDIENTI_API_URL && process.env.AUDIENTI_API_TOKEN)) {
|
|
180
|
+
// The profile named by AUDIENTI_PROFILE (#448), unless the URL and token are in the environment.
|
|
181
|
+
const { configuration } = await import('./workspaces.mjs');
|
|
182
|
+
profileConfig = configuration(undefined, 'account:read');
|
|
183
|
+
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');
|
|
175
184
|
}
|
|
176
185
|
if (args[0] !== 'whoami' || args.length > 2 || (args[1] && args[1] !== '--json')) throw new Error('Unknown command or option. Use audienti --help.');
|
|
177
186
|
const rawUrl = profileConfig ? profileConfig.url : process.env.AUDIENTI_API_URL;
|
|
178
187
|
const token = profileConfig ? profileConfig.token : process.env.AUDIENTI_API_TOKEN;
|
|
179
|
-
if (!rawUrl || !token) throw new Error('Set AUDIENTI_API_URL and AUDIENTI_API_TOKEN. Create a token from your Account page.');
|
|
188
|
+
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.');
|
|
180
189
|
let base;
|
|
181
190
|
try { base = new URL(rawUrl); } catch { throw new Error('AUDIENTI_API_URL must be a valid API base URL.'); }
|
|
182
191
|
if (base.username || base.password || base.search || base.hash) throw new Error('The API URL must not contain credentials, query parameters, or a fragment.');
|
|
@@ -202,4 +211,4 @@ async function main() {
|
|
|
202
211
|
console.log(args[1] === '--json' ? JSON.stringify({ data: profile }) : `${profile.email ?? profile.username ?? profile.id}\nAccount ID: ${profile.id}`);
|
|
203
212
|
}
|
|
204
213
|
|
|
205
|
-
main().catch(error => { console.error(
|
|
214
|
+
main().catch(error => { console.error(JSON.stringify({ error: error.toJSON ? error.toJSON() : { code: 'client_error', message: error.message } })); process.exitCode = 1; });
|
|
@@ -6,6 +6,13 @@ const result=z.discriminatedUnion('kind',[
|
|
|
6
6
|
z.object({kind:z.literal('linkedin.accept'),person:uuid,at:time}).strict(),
|
|
7
7
|
z.object({kind:z.literal('linkedin.invite_closed'),person:uuid,reason:z.enum(['withdrawn','expired']),at:time.optional()}).strict(),
|
|
8
8
|
z.object({kind:z.literal('linkedin.profile_degree'),person:uuid,degree:z.number().int().min(1).max(3),at:time.optional()}).strict(),
|
|
9
|
+
// #413: a steps.run task (a comment, react or withdraw step): the post engaged with, or why the step was skipped.
|
|
10
|
+
z.object({kind:z.literal('linkedin.comment'),person:uuid,postUrl:z.string().url().max(500),at:time.optional()}).strict(),
|
|
11
|
+
z.object({kind:z.literal('linkedin.reaction'),person:uuid,postUrl:z.string().url().max(500),reaction:z.enum(['like','celebrate','support','love','insightful','funny']).optional(),at:time.optional()}).strict(),
|
|
12
|
+
z.object({kind:z.literal('linkedin.follow'),person:uuid,at:time.optional()}).strict(),
|
|
13
|
+
z.object({kind:z.literal('linkedin.unfollow'),person:uuid,at:time.optional()}).strict(),
|
|
14
|
+
z.object({kind:z.literal('linkedin.invitation_received'),ref:z.string().trim().min(1).max(300).optional(),profileUrl:z.string().trim().url().max(500).optional(),name:z.string().trim().min(1).max(200).optional(),sentAt:time.optional()}).strict().refine(value=>!!(value.ref??value.profileUrl),{message:'Give the request id (ref) or the profile address (profileUrl).'}),
|
|
15
|
+
z.object({kind:z.literal('linkedin.step_skipped'),person:uuid,reason:z.string().min(1).max(500),at:time.optional()}).strict(),
|
|
9
16
|
// #279: a replies.check task (was there a reply to the email).
|
|
10
17
|
z.object({kind:z.literal('email.no_reply'),person:uuid}).strict(),
|
|
11
18
|
z.object({kind:z.literal('email.reply'),person:uuid,evidence:uuid}).strict(),
|
|
@@ -0,0 +1,62 @@
|
|
|
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 [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
|
|
7
|
+
about: 'One page of summaries, 50 by default: id, name, currentRevision, system, systemKey, stages, steps, recommended, updatedAt. 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
|
+
show: {
|
|
10
|
+
usage: 'playbook show PLAYBOOK_ID [--revision N] [--workspace ID] [--profile PATH]',
|
|
11
|
+
about: 'The whole playbook (fields) and the campaigns that use it. Without --revision it returns the current one. PLAYBOOK_ID comes from playbook list.'
|
|
12
|
+
},
|
|
13
|
+
save: {
|
|
14
|
+
usage: 'playbook save --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
15
|
+
about: 'Create a playbook or save a new revision. 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. fields: name, stages (2 or more, each with a UUID key), goalKey (a stage key after the first stage), offRamps. Field rules: audienti skills read playbook-design references/fields.md',
|
|
16
|
+
example: {
|
|
17
|
+
requestId: 'UUID',
|
|
18
|
+
expectedRevision: 0,
|
|
19
|
+
fields: {
|
|
20
|
+
name: 'Email only',
|
|
21
|
+
stages: [
|
|
22
|
+
{ key: 'STAGE_A_UUID', name: 'Ready', owner: 'Agent', goalDays: 3, steps: [{ key: 'UUID', channel: 'Email', action: 'Email 1', condition: 'Always', instructions: 'Three sentences.', wait: { until: 'reply', days: 4 } }], completion: { kind: 'next' } },
|
|
23
|
+
{ key: 'STAGE_B_UUID', name: 'Meeting booked', on: ['meeting'], owner: 'Assignee', goalDays: 0, steps: [], completion: { kind: 'stay' } }
|
|
24
|
+
],
|
|
25
|
+
goalKey: 'STAGE_B_UUID',
|
|
26
|
+
offRamps: [{ key: 'UUID', name: 'Unresponsive', reason: 'unresponsive' }]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
progress: {
|
|
31
|
+
usage: 'playbook progress PARTICIPATION_ID [--workspace ID] [--profile PATH]',
|
|
32
|
+
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.'
|
|
33
|
+
},
|
|
34
|
+
record: {
|
|
35
|
+
usage: 'playbook record --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
36
|
+
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}).'
|
|
37
|
+
},
|
|
38
|
+
suggest: {
|
|
39
|
+
usage: 'playbook suggest --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
40
|
+
about: 'The same request as record, but it asks a person to approve the move in Operator. Nothing changes until they approve.'
|
|
41
|
+
},
|
|
42
|
+
suggestions: {
|
|
43
|
+
usage: 'playbook suggestions [--limit 1..100] [--cursor ID] [--workspace ID] [--profile PATH]',
|
|
44
|
+
about: 'Pending suggestions that wait for a person to approve or dismiss them.'
|
|
45
|
+
},
|
|
46
|
+
decide: {
|
|
47
|
+
usage: 'playbook decide --file REQUEST.json [--workspace ID] [--profile PATH]',
|
|
48
|
+
about: 'Approve or dismiss one pending suggestion: {"id": SUGGESTION_ID, "requestId": UUID, "decision": "approve" or "dismiss"}. People only; an agent cannot approve.'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const lines = Object.values(playbookCommands).map(command => command.usage);
|
|
52
|
+
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.`;
|
|
53
|
+
/** Help for `audienti playbook [SUBCOMMAND] --help`. */
|
|
54
|
+
export function playbookHelp(subcommand) {
|
|
55
|
+
const names = playbookCommands[subcommand] ? [subcommand] : Object.keys(playbookCommands);
|
|
56
|
+
const text = names.map(name => {
|
|
57
|
+
const command = playbookCommands[name];
|
|
58
|
+
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)}` : ''}`;
|
|
59
|
+
}).join('\n\n');
|
|
60
|
+
const conflict = names.includes('save') ? ' A revision_conflict from save also gives currentRevision.' : '';
|
|
61
|
+
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.`;
|
|
62
|
+
}
|
|
@@ -3,19 +3,25 @@ const uuid=z.string().uuid(),scope={workspaceId:uuid.optional()};
|
|
|
3
3
|
const label=z.string().trim().min(1).max(120).refine(value=>!/[\u0000-\u001f\u007f]/.test(value),'Control characters are not allowed.');
|
|
4
4
|
const text=z.string().trim().max(5000);
|
|
5
5
|
const identity=z.object({key:z.string().uuid(),name:label}).strict();
|
|
6
|
-
const
|
|
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 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.'),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();
|
|
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
|
+
const playbookStepSchema=z.object({key:z.string().uuid(),channel:z.enum(['LinkedIn','Email']),action:label,condition:z.enum(['Always','Only if viable','Only if connected']),viability:text.optional(),instructions:text.optional(),wait:z.object({until:z.enum(['accept','reply','meeting','timeout']),days:z.number().int().min(1).max(60),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().optional(),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();
|
|
7
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()]);
|
|
8
|
-
const stage=identity.extend({movesInWhen:text.optional(),owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional(),optional:z.boolean().optional(),enabled:z.boolean().optional(),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting','manual'])).max(4).optional()});
|
|
9
|
-
|
|
10
|
-
const
|
|
12
|
+
const stage=identity.extend({movesInWhen:text.optional(),owner:z.enum(['Agent','Assignee']).optional(),goalDays:z.number().int().min(0).max(365).optional(),optional:z.boolean().optional(),enabled:z.boolean().optional(),steps:z.array(playbookStepSchema).max(20).optional(),completion:completion.optional(),on:z.array(z.enum(['accept','reply','meeting','manual'])).max(4).optional(),eventRules:z.array(eventRule).max(10).optional()});
|
|
13
|
+
// A playbook saved before conflict 20 may carry autoMove; it is dropped, because approval lives on the account.
|
|
14
|
+
const dropAutoMove=value=>{if(!value||typeof value!=='object'||Array.isArray(value)||!('autoMove' in value))return value;const {autoMove:_dropped,...rest}=value;return rest;};
|
|
15
|
+
const playbookRulesSchema=z.preprocess(dropAutoMove,z.object({flagOverdue:z.boolean().optional(),allowBackward:z.boolean().optional(),emailWindow:z.enum(['weekdays_morning','weekdays','any_day']).optional(),nurtureDays:z.union([z.literal(60),z.literal(90),z.literal(180),z.null()]).optional()}).strict());
|
|
16
|
+
const playbookFieldsSchema=z.object({name:label,stages:z.array(stage).min(2).max(30),goalKey:z.string().uuid(),offRamps:z.array(identity.extend({reason:z.enum(['rejected','not_fit','nurture','unresponsive','other','taken_over','bounced','skipped'])})).max(30),voice:text.optional(),rules:playbookRulesSchema.optional(),eventRules:z.array(eventRule).max(30).optional()}).strict();
|
|
11
17
|
const fields=playbookFieldsSchema;
|
|
12
18
|
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)]));
|
|
13
19
|
export const playbookOperations={
|
|
14
|
-
playbook_list:{schema:z.object({...scope,cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>`${base(a)}/playbooks?${query(a)}`,scoped:true,description:'List
|
|
20
|
+
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.')}).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, stages, steps, recommended, updatedAt) with nextCursor. 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.'},
|
|
15
21
|
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: stage plays, step waits, completion, goal, voice and agent rules. Instructions are executed by the external agent, never by a hosted sequence runner.'},
|
|
16
|
-
playbook_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative(),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.'},
|
|
22
|
+
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.'},
|
|
17
23
|
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.'},
|
|
18
|
-
playbook_progress_get:{schema:z.object({...scope,participationId:uuid}).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.'},
|
|
24
|
+
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.'},
|
|
19
25
|
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.'},
|
|
20
26
|
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.'},
|
|
21
27
|
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.'},
|
package/cli/progress.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
2
2
|
import {readJsonFile} from './json-file.mjs';
|
|
3
|
+
import {playbookUsageError,playbookCommands} from './playbook-help.mjs';
|
|
3
4
|
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','playbook-version','kind','user','format','owner','offer','audience','tag'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error(
|
|
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'].includes(key)||!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]=args[++i];}
|
|
5
6
|
const names={playbook:{list:'playbook_list',show:'playbook_get',save:'playbook_save',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 name=names[positions[0]]?.[positions[1]];if(!name)throw new Error('Use
|
|
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.');
|
|
7
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.');
|
|
8
|
-
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'].includes(name);if(positions.length!==(withId?3:2))throw new Error('Unexpected arguments.');
|
|
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'].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
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.');
|
|
10
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_get'?'id':'participationId']=positions[2];
|
|
11
12
|
for(const[key,value]of Object.entries(flags)){if(['workspace','profile','file','format'].includes(key))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;}
|
package/cli/rooms-operations.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const roomsOperations={
|
|
|
17
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
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}`},
|
|
19
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).'},
|
|
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(),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 user. ${description}`},
|
|
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}`},
|
|
21
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}`},
|
|
22
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.'},
|
|
23
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.'},
|
|
@@ -26,3 +26,23 @@ export const roomsOperations={
|
|
|
26
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}`},
|
|
27
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.'},
|
|
28
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
|
+
});
|
package/cli/rooms.mjs
CHANGED
|
@@ -6,9 +6,9 @@ import {setTimeout as sleep} from 'node:timers/promises';
|
|
|
6
6
|
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
7
7
|
import {readJsonFile} from './json-file.mjs';
|
|
8
8
|
// Agent rooms client. Delivery is polled by the agent; nothing here opens a port.
|
|
9
|
-
const valued=['profile','workspace','sort','since','about','limit','to','room','thread','body','body-file','file','answer-by','expires-at','request-id','with','slug','title','purpose','members','after','q','handle','exec','interval','timeout'];
|
|
10
|
-
const switches=['retired','stop','once','json','unable','clear','brief'];
|
|
11
|
-
export const roomsUsage='Rooms: audienti rooms directory [--sort load] | inbox [--since ITEM] [--about TYPE:ID] [--limit N] [--brief] | ack ITEM... | send (--to HANDLE|--room ROOM) [--thread ID] (--body TEXT|--body-file PATH|-) [--about TYPE:ID,...] [--answer-by ISO] [--expires-at ISO] [--unable] [--request-id UUID] | note (--body TEXT|--body-file PATH|-|--clear) | thread ID [--after N] | threads --about TYPE:ID | create (--with HANDLE | --slug S --title T [--purpose P] [--members a,b]) | list [--retired] [--q TEXT] | read ROOM [--q TEXT] | retire ROOM | restore ROOM | redact MESSAGE | resume THREAD | pause on|off | profile AGENT_ID [--handle H] [--about TEXT] | hook [--stop] | watch --exec CMD [--interval SECONDS] [--once] | wait --thread ID [--timeout SECONDS]';
|
|
9
|
+
const valued=['profile','workspace','sort','since','about','limit','to','room','thread','body','body-file','file','answer-by','expires-at','request-id','with','slug','title','purpose','members','after','q','handle','exec','interval','timeout','question','options','deciders','rule','deadline','default','if-no-answer','supersedes','summary','due','for','status','note','goal','state','next-action','open-items','risks','action','stance','option','reason','question-id','kind','name'];
|
|
10
|
+
const switches=['retired','stop','once','json','unable','clear','brief','any-one'];
|
|
11
|
+
export const roomsUsage='Rooms: audienti rooms directory [--sort load] | inbox [--since ITEM] [--about TYPE:ID] [--limit N] [--brief] | ack ITEM... | send (--to HANDLE|--room ROOM) [--thread ID] (--body TEXT|--body-file PATH|-) [--about TYPE:ID,...] [--answer-by ISO [--if-no-answer TEXT] [--any-one]] [--expires-at ISO] [--unable] [--request-id UUID] | note (--body TEXT|--body-file PATH|-|--clear) | thread ID [--after N] | threads --about TYPE:ID | create (--with HANDLE | --slug S --title T [--purpose P] [--members a,b]) | list [--retired] [--q TEXT] | read ROOM [--q TEXT] | retire ROOM | restore ROOM | redact MESSAGE | resume THREAD | pause on|off | profile AGENT_ID [--handle H] [--about TEXT] | request-decision --question T --options A|B[|C] --deciders h1,h2 --rule one|any|all|majority --deadline ISO [--default OPTION] [--if-no-answer T] [--supersedes ID] [--room R|--thread ID] | respond DECISION --stance support|object|abstain [--option O] --reason T | withdraw DECISION | commit --summary T [--due ISO] [--for HANDLE] [--room R|--thread ID] | commit-update COMMITMENT --status done|released [--note T] | handoff COMMITMENT (--to HANDLE|--any-one) --goal T --state T --next-action T [--open-items a|b] [--risks a|b] [--answer-by ISO] | handoff-reply COMMITMENT --action accept|decline [--note T] | claim MESSAGE | ask-owner --question-id ID [--room R|--thread ID] [--answer-by ISO] [--if-no-answer T] | owner-create (--kind direct --members a,b[,c] | --kind group --name N [--purpose P] --members a,b) | owner-post ROOM [--thread ID] --body TEXT | member-add ROOM AGENT_ID | hook [--stop] | watch --exec CMD [--interval SECONDS] [--once] | wait --thread ID [--timeout SECONDS]';
|
|
12
12
|
export const WATCH_MIN_INTERVAL=60,WAIT_MAX_TIMEOUT=900,WAIT_POLL=30,HOOK_CACHE=60;
|
|
13
13
|
function parse(args){
|
|
14
14
|
const position=[],flags={};
|
|
@@ -29,6 +29,10 @@ const references=value=>value===undefined?undefined:value.split(',').map(entry=>
|
|
|
29
29
|
const type=entry.slice(0,at),rest=entry.slice(at+1);
|
|
30
30
|
return type==='url'?{type,url:rest,title:rest}:{type,id:rest};
|
|
31
31
|
});
|
|
32
|
+
const list=(value,separator)=>value===undefined?undefined:value.split(separator).map(item=>item.trim()).filter(Boolean);
|
|
33
|
+
const pick=(flags,pairs)=>Object.fromEntries(pairs.filter(([flag])=>flags[flag]!==undefined).map(([flag,key])=>[key,flags[flag]]));
|
|
34
|
+
const placeOf=flags=>({...(flags.room?{room:flags.room}:{}),...(flags.thread?{threadId:flags.thread}:{})});
|
|
35
|
+
const requestId=flags=>flags['request-id']??randomUUID();
|
|
32
36
|
function messageBody(flags,rest){
|
|
33
37
|
const sources=[flags.body!==undefined,flags['body-file']!==undefined,rest[0]==='-'].filter(Boolean).length;
|
|
34
38
|
if(sources!==1)throw new Error('Supply exactly one of --body TEXT, --body-file PATH or - for standard input.');
|
|
@@ -111,6 +115,8 @@ export async function roomsCli(args){
|
|
|
111
115
|
for(const [flag,key] of [['to','to'],['room','room'],['thread','threadId'],['answer-by','answerBy'],['expires-at','expiresAt']])if(flags[flag]!==undefined)input[key]=flags[flag];
|
|
112
116
|
if(flags.about)input.about=references(flags.about);
|
|
113
117
|
if(flags.unable)input.unable=true;
|
|
118
|
+
if(flags['if-no-answer']!==undefined)input.ifNoAnswer=flags['if-no-answer'];
|
|
119
|
+
if(flags['any-one'])input.anyOne=true;
|
|
114
120
|
return print(await call('rooms_send',input,flags));
|
|
115
121
|
}
|
|
116
122
|
case 'note':{
|
|
@@ -127,6 +133,25 @@ export async function roomsCli(args){
|
|
|
127
133
|
case 'resume':need(1,'resume THREAD');return print(await call('rooms_resume',{threadId:rest[0]},flags));
|
|
128
134
|
case 'pause':if(rest.length!==1||!['on','off'].includes(rest[0]))throw new Error('Use audienti rooms pause on|off.');return print(await call('rooms_pause',{paused:rest[0]==='on'},flags));
|
|
129
135
|
case 'profile':need(1,'profile AGENT_ID [--handle H] [--about TEXT]');return print(await call('rooms_profile_set',{agentId:rest[0],...(flags.handle?{handle:flags.handle}:{}),...(flags.about!==undefined?{about:flags.about}:{})},flags));
|
|
136
|
+
case 'request-decision':{
|
|
137
|
+
need(0,'request-decision --question T --options A|B --deciders h1,h2 --rule one --deadline ISO');
|
|
138
|
+
return print(await call('rooms_decision_request',{...placeOf(flags),question:flags.question,options:list(flags.options,'|'),deciders:list(flags.deciders,','),rule:flags.rule,deadline:flags.deadline,...pick(flags,[['default','defaultOption'],['if-no-answer','ifNoAnswer'],['supersedes','supersedes']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
139
|
+
}
|
|
140
|
+
case 'respond':need(1,'respond DECISION --stance support|object|abstain --reason T');return print(await call('rooms_decision_respond',{decisionId:rest[0],stance:flags.stance,reason:flags.reason,...pick(flags,[['option','option']]),requestId:requestId(flags)},flags));
|
|
141
|
+
case 'withdraw':need(1,'withdraw DECISION');return print(await call('rooms_decision_withdraw',{decisionId:rest[0]},flags));
|
|
142
|
+
case 'commit':need(0,'commit --summary T');return print(await call('rooms_commit',{...placeOf(flags),summary:flags.summary,...pick(flags,[['due','dueAt'],['for','for']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
143
|
+
case 'commit-update':need(1,'commit-update COMMITMENT --status done|released');return print(await call('rooms_commit_update',{commitmentId:rest[0],status:flags.status,...pick(flags,[['note','note']]),requestId:requestId(flags)},flags));
|
|
144
|
+
case 'handoff':{
|
|
145
|
+
need(1,'handoff COMMITMENT (--to HANDLE|--any-one) --goal T --state T --next-action T');
|
|
146
|
+
const note={goal:flags.goal,state:flags.state,nextAction:flags['next-action'],...(flags['open-items']?{openItems:list(flags['open-items'],'|')}:{}),...(flags.risks?{risks:list(flags.risks,'|')}:{})};
|
|
147
|
+
return print(await call('rooms_handoff',{commitmentId:rest[0],...(flags.to?{to:flags.to}:{}),...(flags['any-one']?{anyOne:true}:{}),note,...pick(flags,[['answer-by','answerBy']]),...(flags.about?{about:references(flags.about)}:{}),requestId:requestId(flags)},flags));
|
|
148
|
+
}
|
|
149
|
+
case 'handoff-reply':need(1,'handoff-reply COMMITMENT --action accept|decline');return print(await call('rooms_handoff_reply',{commitmentId:rest[0],action:flags.action,...pick(flags,[['note','note']]),requestId:requestId(flags)},flags));
|
|
150
|
+
case 'claim':need(1,'claim MESSAGE');return print(await call('rooms_claim',{messageId:rest[0],requestId:requestId(flags)},flags));
|
|
151
|
+
case 'ask-owner':need(0,'ask-owner --question-id ID');return print(await call('rooms_ask_owner',{...placeOf(flags),questionId:flags['question-id'],...pick(flags,[['answer-by','answerBy'],['if-no-answer','ifNoAnswer']]),requestId:requestId(flags)},flags));
|
|
152
|
+
case 'owner-create':need(0,'owner-create --kind direct|group --members a,b');return print(await call('rooms_owner_create',{kind:flags.kind,members:list(flags.members,','),...pick(flags,[['name','name'],['purpose','purpose']])},flags));
|
|
153
|
+
case 'owner-post':need(1,'owner-post ROOM --body TEXT');return print(await call('rooms_owner_post',{room:rest[0],...(flags.thread?{threadId:flags.thread}:{}),body:flags.body,requestId:requestId(flags)},flags));
|
|
154
|
+
case 'member-add':need(2,'member-add ROOM AGENT_ID');return print(await call('rooms_member_add',{room:rest[0],agentId:rest[1]},flags));
|
|
130
155
|
case 'hook':need(0,'hook [--stop]');return hook(flags);
|
|
131
156
|
case 'watch':need(0,'watch --exec CMD');return watch(flags);
|
|
132
157
|
case 'wait':need(0,'wait --thread ID');return wait(flags);
|
|
@@ -5,7 +5,7 @@ const base={requestId:uuid,reason:text(1,2000).describe('Trimmed NFC; no control
|
|
|
5
5
|
const alias={provider:z.enum(['email','linkedin']),accountRef:text(1,200).regex(/^[A-Za-z0-9@._:+\/-]+$/),expectedAliasVersion:version};
|
|
6
6
|
const identity=z.object({namespace:text(1,64).describe('Discovery namespace: trimmed and lowercased.'),accountId:text(1,256).describe('Trimmed printable ASCII provider identity; case-sensitive assertion, not verification.')}).strict();
|
|
7
7
|
const actionType=z.enum(['email.send','linkedin.send']);
|
|
8
|
-
const capability=z.enum(['profile.read','profile.connection.request','profile.follow','profile.message.send','profile.inmail.send','profile.inbox.read','people.search']).describe('Account action. Audienti gates profile.connection.request
|
|
8
|
+
const capability=z.enum(['profile.read','profile.connection.request','profile.connection.withdraw','profile.follow','profile.message.send','profile.inmail.send','profile.inbox.read','people.search','post.comment','post.react','post.publish','profile.visit','profile.views.read']).describe('Account action. Audienti gates profile.connection.request, profile.connection.withdraw and profile.message.send at send time; the agent follows the user\'s mode for the others. Email accounts have profile.message.send and inbox.read.');
|
|
9
9
|
const useIn=z.object({allWorkspaces:z.boolean(),workspaceIds:z.array(uuid).max(200)}).strict().describe('Where the account is used: all of the user\'s workspaces, or only the listed ones. Left out, all of them.');
|
|
10
10
|
const pageLimit=z.number().int().min(1).max(100).optional();
|
|
11
11
|
const query=a=>{const q=new URLSearchParams(Object.entries(a).map(([k,v])=>[k,String(v)]));return q.size?'?'+q:'';};
|
package/cli/work-operations.mjs
CHANGED
|
@@ -7,7 +7,7 @@ const accountRef = z.string().min(1).max(200).regex(/^[A-Za-z0-9@._:+\/-]+$/);
|
|
|
7
7
|
const evidence = z.array(uuid).max(100);
|
|
8
8
|
const commonAction = {clientActionId: clientId, capability: z.enum(['profile.connection.request','profile.message.send']).optional().describe('The owner action: profile.connection.request or profile.message.send on linkedin.send, profile.message.send on email.send. The sending account\'s mode for it decides whether approval is needed or the action is not allowed. Without it the old per-channel automation switch applies, unless a mode restricts it.'), reason: z.string().min(1), evidenceIds: evidence, replyToEvidenceId: uuid.optional(), playbookStepKey: uuid.optional().describe("The playbook step this action runs; a succeeded result starts that step's wait. Saving the plan checks it: the key must name a step of the playbook pinned to the person's active participation in the plan's Campaign, and the step's channel must match the action type (email.send only with an Email step, linkedin.send only with a LinkedIn step); otherwise the save is refused with invalid_fields.")};
|
|
9
9
|
const email = z.object({...commonAction, type:z.literal('email.send'), content:z.object({subject:z.string(),body:z.string()}).strict(),recipient:z.object({prospectId:uuid,email:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('email'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
10
|
-
const linkedin = z.object({...commonAction,type:z.literal('linkedin.send'),content:z.object({body:z.string()}).strict(),recipient:z.object({prospectId:uuid,profileUrl:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('linkedin'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
10
|
+
const linkedin = z.object({...commonAction,type:z.literal('linkedin.send'),content:z.object({body:z.string(),attachments:z.array(z.object({id:uuid}).strict()).min(1).max(1).optional()}).strict(),recipient:z.object({prospectId:uuid,profileUrl:z.string(),contactId:uuid.optional()}).strict(),sender:z.object({provider:z.literal('linkedin'),accountRef,senderAccountId:uuid.optional()}).strict()}).strict();
|
|
11
11
|
const cadence = z.object({notBefore:z.string().datetime().optional(),clientActionId:clientId,afterClientActionId:clientId.nullable(),trigger:z.literal('reported_success').nullable(),at:z.string().nullable(),waitSeconds:z.number().int().min(0).max(7776000).nullable(),condition:z.literal('no_pending_reply').nullable()}).strict();
|
|
12
12
|
const located = {...workspace,planId:uuid,clientActionId:clientId};
|
|
13
13
|
const mutation = {...located,expectedRevision:z.number().int().positive(),requestId:uuid};
|
package/cli/workspaces.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {roomsOperations} from './rooms-operations.mjs';
|
|
|
4
4
|
import {queueOperations} from './queue-operations.mjs';
|
|
5
5
|
import {kvOperations} from './kv-operations.mjs';
|
|
6
6
|
import {outcomeOperations} from './outcome-operations.mjs';
|
|
7
|
+
import {attachmentOperations} from './attachment-operations.mjs';
|
|
7
8
|
import {questionOperations} from './question-operations.mjs';
|
|
8
9
|
import {searchOperations} from './search-operations.mjs';
|
|
9
10
|
import {fileOperations,uploadFileContent} from './file-operations.mjs';
|
|
@@ -43,6 +44,7 @@ export const operations = {
|
|
|
43
44
|
...queueOperations,
|
|
44
45
|
...kvOperations,
|
|
45
46
|
...outcomeOperations,
|
|
47
|
+
...attachmentOperations,
|
|
46
48
|
...questionOperations,
|
|
47
49
|
...fileOperations,
|
|
48
50
|
...playbookOperations,
|
|
@@ -116,7 +118,11 @@ export function writeProfile(path, profile) {
|
|
|
116
118
|
const target=profilePath(path); const temporary=`${target}.${process.pid}.tmp`;
|
|
117
119
|
writeFileSync(temporary,JSON.stringify(profile)+'\n',{mode:0o600,flag:'wx'}); renameSync(temporary,target);
|
|
118
120
|
}
|
|
121
|
+
/** The profile path: --profile wins, then AUDIENTI_PROFILE (the variable the MCP server reads), else none (#448). */
|
|
122
|
+
export const profileFrom = flag => flag ?? (process.env.AUDIENTI_PROFILE || undefined);
|
|
123
|
+
export const profileHint = 'Pass --profile PATH, or set AUDIENTI_PROFILE to the profile file.';
|
|
119
124
|
export function configuration(path, scope='workspace:access') {
|
|
125
|
+
path=profileFrom(path);
|
|
120
126
|
const profile=readProfile(path);
|
|
121
127
|
const agentProfile = Boolean(profile.agent || profile.pendingAgent || profile.connectionSecret);
|
|
122
128
|
return { url:agentProfile ? profile.url : process.env.AUDIENTI_API_URL ?? profile.url, token:agentProfile ? (scope === 'workspace:access' ? profile.tokens?.['workspace:read'] : undefined) : process.env.AUDIENTI_API_TOKEN ?? (scope === 'workspace:access' ? profile.tokens?.['workspace:read'] ?? profile.tokens?.[scope] : profile.tokens?.[scope]), workspaceId:profile.workspaceId, profile };
|
|
@@ -127,9 +133,13 @@ export function validateUrl(value) {
|
|
|
127
133
|
if(url.protocol!=='https:' && !(url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname))) throw new Error('Use HTTPS for remote API hosts.');
|
|
128
134
|
return url;
|
|
129
135
|
}
|
|
136
|
+
// A refused request that never left this machine. It prints like an API error, so a caller reads one format (#451).
|
|
137
|
+
export class InvalidArguments extends Error {
|
|
138
|
+
toJSON() { return {code:'invalid_arguments',message:this.message}; }
|
|
139
|
+
}
|
|
130
140
|
export class WorkspaceRequestError extends Error {
|
|
131
141
|
constructor(status, code, message, retryAt, blockers, requestTraceId) { super(message); this.status = status; this.code = code; this.retryAt = retryAt; this.blockers = blockers; this.requestTraceId = requestTraceId; }
|
|
132
|
-
toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
|
|
142
|
+
toJSON() { return { ...(this.requestId ? {requestId:this.requestId,recovery:this.recovery} : {}), ...(this.requestTraceId ? {requestTraceId:this.requestTraceId} : {}), code:this.code, status:this.status, message:this.message, ...(this.currentRevision!==undefined ? {currentRevision:this.currentRevision} : {}), ...(this.retryAt ? {retryAt:this.retryAt} : {}), ...(this.blockers ? {blockers:this.blockers} : {}), ...(this.charged ? {charged:this.charged} : {}) }; }
|
|
133
143
|
}
|
|
134
144
|
export class HostedResponseUnknown extends Error {
|
|
135
145
|
constructor(requestId, reason, requestTraceId) { super('The server response is unknown. Paid work may have started. Retry using the same requestId and principal; never generate a new ID to recover this call.'); this.requestId=requestId; this.reason=reason; this.requestTraceId=requestTraceId; }
|
|
@@ -137,12 +147,12 @@ export class HostedResponseUnknown extends Error {
|
|
|
137
147
|
}
|
|
138
148
|
export async function callWorkspace(name, args, config) {
|
|
139
149
|
const operation=operations[name]; if(!operation) throw new Error('Unknown workspace operation.');
|
|
140
|
-
const parsed=operation.schema.safeParse(args);if(!parsed.success) throw new
|
|
150
|
+
const parsed=operation.schema.safeParse(args);if(!parsed.success) throw new InvalidArguments(`Invalid workspace arguments. Check IDs and required fields: ${parsed.error.issues.slice(0,5).map(i=>`${i.path.join('.')||'input'}: ${i.message}`).join('; ')}.`);
|
|
141
151
|
const input={...parsed.data};
|
|
142
152
|
if(operation.localUpload)return uploadFileContent(input,(operationName,fields)=>callWorkspace(operationName,fields,config));
|
|
143
153
|
// #333: an agent's own queue covers all its workspaces; a user's reads the selected workspace, as before.
|
|
144
|
-
if(operation.scoped || (operation.agentWide && !/^agt_/.test(config.token ?? ''))) { input.workspaceId ??= config.workspaceId;if(!uuid.safeParse(input.workspaceId).success) throw new Error(
|
|
145
|
-
const url=validateUrl(config.url); if(!config.token) throw new Error(
|
|
154
|
+
if(operation.scoped || (operation.agentWide && !/^agt_/.test(config.token ?? ''))) { input.workspaceId ??= config.workspaceId;if(!uuid.safeParse(input.workspaceId).success) throw new Error(`Select a workspace or supply --workspace ID. ${config.profile&&Object.keys(config.profile).length?'Run workspace use WORKSPACE_ID once for this profile.':profileHint}`); }
|
|
155
|
+
const url=validateUrl(config.url); if(!config.token) throw new Error(`A workspace:access token is required. ${profileHint} Or set AUDIENTI_API_URL and AUDIENTI_API_TOKEN.`);
|
|
146
156
|
const target=new URL(`/api/v1${operation.path(input)}`,url);
|
|
147
157
|
let response;try {response=await fetch(target,{method:operation.method,headers:{Authorization:`Bearer ${config.token}`,Accept:'application/json','Content-Type':'application/json',...((operation.traced||name.startsWith('tools_'))?{'X-Audienti-Interface':config.interface==='mcp'?'mcp':'cli'}:{})},...(['POST','PUT','PATCH'].includes(operation.method)?{body:JSON.stringify(operation.body?.(input)??{})}:{}),redirect:'error',signal:AbortSignal.timeout(operation.timeoutMs ?? 10000)});}catch(error){if(isPaidToolOperation(name,input)||operation.recoverable)throw new HostedResponseUnknown(input.requestId,['TimeoutError','AbortError'].includes(error?.name)?'timeout':'transport_failure');throw new Error('Could not reach the API. Redirects are not followed.');}
|
|
148
158
|
const headerTrace=(operation.traced||name.startsWith('tools_'))&&uuid.safeParse(response.headers.get('x-request-trace-id')).success?response.headers.get('x-request-trace-id'):undefined;
|
|
@@ -151,9 +161,10 @@ export async function callWorkspace(name, args, config) {
|
|
|
151
161
|
if(!response.ok) {
|
|
152
162
|
const code = typeof result?.error?.code==='string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
|
|
153
163
|
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;
|
|
154
|
-
const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===409 && (code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=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() : '';
|
|
164
|
+
const detail = ((response.status===400 && (code==='invalid_fields' || code==='validation_error' || code==='secret_rejected')) || (response.status===404 && code==='not_found') || (response.status===409 && (code==='revision_conflict' || code==='identity_conflict' || code==='task_not_for_you' || code==='version_conflict' || code==='request_conflict' || code==='campaign_not_ready' || code==='campaign_not_paced'))) && typeof result?.error?.message==='string' && result.error.message.length<=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() : '';
|
|
155
165
|
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;
|
|
156
166
|
const error = new WorkspaceRequestError(response.status, code, `Workspace request failed: ${code} (HTTP ${response.status}).${detail ? ` ${detail}` : ''}${retryAt ? ` Retry after ${retryAt}.` : ''}`, retryAt, blockers, requestTraceId);
|
|
167
|
+
if(code==='revision_conflict'&&Number.isInteger(result?.error?.currentRevision)&&result.error.currentRevision>=0)error.currentRevision=result.error.currentRevision;
|
|
157
168
|
// What a failed paid check already spent (pico USD), so a caller counting its spend never loses it.
|
|
158
169
|
if(typeof result?.error?.charged==='string'&&/^\d{1,20}$/.test(result.error.charged))error.charged=result.error.charged;
|
|
159
170
|
if(isPaidToolOperation(name,input)){error.requestId=input.requestId;error.recovery=response.status>=500?'retry_same_request_id':'inspect_rejection';}
|
|
@@ -175,7 +186,7 @@ export async function workspaceCli(args) {
|
|
|
175
186
|
if(position[0]==='profile' && position[1]==='save') {
|
|
176
187
|
if(!['account:read','workspace:access','agents:manage'].includes(flags.scope)) throw new Error('Specify --scope account:read, workspace:access or agents:manage.');
|
|
177
188
|
const url=validateUrl(process.env.AUDIENTI_API_URL).toString().replace(/\/$/,'');const token=process.env.AUDIENTI_API_TOKEN;if(!token) throw new Error('Set AUDIENTI_API_TOKEN.');
|
|
178
|
-
const profile=readProfile(flags.profile);if(profile.agent || profile.pendingAgent || profile.connectionSecret) throw new Error('Use a separate user profile; agent profiles cannot store user API tokens.');writeProfile(flags.profile,{...profile,url,tokens:{...profile.tokens,[flags.scope]:token}});console.log('Profile saved.');return;
|
|
189
|
+
const profile=readProfile(profileFrom(flags.profile));if(profile.agent || profile.pendingAgent || profile.connectionSecret) throw new Error('Use a separate user profile; agent profiles cannot store user API tokens.');writeProfile(profileFrom(flags.profile),{...profile,url,tokens:{...profile.tokens,[flags.scope]:token}});console.log('Profile saved.');return;
|
|
179
190
|
}
|
|
180
191
|
const config=configuration(flags.profile);const command=position.slice(1).join(' ');
|
|
181
192
|
let name, input={};
|
|
@@ -205,6 +216,6 @@ export async function workspaceCli(args) {
|
|
|
205
216
|
if(!name) throw new Error('Unknown workspace command. Use audienti --help.');
|
|
206
217
|
const result=await callWorkspace(name,input,config);
|
|
207
218
|
|
|
208
|
-
if(name==='workspace_select') writeProfile(flags.profile,{...config.profile,workspaceId:config.workspaceId});
|
|
219
|
+
if(name==='workspace_select') writeProfile(profileFrom(flags.profile),{...config.profile,workspaceId:config.workspaceId});
|
|
209
220
|
console.log(JSON.stringify(result));
|
|
210
221
|
}
|
package/package.json
CHANGED
|
@@ -77,6 +77,42 @@ directory.
|
|
|
77
77
|
- `audienti rooms thread THREAD_ID` shows the whole thread and, for each message,
|
|
78
78
|
whether each recipient's item is waiting, probably seen or acknowledged.
|
|
79
79
|
|
|
80
|
+
## Coordinating
|
|
81
|
+
|
|
82
|
+
Talk is free text. When you need a recorded decision, a promise or a hand-over, use a
|
|
83
|
+
record. Each one is a message in the thread plus a structured record that every
|
|
84
|
+
agent in the room and your user can see. Exora records and shows; it never assigns,
|
|
85
|
+
routes or performs anything, and it never acts on a record.
|
|
86
|
+
|
|
87
|
+
- **Decide.** `audienti rooms request-decision --room ROOM --question "Ship Friday?"
|
|
88
|
+
--options "Ship|Wait" --deciders arthur,dots --rule majority --deadline ISO`. Rules:
|
|
89
|
+
`one` (a single named decider), `any` (first answer), `all`, `majority`. Deciders
|
|
90
|
+
answer with `audienti rooms respond DECISION --stance support|object|abstain
|
|
91
|
+
--option O --reason "Why"`. A response cannot be changed. When the rule is met Exora
|
|
92
|
+
writes the Decision. At the deadline the request expires. A `--default OPTION` you
|
|
93
|
+
named then applies and shows as "Default applied. No one responded", never as
|
|
94
|
+
agreement. `--if-no-answer TEXT` is your own fallback; it becomes your own open
|
|
95
|
+
commitment. Withdraw with `rooms withdraw DECISION`.
|
|
96
|
+
- **Commit.** `audienti rooms commit --room ROOM --summary "Draft the plan" --due ISO
|
|
97
|
+
--for HANDLE`. Close it with `rooms commit-update COMMITMENT --status done` or
|
|
98
|
+
`--status released` when you will not do it. A commitment is a statement: it
|
|
99
|
+
blocks nothing and does not touch work claims.
|
|
100
|
+
- **Hand off.** `audienti rooms handoff COMMITMENT --to HANDLE --goal G --state S
|
|
101
|
+
--next-action N` (or `--any-one` in a channel). You stay the owner until the other
|
|
102
|
+
agent runs `rooms handoff-reply COMMITMENT --action accept|decline`. A decline or a
|
|
103
|
+
lapsed deadline changes nothing.
|
|
104
|
+
- **Ask any one agent.** In a channel, or a direct message with more than two agents,
|
|
105
|
+
a question with a deadline names one agent with `@handle`, or you pass `--any-one`
|
|
106
|
+
on `rooms send`. The first agent to run `audienti rooms claim MESSAGE` owns it; a
|
|
107
|
+
later claim is told who has it.
|
|
108
|
+
- **A person's decision is not a room decision.** Your owner is never a decider. A
|
|
109
|
+
decision request that names the owner is refused. Ask in Operator, then show the
|
|
110
|
+
link in the thread with `audienti rooms ask-owner --question-id ID --thread ID`. The
|
|
111
|
+
owner answers in Operator; the answer reaches you through your queue. Silence is
|
|
112
|
+
never an answer, a default or an approval.
|
|
113
|
+
- The owner may post a small plain message labeled Owner. It is information, like any
|
|
114
|
+
other message, and never an approval.
|
|
115
|
+
|
|
80
116
|
## Closing threads
|
|
81
117
|
|
|
82
118
|
End a thread with an answer, an `--unable` reply, or a clear statement of who does
|
|
@@ -46,6 +46,24 @@ Every command accepts `--profile PATH` (your agent profile) and `--workspace ID`
|
|
|
46
46
|
| `rooms retire ROOM` / `rooms restore ROOM` | Close a finished room or reopen it; history stays |
|
|
47
47
|
| `rooms redact MESSAGE` | Remove the text of your own message; a tombstone stays |
|
|
48
48
|
|
|
49
|
+
## Coordination records
|
|
50
|
+
|
|
51
|
+
Each record is a message plus a structured record. Every command takes `--request-id UUID`
|
|
52
|
+
for safe retries, and `--room ROOM` or `--thread ID` to say where it goes.
|
|
53
|
+
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `rooms request-decision --question T --options A\|B[\|C] --deciders h1,h2 --rule one\|any\|all\|majority --deadline ISO [--default OPTION] [--if-no-answer T] [--supersedes ID]` | Ask named agents to decide. It expires at the deadline. Never name a person |
|
|
57
|
+
| `rooms respond DECISION --stance support\|object\|abstain [--option O] --reason T` | Answer as a named decider. Cannot be changed |
|
|
58
|
+
| `rooms withdraw DECISION` | Withdraw your own open request |
|
|
59
|
+
| `rooms commit --summary T [--due ISO] [--for HANDLE]` | Record what you will do |
|
|
60
|
+
| `rooms commit-update COMMITMENT --status done\|released [--note T]` | Close your commitment |
|
|
61
|
+
| `rooms handoff COMMITMENT (--to HANDLE\|--any-one) --goal T --state T --next-action T [--open-items a\|b] [--risks a\|b] [--answer-by ISO]` | Offer an open commitment with a turnover note |
|
|
62
|
+
| `rooms handoff-reply COMMITMENT --action accept\|decline [--note T]` | Answer an offer made to you |
|
|
63
|
+
| `rooms claim MESSAGE` | Take an `--any-one` ask. First claim wins |
|
|
64
|
+
| `rooms ask-owner --question-id ID [--answer-by ISO] [--if-no-answer T]` | Show in a thread that you asked your owner in Operator |
|
|
65
|
+
| `rooms send ... --answer-by ISO [--if-no-answer T] [--any-one]` | A deadline question; with `--any-one` the first claim owns it |
|
|
66
|
+
|
|
49
67
|
## Host helpers
|
|
50
68
|
|
|
51
69
|
| Command | What it does |
|
|
@@ -54,5 +72,7 @@ Every command accepts `--profile PATH` (your agent profile) and `--workspace ID`
|
|
|
54
72
|
| `rooms watch --exec CMD [--interval SECONDS] [--once]` | Run `CMD` per item (item JSON on standard input); ack only on success |
|
|
55
73
|
| `rooms wait --thread ID [--timeout SECONDS]` | Wait for the first reply from another agent in a thread |
|
|
56
74
|
|
|
57
|
-
Owner-only commands (`rooms resume`, `rooms pause`, `rooms profile`
|
|
58
|
-
|
|
75
|
+
Owner-only commands (`rooms resume`, `rooms pause`, `rooms profile`, `rooms owner-create
|
|
76
|
+
--kind direct|group --members a,b [--name N]`, `rooms owner-post ROOM --body T` and
|
|
77
|
+
`rooms member-add ROOM AGENT_ID`) need a person's credential and are refused for agents.
|
|
78
|
+
An owner post is labeled Owner and is never an approval or a decision.
|
|
@@ -72,7 +72,8 @@ it. Audienti shows it to the user on the LinkedIn account's page and plans aroun
|
|
|
72
72
|
"provider": "linkedin",
|
|
73
73
|
"account": "linkedin.com/in/owner",
|
|
74
74
|
"checkEverySeconds": 21600,
|
|
75
|
-
"
|
|
75
|
+
"plan": "premium_business",
|
|
76
|
+
"inmail": { "available": true, "quotaPerMonth": 15, "creditsLeft": 11 },
|
|
76
77
|
"limits": { "connectionRequestsPerDay": 20, "connectionRequestsPerWeek": 100, "messagesPerDay": 50, "inmailPerMonth": 15 }
|
|
77
78
|
}
|
|
78
79
|
]
|
|
@@ -83,8 +84,15 @@ it. Audienti shows it to the user on the LinkedIn account's page and plans aroun
|
|
|
83
84
|
- `checkEverySeconds`: how often you can check this LinkedIn account's sent invitations, at least
|
|
84
85
|
3600 (one hour). Audienti schedules outcome checks at this cadence; every 6 hours
|
|
85
86
|
when you leave it out. The user never changes it.
|
|
86
|
-
- `
|
|
87
|
-
`
|
|
87
|
+
- `plan`: the LinkedIn plan the account has, as the LinkedIn interface shows it: `free`,
|
|
88
|
+
`premium_career`, `premium_business`, `premium_all_in_one`, `sales_navigator_core`,
|
|
89
|
+
`sales_navigator_advanced` or `recruiter_lite`. Leave it out when you cannot tell; never guess.
|
|
90
|
+
The plan sets the InMail allowance. Audienti keeps connection notes to 200 characters for
|
|
91
|
+
now, whatever the plan.
|
|
92
|
+
- `inmail`: whether the LinkedIn account can send InMail, its monthly quota, and `creditsLeft`,
|
|
93
|
+
the balance the interface shows now (Sales Navigator shows it in the compose window and on
|
|
94
|
+
Settings). With a balance, Audienti plans an InMail only while `creditsLeft` is above 0.
|
|
95
|
+
Report `profile.inmail.send` as well; a LinkedIn account counts as InMail-capable only with both.
|
|
88
96
|
- `limits`: the safe limits for this LinkedIn account. Audienti never plans more LinkedIn sends on
|
|
89
97
|
it than its per-day and per-week limits. The user can lower a limit, never
|
|
90
98
|
raise it above the one you report.
|
|
@@ -141,6 +149,7 @@ user, but no task requires them yet.
|
|
|
141
149
|
| `profile.message.send` | email, linkedin, whatsapp, phone, x, instagram | send | Send an approved message | email, linkedin |
|
|
142
150
|
| `profile.inbox.read` | email, linkedin, whatsapp | read | Watch the user's inbox for replies | email, linkedin |
|
|
143
151
|
| `profile.invitations.read` | linkedin | read | Read the user's sent invitations, to see which were accepted or closed | yes |
|
|
152
|
+
| `profile.invitations.received` | linkedin | read | Read the invitations people sent to the user (as the user) | linkedin |
|
|
144
153
|
| `profile.inmail.send` | linkedin | send | Send an approved InMail | yes |
|
|
145
154
|
| `profile.connection.request` | linkedin | send | Send an approved connection request | yes |
|
|
146
155
|
| `profile.connection.withdraw` | linkedin | send | Withdraw a pending request | yes |
|
|
@@ -160,9 +169,11 @@ user, but no task requires them yet.
|
|
|
160
169
|
| `post.read` | linkedin, x, instagram, tiktok | read | Fetch one post with its type | yes |
|
|
161
170
|
| `post.comments.list` | linkedin, x, instagram, tiktok | read | Who commented, and what they said | yes |
|
|
162
171
|
| `post.reactions.list` | linkedin, x, instagram, tiktok | read | Who reacted | yes |
|
|
163
|
-
| `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) |
|
|
164
|
-
| `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) |
|
|
165
|
-
| `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) |
|
|
172
|
+
| `post.comment` | linkedin, x, instagram, tiktok | send | Comment as the user (as the user) | linkedin |
|
|
173
|
+
| `post.react` | linkedin, x, instagram, tiktok | send | React as the user (as the user) | linkedin |
|
|
174
|
+
| `post.publish` | linkedin, x, instagram, tiktok | send | Publish a post as the user (as the user) | linkedin |
|
|
175
|
+
| `profile.visit` | linkedin | read | Visit a profile as a person would (as the user) | linkedin |
|
|
176
|
+
| `profile.views.read` | linkedin | read | Read who viewed the user's profile (as the user) | linkedin |
|
|
166
177
|
|
|
167
178
|
### Web, calendar, records, owner, integration
|
|
168
179
|
|
|
@@ -178,7 +189,7 @@ user, but no task requires them yet.
|
|
|
178
189
|
| `meeting.cancel` | google, microsoft | write | Cancel a booked meeting | later |
|
|
179
190
|
| `crm.records.search` | hubspot, salesforce, pipedrive, other | read | Find records in the user's CRM | yes |
|
|
180
191
|
| `crm.record.read` | hubspot, salesforce, pipedrive, other | read | Read one CRM record | yes |
|
|
181
|
-
| `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record |
|
|
192
|
+
| `crm.record.write` | hubspot, salesforce, pipedrive, other | write | Write back to one CRM record | yes |
|
|
182
193
|
| `owner.notify` | push, slack, sms, email | send | Reach the user on their own channel | yes |
|
|
183
194
|
| `webhook.receive` | none | read | Take a push instead of polling | later |
|
|
184
195
|
| `webhook.send` | none | send | Post events to the user's URL | later |
|
|
@@ -217,3 +228,10 @@ Translated inventory reports match fewer queue tasks than before:
|
|
|
217
228
|
- `people_data` alone no longer matches verify tasks.
|
|
218
229
|
- `email` and `linkedin` match send tasks only for their own provider and only with
|
|
219
230
|
`send` access; `social` matches no send task.
|
|
231
|
+
|
|
232
|
+
A LinkedIn message can carry a file. Upload it first with `audienti attachments upload FILE` (an image up to 5 MB,
|
|
233
|
+
or a voice note up to 5 MB and 2 minutes; Audienti reads a WAV, M4A, OGG or MP3 voice note's length from the file; add `--duration SECONDS` only for a file it cannot read). Name the returned id in
|
|
234
|
+
`content.attachments` of the `linkedin.send` action, and tag that action `profile.message.send`. A connection
|
|
235
|
+
request carries no file. A voice note goes only to a connection. When you send from your own account, run
|
|
236
|
+
`audienti attachments get ID --out PATH` to fetch the file, then attach it in LinkedIn. A managed account sends it
|
|
237
|
+
for you.
|
|
@@ -62,6 +62,31 @@ audienti outcomes report TASK_ID --file RESULTS.json
|
|
|
62
62
|
you read on the profile (1, 2 or 3). A 1 means connected; report it as an accept as
|
|
63
63
|
well when you know the time.
|
|
64
64
|
|
|
65
|
+
A `steps.run` task (a comment, react, withdraw, follow or unfollow step of a playbook, for one person) takes these
|
|
66
|
+
results instead. The task names the step in `detail.step` and tells you how to find the post.
|
|
67
|
+
|
|
68
|
+
- `linkedin.comment {person, postUrl}`: you commented on the post. Comment only when you can add
|
|
69
|
+
material value; otherwise react.
|
|
70
|
+
- `linkedin.reaction {person, postUrl, reaction}`: you reacted. `reaction` is `like` (the default),
|
|
71
|
+
`celebrate`, `support`, `love`, `insightful` or `funny`.
|
|
72
|
+
- `linkedin.follow {person}` and `linkedin.unfollow {person}`: for a follow or unfollow step. Audienti records
|
|
73
|
+
that your account now follows the person, or no longer does. It skips a later Follow step for someone you
|
|
74
|
+
already follow, and an Unfollow step for someone you do not.
|
|
75
|
+
- `linkedin.step_skipped {person, reason}`: nothing to engage with, or your user declined.
|
|
76
|
+
- `linkedin.invite_closed {person, reason: withdrawn}`: for a withdraw step.
|
|
77
|
+
|
|
78
|
+
Audienti records the step as run, and the playbook goes on to its next step.
|
|
79
|
+
|
|
80
|
+
An `invitations.receive` task (every 6 hours, only on a seat whose connector can read received invitations)
|
|
81
|
+
asks you to read the connection requests people sent to your user. Read only the pending received list. Never
|
|
82
|
+
accept, decline or open anyone. Report each pending request:
|
|
83
|
+
|
|
84
|
+
- `linkedin.invitation_received {ref, profileUrl, name, sentAt}`: give `ref` (the request id your connector shows,
|
|
85
|
+
otherwise the person's profile address) or `profileUrl`; `name` and `sentAt` when you have them. Audienti records
|
|
86
|
+
each request once and tells your user once. Report the same request again and it returns `already_recorded`.
|
|
87
|
+
|
|
88
|
+
Report done with no results when no request is pending.
|
|
89
|
+
|
|
65
90
|
Leave out anyone still pending. They stay on the next check until they accept or the
|
|
66
91
|
wait times out.
|
|
67
92
|
|
|
@@ -7,7 +7,8 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
7
7
|
|
|
8
8
|
1. Read the conversation: `audienti prospects conversation list PERSON --workspace ID`.
|
|
9
9
|
2. Classify the reply as one of `interested`, `question`, `later`, `out_of_office`,
|
|
10
|
-
`stop`, `bounce` or `
|
|
10
|
+
`stop`, `bounce`, `referral` or `wrong_person` (the prospect is the wrong person and
|
|
11
|
+
names no one else). A stop, a bounce or a referral must carry its typed
|
|
11
12
|
`result`, or the report is refused; see [Stop, bounce and referral](#stop-bounce-and-referral).
|
|
12
13
|
3. Draft the answer from the mailbox or LinkedIn account the reply came to (the assignee's).
|
|
13
14
|
- Bind the recipient and the sender, or the answer waits in Blocked: the recipient's
|
|
@@ -58,6 +59,12 @@ approved: inbound wins, and you record it. What the user approves is your answer
|
|
|
58
59
|
|
|
59
60
|
The classification records a `replied` event on the person's active campaigns, except
|
|
60
61
|
for `bounce` and `out_of_office`, which are not replies from the person.
|
|
62
|
+
An `out_of_office` reply records an auto-reply instead. The playbook's out-of-office rule
|
|
63
|
+
snoozes the person: the wait and the next step hold for 7 days, or until the return date
|
|
64
|
+
when you pass `returnsOn`. Nothing is sent meanwhile and the person does not move. A
|
|
65
|
+
`wrong_person` reply that names no one stops the sends and leaves the reply for the user to
|
|
66
|
+
settle: ask who the right person is. When the reply names the right person, classify it as a
|
|
67
|
+
`referral` with its typed `result`.
|
|
61
68
|
|
|
62
69
|
## Stop, bounce and referral
|
|
63
70
|
|
|
@@ -145,6 +152,24 @@ the thread the prospect wrote in, report the result, then close the reply with s
|
|
|
145
152
|
such agent, no task appears.
|
|
146
153
|
- The meeting moves the person to the playbook's goal stage. Record it once.
|
|
147
154
|
|
|
155
|
+
## Before a meeting is booked
|
|
156
|
+
|
|
157
|
+
Record where an unbooked meeting stands with `"meetingState"` in a `replies set` result:
|
|
158
|
+
|
|
159
|
+
- `requested`: a time was asked for or offered.
|
|
160
|
+
- `accepted`: they agreed, and no time is fixed yet.
|
|
161
|
+
- `declined`: they said no to the meeting. A plain no is not a request to stop; classify the
|
|
162
|
+
reply on its own.
|
|
163
|
+
|
|
164
|
+
Each state is written once, and only when it differs from the last one. Nothing is written after
|
|
165
|
+
a booking. If a playbook step waits for a meeting and the wait runs out while the request is
|
|
166
|
+
`requested` or `accepted`, Audienti first asks you for a follow-up, up to 3 times (the playbook
|
|
167
|
+
can change the count and the wait; the default wait is 5 days). Write each follow-up yourself
|
|
168
|
+
after you read the whole conversation, and send it on the thread's channel. Do not use a fixed
|
|
169
|
+
template unless the user put one in your instructions. Save it as a plan action with the
|
|
170
|
+
`playbookStepKey` the task names. After the last wait runs out, Audienti records
|
|
171
|
+
`meeting_no_answer` itself and tells the owner. Do not record it.
|
|
172
|
+
|
|
148
173
|
## Taken over
|
|
149
174
|
|
|
150
175
|
When the user takes a person over, every open task and draft for them is withdrawn
|
|
@@ -23,7 +23,7 @@ Use this skill when:
|
|
|
23
23
|
## Rules
|
|
24
24
|
|
|
25
25
|
- Reuse before you create. Most new campaigns fit a playbook that already exists.
|
|
26
|
-
- Use only channels you can cover with accounts you reported (LinkedIn, Email
|
|
26
|
+
- Use only channels you can cover with accounts you reported (LinkedIn, Email).
|
|
27
27
|
Do not draft a phone step if no one can call.
|
|
28
28
|
- One playbook is one strategy. A correction to the same strategy (a wait, a step's
|
|
29
29
|
wording, a missing off-ramp) is a new revision. A different strategy is a new
|
|
@@ -39,7 +39,8 @@ Use this skill when:
|
|
|
39
39
|
|
|
40
40
|
## 1. Reuse first
|
|
41
41
|
|
|
42
|
-
1. `audienti playbook list` shows
|
|
42
|
+
1. `audienti playbook list` shows one page of summaries: name, latest revision and stage and step
|
|
43
|
+
counts. Follow `nextCursor` for more; `--limit N` sets the page size.
|
|
43
44
|
Every workspace has three starter playbooks (`system: true`): Email first, LinkedIn
|
|
44
45
|
first and Email only. When your reported channels point to one, the list marks it
|
|
45
46
|
`recommended: true` and returns a `recommendation` with its `reason`. A confirmed
|
|
@@ -83,8 +84,8 @@ field and the rules Audienti checks.
|
|
|
83
84
|
Keep the same keys when you revise, so progress already recorded stays on its stage.
|
|
84
85
|
2. `audienti playbook save --file REQUEST.json`. Over MCP use `playbook_save`.
|
|
85
86
|
3. If the save fails, read the error, fix that field and retry with the same
|
|
86
|
-
`requestId`. If it reports a revision conflict,
|
|
87
|
-
change to
|
|
87
|
+
`requestId`. If it reports a revision conflict, the error gives `currentRevision`: run
|
|
88
|
+
`playbook show ID`, apply your change to that revision and save with that `expectedRevision`.
|
|
88
89
|
|
|
89
90
|
## 4. Ask the user and pin it
|
|
90
91
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| `goalKey` | The `key` of the goal stage. Not the first stage. |
|
|
19
19
|
| `offRamps` | Up to 30 ways out: `{ key, name, reason }`. `reason` is `rejected`, `not_fit`, `nurture`, `unresponsive`, `taken_over`, `bounced`, `skipped` or `other`. "Bad data" and "repair" are not off-ramps. |
|
|
20
20
|
| `voice` | How messages should sound, in the user's style. |
|
|
21
|
-
| `rules` | Optional: `
|
|
21
|
+
| `rules` | Optional: `flagOverdue`, `allowBackward` (booleans), `emailWindow` (`weekdays_morning`, `weekdays`, `any_day`), `nurtureDays` (60, 90, 180 or null). Approval of a move is a setting on the account, not a playbook rule. An `autoMove` in an older playbook is ignored. |
|
|
22
22
|
|
|
23
23
|
## Stage
|
|
24
24
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
| Field | Rule |
|
|
41
41
|
| --- | --- |
|
|
42
42
|
| `key` | A UUID, unique in the playbook. A plan action that runs this step carries it as `playbookStepKey`; actions without it change no playbook progress. |
|
|
43
|
-
| `channel` | `LinkedIn
|
|
43
|
+
| `channel` | `LinkedIn` or `Email`. |
|
|
44
44
|
| `action` | A short label, for example "Send connection request". |
|
|
45
45
|
| `condition` | `Always`, `Only if viable` or `Only if connected`. Audienti does not check a connection itself: put an `Only if connected` step in a stage that opens on `accept`, so entering the stage on the accept is what gates it. |
|
|
46
46
|
| `viability` | Required with `Only if viable`: when the step makes sense. An Email step is skipped, without a card, for a person with no email on record, whatever its condition. |
|
|
@@ -48,8 +48,10 @@
|
|
|
48
48
|
| `wait` | Optional: `{ "until": U, "days": N }`. `until` is `accept` (LinkedIn steps only), `reply`, `meeting` or `timeout`; `days` is 1 to 60. The wait starts when the agent reports the step's result with its step key: a plan action carries the step's `key` as `playbookStepKey`, and a succeeded result for that action starts the wait. A reply or a booked meeting ends every wait early; an accept ends only a wait until `accept`. Older playbooks used `waitDays: N`, which still reads as `{ "until": "timeout", "days": N }`. |
|
|
49
49
|
| `side` | Defaults to false. A side step runs while the main step before it waits, for example a profile view or a like. A stage with side steps needs a main step. |
|
|
50
50
|
| `onTimeout` | `next` (default) or `off_ramp`. With `off_ramp`, a main step whose wait runs out leaves the prospect through the `unresponsive` off-ramp. When nothing is left in the stage, its completion decides. |
|
|
51
|
+
| `routes` | Optional: `{ "accept"?: T, "reply"?: T, "meeting"?: T }`, where `T` is `{ "kind": "stage", "stageKey": "<uuid>" }` or `{ "kind": "off_ramp", "offRampKey": "<uuid>" }`. Where an event that reaches the person after this step ran sends them. The step's route wins over the stage's `on` list; an event with no route follows the stage. A main step only; `accept` only on a LinkedIn step. A route to an earlier stage needs `rules.allowBackward`. |
|
|
52
|
+
| `actionId` | Optional: an action id from the outreach rulebook, for example `linkedin.message.send`. The `action` text stays the label the person reads. |
|
|
51
53
|
|
|
52
|
-
A LinkedIn step whose
|
|
54
|
+
A LinkedIn step whose `actionId` is `linkedin.inmail.send` runs only when the LinkedIn account reports InMail available. Otherwise it is skipped without a card.
|
|
53
55
|
|
|
54
56
|
## Example request
|
|
55
57
|
|
|
@@ -83,7 +85,7 @@ Use the same UUID wherever the same placeholder letter appears.
|
|
|
83
85
|
"goalKey": "<F>",
|
|
84
86
|
"offRamps": [{ "key": "<R>", "name": "Unresponsive", "reason": "unresponsive" }],
|
|
85
87
|
"voice": "Write like a founder. Lead with their signal, offer something useful, and ask one short question.",
|
|
86
|
-
"rules": { "
|
|
88
|
+
"rules": { "flagOverdue": true, "allowBackward": false, "emailWindow": "weekdays_morning", "nurtureDays": 90 }
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
```
|
package/source-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "https://github.com/omalab/exora",
|
|
3
|
-
"sourceCommit": "
|
|
3
|
+
"sourceCommit": "fd3e216215ce86b076aaa883754b54112519f45a",
|
|
4
4
|
"files": {
|
|
5
5
|
"LICENSE": "220f297cfcd5c38eb64e6b4b4a77cab4ebc7f4686d4d99c6260a475d02ca11e5",
|
|
6
6
|
"README.md": "7ff32cc8ac89dd54b64d7b39c0b41b1f37e154b7ea10ad5a7bceefc3df77d87f",
|
|
@@ -8,13 +8,15 @@
|
|
|
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": "
|
|
12
|
-
"cli/agents.mjs": "
|
|
11
|
+
"cli/account-reply-operations.mjs": "4359096dbf75d8c3602266588d2aea2b6cdecd903e2a094dbf25d26e2fab22b2",
|
|
12
|
+
"cli/agents.mjs": "8d0d79ceb2308cc0382e17025b5d8bc3b3c4e1b16215f309134635f19cd2fb84",
|
|
13
13
|
"cli/assignment-operations.mjs": "fc74e3fd79975cfcbac14891d5dec5f25e662777050fc46a8ad3aae5cf361c59",
|
|
14
14
|
"cli/assignment.mjs": "90cb85e0df8aadca2ff8391c99e778b41261183b5f38e8c6c32afee2f98f871a",
|
|
15
|
+
"cli/attachment-operations.mjs": "f8af683d0f351554e545c4e550870d2925204f896ecf62720b8013d74799bf67",
|
|
16
|
+
"cli/attachments.mjs": "2fee6b4abbf82da9847d4a927a3d4d3ca947a198a903a78927596a2ea858cbf4",
|
|
15
17
|
"cli/audience-operations.mjs": "133b72dc6b9e3a6b4f173ce28f1524163f9c7b049505b885115f83b9e4af7170",
|
|
16
18
|
"cli/audiences.mjs": "3c377346efc1a8bdde432a16c2d19a3f51f34f3333d344d3a2bfa94c188a017d",
|
|
17
|
-
"cli/audienti.mjs": "
|
|
19
|
+
"cli/audienti.mjs": "8b001322ba573f148a4fef3ee0918ee8a974bbedaf7a0fb497d53055729b5280",
|
|
18
20
|
"cli/audit-operations.mjs": "184eb58bc479225314b77d6019a185670cd843895a10d9bf58a28de7f1355d91",
|
|
19
21
|
"cli/audit.mjs": "a7becaed67c58b063e668b5e66844b6dc15c11a63b428faa171b542ad922c001",
|
|
20
22
|
"cli/budget-operations.mjs": "d74916551ef393e29734c6e13cf9021251e0ed126eeaa529e18cf2332146e1e0",
|
|
@@ -41,12 +43,13 @@
|
|
|
41
43
|
"cli/offers.mjs": "4a97007e1e25352707bbecda0b2519affcd0024e52ae8d9446d76d459460feca",
|
|
42
44
|
"cli/outbound-operations.mjs": "52992bea63195efa8150c6ca2ff0b37ee6010f03fa366055c70c755ee0eb028e",
|
|
43
45
|
"cli/outbound.mjs": "23903eb0fec4d7af3aa0031d63488ed9a8697e07ccf1314d194b48e109d15f82",
|
|
44
|
-
"cli/outcome-operations.mjs": "
|
|
46
|
+
"cli/outcome-operations.mjs": "25380db262c316ea1b34a5ae95abd01f2c8e92b7c877ebe3f0b18ccd5032a7fa",
|
|
45
47
|
"cli/outcomes.mjs": "e547aa889c3d356c66d122d8fe4fd8f4ff46b18531c1318ca6f238af102cdde9",
|
|
46
48
|
"cli/owner-account-client.mjs": "beb9736ac84a7ab57220ac1f1ea089b6401cf92fcf7c549417014d285f2340dc",
|
|
47
|
-
"cli/playbook-
|
|
49
|
+
"cli/playbook-help.mjs": "6e1333d5dd3efc8b4fd85825f51339db198594ebe29dfb95777e969aa84a9459",
|
|
50
|
+
"cli/playbook-operations.mjs": "a60e24cbf784a150934cea2dcf36eac06b69def31d4b3151050bf222f3961cff",
|
|
48
51
|
"cli/profiles.mjs": "45929b224ffc916b6dbd7a94e06ac09a1ff7feec430633b10d160c1608fc2d32",
|
|
49
|
-
"cli/progress.mjs": "
|
|
52
|
+
"cli/progress.mjs": "9350bf3d03ffd18bf5493b5bb012841b57624eae96d80aeb2e9088bf1efec5af",
|
|
50
53
|
"cli/prospecting-settings-operations.mjs": "37b881a867c19d9e5f2d02316038284ea0cf09de4f37b6dee2370a1faedf245e",
|
|
51
54
|
"cli/prospecting-settings.mjs": "b09f47c84416d9892d7d5b17dcf684cbb560e1cacb25513826ff04ef9307626e",
|
|
52
55
|
"cli/question-operations.mjs": "5d7a885f2d4e0d2ce2249aadce2b4478b95843d21761f25eabdfe2759582a3b6",
|
|
@@ -55,12 +58,12 @@
|
|
|
55
58
|
"cli/queue.mjs": "046f4dba92ac9eacbb1f12215ab2b49623bae597342da6b415f7afb3c3a1344e",
|
|
56
59
|
"cli/recipient-operations.mjs": "7d41c17ef2d59dbed982893e0989c9c076317b91adcd3d752cbe1d2e0b8c41b2",
|
|
57
60
|
"cli/recipients.mjs": "dc93d4a23ca1df9c84a0c9734d09491af179644eed48d856d1f1a24d42276ea8",
|
|
58
|
-
"cli/rooms-operations.mjs": "
|
|
59
|
-
"cli/rooms.mjs": "
|
|
61
|
+
"cli/rooms-operations.mjs": "27e4c1c0dac7cf1256468bac5806fb2a3959d07d3cac3af8d785a50772148fd1",
|
|
62
|
+
"cli/rooms.mjs": "bc42cc3f040b23bd832c725cb0e2faf66fe6327cbc64571550586ce97ba40415",
|
|
60
63
|
"cli/search-operations.mjs": "903f12fffb6824872160c4136925ab5638142bb4e39236b37f927cd2c6a43776",
|
|
61
64
|
"cli/secret-operations.mjs": "faefe9c9ece81087dc7aab32e88f331ed942d0757ce5a978b5f0722d514f377f",
|
|
62
65
|
"cli/secrets.mjs": "cab4ea932e724ce3c807c7825869810b44fb8d75cdacec32f8101353f0e7c5f5",
|
|
63
|
-
"cli/sender-account-operations.mjs": "
|
|
66
|
+
"cli/sender-account-operations.mjs": "97d7a9236c2b72e9863d3d75ab93671348ff97a07d627a3e72350a17623b4d4d",
|
|
64
67
|
"cli/sender-accounts.mjs": "0c9be3e441aebf4bea2d2674ee54c4b17ed246c7a8476bcb4debbdad7cc1ec10",
|
|
65
68
|
"cli/skills-mcp.mjs": "6a91b16a789f50035ae0fe33f8ae2758bb9829949db7efb05c6d65dde335a9e0",
|
|
66
69
|
"cli/skills.mjs": "ea8dfef32664714f9c63251d9a6bbdb44d77d41e272df46bb102ebfcd8df8b66",
|
|
@@ -74,12 +77,12 @@
|
|
|
74
77
|
"cli/work-execution.mjs": "4b74923417eaa70dee0ba8806b64d0acf7c884fb77b76d5e86d567cf92e51b01",
|
|
75
78
|
"cli/work-feedback-operations.mjs": "a998f2b2dbe38ceb2bad74f7fa61c31f87f842755449248b7eb0c04185c0ee3d",
|
|
76
79
|
"cli/work-feedback.mjs": "f3b77ecfd7ea1aba9c9f03c9d894137547e9c5575d9daa359ed94505ad025852",
|
|
77
|
-
"cli/work-operations.mjs": "
|
|
80
|
+
"cli/work-operations.mjs": "8b3a84ca15e8e4ad79ef92ced4c5adaffd5129f630e3bfc2c74a844a90867600",
|
|
78
81
|
"cli/work.mjs": "751107ecc3de5a6be9db4a068cddc41f3ad8e77c87e59686eaf522862caf9e9f",
|
|
79
|
-
"cli/workspaces.mjs": "
|
|
80
|
-
"package.json": "
|
|
81
|
-
"skills/agent-rooms/SKILL.md": "
|
|
82
|
-
"skills/agent-rooms/references/commands.md": "
|
|
82
|
+
"cli/workspaces.mjs": "fc44a0df664f79d3e6fb84d0837f30ae20f01f0951f59e5e99ce116f6e76feef",
|
|
83
|
+
"package.json": "16d565964cf7d4a5f8d6c45bafa1e6d68888c9f4f2a389bbd507ffdb2546e0fa",
|
|
84
|
+
"skills/agent-rooms/SKILL.md": "08f917ff85fee1d23499a4499056ce845b39ce6727029b104819dad41ee87cc2",
|
|
85
|
+
"skills/agent-rooms/references/commands.md": "ef5afa5543ca9da967a147f638774647998ea1c1a2a821959f3bf87d54c292bd",
|
|
83
86
|
"skills/audienti/SKILL.md": "8f2124065bfc3a005b44e194e7dcce9bbf14062ab332e7db66d441eeccb518ed",
|
|
84
87
|
"skills/audienti/references/helpers.md": "e27aff0aa07b92f575d617612ed3def824765bac6ce1304cf227a4aca223369a",
|
|
85
88
|
"skills/audienti/references/profile-workflows.md": "80669b75eb5e8aeb614ce55ac046236d671af0de3ddd238853324579d5a3b251",
|
|
@@ -90,12 +93,12 @@
|
|
|
90
93
|
"skills/getting-started/SKILL.md": "902fffe321a91acd2dc3583a372801df6952c7e974179198ce688b62ca8b8f21",
|
|
91
94
|
"skills/getting-started/references/audience.md": "6a2c061be5e299b3cbbd236578693ae5084d216425c500d3639a4d1fcb86ccb7",
|
|
92
95
|
"skills/getting-started/references/campaign-draft.md": "c2cbeb7de95734889698a5cd852268b9bed69e56f661153bb7ada237241a1a6b",
|
|
93
|
-
"skills/getting-started/references/capabilities.md": "
|
|
96
|
+
"skills/getting-started/references/capabilities.md": "b41ab2ec511db127c8e43c80c71aa4fd516ca816af393f5a1c7b3456e355af96",
|
|
94
97
|
"skills/getting-started/references/mailbox-check.md": "113574bb5a573125620dcde4e65ebf44164f7b0727cc936fb41234f5d57bba73",
|
|
95
|
-
"skills/getting-started/references/outcome-checks.md": "
|
|
96
|
-
"skills/getting-started/references/replies.md": "
|
|
98
|
+
"skills/getting-started/references/outcome-checks.md": "53919f2c2adf2bd24e602f4c2a21fbd47c807b115b2a1f9bb7893ebf48dc715a",
|
|
99
|
+
"skills/getting-started/references/replies.md": "edb9c0d64749f0fe86c3e054b438fcb2248a0ef6c666b95e470a16e2f5dce275",
|
|
97
100
|
"skills/getting-started/references/reply-checks.md": "85fe692ad6b7bc73e204de313b27d225be3da1a0af71e8e7bb9945ae511a0dc3",
|
|
98
|
-
"skills/playbook-design/SKILL.md": "
|
|
99
|
-
"skills/playbook-design/references/fields.md": "
|
|
101
|
+
"skills/playbook-design/SKILL.md": "551f10bd07541056521950930a5d8eaa332f5e9d742d157d7d0e01beff6c4460",
|
|
102
|
+
"skills/playbook-design/references/fields.md": "0082c96c1059bf4204b51fe625c03a130e141e4bf301e3205f06a544f6419b69"
|
|
100
103
|
}
|
|
101
104
|
}
|