@audienti/cli 0.1.144 → 0.2.0-exora.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -902
- package/bin/audienti-mcp.mjs +2 -0
- package/cli/account-intake-operations.mjs +28 -0
- package/cli/account-intake.mjs +33 -0
- package/cli/account-replies.mjs +35 -0
- package/cli/account-reply-operations.mjs +30 -0
- package/cli/agents.mjs +107 -0
- package/cli/approach-operations.mjs +12 -0
- package/cli/assignment-operations.mjs +218 -0
- package/cli/assignment.mjs +116 -0
- package/cli/audience-operations.mjs +16 -0
- package/cli/audiences.mjs +26 -0
- package/cli/audienti.mjs +124 -0
- package/cli/connection-registry-operations.mjs +7 -0
- package/cli/content.mjs +29 -0
- package/cli/discovery-operations.mjs +27 -0
- package/cli/discovery.mjs +14 -0
- package/cli/experiment-operations.mjs +25 -0
- package/cli/experiments.mjs +26 -0
- package/cli/json-file.mjs +13 -0
- package/cli/mcp.mjs +47 -0
- package/cli/offer-operations.mjs +12 -0
- package/cli/offers.mjs +24 -0
- package/cli/owner-account-client.mjs +18 -0
- package/cli/profiles.mjs +29 -0
- package/cli/progress.mjs +12 -0
- package/cli/prospecting-settings-operations.mjs +16 -0
- package/cli/prospecting-settings.mjs +15 -0
- package/cli/recipient-operations.mjs +13 -0
- package/cli/recipients.mjs +30 -0
- package/cli/sender-account-operations.mjs +24 -0
- package/cli/sender-accounts.mjs +23 -0
- package/cli/skills-mcp.mjs +24 -0
- package/cli/skills.mjs +56 -0
- package/cli/suppression-operations.mjs +8 -0
- package/cli/suppression.mjs +12 -0
- package/cli/tools-operations.mjs +70 -0
- package/cli/tools.mjs +72 -0
- package/cli/work-execution-operations.mjs +35 -0
- package/cli/work-execution.mjs +29 -0
- package/cli/work-feedback-operations.mjs +17 -0
- package/cli/work-feedback.mjs +18 -0
- package/cli/work-operations.mjs +28 -0
- package/cli/work.mjs +27 -0
- package/cli/workspaces.mjs +158 -0
- package/package.json +15 -26
- package/skills/audienti/SKILL.md +61 -323
- package/skills/audienti/references/helpers.md +149 -0
- package/skills/audienti/references/profile-workflows.md +212 -0
- package/skills/audienti/references/workflow-receipt.md +49 -0
- package/skills/audienti/scripts/contracts.mjs +72 -0
- package/skills/audienti/scripts/profile-helpers.mjs +138 -0
- package/skills/audienti/scripts/run.mjs +40 -0
- package/source-manifest.json +59 -0
- package/.mcp.json +0 -13
- package/CHANGELOG.md +0 -1012
- package/bin/audienti-mcp.js +0 -5
- package/bin/audienti.js +0 -6
- package/install +0 -83
- package/skills/audienti/references/methodology.md +0 -111
- package/skills/audienti-gift-research/SKILL.md +0 -47
- package/src/agent-skills.js +0 -60
- package/src/api-client.js +0 -1639
- package/src/cli.js +0 -16992
- package/src/config.js +0 -130
- package/src/mcp.js +0 -103
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const uuid = z.string().uuid();
|
|
3
|
+
// JSON Schema counts Unicode code points. Domain validation and normalization stay
|
|
4
|
+
// server-owned; z.string().max() would instead reject valid astral text by UTF-16 length.
|
|
5
|
+
const bounded = maxLength => z.string().meta({ maxLength });
|
|
6
|
+
const dateTime = () => z.string().meta({ format: 'date-time' });
|
|
7
|
+
const identity = z.union([
|
|
8
|
+
z.object({ kind: z.literal('person'), provider: bounded(64), providerId: bounded(256) }).strict(),
|
|
9
|
+
z.object({ kind: z.literal('person'), provider: bounded(64), url: bounded(2048) }).strict(),
|
|
10
|
+
]);
|
|
11
|
+
const target = z.union([
|
|
12
|
+
z.object({ kind: z.literal('existing'), prospectId: uuid }).strict(),
|
|
13
|
+
z.object({ kind: z.literal('create'), identity, displayName: bounded(200).optional(), profileObservedAt: dateTime().nullable().optional() }).strict(),
|
|
14
|
+
]);
|
|
15
|
+
const payload = z.object({
|
|
16
|
+
direction: z.enum(['inbound', 'outbound']), conversationType: z.enum(['direct', 'public_thread']), occurredAt: dateTime(),
|
|
17
|
+
subject: bounded(512).nullable(), body: bounded(32000).nullable(),
|
|
18
|
+
participants: z.array(z.object({ role: z.enum(['sender', 'recipient', 'cc', 'bcc']), value: bounded(320) }).strict()).max(20).nullable(),
|
|
19
|
+
}).strict();
|
|
20
|
+
const item = z.object({ source: z.object({ provider: bounded(64), accountRef: bounded(200), itemId: bounded(256), threadId: bounded(256).nullable().optional() }).strict(), payload }).strict();
|
|
21
|
+
const body = ({ workspaceId, prospectId, ...rest }) => rest;
|
|
22
|
+
const root = a => `/workspaces/${a.workspaceId}/prospects/${a.prospectId}/conversation-evidence`;
|
|
23
|
+
export const accountIntakeOperations = {
|
|
24
|
+
workspace_prospect_intake: { schema: z.object({ workspaceId: uuid.optional(), requestId: uuid, target, item }).strict(), method: 'POST', scoped: true, path: a => `/workspaces/${a.workspaceId}/prospects/intake`, body },
|
|
25
|
+
workspace_conversation_append: { schema: z.object({ workspaceId: uuid.optional(), prospectId: uuid, requestId: uuid, item }).strict(), method: 'POST', scoped: true, path: root, body },
|
|
26
|
+
workspace_conversation_list: { schema: z.object({ workspaceId: uuid.optional(), prospectId: uuid, cursor: uuid.optional(), limit: z.number().int().min(1).max(100).optional() }).strict(), method: 'GET', scoped: true,
|
|
27
|
+
path: a => { const q = new URLSearchParams(); for (const k of ['cursor', 'limit']) if (a[k] !== undefined) q.set(k, String(a[k])); return root(a) + (q.size ? '?' + q : ''); } },
|
|
28
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
3
|
+
export async function accountIntakeCli(args) {
|
|
4
|
+
const flags = {}, positions = [];
|
|
5
|
+
for (let i = 0; i < args.length; i++) {
|
|
6
|
+
const arg = args[i]; if (arg === '--json') continue;
|
|
7
|
+
if (!arg.startsWith('--')) { positions.push(arg); continue; }
|
|
8
|
+
const key = arg.slice(2);
|
|
9
|
+
if (!['workspace', 'profile', 'file', 'cursor', 'limit'].includes(key) || Object.hasOwn(flags, key) || !args[i+1] || args[i+1].startsWith('--')) throw Error('Unknown, repeated or incomplete intake option.');
|
|
10
|
+
flags[key] = args[++i];
|
|
11
|
+
}
|
|
12
|
+
const intake = positions[1] === 'intake';
|
|
13
|
+
const command = intake ? 'intake' : positions[2];
|
|
14
|
+
if (positions[0] !== 'prospects' || (!intake && positions[1] !== 'conversation') || !['intake', 'append', 'list'].includes(command) || positions.length !== (intake ? 2 : 4)) throw Error('Use prospects intake or prospects conversation append|list PERSON --workspace ID.');
|
|
15
|
+
const allowed = ['workspace', 'profile', ...(command === 'list' ? ['cursor', 'limit'] : ['file'])];
|
|
16
|
+
if (Object.keys(flags).some(k => !allowed.includes(k))) throw Error('Option does not apply to this intake command.');
|
|
17
|
+
let input = {};
|
|
18
|
+
if (command !== 'list') {
|
|
19
|
+
if (!flags.file) throw Error('--file INPUT.json is required.');
|
|
20
|
+
let raw; try { raw = readFileSync(flags.file); } catch { throw Error('Could not read the intake input file.'); }
|
|
21
|
+
if (raw.length > 65536) throw Error('payload_too_large');
|
|
22
|
+
try { input = JSON.parse(raw.toString('utf8')); } catch { throw Error('validation_error'); }
|
|
23
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || ['workspaceId', 'prospectId'].some(k => Object.hasOwn(input, k))) throw Error('invalid_fields');
|
|
24
|
+
}
|
|
25
|
+
input.workspaceId = flags.workspace;
|
|
26
|
+
if (!intake) input.prospectId = positions[3];
|
|
27
|
+
if (flags.cursor !== undefined) input.cursor = flags.cursor;
|
|
28
|
+
if (flags.limit !== undefined) {
|
|
29
|
+
if (!/^\d+$/.test(flags.limit)) throw Error('invalid_fields');
|
|
30
|
+
input.limit = Number(flags.limit);
|
|
31
|
+
}
|
|
32
|
+
console.log(JSON.stringify(await callWorkspace({ intake: 'workspace_prospect_intake', append: 'workspace_conversation_append', list: 'workspace_conversation_list' }[command], input, configuration(flags.profile))));
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { callWorkspace, configuration } from './workspaces.mjs';
|
|
3
|
+
export async function accountRepliesCli(args) {
|
|
4
|
+
const flags = {}, positions = [];
|
|
5
|
+
for (let i = 0; i < args.length; i++) {
|
|
6
|
+
const arg = args[i];
|
|
7
|
+
if (arg === '--json') continue;
|
|
8
|
+
if (!arg.startsWith('--')) { positions.push(arg); continue; }
|
|
9
|
+
const key = arg.slice(2);
|
|
10
|
+
if (!['workspace', 'profile', 'file', 'evidence', 'cursor', 'limit', 'history-cursor', 'history-limit', 'status'].includes(key) || Object.hasOwn(flags, key) || !args[i+1] || args[i+1].startsWith('--')) throw Error('Unknown, repeated or incomplete reply option.');
|
|
11
|
+
flags[key] = args[++i];
|
|
12
|
+
}
|
|
13
|
+
const command = positions[2];
|
|
14
|
+
if (positions[0] !== 'prospects' || positions[1] !== 'replies' || !['set', 'get', 'list'].includes(command) || positions.length !== 4) throw Error('Use prospects replies set|get|list PERSON --workspace ID.');
|
|
15
|
+
const allowed = ['workspace', 'profile', ...(command === 'set' ? ['file', 'evidence'] : command === 'get' ? ['evidence', 'history-cursor', 'history-limit'] : ['cursor', 'limit', 'status'])];
|
|
16
|
+
if (Object.keys(flags).some(k => !allowed.includes(k))) throw Error('Option does not apply to this reply command.');
|
|
17
|
+
if (command !== 'list' && !flags.evidence) throw Error('--evidence EVIDENCE is required.');
|
|
18
|
+
let input = {};
|
|
19
|
+
if (command === 'set') {
|
|
20
|
+
if (!flags.file) throw Error('--file INPUT.json is required.');
|
|
21
|
+
let raw; try { raw = readFileSync(flags.file); } catch { throw Error('Could not read the reply input file.'); }
|
|
22
|
+
if (raw.length > 65536) throw Error('payload_too_large');
|
|
23
|
+
try { input = JSON.parse(raw.toString('utf8')); } catch { throw Error('validation_error'); }
|
|
24
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || ['workspaceId', 'prospectId', 'evidenceId'].some(k => Object.hasOwn(input, k))) throw Error('invalid_fields');
|
|
25
|
+
}
|
|
26
|
+
input.workspaceId = flags.workspace;
|
|
27
|
+
input.prospectId = positions[3];
|
|
28
|
+
if (flags.evidence !== undefined) input.evidenceId = flags.evidence;
|
|
29
|
+
for (const [flag, key] of [['cursor', 'cursor'], ['history-cursor', 'historyCursor'], ['status', 'status']]) if (flags[flag] !== undefined) input[key] = flags[flag];
|
|
30
|
+
for (const [flag, key] of [['limit', 'limit'], ['history-limit', 'historyLimit']]) if (flags[flag] !== undefined) {
|
|
31
|
+
if (!/^\d+$/.test(flags[flag])) throw Error('invalid_fields');
|
|
32
|
+
input[key] = Number(flags[flag]);
|
|
33
|
+
}
|
|
34
|
+
console.log(JSON.stringify(await callWorkspace(`workspace_reply_${command}`, input, configuration(flags.profile))));
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const uuid = z.string().uuid();
|
|
3
|
+
const scope = { workspaceId: uuid.optional(), prospectId: uuid };
|
|
4
|
+
const item = { ...scope, evidenceId: uuid };
|
|
5
|
+
const status = z.enum(['needs_handling', 'handled', 'not_reply']);
|
|
6
|
+
const outcome = z.enum(['response_recorded', 'closed_without_response', 'suppressed']);
|
|
7
|
+
const limit = z.number().int().min(1).max(100);
|
|
8
|
+
const root = a => `/workspaces/${a.workspaceId}/prospects/${a.prospectId}`;
|
|
9
|
+
const detail = a => `${root(a)}/conversation-evidence/${a.evidenceId}/reply`;
|
|
10
|
+
const query = (path, a, keys) => {
|
|
11
|
+
const q = new URLSearchParams();
|
|
12
|
+
for (const key of keys) if (a[key] !== undefined) q.set(key, String(a[key]));
|
|
13
|
+
return path + (q.size ? `?${q}` : '');
|
|
14
|
+
};
|
|
15
|
+
export const accountReplyOperations = {
|
|
16
|
+
workspace_reply_set: {
|
|
17
|
+
schema: z.object({ ...item, requestId: uuid, expectedVersion: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER), status,
|
|
18
|
+
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.") }).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."),
|
|
19
|
+
method: 'POST', scoped: true, path: detail,
|
|
20
|
+
body: ({ workspaceId, prospectId, evidenceId, ...rest }) => rest,
|
|
21
|
+
},
|
|
22
|
+
workspace_reply_get: {
|
|
23
|
+
schema: z.object({ ...item, historyCursor: uuid.optional(), historyLimit: limit.optional() }).strict(),
|
|
24
|
+
method: 'GET', scoped: true, path: a => query(detail(a), a, ['historyCursor', 'historyLimit']),
|
|
25
|
+
},
|
|
26
|
+
workspace_reply_list: {
|
|
27
|
+
schema: z.object({ ...scope, cursor: uuid.optional(), limit: limit.optional(), status: status.optional() }).strict(),
|
|
28
|
+
method: 'GET', scoped: true, path: a => query(`${root(a)}/replies`, a, ['cursor', 'limit', 'status']),
|
|
29
|
+
},
|
|
30
|
+
};
|
package/cli/agents.mjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readProfile, writeProfile, validateUrl, configuration, WorkspaceRequestError } from './workspaces.mjs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {readJsonFile} from './json-file.mjs';
|
|
4
|
+
|
|
5
|
+
export const agentManagementOperations = ['agent_list','agent_revoke','agent_permission','agent_capabilities_get','agent_capability_sharing_get','agent_capability_sharing_set','agent_account_authorizations_get','agent_account_authorization_set'];
|
|
6
|
+
export const agentOperations = {
|
|
7
|
+
agent_account_authorizations_get:{schema:z.object({id:z.string().uuid()}).strict()},
|
|
8
|
+
agent_account_authorizations_show:{schema:z.object({}).strict()},
|
|
9
|
+
agent_account_authorization_set:{schema:z.object({id:z.string().uuid(),provider:z.enum(['email','linkedin']),accountRef:z.string().min(1).max(200),authorized:z.boolean()}).strict()},
|
|
10
|
+
agent_capability_sharing_get: {schema:z.object({id:z.string().uuid()}).strict()},
|
|
11
|
+
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()},
|
|
12
|
+
agent_capabilities_report: {schema: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(z.object({capability:z.string(),tools:z.array(z.string()).min(1).max(8)}).strict()).max(16)}).strict()},
|
|
13
|
+
agent_capabilities_show: {schema:z.object({}).strict()},
|
|
14
|
+
agent_capabilities_get: {schema:z.object({id:z.string().uuid()}).strict()},
|
|
15
|
+
agent_permission: {schema:z.object({id:z.string().uuid(),scope:z.enum(['workspace:read','workspace:access'])}).strict()},
|
|
16
|
+
agent_connect: { schema: z.object({ name: z.string().trim().min(1).max(80) }).strict() },
|
|
17
|
+
agent_poll: { schema: z.object({}).strict() },
|
|
18
|
+
agent_cancel: { schema: z.object({}).strict() },
|
|
19
|
+
agent_list: { schema: z.object({}).strict() },
|
|
20
|
+
agent_revoke: { schema: z.object({ id: z.string().uuid() }).strict() },
|
|
21
|
+
};
|
|
22
|
+
async function request(config, path, body, authenticated = false, method = body === undefined ? 'GET' : 'POST') {
|
|
23
|
+
const url = validateUrl(config.url);
|
|
24
|
+
let response;
|
|
25
|
+
try { response = await fetch(new URL(`/api/v1${path}`, url), { method, headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...(authenticated ? { Authorization: `Bearer ${config.token ?? ''}` } : {}) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'error', signal: AbortSignal.timeout(10000) }); }
|
|
26
|
+
catch { throw new Error('Could not reach the API. Redirects are not followed.'); }
|
|
27
|
+
let result; try { result = await response.json(); } catch { throw new Error('The API returned an invalid response.'); }
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const code = typeof result?.error?.code === 'string' && /^[a-z_]+$/.test(result.error.code) ? result.error.code : 'request_failed';
|
|
30
|
+
throw new WorkspaceRequestError(response.status, code, `Agent request failed: ${code} (HTTP ${response.status}).`);
|
|
31
|
+
}
|
|
32
|
+
if (!result || !Object.hasOwn(result, 'data')) throw new Error('The API returned an invalid response.');
|
|
33
|
+
return result.data;
|
|
34
|
+
}
|
|
35
|
+
export async function callAgent(name, args, config, profilePath) {
|
|
36
|
+
const input = agentOperations[name]?.schema.safeParse(args);
|
|
37
|
+
if (!input?.success) throw new Error('Invalid agent command or arguments.');
|
|
38
|
+
if (agentManagementOperations.includes(name) && (config.profile?.agent || config.profile?.pendingAgent)) throw new Error('Use a separate user management profile with an agents:manage token.');
|
|
39
|
+
if(name==='agent_account_authorizations_get') return {data:await request(config,`/agents/${input.data.id}/account-authorizations`,undefined,true)};
|
|
40
|
+
if(name==='agent_account_authorization_set') {const {id,...body}=input.data;return {data:await request(config,`/agents/${id}/account-authorizations`,body,true)};}
|
|
41
|
+
if(name==='agent_account_authorizations_show') {
|
|
42
|
+
if(!config.profile?.agent || !/^agt_/.test(config.token ?? '')) throw new Error('Use an active agent profile for authorization self readback.');
|
|
43
|
+
return {data:await request(config,'/agent-account-authorizations',undefined,true)};
|
|
44
|
+
}
|
|
45
|
+
if (name === 'agent_capability_sharing_get') return {data:await request(config,`/agents/${input.data.id}/capability-sharing`,undefined,true)};
|
|
46
|
+
if (name === 'agent_capability_sharing_set') {const {id,...body}=input.data;return {data:await request(config,`/agents/${id}/capability-sharing`,body,true)};}
|
|
47
|
+
if (name === 'agent_capabilities_get') return {data:await request(config,`/agents/${input.data.id}/capabilities`,undefined,true)};
|
|
48
|
+
if (name === 'agent_capabilities_show' || name === 'agent_capabilities_report') {
|
|
49
|
+
if (!config.profile?.agent || !/^agt_/.test(config.token ?? '')) throw new Error('Use an active agent profile for capability check-in and self readback.');
|
|
50
|
+
return {data:await request(config,'/agent-capabilities',name==='agent_capabilities_report' ? input.data : undefined,true,name==='agent_capabilities_report' ? 'PUT' : 'GET')};
|
|
51
|
+
}
|
|
52
|
+
if (name === 'agent_list') return { data: await request(config, '/agents', undefined, true) };
|
|
53
|
+
if (name === 'agent_permission') return {data:await request(config, `/agents/${input.data.id}/permission`, {scope:input.data.scope}, true)};
|
|
54
|
+
if (name === 'agent_revoke') return { data: await request(config, `/agents/${input.data.id}/revoke`, {}, true) };
|
|
55
|
+
if (!profilePath) throw new Error('Use a separate private --profile PATH for each agent.');
|
|
56
|
+
const profile = readProfile(profilePath);
|
|
57
|
+
const url = validateUrl(config.url).toString().replace(/\/$/, '');
|
|
58
|
+
if (name === 'agent_connect') {
|
|
59
|
+
if (profile.pendingAgent || profile.agent) throw new Error('This profile already has an agent connection. Poll/cancel the pending connection or choose a new profile.');
|
|
60
|
+
if (Object.keys(profile.tokens ?? {}).length) throw new Error('Use a new agent profile without user API tokens.');
|
|
61
|
+
// Prove that private persistence is possible before asking the server for secrets.
|
|
62
|
+
writeProfile(profilePath, { ...profile, url });
|
|
63
|
+
const data = await request(config, '/agent-connections', input.data);
|
|
64
|
+
if (!/^adc_[A-Za-z0-9_-]{43}$/.test(data.secret) || !/^agt_[A-Za-z0-9_-]{43}$/.test(data.credential) || !z.string().uuid().safeParse(data.id).success || !/^[A-F0-9]{4}-[A-F0-9]{4}$/.test(data.code)) throw new Error('Invalid connection response.');
|
|
65
|
+
writeProfile(profilePath, { ...profile, url, pendingAgent: { id: data.id, secret: data.secret, credential: data.credential, expiresAt: data.expiresAt } });
|
|
66
|
+
config.token = undefined; config.workspaceId = undefined;
|
|
67
|
+
return { data: { status: 'pending', id: data.id, code: data.code, approvalUrl: data.approvalUrl, approvalPath: `/agents/connect/${data.id}`, expiresAt: data.expiresAt, interval: 5, instructions: 'Open the approval URL, sign in, enter the matching code and approve. Then run agent poll with this profile.' } };
|
|
68
|
+
}
|
|
69
|
+
const secret = profile.pendingAgent?.secret ?? profile.connectionSecret;
|
|
70
|
+
if (!secret) throw new Error('No connection in this profile.');
|
|
71
|
+
if (url !== profile.url) throw new Error('The pending connection belongs to a different API host.');
|
|
72
|
+
const data = await request(config, name === 'agent_poll' ? '/agent-connections/poll' : '/agent-connections/cancel', { secret });
|
|
73
|
+
if (data.status === 'active') {
|
|
74
|
+
const credential = profile.pendingAgent?.credential ?? profile.tokens?.['workspace:read'];
|
|
75
|
+
writeProfile(profilePath, { ...profile, agent: data.agent, connectionSecret: secret, pendingAgent: undefined, tokens: { 'workspace:read': credential } });
|
|
76
|
+
config.token = credential;
|
|
77
|
+
} else if (['denied', 'expired'].includes(data.status)) { writeProfile(profilePath, { ...profile, pendingAgent: undefined, agent: undefined, connectionSecret: undefined, tokens: undefined, workspaceId: undefined }); config.token = undefined; config.workspaceId = undefined; }
|
|
78
|
+
return { data };
|
|
79
|
+
}
|
|
80
|
+
export async function agentCli(args) {
|
|
81
|
+
const flags = {}; const positions = [];
|
|
82
|
+
for (let i = 0; i < args.length; i++) {
|
|
83
|
+
if (args[i] === '--json') continue;
|
|
84
|
+
if (args[i].startsWith('--')) {
|
|
85
|
+
const key = args[i].slice(2);
|
|
86
|
+
if (!['profile', 'name','scope','file','id','provider','account','workspace','shared','authorized'].includes(key) || !args[i+1] || args[i+1].startsWith('--')) throw new Error('Unknown or incomplete agent option.');
|
|
87
|
+
flags[key] = args[++i];
|
|
88
|
+
} else positions.push(args[i]);
|
|
89
|
+
}
|
|
90
|
+
let name = `agent_${positions[1]}`;
|
|
91
|
+
let input = positions[1] === 'permission' ? {id:positions[2],scope:flags.scope} : positions[1] === 'connect' ? { name: flags.name } : positions[1] === 'revoke' ? { id: positions[2] } : {};
|
|
92
|
+
if(positions[1]==='accounts') {
|
|
93
|
+
if(positions[2]==='show') {name=flags.id ? 'agent_account_authorizations_get' : 'agent_account_authorizations_show';input=flags.id ? {id:flags.id} : {};}
|
|
94
|
+
else if(positions[2]==='authorize' && ['true','false'].includes(flags.authorized)) {name='agent_account_authorization_set';input={id:flags.id,provider:flags.provider,accountRef:flags.account,authorized:flags.authorized==='true'};}
|
|
95
|
+
else throw new Error('Use agent accounts show [--id ID] or authorize --id ID --provider PROVIDER --account REF --authorized true|false.');
|
|
96
|
+
}
|
|
97
|
+
if (positions[1] === 'capabilities') {
|
|
98
|
+
if (positions[2] === 'sharing' && flags.id) {name='agent_capability_sharing_get';input={id:flags.id};}
|
|
99
|
+
else if (positions[2] === 'share' && flags.id && ['true','false'].includes(flags.shared)) {name='agent_capability_sharing_set';input={id:flags.id,provider:flags.provider,accountRef:flags.account,workspaceId:flags.workspace,shared:flags.shared==='true'};}
|
|
100
|
+
else if (positions[2] === 'report' && flags.file && !flags.id) {name='agent_capabilities_report';input=readCapabilityFile(flags.file);}
|
|
101
|
+
else if (positions[2] === 'show' && !flags.file) {name=flags.id ? 'agent_capabilities_get' : 'agent_capabilities_show';input=flags.id ? {id:flags.id} : {};}
|
|
102
|
+
else throw new Error('Use agent capabilities report --file PATH or show [--id ID].');
|
|
103
|
+
}
|
|
104
|
+
console.log(JSON.stringify(await callAgent(name, input, configuration(flags.profile, agentManagementOperations.includes(name) ? 'agents:manage' : 'workspace:access'), flags.profile)));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readCapabilityFile(path) {return readJsonFile(path,32768,'Capability file');}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
const uuid=z.string().uuid(),scope={workspaceId:uuid.optional()},stage=z.object({key:uuid,name:z.string().trim().min(1).max(120)}).strict();
|
|
3
|
+
const fields=z.object({name:z.string().trim().min(1).max(120),stages:z.array(stage).min(2).max(30),goalKey:uuid,offRamps:z.array(stage.extend({reason:z.enum(['rejected','not_fit','nurture','unresponsive','other'])})).max(30)}).strict();
|
|
4
|
+
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)]));
|
|
5
|
+
export const approachOperations={
|
|
6
|
+
approach_list:{schema:z.object({...scope,cursor:uuid.optional(),limit:z.number().int().min(1).max(100).optional()}).strict(),method:'GET',path:a=>`${base(a)}/approaches?${query(a)}`,scoped:true,description:'List immutable versioned approaches in the current workspace. Free CRM read.'},
|
|
7
|
+
approach_get:{schema:z.object({...scope,id:uuid,revision:z.number().int().positive().optional()}).strict(),method:'GET',path:a=>`${base(a)}/approaches/${a.id}?${query(a,['id'])}`,scoped:true,description:'Read an approach version: stable stage keys, goal and terminal off-ramps.'},
|
|
8
|
+
approach_save:{schema:z.object({...scope,id:uuid.optional(),requestId:uuid,expectedRevision:z.number().int().nonnegative(),fields}).strict(),method:'POST',path:a=>`${base(a)}/approaches`,body:({workspaceId,...a})=>a,scoped:true,description:'Create or revise an approach with optimistic version and idempotent request. Does not change existing experiment or participation pins.'},
|
|
9
|
+
approach_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 approach pin and recorded stage/outcome history.'},
|
|
10
|
+
approach_progress_record:{schema:z.object({...scope,participationId:uuid,expectedVersion:z.number().int().nonnegative(),requestId:uuid,stageKey:uuid.optional(),offRampKey:uuid.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:'Record one forward stage or terminal off-ramp using stored evidence for this person. Assigned owner/Admin only. Goal ends prospecting; post-goal forward reporting never reactivates. No send or provider call.'},
|
|
11
|
+
workspace_analytics_get:{schema:z.object({...scope,experimentId:uuid.optional(),pinId:uuid.optional(),from:z.string().datetime(),to:z.string().datetime()}).strict(),method:'GET',path:a=>`${base(a)}/analytics?${query(a)}`,scoped:true,description:'Read UTC-period distinct outcomes and explicit stage events. Missing activity or cost attribution is not tracked, never fabricated zero. Maximum 366 days; narrow large scopes.'}
|
|
12
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const uuid = z.string().uuid();
|
|
3
|
+
const workspace = { workspaceId: uuid.optional() };
|
|
4
|
+
const request = { ...workspace, requestId: uuid };
|
|
5
|
+
const version = z.number().int().nonnegative();
|
|
6
|
+
const page = z
|
|
7
|
+
.object({
|
|
8
|
+
cursor: uuid.optional(),
|
|
9
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
const location = z
|
|
13
|
+
.object({
|
|
14
|
+
key: z.string().nullable().optional(),
|
|
15
|
+
providerId: z.string().nullable().optional(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
const criteria = z
|
|
20
|
+
.object({
|
|
21
|
+
experimentIds: z.array(uuid).max(50).optional(),
|
|
22
|
+
companyLocations: z.array(location).max(50).optional(),
|
|
23
|
+
personLocations: z.array(location).max(50).optional(),
|
|
24
|
+
personTitles: z.array(z.string()).max(50).optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
const rule = z
|
|
28
|
+
.object({
|
|
29
|
+
id: uuid,
|
|
30
|
+
name: z.string(),
|
|
31
|
+
enabled: z.boolean(),
|
|
32
|
+
assigneeUserId: uuid,
|
|
33
|
+
criteria,
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
const root = (a) => `/workspaces/${a.workspaceId}`;
|
|
37
|
+
const person = (a) => `${root(a)}/prospects/${a.prospectId}`;
|
|
38
|
+
const omit = (a, keys) =>
|
|
39
|
+
Object.fromEntries(Object.entries(a).filter(([k]) => !keys.includes(k)));
|
|
40
|
+
const body = (a) => omit(a, ["workspaceId", "prospectId"]);
|
|
41
|
+
function qs(a, keys = ["workspaceId", "prospectId", "experimentId"]) {
|
|
42
|
+
const values = {};
|
|
43
|
+
for (const [k, v] of Object.entries(omit(a, keys))) {
|
|
44
|
+
if (v === undefined) continue;
|
|
45
|
+
if (typeof v === "object" && v !== null)
|
|
46
|
+
for (const [child, value] of Object.entries(v))
|
|
47
|
+
values[k + child[0].toUpperCase() + child.slice(1)] = String(value);
|
|
48
|
+
else values[k] = String(v);
|
|
49
|
+
}
|
|
50
|
+
return new URLSearchParams(values).toString();
|
|
51
|
+
}
|
|
52
|
+
export const assignmentOperations = {
|
|
53
|
+
workspace_prospect_assignments: {
|
|
54
|
+
schema: z
|
|
55
|
+
.object({
|
|
56
|
+
...workspace,
|
|
57
|
+
assignedToUserId: uuid.optional(),
|
|
58
|
+
unassigned: z.boolean().optional(),
|
|
59
|
+
experimentId: uuid.optional(),
|
|
60
|
+
search: z.string().optional(),
|
|
61
|
+
cursor: uuid.optional(),
|
|
62
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
63
|
+
})
|
|
64
|
+
.strict(),
|
|
65
|
+
method: "GET",
|
|
66
|
+
path: (a) => root(a) + "/assignments?" + qs(a, ["workspaceId"]),
|
|
67
|
+
scoped: true,
|
|
68
|
+
},
|
|
69
|
+
workspace_prospect_assignment: {
|
|
70
|
+
schema: z
|
|
71
|
+
.object({
|
|
72
|
+
...workspace,
|
|
73
|
+
prospectId: uuid,
|
|
74
|
+
history: page.optional(),
|
|
75
|
+
participations: page.optional(),
|
|
76
|
+
participationEvents: page.optional(),
|
|
77
|
+
routingReports: page.optional(),
|
|
78
|
+
})
|
|
79
|
+
.strict(),
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: (a) => person(a) + "/assignment?" + qs(a),
|
|
82
|
+
scoped: true,
|
|
83
|
+
},
|
|
84
|
+
workspace_prospect_experiment_fit: {
|
|
85
|
+
schema: z
|
|
86
|
+
.object({
|
|
87
|
+
...workspace,
|
|
88
|
+
prospectId: uuid,
|
|
89
|
+
experimentId: uuid,
|
|
90
|
+
overrides: page.optional(),
|
|
91
|
+
})
|
|
92
|
+
.strict(),
|
|
93
|
+
method: "GET",
|
|
94
|
+
path: (a) => person(a) + `/experiments/${a.experimentId}/fit?` + qs(a),
|
|
95
|
+
scoped: true,
|
|
96
|
+
},
|
|
97
|
+
workspace_prospect_assignment_set: {
|
|
98
|
+
schema: z
|
|
99
|
+
.object({
|
|
100
|
+
...request,
|
|
101
|
+
prospectId: uuid,
|
|
102
|
+
assigneeUserId: uuid.nullable(),
|
|
103
|
+
expectedAssignmentVersion: version,
|
|
104
|
+
reason: z.string(),
|
|
105
|
+
})
|
|
106
|
+
.strict(),
|
|
107
|
+
method: "POST",
|
|
108
|
+
path: (a) => person(a) + "/assignment",
|
|
109
|
+
body,
|
|
110
|
+
scoped: true,
|
|
111
|
+
},
|
|
112
|
+
workspace_routing_rules_get: {
|
|
113
|
+
schema: z.object(workspace).strict(),
|
|
114
|
+
method: "GET",
|
|
115
|
+
path: (a) => root(a) + "/routing-rules",
|
|
116
|
+
scoped: true,
|
|
117
|
+
},
|
|
118
|
+
workspace_routing_rules_replace: {
|
|
119
|
+
schema: z
|
|
120
|
+
.object({
|
|
121
|
+
...request,
|
|
122
|
+
expectedRulesVersion: version,
|
|
123
|
+
rules: z.array(rule).max(100),
|
|
124
|
+
})
|
|
125
|
+
.strict(),
|
|
126
|
+
method: "PUT",
|
|
127
|
+
path: (a) => root(a) + "/routing-rules",
|
|
128
|
+
body: (a) => omit(a, ["workspaceId"]),
|
|
129
|
+
scoped: true,
|
|
130
|
+
},
|
|
131
|
+
workspace_routing_apply: {
|
|
132
|
+
schema: z
|
|
133
|
+
.object({
|
|
134
|
+
...request,
|
|
135
|
+
prospectId: uuid,
|
|
136
|
+
experimentId: uuid.optional(),
|
|
137
|
+
expectedAssignmentVersion: version,
|
|
138
|
+
rulesVersion: version,
|
|
139
|
+
outcomes: z
|
|
140
|
+
.array(
|
|
141
|
+
z
|
|
142
|
+
.object({
|
|
143
|
+
ruleId: uuid,
|
|
144
|
+
result: z.enum(["match", "no_match", "unknown"]),
|
|
145
|
+
reason: z.string().optional(),
|
|
146
|
+
})
|
|
147
|
+
.strict(),
|
|
148
|
+
)
|
|
149
|
+
.max(100),
|
|
150
|
+
skill: z.object({ id: z.string(), version: z.string() }).strict(),
|
|
151
|
+
})
|
|
152
|
+
.strict(),
|
|
153
|
+
method: "POST",
|
|
154
|
+
path: (a) => person(a) + "/routing",
|
|
155
|
+
body,
|
|
156
|
+
scoped: true,
|
|
157
|
+
},
|
|
158
|
+
workspace_fit_override_set: {
|
|
159
|
+
schema: z
|
|
160
|
+
.object({
|
|
161
|
+
...request,
|
|
162
|
+
prospectId: uuid,
|
|
163
|
+
experimentId: uuid,
|
|
164
|
+
experimentRevision: z.number().int().positive(),
|
|
165
|
+
expectedOverrideVersion: version,
|
|
166
|
+
action: z.enum(["set", "clear"]),
|
|
167
|
+
value: z.enum(["high", "moderate", "low", "no_fit"]).optional(),
|
|
168
|
+
reason: z.string(),
|
|
169
|
+
})
|
|
170
|
+
.strict(),
|
|
171
|
+
method: "POST",
|
|
172
|
+
path: (a) => person(a) + "/fit-override",
|
|
173
|
+
body,
|
|
174
|
+
scoped: true,
|
|
175
|
+
},
|
|
176
|
+
workspace_prospect_move: {
|
|
177
|
+
schema: z
|
|
178
|
+
.object({
|
|
179
|
+
...request,
|
|
180
|
+
prospectId: uuid,
|
|
181
|
+
expectedSourceParticipationId: uuid,
|
|
182
|
+
sourceExperimentId: uuid,
|
|
183
|
+
destinationExperimentId: uuid,
|
|
184
|
+
destinationExperimentRevision: z.number().int().positive(),
|
|
185
|
+
destinationQualificationReceiptId: uuid,
|
|
186
|
+
expectedAssignmentVersion: version,
|
|
187
|
+
reason: z.string(),
|
|
188
|
+
})
|
|
189
|
+
.strict(),
|
|
190
|
+
method: "POST",
|
|
191
|
+
path: (a) => person(a) + "/move",
|
|
192
|
+
body,
|
|
193
|
+
scoped: true,
|
|
194
|
+
},
|
|
195
|
+
workspace_member_remove: {
|
|
196
|
+
schema: z
|
|
197
|
+
.object({
|
|
198
|
+
...request,
|
|
199
|
+
expectedMembershipId: uuid,
|
|
200
|
+
memberUserId: uuid,
|
|
201
|
+
reason: z.string(),
|
|
202
|
+
})
|
|
203
|
+
.strict(),
|
|
204
|
+
method: "POST",
|
|
205
|
+
path: (a) => root(a) + "/members/remove",
|
|
206
|
+
body: (a) => omit(a, ["workspaceId"]),
|
|
207
|
+
scoped: true,
|
|
208
|
+
},
|
|
209
|
+
workspace_leave: {
|
|
210
|
+
schema: z
|
|
211
|
+
.object({ ...request, expectedMembershipId: uuid, reason: z.string() })
|
|
212
|
+
.strict(),
|
|
213
|
+
method: "POST",
|
|
214
|
+
path: (a) => root(a) + "/members/leave",
|
|
215
|
+
body: (a) => omit(a, ["workspaceId"]),
|
|
216
|
+
scoped: true,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { callWorkspace, configuration } from "./workspaces.mjs";
|
|
2
|
+
import { readJsonFile } from "./json-file.mjs";
|
|
3
|
+
export function isAssignmentCommand(args) {
|
|
4
|
+
return (
|
|
5
|
+
args[0] === "workspace" &&
|
|
6
|
+
(["assignments", "prospect", "routing-rules", "routing", "leave"].includes(
|
|
7
|
+
args[1],
|
|
8
|
+
) ||
|
|
9
|
+
(args[1] === "member" && args[2] === "remove"))
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
export async function assignmentCli(args) {
|
|
13
|
+
const flags = {},
|
|
14
|
+
position = [];
|
|
15
|
+
const children = [
|
|
16
|
+
"history",
|
|
17
|
+
"participations",
|
|
18
|
+
"participation-events",
|
|
19
|
+
"routing-reports",
|
|
20
|
+
"overrides",
|
|
21
|
+
];
|
|
22
|
+
const allowed = [
|
|
23
|
+
"profile",
|
|
24
|
+
"workspace",
|
|
25
|
+
"file",
|
|
26
|
+
"request-id",
|
|
27
|
+
"prospect-id",
|
|
28
|
+
"experiment-id",
|
|
29
|
+
"assigned-to-user-id",
|
|
30
|
+
"unassigned",
|
|
31
|
+
"search",
|
|
32
|
+
"cursor",
|
|
33
|
+
"limit",
|
|
34
|
+
...children.flatMap((k) => [k + "-cursor", k + "-limit"]),
|
|
35
|
+
];
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i] === "--json") continue;
|
|
38
|
+
if (!args[i].startsWith("--")) {
|
|
39
|
+
position.push(args[i]);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const key = args[i].slice(2);
|
|
43
|
+
if (
|
|
44
|
+
!allowed.includes(key) ||
|
|
45
|
+
args[i + 1] === undefined ||
|
|
46
|
+
args[i + 1].startsWith("--") ||
|
|
47
|
+
Object.hasOwn(flags, key)
|
|
48
|
+
)
|
|
49
|
+
throw new Error("Unknown, repeated, or incomplete assignment option.");
|
|
50
|
+
flags[key] = args[++i];
|
|
51
|
+
}
|
|
52
|
+
const command = position.join(" ");
|
|
53
|
+
const names = {
|
|
54
|
+
"workspace assignments list": "workspace_prospect_assignments",
|
|
55
|
+
"workspace prospect assignment get": "workspace_prospect_assignment",
|
|
56
|
+
"workspace prospect assignment set": "workspace_prospect_assignment_set",
|
|
57
|
+
"workspace prospect experiment-fit get":
|
|
58
|
+
"workspace_prospect_experiment_fit",
|
|
59
|
+
"workspace routing-rules get": "workspace_routing_rules_get",
|
|
60
|
+
"workspace routing-rules replace": "workspace_routing_rules_replace",
|
|
61
|
+
"workspace routing apply": "workspace_routing_apply",
|
|
62
|
+
"workspace prospect fit-override set": "workspace_fit_override_set",
|
|
63
|
+
"workspace prospect fit-override clear": "workspace_fit_override_set",
|
|
64
|
+
"workspace prospect move": "workspace_prospect_move",
|
|
65
|
+
"workspace member remove": "workspace_member_remove",
|
|
66
|
+
"workspace leave": "workspace_leave",
|
|
67
|
+
};
|
|
68
|
+
const name = names[command];
|
|
69
|
+
if (!name)
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Unknown assignment command. Use workspace assignments list, prospect assignment get/set, routing-rules get/replace, routing apply, prospect experiment-fit get, prospect fit-override set/clear, prospect move, member remove, or leave. Mutations require --file INPUT.json.",
|
|
72
|
+
);
|
|
73
|
+
const read = [
|
|
74
|
+
"workspace_prospect_assignments",
|
|
75
|
+
"workspace_prospect_assignment",
|
|
76
|
+
"workspace_prospect_experiment_fit",
|
|
77
|
+
"workspace_routing_rules_get",
|
|
78
|
+
].includes(name);
|
|
79
|
+
let input = read ? {} : readJsonFile(flags.file, 65536, "Assignment file");
|
|
80
|
+
for (const [flag, key] of [
|
|
81
|
+
["workspace", "workspaceId"],
|
|
82
|
+
["request-id", "requestId"],
|
|
83
|
+
["prospect-id", "prospectId"],
|
|
84
|
+
["experiment-id", "experimentId"],
|
|
85
|
+
["assigned-to-user-id", "assignedToUserId"],
|
|
86
|
+
["search", "search"],
|
|
87
|
+
["cursor", "cursor"],
|
|
88
|
+
])
|
|
89
|
+
if (flags[flag] !== undefined) input[key] = flags[flag];
|
|
90
|
+
if (flags.limit !== undefined) input.limit = Number(flags.limit);
|
|
91
|
+
if (flags.unassigned !== undefined) {
|
|
92
|
+
if (!["true", "false"].includes(flags.unassigned))
|
|
93
|
+
throw new Error("--unassigned must be true or false.");
|
|
94
|
+
input.unassigned = flags.unassigned === "true";
|
|
95
|
+
}
|
|
96
|
+
for (const child of children) {
|
|
97
|
+
const cursor = flags[child + "-cursor"],
|
|
98
|
+
limit = flags[child + "-limit"];
|
|
99
|
+
if (cursor !== undefined || limit !== undefined)
|
|
100
|
+
input[child.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = {
|
|
101
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
102
|
+
...(limit === undefined ? {} : { limit: Number(limit) }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (name === "workspace_fit_override_set") {
|
|
106
|
+
const action = position.at(-1);
|
|
107
|
+
if (input.action !== undefined && input.action !== action)
|
|
108
|
+
throw new Error("File action must match the command.");
|
|
109
|
+
input.action = action;
|
|
110
|
+
}
|
|
111
|
+
console.log(
|
|
112
|
+
JSON.stringify(
|
|
113
|
+
await callWorkspace(name, input, configuration(flags.profile)),
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {z} from 'zod';
|
|
2
|
+
const uuid=z.string().uuid(),workspace={workspaceId:uuid.optional()},locator={...workspace,audienceId:uuid};
|
|
3
|
+
const criterion=z.object({name:z.string(),providerId:z.string().nullable().optional(),negative:z.boolean().optional()}).strict();
|
|
4
|
+
const location=criterion.extend({key:z.string().nullable().optional()}).strict();
|
|
5
|
+
const strings=z.array(z.string()).optional(),criteria=z.array(criterion).optional(),locations=z.array(location).optional();
|
|
6
|
+
const fields=z.object({name:z.string(),tags:strings,textCriteria:z.string().optional(),notes:z.string().optional(),discoveryKeyword:z.string().optional(),seniorityMatchMode:z.enum(['at_least','exact']).optional(),company:z.object({industries:criteria,industryGroups:strings,sizes:criteria,types:criteria,locations,keywords:z.object({include:strings,exclude:strings}).strict().optional()}).strict(),person:z.object({locations,currentTitles:criteria,pastTitles:criteria,seniorities:criteria,functions:criteria,bioPhrases:z.array(z.object({phrase:z.string(),negative:z.boolean().optional()}).strict()).optional(),allowedTitleExceptions:strings}).strict()}).strict();
|
|
7
|
+
const path=a=>`/workspaces/${a.workspaceId}/audiences`;
|
|
8
|
+
export const audienceOperations={
|
|
9
|
+
audience_options:{schema:z.object({...workspace,kind:z.enum(['industries','industry_groups','sizes','types','seniorities','titles','locations','functions']),q:z.string().max(200).optional()}).strict(),method:'GET',path:a=>`/workspaces/${a.workspaceId}/audience-options?`+new URLSearchParams({kind:a.kind,...(a.q!==undefined?{q:a.q}:{})}),scoped:true},
|
|
10
|
+
audience_list:{schema:z.object({...workspace,archived:z.boolean().optional(),limit:z.number().int().optional(),after:uuid.optional(),q:z.string().optional()}).strict(),method:'GET',path:a=>path(a)+'?'+new URLSearchParams(Object.fromEntries(Object.entries({archived:a.archived,limit:a.limit,after:a.after,q:a.q}).filter(([,v])=>v!==undefined).map(([k,v])=>[k,String(v)]))),scoped:true},
|
|
11
|
+
audience_get:{schema:z.object({...locator,revision:z.number().int().optional()}).strict(),method:'GET',path:a=>path(a)+`/${a.audienceId}`+(a.revision===undefined?'':`?revision=${a.revision}`),scoped:true},
|
|
12
|
+
audience_create:{schema:z.object({...workspace,requestId:uuid,fields}).strict(),method:'POST',path,body:a=>({requestId:a.requestId,fields:a.fields}),scoped:true},
|
|
13
|
+
audience_update:{schema:z.object({...locator,expectedRevision:z.number().int(),fields}).strict(),method:'POST',path:a=>path(a)+`/${a.audienceId}/revisions`,body:a=>({expectedRevision:a.expectedRevision,fields:a.fields}),scoped:true},
|
|
14
|
+
audience_archive:{schema:z.object(locator).strict(),method:'POST',path:a=>path(a)+`/${a.audienceId}/archive`,scoped:true},
|
|
15
|
+
audience_restore:{schema:z.object(locator).strict(),method:'POST',path:a=>path(a)+`/${a.audienceId}/restore`,scoped:true},
|
|
16
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {callWorkspace,configuration} from './workspaces.mjs';
|
|
2
|
+
import {readJsonFile} from './json-file.mjs';
|
|
3
|
+
export async function audienceCli(args){
|
|
4
|
+
const position=[],flags={};for(let i=0;i<args.length;i++){
|
|
5
|
+
if(!args[i].startsWith('--')){position.push(args[i]);continue}
|
|
6
|
+
const key=args[i].slice(2);if(!['profile','workspace','file','request-id','expected-revision','revision','archived','limit','after','q','kind'].includes(key)||!args[i+1]||args[i+1].startsWith('--')||Object.hasOwn(flags,key))throw new Error('Unknown or incomplete audience option.');
|
|
7
|
+
flags[key]=args[++i];
|
|
8
|
+
}
|
|
9
|
+
const command=position[1],name={options:'audience_options',list:'audience_list',show:'audience_get',create:'audience_create',update:'audience_update',archive:'audience_archive',restore:'audience_restore'}[command];
|
|
10
|
+
if(!name||position.length!==(['list','create','options'].includes(command)?2:3))throw new Error('Use audience options --kind KIND [--q TEXT]|list|show ID|create|update ID|archive ID|restore ID.');
|
|
11
|
+
const input={workspaceId:flags.workspace};
|
|
12
|
+
if(position[2])input.audienceId=position[2];
|
|
13
|
+
if(command==='options'){input.kind=flags.kind;if(flags.q!==undefined)input.q=flags.q;}
|
|
14
|
+
if(command==='list'){
|
|
15
|
+
if(flags.archived!==undefined&&!['true','false'].includes(flags.archived))throw new Error('Archived must be true or false.');
|
|
16
|
+
if(flags.archived!==undefined)input.archived=flags.archived==='true';
|
|
17
|
+
if(flags.limit!==undefined)input.limit=Number(flags.limit);
|
|
18
|
+
if(flags.after)input.after=flags.after;
|
|
19
|
+
if(flags.q!==undefined)input.q=flags.q;
|
|
20
|
+
}
|
|
21
|
+
if(command==='show'&&flags.revision!==undefined)input.revision=Number(flags.revision);
|
|
22
|
+
if(['create','update'].includes(command)){if(!flags.file)throw new Error('Supply --file PATH containing Audience fields.');input.fields=readJsonFile(flags.file,65536,'Audience file');}
|
|
23
|
+
if(command==='create')input.requestId=flags['request-id'];
|
|
24
|
+
if(command==='update')input.expectedRevision=Number(flags['expected-revision']);
|
|
25
|
+
console.log(JSON.stringify(await callWorkspace(name,input,configuration(flags.profile))));
|
|
26
|
+
}
|