@audienti/cli 0.1.30 → 0.1.32
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/.mcp.json +13 -0
- package/CHANGELOG.md +14 -0
- package/README.md +81 -5
- package/bin/audienti-mcp.js +5 -0
- package/package.json +4 -2
- package/skills/audienti/SKILL.md +48 -7
- package/src/api-client.js +15 -0
- package/src/cli.js +421 -4
- package/src/mcp.js +103 -0
package/.mcp.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"audienti": {
|
|
4
|
+
"title": "Audienti",
|
|
5
|
+
"description": "Bridge MCP clients to Audienti's app-hosted /mcp endpoint for accounts, plays, prospects, lists, analytics, and operator workflows.",
|
|
6
|
+
"cwd": ".",
|
|
7
|
+
"command": "node",
|
|
8
|
+
"args": [
|
|
9
|
+
"./bin/audienti-mcp.js"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to the Audienti CLI are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.32] - 2026-08-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Add `audienti auth login` for browser-based local authentication through a loopback callback.
|
|
12
|
+
- Add the `audienti-mcp` stdio bridge and Codex MCP manifest for Audienti's app-hosted MCP endpoint, including stage analytics and cohort list creation.
|
|
13
|
+
|
|
14
|
+
## [0.1.31] - 2026-08-10
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Add `audienti setup play preflight` for checking LinkedIn connected-account readiness and returning direct setup, mapping, or edit URLs before an agent activates a play.
|
|
19
|
+
- Add `audienti analytics stages` for weekly or monthly stage conversion cohorts and current stage aging/overdue metrics.
|
|
20
|
+
|
|
7
21
|
## [0.1.30] - 2026-08-04
|
|
8
22
|
|
|
9
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Audienti CLI
|
|
2
2
|
|
|
3
3
|
Audienti CLI is the agent-first command-line client for the Audienti production
|
|
4
|
-
API
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
API and the local bridge for Audienti's app-hosted MCP endpoint. It lets local
|
|
5
|
+
coding agents and operators inspect accounts,
|
|
6
|
+
create and manage plays, import prospects, build lists, manage task reminders,
|
|
7
|
+
and work supported operator flows.
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -25,16 +26,26 @@ For one-off use, run `npx @audienti/cli --help`.
|
|
|
25
26
|
|
|
26
27
|
## Authenticate
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Authenticate through the browser, then configure the default account context:
|
|
29
30
|
|
|
30
31
|
```bash
|
|
31
|
-
audienti auth
|
|
32
|
+
audienti auth login
|
|
32
33
|
audienti accounts list --json
|
|
33
34
|
audienti accounts select <acct_id>
|
|
34
35
|
audienti users list
|
|
35
36
|
audienti users select <account_user_id|email|name|me>
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
`audienti auth login` opens Audienti in your browser, creates an API token from
|
|
40
|
+
your signed-in web session, sends it back to a temporary `127.0.0.1` callback,
|
|
41
|
+
and stores it in the local CLI config after validating it.
|
|
42
|
+
|
|
43
|
+
If you already have a token, you can still configure it directly:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
audienti auth token <token>
|
|
47
|
+
```
|
|
48
|
+
|
|
38
49
|
The CLI writes its local configuration to `~/.config/audienti/config.json` with
|
|
39
50
|
owner-only permissions. Do not place a production token in an agent prompt,
|
|
40
51
|
repository file, issue, or CI secret.
|
|
@@ -55,6 +66,71 @@ Use `--json` whenever another program or agent will consume the result. Inspect
|
|
|
55
66
|
the target state before mutations, and use the command-specific help before
|
|
56
67
|
creating, changing, or deleting data.
|
|
57
68
|
|
|
69
|
+
## MCP
|
|
70
|
+
|
|
71
|
+
Audienti serves MCP from the app at `/mcp`. The package ships a small stdio
|
|
72
|
+
bridge for MCP hosts that need a local command:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
audienti-mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use it this way:
|
|
79
|
+
|
|
80
|
+
1. Install the package.
|
|
81
|
+
2. Run `audienti auth login`.
|
|
82
|
+
3. Run `audienti accounts select <acct_id>`.
|
|
83
|
+
4. Configure your MCP host with the packaged `.mcp.json`, or point it at the
|
|
84
|
+
`audienti-mcp` command.
|
|
85
|
+
5. In the MCP host, list tools and call the account-scoped tool you need.
|
|
86
|
+
|
|
87
|
+
The bridge reads the same `~/.config/audienti/config.json` as the CLI, forwards
|
|
88
|
+
MCP JSON-RPC messages to the app, and injects the selected account when a tool
|
|
89
|
+
call does not include `account_id`. If you want a tool to run against a
|
|
90
|
+
different account, pass `account_id` in that tool call.
|
|
91
|
+
|
|
92
|
+
MCP clients that can call HTTP directly may use the hosted endpoint instead of
|
|
93
|
+
the stdio bridge:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
curl https://app.audienti.com/mcp \
|
|
97
|
+
-H "Authorization: Bearer $AUDIENTI_API_TOKEN" \
|
|
98
|
+
-H "Content-Type: application/json" \
|
|
99
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Example tool call:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"jsonrpc": "2.0",
|
|
107
|
+
"id": 2,
|
|
108
|
+
"method": "tools/call",
|
|
109
|
+
"params": {
|
|
110
|
+
"name": "analytics.stages",
|
|
111
|
+
"arguments": {
|
|
112
|
+
"account_id": "acct_...",
|
|
113
|
+
"query": {
|
|
114
|
+
"interval": "weekly",
|
|
115
|
+
"cohort_start_date": "2026-07-01",
|
|
116
|
+
"cohort_end_date": "2026-07-31"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Useful MCP tools to start with:
|
|
124
|
+
|
|
125
|
+
- `auth.me` confirms the token identity.
|
|
126
|
+
- `accounts.list` shows accessible accounts.
|
|
127
|
+
- `setup.play_preflight` returns LinkedIn connected-account readiness and setup
|
|
128
|
+
or mapping URLs.
|
|
129
|
+
- `offers.create`, `icps.create`, and `motions.create` set up the offer, ICP,
|
|
130
|
+
and play.
|
|
131
|
+
- `analytics.stages` returns stage conversion cohorts and stage aging.
|
|
132
|
+
- `analytics.cohort_lists.create` materializes event cohorts as reusable lists.
|
|
133
|
+
|
|
58
134
|
Common inspection commands:
|
|
59
135
|
|
|
60
136
|
```bash
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audienti/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Agent-first command-line client for Audienti.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"audienti": "./bin/audienti.js"
|
|
7
|
+
"audienti": "./bin/audienti.js",
|
|
8
|
+
"audienti-mcp": "./bin/audienti-mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin/",
|
|
11
12
|
"src/",
|
|
12
13
|
"skills/",
|
|
14
|
+
".mcp.json",
|
|
13
15
|
"install",
|
|
14
16
|
"README.md",
|
|
15
17
|
"LICENSE",
|
package/skills/audienti/SKILL.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audienti
|
|
3
|
-
description: Use when the user wants to operate Audienti through the production CLI, including account selection, plays, prospect imports, lists, message previews, or supported operator outcomes.
|
|
3
|
+
description: Use when the user wants to operate Audienti through the production CLI or app-hosted MCP endpoint, including account selection, plays, prospect imports, lists, message previews, analytics, or supported operator outcomes.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Audienti CLI
|
|
6
|
+
# Audienti CLI and MCP
|
|
7
7
|
|
|
8
|
-
Use the installed `audienti` command
|
|
9
|
-
|
|
8
|
+
Use the installed `audienti` command or Audienti's app-hosted MCP endpoint as
|
|
9
|
+
the production contract. The packaged `audienti-mcp` command is only a local
|
|
10
|
+
stdio bridge for MCP hosts. Do not build a parallel wrapper or call
|
|
11
|
+
undocumented API endpoints.
|
|
10
12
|
|
|
11
13
|
## Setup
|
|
12
14
|
|
|
@@ -23,9 +25,15 @@ curl -fsSL https://cli.audienti.com/install | bash
|
|
|
23
25
|
```
|
|
24
26
|
|
|
25
27
|
3. Authentication is explicit and per machine. Do not ask a user to paste a
|
|
26
|
-
production token into chat, a repository file, an issue, or a CI secret.
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
production token into chat, a repository file, an issue, or a CI secret. Prefer
|
|
29
|
+
browser login:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
audienti auth login
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `audienti auth token` only after the user supplies a token through an
|
|
36
|
+
approved secure channel.
|
|
29
37
|
|
|
30
38
|
4. Start with discovery, not mutation:
|
|
31
39
|
|
|
@@ -35,6 +43,39 @@ audienti accounts list --json
|
|
|
35
43
|
audienti help agent-workflows
|
|
36
44
|
```
|
|
37
45
|
|
|
46
|
+
For MCP hosts, configure the packaged `.mcp.json` or run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
audienti-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The bridge reads the same local config as the CLI and forwards MCP JSON-RPC to
|
|
53
|
+
the app-hosted `/mcp` endpoint. Before starting the bridge, select account
|
|
54
|
+
context:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
audienti auth login
|
|
58
|
+
audienti accounts list --json
|
|
59
|
+
audienti accounts select <acct_id>
|
|
60
|
+
audienti users select me
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If the MCP host can call HTTP directly, use `POST /mcp` with the same bearer
|
|
64
|
+
token as the API. Call `tools/list` first, then call named tools with
|
|
65
|
+
`tools/call`. Account-scoped tools accept `account_id`; the local bridge injects
|
|
66
|
+
the selected CLI account when it is omitted.
|
|
67
|
+
|
|
68
|
+
Useful MCP tools:
|
|
69
|
+
|
|
70
|
+
- `auth.me` confirms the token identity.
|
|
71
|
+
- `accounts.list` shows accessible accounts.
|
|
72
|
+
- `setup.play_preflight` returns connected-account readiness and setup or
|
|
73
|
+
mapping URLs.
|
|
74
|
+
- `offers.create`, `icps.create`, and `motions.create` set up the offer, ICP,
|
|
75
|
+
and play.
|
|
76
|
+
- `analytics.stages` returns stage conversion cohorts and stage aging.
|
|
77
|
+
- `analytics.cohort_lists.create` materializes event cohorts as reusable lists.
|
|
78
|
+
|
|
38
79
|
## Operating Rules
|
|
39
80
|
|
|
40
81
|
- Use `--json` whenever another agent or tool will consume the response.
|
package/src/api-client.js
CHANGED
|
@@ -40,10 +40,21 @@ export class AudientiClient {
|
|
|
40
40
|
return this.requestJson("/api/v1/accounts.json");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
mcp(message) {
|
|
44
|
+
return this.requestJson("/mcp", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
body: message
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
users(accountId) {
|
|
44
51
|
return this.requestJson(accountPath(accountId, ["users"]));
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
socialCookies(accountId, query = {}) {
|
|
55
|
+
return this.requestJson(accountPath(accountId, ["social_cookies"], query));
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
userActivity(accountId, userId, query = {}) {
|
|
48
59
|
return this.requestJson(accountPath(accountId, ["operations", "users", userId, "activity"], query));
|
|
49
60
|
}
|
|
@@ -572,6 +583,10 @@ export class AudientiClient {
|
|
|
572
583
|
return this.requestJson(accountPath(accountId, ["analytics", "dashboard"], query));
|
|
573
584
|
}
|
|
574
585
|
|
|
586
|
+
analyticsStages(accountId, query = {}) {
|
|
587
|
+
return this.requestJson(accountPath(accountId, ["analytics", "stages"], query));
|
|
588
|
+
}
|
|
589
|
+
|
|
575
590
|
createAnalyticsCohortList(accountId, body = {}) {
|
|
576
591
|
return this.requestJson(accountPath(accountId, ["analytics", "cohort_lists"]), {
|
|
577
592
|
method: "POST",
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
2
5
|
import { readFile } from "node:fs/promises";
|
|
3
6
|
import { basename, join } from "node:path";
|
|
4
7
|
import { ApiError, AudientiClient, DEFAULT_HOST, normalizeHost } from "./api-client.js";
|
|
@@ -17,6 +20,7 @@ const DEFAULT_LIST_LIMIT = 20;
|
|
|
17
20
|
const API_MAX_LIST_LIMIT = 100;
|
|
18
21
|
const DEFAULT_LOOKUP_TIMEOUT_SECONDS = 60;
|
|
19
22
|
const DEFAULT_LOOKUP_POLL_INTERVAL_SECONDS = 2;
|
|
23
|
+
const DEFAULT_AUTH_LOGIN_TIMEOUT_SECONDS = 180;
|
|
20
24
|
const DEFAULT_WRITER_TEST_RUN_TIMEOUT_SECONDS = 180;
|
|
21
25
|
const DEFAULT_WRITER_TEST_RUN_POLL_INTERVAL_SECONDS = 2;
|
|
22
26
|
const PACKAGE_NAME = "@audienti/cli";
|
|
@@ -61,6 +65,7 @@ const TASKS_LIST_USAGE = "Usage: audienti tasks list [--status <open|completed>]
|
|
|
61
65
|
const TASKS_ADD_USAGE = "Usage: audienti tasks add --title <text> --due <time> [--prospect <prsp_id>] [--list <list_id>] [--assigned-user <id|me>] [--notes <text>] [--json] [--account <acct_id>]";
|
|
62
66
|
const TASKS_COMPLETE_USAGE = "Usage: audienti tasks complete <ptsk_id> [--json] [--account <acct_id>]";
|
|
63
67
|
const USERS_ACTIVITY_USAGE = "Usage: audienti users activity [account_user_id|me] [--mode <actor|account_usage|related>] [--window <24h|7d|30d>] [--platform <linkedin|email|gmail>] [--query <text>] [--limit <n>] [--page <n>] [--json] [--account <acct_id>]";
|
|
68
|
+
const SETUP_PLAY_PREFLIGHT_USAGE = "Usage: audienti setup play preflight [--principal <account_user_id|email|name|me>] [--platform linkedin] [--json] [--account <acct_id>]";
|
|
64
69
|
const OFFERS_SHOW_USAGE = "Usage: audienti offers show <offr_id> [--json] [--account <acct_id>]";
|
|
65
70
|
const OFFERS_UPDATE_USAGE = "Usage: audienti offers update <offr_id> [--name <text>] [--description <text>] [--url <url>] [--json] [--account <acct_id>]";
|
|
66
71
|
const OFFERS_DELETE_USAGE = "Usage: audienti offers delete <offr_id> --confirm <yes|true|Y|y> [--json] [--account <acct_id>]";
|
|
@@ -94,6 +99,7 @@ const ANALYTICS_PROSPECTS_USAGE = "Usage: audienti analytics prospects [--window
|
|
|
94
99
|
const ANALYTICS_PROSPECTS_COHORT_ANALYSIS_USAGE = "Usage: audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--list <list_id>] [--provenance <source>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
95
100
|
const ANALYTICS_USERS_USAGE = "Usage: audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--list <list_id>] [--provenance <source>] [--platform <linkedin|email|gmail>] [--json] [--account <acct_id>]";
|
|
96
101
|
const ANALYTICS_DASHBOARD_USAGE = "Usage: audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--list <list_id>] [--offer <offr_id>] [--icp <icp_id>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
102
|
+
const ANALYTICS_STAGES_USAGE = "Usage: audienti analytics stages [--interval weekly|monthly] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--list <list_id>] [--offer <offr_id>] [--icp <icp_id>] [--user <account_user_id|email|name|me>] [--json] [--account <acct_id>]";
|
|
97
103
|
const ANALYTICS_COHORT_LIST_USAGE = "Usage: audienti analytics cohorts create-list --name <text> --start YYYY-MM-DD --end YYYY-MM-DD [--event connection_request_sent] [--user <account_user_id|email|name|me>] [--note-mode <any|with_note|blank>] [--motion <motn_id>] [--offer <offr_id>] [--icp <icp_id>] [--play-tag <tag>] [--json] [--account <acct_id>]";
|
|
98
104
|
const TOOLS_LIST_USAGE = "Usage: audienti tools list [--json]";
|
|
99
105
|
const TOOLS_LINKEDIN_REVIEW_USAGE = "Usage: audienti tools linkedin-review --url <linkedin_url> [--icp <icp_id>] [--json] [--account <acct_id>]";
|
|
@@ -180,6 +186,7 @@ async function dispatch(argv, context) {
|
|
|
180
186
|
const normalizedResource = normalizeResource(resource);
|
|
181
187
|
|
|
182
188
|
if (normalizedResource === "auth" && action === "token") return authToken(rest, context);
|
|
189
|
+
if (normalizedResource === "auth" && action === "login") return authLogin(rest, context);
|
|
183
190
|
if (normalizedResource === "auth" && action === "status") return authStatus(rest, context, { accountOverride });
|
|
184
191
|
if (normalizedResource === "auth" && action === "logout") return authLogout(rest, context);
|
|
185
192
|
if (normalizedResource === "config" && action === "list") return configList(rest, context);
|
|
@@ -189,6 +196,7 @@ async function dispatch(argv, context) {
|
|
|
189
196
|
if (normalizedResource === "users" && action === "list") return usersList(rest, context, { accountOverride });
|
|
190
197
|
if (normalizedResource === "users" && action === "select") return usersSelect(rest, context, { accountOverride });
|
|
191
198
|
if (normalizedResource === "users" && action === "activity") return usersActivity(rest, context, { accountOverride });
|
|
199
|
+
if (normalizedResource === "setup" && action === "play" && rest[0] === "preflight") return setupPlayPreflight(rest.slice(1), context, { accountOverride });
|
|
192
200
|
if (normalizedResource === "offers" && action === "list") return offersList(rest, context, { accountOverride });
|
|
193
201
|
if (normalizedResource === "offers" && action === "show") return offersShow(rest, context, { accountOverride });
|
|
194
202
|
if (normalizedResource === "offers" && action === "create") return offersCreate(rest, context, { accountOverride });
|
|
@@ -288,6 +296,7 @@ async function dispatch(argv, context) {
|
|
|
288
296
|
if (normalizedResource === "analytics" && ["visibility", "visops"].includes(action)) return analyticsVisibility(rest, context, { accountOverride });
|
|
289
297
|
if (normalizedResource === "analytics" && action === "content") return analyticsContent(rest, context, { accountOverride });
|
|
290
298
|
if (normalizedResource === "analytics" && ["dashboard", "campaign", "campaigns"].includes(action)) return analyticsDashboard(rest, context, { accountOverride });
|
|
299
|
+
if (normalizedResource === "analytics" && action === "stages") return analyticsStages(rest, context, { accountOverride });
|
|
291
300
|
if (normalizedResource === "analytics" && ["cohorts", "cohort"].includes(action)) return analyticsCohorts(rest, context, { accountOverride });
|
|
292
301
|
|
|
293
302
|
throw new CommandError(usage(), { exitCode: resource ? 1 : 0 });
|
|
@@ -402,12 +411,148 @@ async function authToken(args, context) {
|
|
|
402
411
|
writeLine(context.stdout, "Run `audienti accounts list` to choose an account.");
|
|
403
412
|
}
|
|
404
413
|
|
|
414
|
+
async function authLogin(args, context) {
|
|
415
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
416
|
+
...jsonOptions(),
|
|
417
|
+
host: { type: "string" },
|
|
418
|
+
"timeout-seconds": { type: "string" },
|
|
419
|
+
"no-open": { type: "boolean" }
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
if (positionals.length > 0) {
|
|
423
|
+
throw new CommandError("Usage: audienti auth login [--host https://app.audienti.com] [--timeout-seconds <n>] [--no-open] [--json]");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const host = normalizeHost(values.host || DEFAULT_HOST);
|
|
427
|
+
const timeoutSeconds = normalizeOptionalPositiveInteger(values["timeout-seconds"], "--timeout-seconds") || DEFAULT_AUTH_LOGIN_TIMEOUT_SECONDS;
|
|
428
|
+
const state = randomBytes(24).toString("hex");
|
|
429
|
+
const callback = await createAuthCallbackServer({ state, context });
|
|
430
|
+
const authUrl = new URL("/cli/auth", host);
|
|
431
|
+
authUrl.searchParams.set("redirect_uri", callback.redirectUri);
|
|
432
|
+
authUrl.searchParams.set("state", state);
|
|
433
|
+
|
|
434
|
+
if (values.json) {
|
|
435
|
+
writeJson(context.stdout, {
|
|
436
|
+
kind: "auth_login_start",
|
|
437
|
+
auth_url: authUrl.toString(),
|
|
438
|
+
callback_url: callback.redirectUri,
|
|
439
|
+
timeout_seconds: timeoutSeconds
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
writeLine(context.stdout, "Open this URL to authenticate Audienti CLI:");
|
|
443
|
+
writeLine(context.stdout, authUrl.toString());
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!values["no-open"]) openBrowser(authUrl.toString(), context);
|
|
447
|
+
|
|
448
|
+
let payload;
|
|
449
|
+
try {
|
|
450
|
+
payload = await callback.wait(timeoutSeconds);
|
|
451
|
+
} finally {
|
|
452
|
+
await callback.close();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const client = new AudientiClient({ host: payload.host || host, token: payload.token, fetchImpl: context.fetchImpl });
|
|
456
|
+
const user = await client.me();
|
|
457
|
+
await writeConfig({
|
|
458
|
+
host: client.host,
|
|
459
|
+
token: payload.token,
|
|
460
|
+
accountId: payload.account_id || undefined,
|
|
461
|
+
accountName: payload.account_name || undefined
|
|
462
|
+
}, { env: context.env });
|
|
463
|
+
|
|
464
|
+
const result = {
|
|
465
|
+
kind: "auth_login_complete",
|
|
466
|
+
host: client.host,
|
|
467
|
+
user: user?.name || payload.user_name || payload.user_email || user?.email || null,
|
|
468
|
+
account_id: payload.account_id || null,
|
|
469
|
+
account_name: payload.account_name || null
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
if (values.json) return writeJson(context.stdout, result);
|
|
473
|
+
|
|
474
|
+
writeLine(context.stdout, `Authenticated to ${client.host} as ${result.user || "current user"}.`);
|
|
475
|
+
if (result.account_id) writeLine(context.stdout, `Selected account ${result.account_name || result.account_id} (${result.account_id}).`);
|
|
476
|
+
if (!result.account_id) writeLine(context.stdout, "Run `audienti accounts list` to choose an account.");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function createAuthCallbackServer({ state, context }) {
|
|
480
|
+
let resolvePayload;
|
|
481
|
+
let rejectPayload;
|
|
482
|
+
const waitPromise = new Promise((resolve, reject) => {
|
|
483
|
+
resolvePayload = resolve;
|
|
484
|
+
rejectPayload = reject;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const server = createServer((request, response) => {
|
|
488
|
+
const url = new URL(request.url, "http://127.0.0.1");
|
|
489
|
+
if (url.pathname !== "/callback") {
|
|
490
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
491
|
+
response.end("Not found.");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const payload = Object.fromEntries(url.searchParams.entries());
|
|
496
|
+
if (payload.state !== state) {
|
|
497
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
498
|
+
response.end("Audienti CLI authentication failed: state mismatch.");
|
|
499
|
+
rejectPayload(new CommandError("Browser authentication state mismatch."));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!payload.token) {
|
|
504
|
+
response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
505
|
+
response.end("Audienti CLI authentication failed: missing token.");
|
|
506
|
+
rejectPayload(new CommandError("Browser authentication callback did not include a token."));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
511
|
+
response.end("<!doctype html><title>Audienti CLI authenticated</title><p>Audienti CLI is authenticated. You can close this window.</p>");
|
|
512
|
+
resolvePayload(payload);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await new Promise((resolve, reject) => {
|
|
516
|
+
server.once("error", reject);
|
|
517
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const { port } = server.address();
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
524
|
+
wait(timeoutSeconds) {
|
|
525
|
+
let timeoutId;
|
|
526
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
527
|
+
timeoutId = setTimeout(() => reject(new CommandError(`Timed out waiting for browser authentication after ${timeoutSeconds} seconds.`)), timeoutSeconds * 1000);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
return Promise.race([waitPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
531
|
+
},
|
|
532
|
+
close() {
|
|
533
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function openBrowser(url, context) {
|
|
539
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
540
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
544
|
+
child.unref();
|
|
545
|
+
} catch (error) {
|
|
546
|
+
writeLine(context.stderr, `Could not open browser automatically: ${error.message}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
405
550
|
async function authStatus(args, context, { accountOverride } = {}) {
|
|
406
551
|
assertNoPositionals(args, "Usage: audienti auth status [--account <acct_id>]");
|
|
407
552
|
|
|
408
553
|
const config = await readConfig({ env: context.env });
|
|
409
554
|
if (!config.token) {
|
|
410
|
-
writeLine(context.stdout, "Not authenticated. Run `audienti auth
|
|
555
|
+
writeLine(context.stdout, "Not authenticated. Run `audienti auth login`.");
|
|
411
556
|
return 1;
|
|
412
557
|
}
|
|
413
558
|
|
|
@@ -722,6 +867,24 @@ async function usersActivity(args, context, { accountOverride } = {}) {
|
|
|
722
867
|
renderUserActivity(payload, context);
|
|
723
868
|
}
|
|
724
869
|
|
|
870
|
+
async function setupPlayPreflight(args, context, { accountOverride } = {}) {
|
|
871
|
+
const { values, positionals } = parseCommandArgs(args, {
|
|
872
|
+
...jsonOptions(),
|
|
873
|
+
principal: { type: "string" },
|
|
874
|
+
platform: { type: "string" }
|
|
875
|
+
});
|
|
876
|
+
if (positionals.length > 0) throw new CommandError(SETUP_PLAY_PREFLIGHT_USAGE);
|
|
877
|
+
|
|
878
|
+
const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
|
|
879
|
+
const payload = await client.socialCookies(accountId, compactObject({
|
|
880
|
+
account_user_id: resolveAccountUserId(values.principal || "me", config, { accountOverride }),
|
|
881
|
+
platform: values.platform || "linkedin"
|
|
882
|
+
}));
|
|
883
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
884
|
+
|
|
885
|
+
renderSetupPlayPreflight(payload, context);
|
|
886
|
+
}
|
|
887
|
+
|
|
725
888
|
async function offersList(args, context, { accountOverride } = {}) {
|
|
726
889
|
const { values, positionals } = parseCommandArgs(args, jsonOptions());
|
|
727
890
|
if (positionals.length > 0) throw new CommandError("Usage: audienti offers list [--json] [--account <acct_id>]");
|
|
@@ -2751,6 +2914,18 @@ async function analyticsDashboard(args, context, { accountOverride } = {}) {
|
|
|
2751
2914
|
renderAnalyticsDashboard(payload, context);
|
|
2752
2915
|
}
|
|
2753
2916
|
|
|
2917
|
+
async function analyticsStages(args, context, { accountOverride } = {}) {
|
|
2918
|
+
const { values, positionals } = parseCommandArgs(args, analyticsStagesOptions());
|
|
2919
|
+
if (positionals.length > 0) throw new CommandError(ANALYTICS_STAGES_USAGE);
|
|
2920
|
+
validateDatePair(values["cohort-start"], values["cohort-end"], "--cohort-start", "--cohort-end");
|
|
2921
|
+
|
|
2922
|
+
const { client, accountId, config } = await requireAccountContext(context, { accountOverride });
|
|
2923
|
+
const payload = await client.analyticsStages(accountId, analyticsStagesQuery(values, config, { accountOverride }));
|
|
2924
|
+
if (values.json) return writeJson(context.stdout, payload);
|
|
2925
|
+
|
|
2926
|
+
renderAnalyticsStages(payload, context);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2754
2929
|
async function analyticsCohorts(args, context, { accountOverride } = {}) {
|
|
2755
2930
|
const action = args[0];
|
|
2756
2931
|
if (action !== "create-list") throw new CommandError(ANALYTICS_COHORT_LIST_USAGE);
|
|
@@ -2799,7 +2974,7 @@ function assertNoPositionals(args, usageText) {
|
|
|
2799
2974
|
async function requireAuthenticatedConfig(context) {
|
|
2800
2975
|
const config = await readConfig({ env: context.env });
|
|
2801
2976
|
if (!config.token) {
|
|
2802
|
-
throw new CommandError("Not authenticated. Run `audienti auth token <token>`.");
|
|
2977
|
+
throw new CommandError("Not authenticated. Run `audienti auth login` or `audienti auth token <token>`.");
|
|
2803
2978
|
}
|
|
2804
2979
|
|
|
2805
2980
|
return config;
|
|
@@ -3024,6 +3199,13 @@ function analyticsDashboardOptions() {
|
|
|
3024
3199
|
};
|
|
3025
3200
|
}
|
|
3026
3201
|
|
|
3202
|
+
function analyticsStagesOptions() {
|
|
3203
|
+
return {
|
|
3204
|
+
...analyticsDashboardOptions(),
|
|
3205
|
+
interval: { type: "string" }
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3027
3209
|
function analyticsCohortListOptions() {
|
|
3028
3210
|
return {
|
|
3029
3211
|
...jsonOptions(),
|
|
@@ -3082,6 +3264,13 @@ function analyticsDashboardQuery(values, config = {}, { accountOverride } = {})
|
|
|
3082
3264
|
});
|
|
3083
3265
|
}
|
|
3084
3266
|
|
|
3267
|
+
function analyticsStagesQuery(values, config = {}, { accountOverride } = {}) {
|
|
3268
|
+
return compactObject({
|
|
3269
|
+
...analyticsDashboardQuery(values, config, { accountOverride }),
|
|
3270
|
+
interval: values.interval
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3085
3274
|
function validateDatePair(start, end, startFlag, endFlag) {
|
|
3086
3275
|
if ((start && !end) || (!start && end)) {
|
|
3087
3276
|
throw new CommandError(`${startFlag} and ${endFlag} must be provided together.`);
|
|
@@ -3875,6 +4064,51 @@ function renderUserActivity(payload, context) {
|
|
|
3875
4064
|
}
|
|
3876
4065
|
}
|
|
3877
4066
|
|
|
4067
|
+
function renderSetupPlayPreflight(payload, context) {
|
|
4068
|
+
const accountUser = payload?.account_user || {};
|
|
4069
|
+
const socialCookie = payload?.social_cookie || null;
|
|
4070
|
+
const urls = socialCookie?.urls || {};
|
|
4071
|
+
|
|
4072
|
+
writeLine(
|
|
4073
|
+
context.stdout,
|
|
4074
|
+
`Setup preflight: ${display(payload?.platform, "linkedin")} for ${display(accountUser.name || accountUser.email)} (${display(accountUser.id)})`
|
|
4075
|
+
);
|
|
4076
|
+
writeLine(context.stdout, `Status: ${payload?.ready ? "ready" : "blocked"} (${display(payload?.reason, "unknown")})`);
|
|
4077
|
+
|
|
4078
|
+
if (socialCookie) {
|
|
4079
|
+
writeLine(
|
|
4080
|
+
context.stdout,
|
|
4081
|
+
`Connected account: ${display(socialCookie.username || socialCookie.name)} (${display(socialCookie.prefix_id)})`
|
|
4082
|
+
);
|
|
4083
|
+
writeLine(context.stdout, `Connection status: ${display(socialCookie.status)}`);
|
|
4084
|
+
writeLine(context.stdout, `Mapped to account: ${socialCookie.accessible_to_account ? "yes" : "no"}`);
|
|
4085
|
+
writeLine(context.stdout, `Actionable: ${socialCookie.actionable ? "yes" : "no"}`);
|
|
4086
|
+
renderSetupAutomationSummary(socialCookie.automation || {}, context);
|
|
4087
|
+
} else {
|
|
4088
|
+
writeLine(context.stdout, "Connected account: none");
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
if (!socialCookie && payload?.setup_url) {
|
|
4092
|
+
writeLine(context.stdout, `Setup URL: ${payload.setup_url}`);
|
|
4093
|
+
} else if (payload?.reason === "needs_account_access" && urls.mapping_url) {
|
|
4094
|
+
writeLine(context.stdout, `Mapping URL: ${urls.mapping_url}`);
|
|
4095
|
+
} else if (payload?.reason === "needs_reconnect" && urls.edit_url) {
|
|
4096
|
+
writeLine(context.stdout, `Edit URL: ${urls.edit_url}`);
|
|
4097
|
+
} else if (urls.edit_url) {
|
|
4098
|
+
writeLine(context.stdout, `Edit URL: ${urls.edit_url}`);
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
function renderSetupAutomationSummary(automation, context) {
|
|
4103
|
+
const summary = [
|
|
4104
|
+
`autopilot ${automation.autopilot_enabled ? "on" : "off"}`,
|
|
4105
|
+
`automatic sending ${automation.automatic_sending_enabled ? "on" : "off"}`,
|
|
4106
|
+
`connection requests ${automation.connection_request_autopilot_enabled ? "on" : "off"}`
|
|
4107
|
+
];
|
|
4108
|
+
|
|
4109
|
+
writeLine(context.stdout, `Automation: ${summary.join(", ")}`);
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3878
4112
|
function renderCountRows(context, label, rows) {
|
|
3879
4113
|
if (!Array.isArray(rows) || rows.length === 0) return;
|
|
3880
4114
|
|
|
@@ -5107,6 +5341,82 @@ function renderAnalyticsDashboard(payload, context) {
|
|
|
5107
5341
|
writeCountTable(context, "Current pipeline stages", payload?.pipeline_stage_counts, ["STAGE", "COUNT"], countRow);
|
|
5108
5342
|
}
|
|
5109
5343
|
|
|
5344
|
+
function renderAnalyticsStages(payload, context) {
|
|
5345
|
+
const conversionGrid = payload?.conversion_grid || {};
|
|
5346
|
+
writeLine(context.stdout, `Stage analytics (${display(payload?.cohort?.label, "selected cohort")})`);
|
|
5347
|
+
if (payload?.cohort) {
|
|
5348
|
+
writeLine(context.stdout, `Cohort: ${payload.cohort.start_date} to ${payload.cohort.end_date} (${display(payload.cohort.field, "account_prospects.created_at")})`);
|
|
5349
|
+
}
|
|
5350
|
+
writeDashboardFilters(payload, context);
|
|
5351
|
+
writeLine(context.stdout, `Conversion interval: ${display(conversionGrid.interval, "weekly")}`);
|
|
5352
|
+
writeLine(context.stdout, `Connection maturity window: ${display(conversionGrid.maturity_days, 21)} days`);
|
|
5353
|
+
writeStageConversionTable(conversionGrid, context);
|
|
5354
|
+
writeStageAgingTable(payload?.stage_aging, context);
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function writeStageConversionTable(conversionGrid, context) {
|
|
5358
|
+
const rows = Array.isArray(conversionGrid?.rows) ? conversionGrid.rows : [];
|
|
5359
|
+
const definitions = Array.isArray(conversionGrid?.stage_definitions) ? conversionGrid.stage_definitions : [];
|
|
5360
|
+
const columns = definitions.filter((definition) => definition?.key);
|
|
5361
|
+
|
|
5362
|
+
writeLine(context.stdout, "");
|
|
5363
|
+
writeLine(context.stdout, display(conversionGrid?.window_description, "Conversion cohorts"));
|
|
5364
|
+
if (rows.length === 0 || columns.length === 0) return writeLine(context.stdout, "None");
|
|
5365
|
+
|
|
5366
|
+
const headers = ["COHORT", ...columns.map((column) => display(column.label || column.key))];
|
|
5367
|
+
const tableRows = rows.map((row) => [
|
|
5368
|
+
display(row?.label),
|
|
5369
|
+
...columns.map((column) => stageMetricValueLabel(row?.values?.[column.key]))
|
|
5370
|
+
]);
|
|
5371
|
+
|
|
5372
|
+
writeAlignedTable(context, headers, tableRows);
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
function writeStageAgingTable(stageAging, context) {
|
|
5376
|
+
const rows = Array.isArray(stageAging?.rows) ? stageAging.rows : [];
|
|
5377
|
+
writeLine(context.stdout, "");
|
|
5378
|
+
writeLine(context.stdout, "Stage Aging");
|
|
5379
|
+
if (rows.length === 0) return writeLine(context.stdout, "None");
|
|
5380
|
+
|
|
5381
|
+
const totals = stageAging?.totals || {};
|
|
5382
|
+
writeLine(
|
|
5383
|
+
context.stdout,
|
|
5384
|
+
`Totals: ${display(totals.current_count, 0)} current, ${display(totals.due_soon_count, 0)} due soon, ${display(totals.overdue_count, 0)} overdue (${percentageLabel(totals.overdue_rate)})`
|
|
5385
|
+
);
|
|
5386
|
+
writeAlignedTable(
|
|
5387
|
+
context,
|
|
5388
|
+
["STAGE", "CURRENT", "DUE SOON", "OVERDUE", "OVERDUE %", "MED AGE", "OLDEST IDLE"],
|
|
5389
|
+
rows.map(stageAgingRow),
|
|
5390
|
+
{ numericColumns: [false, true, true, true, true, true, true] }
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
function stageAgingRow(row) {
|
|
5395
|
+
return [
|
|
5396
|
+
display(row?.label || row?.key),
|
|
5397
|
+
display(row?.current_count, 0),
|
|
5398
|
+
display(row?.due_soon_count, 0),
|
|
5399
|
+
display(row?.overdue_count, 0),
|
|
5400
|
+
percentageLabel(row?.overdue_rate),
|
|
5401
|
+
dayCountLabel(row?.median_stage_age_days),
|
|
5402
|
+
dayCountLabel(row?.oldest_idle_days)
|
|
5403
|
+
];
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
function stageMetricValueLabel(value) {
|
|
5407
|
+
if (!value) return "-";
|
|
5408
|
+
if (value.kind === "count") return display(value.count, 0);
|
|
5409
|
+
|
|
5410
|
+
const numerator = display(value.numerator, 0);
|
|
5411
|
+
const denominator = display(value.denominator, 0);
|
|
5412
|
+
const suffix = value.maturing ? " maturing" : "";
|
|
5413
|
+
return `${numerator}/${denominator} ${percentageLabel(value.rate)}${suffix}`;
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
function dayCountLabel(value) {
|
|
5417
|
+
return value === undefined || value === null || value === "" ? "-" : `${value}d`;
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5110
5420
|
function renderAnalyticsCohortList(payload, context) {
|
|
5111
5421
|
const list = payload?.list || {};
|
|
5112
5422
|
writeLine(context.stdout, `Created analytics cohort list ${display(list.name)} (${display(list.prefix_id)}).`);
|
|
@@ -5813,6 +6123,7 @@ const HELP_TOPICS = new Map([
|
|
|
5813
6123
|
" audienti <command> [options]",
|
|
5814
6124
|
"",
|
|
5815
6125
|
"Start:",
|
|
6126
|
+
" audienti auth login Sign in through the browser",
|
|
5816
6127
|
" audienti auth token <token> Save an API token",
|
|
5817
6128
|
" audienti accounts list See accounts available to this token",
|
|
5818
6129
|
" audienti accounts select <acct_id> Use one account by default",
|
|
@@ -5824,6 +6135,7 @@ const HELP_TOPICS = new Map([
|
|
|
5824
6135
|
" audienti auth status",
|
|
5825
6136
|
" audienti config list",
|
|
5826
6137
|
" audienti update check",
|
|
6138
|
+
" audienti setup play preflight",
|
|
5827
6139
|
" audienti users list",
|
|
5828
6140
|
" audienti users select <account_user_id|email|name|me>",
|
|
5829
6141
|
" audienti users activity [account_user_id|me]",
|
|
@@ -5914,6 +6226,7 @@ const HELP_TOPICS = new Map([
|
|
|
5914
6226
|
" Analytics",
|
|
5915
6227
|
" audienti analytics prospects --window 24h",
|
|
5916
6228
|
" audienti analytics dashboard --play-tag <tag>",
|
|
6229
|
+
" audienti analytics stages --interval weekly",
|
|
5917
6230
|
" audienti analytics cohorts create-list --name \"Blank note test\" --start 2026-07-20 --end 2026-07-20 --note-mode blank",
|
|
5918
6231
|
" audienti analytics prospects cohort-analysis --weeks 4 --motion <motn_id>",
|
|
5919
6232
|
" audienti analytics users --user me --window 30d",
|
|
@@ -5934,6 +6247,7 @@ const HELP_TOPICS = new Map([
|
|
|
5934
6247
|
" Preview a campaign: audienti writer test-run <prsp_id>",
|
|
5935
6248
|
" Analyze one motion: audienti motions analytics <motn_id>",
|
|
5936
6249
|
" Count one campaign: audienti analytics dashboard --play-tag <tag>",
|
|
6250
|
+
" Compare stage rates: audienti analytics stages --interval weekly",
|
|
5937
6251
|
" Audit your work: audienti analytics users --user me --window 30d",
|
|
5938
6252
|
" Review reminders: audienti tasks list",
|
|
5939
6253
|
"",
|
|
@@ -5949,6 +6263,7 @@ const HELP_TOPICS = new Map([
|
|
|
5949
6263
|
|
|
5950
6264
|
["auth", [
|
|
5951
6265
|
"Usage:",
|
|
6266
|
+
" audienti auth login [--host <url>] [--timeout-seconds <n>] [--no-open] [--json]",
|
|
5952
6267
|
" audienti auth token <token> [--host <url>]",
|
|
5953
6268
|
" audienti auth status",
|
|
5954
6269
|
" audienti auth logout",
|
|
@@ -5956,11 +6271,32 @@ const HELP_TOPICS = new Map([
|
|
|
5956
6271
|
"Status: implemented",
|
|
5957
6272
|
"",
|
|
5958
6273
|
"Commands:",
|
|
6274
|
+
" audienti auth login Open Audienti in a browser and save the returned token",
|
|
5959
6275
|
" audienti auth token <token> Validate and save a bearer API token",
|
|
5960
6276
|
" audienti auth status Check live auth and show selected account",
|
|
5961
6277
|
" audienti auth logout Delete local CLI auth config",
|
|
5962
6278
|
"",
|
|
5963
|
-
"Run `audienti auth
|
|
6279
|
+
"Run `audienti auth login help` for browser authentication."
|
|
6280
|
+
].join("\n")],
|
|
6281
|
+
|
|
6282
|
+
["auth login", [
|
|
6283
|
+
"Usage:",
|
|
6284
|
+
" audienti auth login [--host <url>] [--timeout-seconds <n>] [--no-open] [--json]",
|
|
6285
|
+
"",
|
|
6286
|
+
"Status: implemented",
|
|
6287
|
+
"",
|
|
6288
|
+
"Options:",
|
|
6289
|
+
" --host <url> Audienti host. Default: https://app.audienti.com",
|
|
6290
|
+
" --timeout-seconds <n> Wait budget before the command fails. Default: 180",
|
|
6291
|
+
" --no-open Print the URL without opening a browser",
|
|
6292
|
+
" --json Print machine-readable start and completion payloads",
|
|
6293
|
+
"",
|
|
6294
|
+
"Flow:",
|
|
6295
|
+
" Starts a temporary 127.0.0.1 callback server, opens /cli/auth in the browser,",
|
|
6296
|
+
" and saves the returned API token to ~/.config/audienti/config.json after validation.",
|
|
6297
|
+
"",
|
|
6298
|
+
"Example:",
|
|
6299
|
+
" audienti auth login --host https://app.audienti.com"
|
|
5964
6300
|
].join("\n")],
|
|
5965
6301
|
|
|
5966
6302
|
["auth token", [
|
|
@@ -6114,6 +6450,53 @@ const HELP_TOPICS = new Map([
|
|
|
6114
6450
|
" Saves accountId and accountName in local CLI config."
|
|
6115
6451
|
].join("\n")],
|
|
6116
6452
|
|
|
6453
|
+
["setup", [
|
|
6454
|
+
"Usage:",
|
|
6455
|
+
" audienti setup play preflight [--principal <account_user_id|email|name|me>] [--platform linkedin] [--json]",
|
|
6456
|
+
"",
|
|
6457
|
+
"Status: implemented",
|
|
6458
|
+
"",
|
|
6459
|
+
"Purpose:",
|
|
6460
|
+
" Preflight account setup before an agent creates or activates an Audienti play.",
|
|
6461
|
+
"",
|
|
6462
|
+
"Commands:",
|
|
6463
|
+
" audienti setup play preflight Check connected-account readiness and direct setup URLs"
|
|
6464
|
+
].join("\n")],
|
|
6465
|
+
|
|
6466
|
+
["setup play", [
|
|
6467
|
+
"Usage:",
|
|
6468
|
+
" audienti setup play preflight [--principal <account_user_id|email|name|me>] [--platform linkedin] [--json]",
|
|
6469
|
+
"",
|
|
6470
|
+
"Status: implemented",
|
|
6471
|
+
"",
|
|
6472
|
+
"Purpose:",
|
|
6473
|
+
" Check the sender identity prerequisites for a new motion or play."
|
|
6474
|
+
].join("\n")],
|
|
6475
|
+
|
|
6476
|
+
["setup play preflight", [
|
|
6477
|
+
"Usage:",
|
|
6478
|
+
` ${SETUP_PLAY_PREFLIGHT_USAGE.slice("Usage: ".length)}`,
|
|
6479
|
+
"",
|
|
6480
|
+
"Status: implemented",
|
|
6481
|
+
"",
|
|
6482
|
+
"Purpose:",
|
|
6483
|
+
" Verify that the selected principal has a connected account mapped to the current Audienti account before a play starts capture or outreach.",
|
|
6484
|
+
"",
|
|
6485
|
+
"Output shape:",
|
|
6486
|
+
" ready: true when the selected principal has a mapped, actionable platform account",
|
|
6487
|
+
" reason: ready | needs_setup | needs_account_access | needs_reconnect",
|
|
6488
|
+
" setup_url: direct operations URL for creating a connected account",
|
|
6489
|
+
" social_cookie.urls.mapping_url: owner URL for granting account access",
|
|
6490
|
+
" social_cookie.urls.edit_url: operations URL for reconnect/settings review when mapped",
|
|
6491
|
+
"",
|
|
6492
|
+
"Examples:",
|
|
6493
|
+
" audienti setup play preflight --principal me --platform linkedin",
|
|
6494
|
+
" audienti setup play preflight --principal 42 --json",
|
|
6495
|
+
"",
|
|
6496
|
+
"API:",
|
|
6497
|
+
" GET /api/v1/accounts/:account_id/social_cookies.json"
|
|
6498
|
+
].join("\n")],
|
|
6499
|
+
|
|
6117
6500
|
["users", [
|
|
6118
6501
|
"Usage:",
|
|
6119
6502
|
" audienti users list [--json]",
|
|
@@ -8442,6 +8825,7 @@ const HELP_TOPICS = new Map([
|
|
|
8442
8825
|
"Usage:",
|
|
8443
8826
|
" audienti analytics prospects [--window 24h] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--list <list_id>] [--user <account_user_id|email|name|me>] [--json]",
|
|
8444
8827
|
" audienti analytics dashboard [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--list <list_id>] [--json]",
|
|
8828
|
+
" audienti analytics stages [--interval weekly|monthly] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--play-tag <tag>] [--motion <motn_id>] [--list <list_id>] [--json]",
|
|
8445
8829
|
" audienti analytics cohorts create-list --name <text> --start YYYY-MM-DD --end YYYY-MM-DD [--note-mode <any|with_note|blank>] [--user <account_user_id|email|name|me>] [--json]",
|
|
8446
8830
|
" audienti analytics prospects cohort-analysis [--weeks <n>] [--window 24h] [--motion <motn_id>] [--list <list_id>] [--user <account_user_id|email|name|me>] [--json]",
|
|
8447
8831
|
" audienti analytics users [--user <account_user_id|email|name|me>] [--window 30d | --start YYYY-MM-DD --end YYYY-MM-DD] [--cohort-start YYYY-MM-DD --cohort-end YYYY-MM-DD] [--motion <motn_id>] [--list <list_id>] [--platform <linkedin|email|gmail>] [--json]",
|
|
@@ -8458,6 +8842,7 @@ const HELP_TOPICS = new Map([
|
|
|
8458
8842
|
" --motion <motn_id> For prospect and user analytics, filter AccountProspect.motion_id to one motion/play.",
|
|
8459
8843
|
" --list <list_id> Filter analytics to prospects in one list, including analytics cohort lists.",
|
|
8460
8844
|
" --play-tag <tag> For dashboard analytics, filter to motions/lists tagged with a campaign tag.",
|
|
8845
|
+
" --interval <weekly|monthly> For stages analytics, select the conversion cohort grain.",
|
|
8461
8846
|
" --provenance <source> Optional lower-level AccountProspect.intake_source filter.",
|
|
8462
8847
|
" --platform <linkedin|email|gmail> For user analytics, filter events.platform. --channel is accepted as an alias.",
|
|
8463
8848
|
" cohort-analysis loops over recent weekly AccountProspect.created_at cohorts and compares their current stages.",
|
|
@@ -8497,6 +8882,35 @@ const HELP_TOPICS = new Map([
|
|
|
8497
8882
|
" GET /api/v1/accounts/:account_id/analytics/dashboard.json"
|
|
8498
8883
|
].join("\n")],
|
|
8499
8884
|
|
|
8885
|
+
["analytics stages", [
|
|
8886
|
+
"Usage:",
|
|
8887
|
+
` ${ANALYTICS_STAGES_USAGE.slice("Usage: ".length)}`,
|
|
8888
|
+
"",
|
|
8889
|
+
"Status: implemented",
|
|
8890
|
+
"",
|
|
8891
|
+
"Purpose:",
|
|
8892
|
+
" Return the stages dashboard read model through the CLI, including weekly or monthly conversion cohorts and current stage aging.",
|
|
8893
|
+
"",
|
|
8894
|
+
"Options:",
|
|
8895
|
+
" --interval <weekly|monthly> Conversion cohort grain. Defaults to weekly.",
|
|
8896
|
+
" --cohort-start <YYYY-MM-DD> --cohort-end <YYYY-MM-DD> Select the AccountProspect.created_at cohort.",
|
|
8897
|
+
" --play-tag <tag> Filter to motions/lists tagged with a campaign tag. --tag is accepted as an alias.",
|
|
8898
|
+
" --motion <motn_id> Filter to one motion/play.",
|
|
8899
|
+
" --list <list_id> Filter to prospects in one list, including analytics cohort lists.",
|
|
8900
|
+
" --offer <offr_id> Filter to one offer.",
|
|
8901
|
+
" --icp <icp_id> Filter to one ICP.",
|
|
8902
|
+
" --user <account_user_id|email|name|me> Filter to prospects assigned to one account user.",
|
|
8903
|
+
"",
|
|
8904
|
+
"Output shape:",
|
|
8905
|
+
" conversion_grid.rows[]: period label, date bounds, totals_by_stage, and values keyed by stage metric",
|
|
8906
|
+
" conversion_grid.stage_definitions[]: stable metric keys and numerator/denominator stage definitions",
|
|
8907
|
+
" stage_aging.rows[]: current, due soon, overdue, age, and idle counts by current pipeline stage",
|
|
8908
|
+
" stage_aging.totals: rollup current, due soon, overdue, overdue_rate, and oldest_idle_days",
|
|
8909
|
+
"",
|
|
8910
|
+
"API:",
|
|
8911
|
+
" GET /api/v1/accounts/:account_id/analytics/stages.json"
|
|
8912
|
+
].join("\n")],
|
|
8913
|
+
|
|
8500
8914
|
["analytics prospects", [
|
|
8501
8915
|
"Usage:",
|
|
8502
8916
|
` ${ANALYTICS_PROSPECTS_USAGE.slice("Usage: ".length)}`,
|
|
@@ -8661,15 +9075,17 @@ const HELP_TOPICS = new Map([
|
|
|
8661
9075
|
" Give a local coding agent the shortest safe path through the common Audienti production workflows.",
|
|
8662
9076
|
"",
|
|
8663
9077
|
"1. Authenticate and select an account",
|
|
8664
|
-
" audienti auth
|
|
9078
|
+
" audienti auth login",
|
|
8665
9079
|
" audienti accounts list",
|
|
8666
9080
|
" audienti accounts select <acct_id>",
|
|
8667
9081
|
" audienti users list",
|
|
8668
9082
|
" audienti users select me",
|
|
9083
|
+
" audienti setup play preflight --principal me --platform linkedin",
|
|
8669
9084
|
" audienti offers list",
|
|
8670
9085
|
" audienti icps list",
|
|
8671
9086
|
"",
|
|
8672
9087
|
"2. Create a motion or play",
|
|
9088
|
+
" audienti setup play preflight --principal <account_user_id|me> --platform linkedin",
|
|
8673
9089
|
" audienti motions create --payload <file.json>",
|
|
8674
9090
|
" audienti motions clone <motn_id> --name \"New subset motion\"",
|
|
8675
9091
|
" audienti motions move-prospects <source_motn_id> --target <target_motn_id> <prsp_id> [prsp_id...]",
|
|
@@ -8725,6 +9141,7 @@ const HELP_TOPICS = new Map([
|
|
|
8725
9141
|
" audienti users activity me --window 7d",
|
|
8726
9142
|
" audienti analytics prospects --window 24h",
|
|
8727
9143
|
" audienti analytics dashboard --play-tag wine_campaign",
|
|
9144
|
+
" audienti analytics stages --interval weekly --play-tag wine_campaign",
|
|
8728
9145
|
" audienti analytics cohorts create-list --name \"Connection requests 2026-07-20\" --start 2026-07-20 --end 2026-07-20",
|
|
8729
9146
|
" audienti analytics users --user me --window 30d",
|
|
8730
9147
|
" audienti analytics visibility --window 24h --user me",
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ApiError, AudientiClient, DEFAULT_HOST } from "./api-client.js";
|
|
2
|
+
import { readConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export async function runMcpServer({
|
|
5
|
+
input = process.stdin,
|
|
6
|
+
output = process.stdout,
|
|
7
|
+
env = process.env,
|
|
8
|
+
fetchImpl = globalThis.fetch
|
|
9
|
+
} = {}) {
|
|
10
|
+
input.setEncoding("utf8");
|
|
11
|
+
|
|
12
|
+
let buffer = "";
|
|
13
|
+
for await (const chunk of input) {
|
|
14
|
+
buffer += chunk;
|
|
15
|
+
const lines = buffer.split(/\r?\n/);
|
|
16
|
+
buffer = lines.pop() || "";
|
|
17
|
+
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
if (!line.trim()) continue;
|
|
20
|
+
const response = await handleRawMessage(line, { env, fetchImpl });
|
|
21
|
+
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handleRawMessage(line, options = {}) {
|
|
27
|
+
try {
|
|
28
|
+
return handleMcpRequest(JSON.parse(line), options);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return errorResponse(null, -32700, error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleMcpRequest(message, { env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
35
|
+
if (!message || message.jsonrpc !== "2.0") {
|
|
36
|
+
return errorResponse(message?.id ?? null, -32600, "Invalid JSON-RPC request.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (message.id === undefined || message.id === null) return null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await remoteMcpResponse(message, { env, fetchImpl });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return errorResponse(message.id, -32603, toolErrorMessage(error), toolErrorPayload(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function remoteMcpResponse(message, { env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
49
|
+
const config = await readConfig({ env });
|
|
50
|
+
if (!config.token) throw new Error("Not authenticated. Run `audienti auth login` or `audienti auth token <token>`.");
|
|
51
|
+
|
|
52
|
+
const client = new AudientiClient({
|
|
53
|
+
host: config.host || DEFAULT_HOST,
|
|
54
|
+
token: config.token,
|
|
55
|
+
fetchImpl
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return client.mcp(withSelectedAccount(message, config));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withSelectedAccount(message, config) {
|
|
62
|
+
if (message.method !== "tools/call" || !config.accountId) return message;
|
|
63
|
+
|
|
64
|
+
const params = message.params || {};
|
|
65
|
+
const args = params.arguments || {};
|
|
66
|
+
if (args.account_id) return message;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...message,
|
|
70
|
+
params: {
|
|
71
|
+
...params,
|
|
72
|
+
arguments: {
|
|
73
|
+
...args,
|
|
74
|
+
account_id: config.accountId
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function errorResponse(id, code, message, data) {
|
|
81
|
+
return {
|
|
82
|
+
jsonrpc: "2.0",
|
|
83
|
+
id,
|
|
84
|
+
error: {
|
|
85
|
+
code,
|
|
86
|
+
message,
|
|
87
|
+
...(data === undefined ? {} : { data })
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toolErrorMessage(error) {
|
|
93
|
+
if (error instanceof ApiError && error.status) return `${error.message} (HTTP ${error.status})`;
|
|
94
|
+
return error.message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toolErrorPayload(error) {
|
|
98
|
+
return {
|
|
99
|
+
error: error.message,
|
|
100
|
+
status: error instanceof ApiError ? error.status || null : null,
|
|
101
|
+
body: error instanceof ApiError ? error.body || null : null
|
|
102
|
+
};
|
|
103
|
+
}
|