@carrierllc/mcp 0.1.0 → 0.2.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/dist/.metadata_never_index +0 -0
- package/dist/apps/app-state.d.ts +19 -0
- package/dist/apps/app-state.d.ts.map +1 -0
- package/dist/apps/app-state.js +28 -0
- package/dist/apps/app-state.js.map +1 -0
- package/dist/apps/balance-topup.d.ts +19 -0
- package/dist/apps/balance-topup.d.ts.map +1 -0
- package/dist/apps/balance-topup.js +121 -0
- package/dist/apps/balance-topup.js.map +1 -0
- package/dist/apps/fleet-health-app.d.ts +29 -0
- package/dist/apps/fleet-health-app.d.ts.map +1 -0
- package/dist/apps/fleet-health-app.js +152 -0
- package/dist/apps/fleet-health-app.js.map +1 -0
- package/dist/apps/index.d.ts +37 -0
- package/dist/apps/index.d.ts.map +1 -0
- package/dist/apps/index.js +42 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/provisioning-wizard.d.ts +11 -0
- package/dist/apps/provisioning-wizard.d.ts.map +1 -0
- package/dist/apps/provisioning-wizard.js +345 -0
- package/dist/apps/provisioning-wizard.js.map +1 -0
- package/dist/audit.d.ts +31 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +42 -0
- package/dist/audit.js.map +1 -0
- package/dist/billing-thresholds.d.ts +71 -0
- package/dist/billing-thresholds.d.ts.map +1 -0
- package/dist/billing-thresholds.js +238 -0
- package/dist/billing-thresholds.js.map +1 -0
- package/dist/billing.d.ts +95 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +239 -0
- package/dist/billing.js.map +1 -0
- package/dist/clerk.d.ts +125 -0
- package/dist/clerk.d.ts.map +1 -0
- package/dist/clerk.js +389 -0
- package/dist/clerk.js.map +1 -0
- package/dist/client.d.ts +12 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +56 -53
- package/dist/client.js.map +1 -1
- package/dist/credits.d.ts +165 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +483 -0
- package/dist/credits.js.map +1 -0
- package/dist/index.d.ts +6 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -23
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +2 -8
- package/dist/intelligence.d.ts.map +1 -1
- package/dist/intelligence.js +399 -154
- package/dist/intelligence.js.map +1 -1
- package/dist/list-recent-ocs-events.d.ts +48 -0
- package/dist/list-recent-ocs-events.d.ts.map +1 -0
- package/dist/list-recent-ocs-events.js +160 -0
- package/dist/list-recent-ocs-events.js.map +1 -0
- package/dist/manus-client.d.ts +429 -0
- package/dist/manus-client.d.ts.map +1 -0
- package/dist/manus-client.js +401 -0
- package/dist/manus-client.js.map +1 -0
- package/dist/manus-common.d.ts +5 -0
- package/dist/manus-common.d.ts.map +1 -0
- package/dist/manus-common.js +5 -0
- package/dist/manus-common.js.map +1 -0
- package/dist/manus-schedule.d.ts +74 -0
- package/dist/manus-schedule.d.ts.map +1 -0
- package/dist/manus-schedule.js +213 -0
- package/dist/manus-schedule.js.map +1 -0
- package/dist/manus-usage.d.ts +32 -0
- package/dist/manus-usage.d.ts.map +1 -0
- package/dist/manus-usage.js +120 -0
- package/dist/manus-usage.js.map +1 -0
- package/dist/manus-webhook.d.ts +60 -0
- package/dist/manus-webhook.d.ts.map +1 -0
- package/dist/manus-webhook.js +361 -0
- package/dist/manus-webhook.js.map +1 -0
- package/dist/pricing-tools.d.ts +26 -0
- package/dist/pricing-tools.d.ts.map +1 -0
- package/dist/pricing-tools.js +428 -0
- package/dist/pricing-tools.js.map +1 -0
- package/dist/projects-tools.d.ts +39 -0
- package/dist/projects-tools.d.ts.map +1 -0
- package/dist/projects-tools.js +545 -0
- package/dist/projects-tools.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +1 -0
- package/dist/prompts.js.map +1 -1
- package/dist/stripe-connect-tools.d.ts +33 -0
- package/dist/stripe-connect-tools.d.ts.map +1 -0
- package/dist/stripe-connect-tools.js +504 -0
- package/dist/stripe-connect-tools.js.map +1 -0
- package/dist/timing-safe-equal.d.ts +5 -0
- package/dist/timing-safe-equal.d.ts.map +1 -0
- package/dist/timing-safe-equal.js +13 -0
- package/dist/timing-safe-equal.js.map +1 -0
- package/dist/tools-backlog.d.ts +37 -0
- package/dist/tools-backlog.d.ts.map +1 -0
- package/dist/tools-backlog.js +410 -0
- package/dist/tools-backlog.js.map +1 -0
- package/dist/tools-carrier-ask.d.ts +138 -0
- package/dist/tools-carrier-ask.d.ts.map +1 -0
- package/dist/tools-carrier-ask.js +913 -0
- package/dist/tools-carrier-ask.js.map +1 -0
- package/dist/tools-pricing.d.ts +18 -0
- package/dist/tools-pricing.d.ts.map +1 -0
- package/dist/tools-pricing.js +172 -0
- package/dist/tools-pricing.js.map +1 -0
- package/dist/tools-ui-agent-ask.d.ts +29 -0
- package/dist/tools-ui-agent-ask.d.ts.map +1 -0
- package/dist/tools-ui-agent-ask.js +272 -0
- package/dist/tools-ui-agent-ask.js.map +1 -0
- package/dist/tools-ui-agent-schedule.d.ts +29 -0
- package/dist/tools-ui-agent-schedule.d.ts.map +1 -0
- package/dist/tools-ui-agent-schedule.js +517 -0
- package/dist/tools-ui-agent-schedule.js.map +1 -0
- package/dist/tools-ui-agent.d.ts +40 -0
- package/dist/tools-ui-agent.d.ts.map +1 -0
- package/dist/tools-ui-agent.js +508 -0
- package/dist/tools-ui-agent.js.map +1 -0
- package/dist/tools.d.ts +44 -13
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +892 -210
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +298 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +15 -8
package/dist/intelligence.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Carrier MCP stdio — Intelligence tools (8 composites).
|
|
3
|
-
*
|
|
4
|
-
* These tools chain multiple OCS read methods to produce structured insights.
|
|
5
|
-
* Stdio variant: no scope enforcement, no audit hook, no Sentry.
|
|
6
|
-
*/
|
|
7
1
|
import { z } from "zod";
|
|
8
|
-
|
|
2
|
+
import { mccToIso } from "@carrier/ocs-spec/mcc-iso";
|
|
3
|
+
import { OcsClient } from "./client.js";
|
|
4
|
+
import { getDefaultResellerId } from "./tools.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
async function safeCall(env, token, method, params = {}) {
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
const client = new OcsClient(env.CARRIER_OCS_BASE_URL, token);
|
|
11
|
+
const data = await client.call(method, params);
|
|
12
|
+
return { data, error: null };
|
|
11
13
|
}
|
|
12
14
|
catch (err) {
|
|
13
15
|
return { data: null, error: err instanceof Error ? err.message : String(err) };
|
|
@@ -21,7 +23,9 @@ function formatBytes(bytes) {
|
|
|
21
23
|
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
22
24
|
}
|
|
23
25
|
function daysUntil(dateStr) {
|
|
24
|
-
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const target = new Date(dateStr);
|
|
28
|
+
return Math.ceil((target.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
25
29
|
}
|
|
26
30
|
function toISODate(d) {
|
|
27
31
|
return d.toISOString().split("T")[0];
|
|
@@ -29,60 +33,60 @@ function toISODate(d) {
|
|
|
29
33
|
function result(text, isError = false) {
|
|
30
34
|
return { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}) };
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
return result(`Error: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
export function registerAllIntelligenceTools(server, client) {
|
|
43
|
-
// -------------------------------------------------------------------------
|
|
44
|
-
// 1. DIAGNOSE SUBSCRIBER
|
|
45
|
-
// -------------------------------------------------------------------------
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// 1. DIAGNOSE SUBSCRIBER — "why is this subscriber offline?"
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export function registerIntelligenceTools(server, ctx) {
|
|
46
40
|
server.registerTool("diagnose_subscriber", {
|
|
47
41
|
title: "Diagnose Subscriber Issues",
|
|
48
|
-
description: "Smart diagnostic that chains multiple API calls to
|
|
49
|
-
|
|
42
|
+
description: "Smart diagnostic that chains multiple API calls to analyze why a subscriber " +
|
|
43
|
+
"may be offline, throttled, or having connectivity issues. Returns a structured " +
|
|
44
|
+
"diagnosis with root cause analysis and recommended actions.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
iccid: z.string().describe("The subscriber ICCID to diagnose"),
|
|
47
|
+
},
|
|
50
48
|
annotations: { readOnlyHint: true },
|
|
51
|
-
},
|
|
49
|
+
}, async ({ iccid }) => {
|
|
50
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
52
51
|
const findings = [];
|
|
53
52
|
const actions = [];
|
|
54
53
|
let severity = "healthy";
|
|
55
|
-
|
|
54
|
+
// 1. Get subscriber details
|
|
55
|
+
const sub = await safeCall(ctx.env, token, "getSingleSubscriber", { iccid });
|
|
56
56
|
if (sub.error)
|
|
57
57
|
return result(`Failed to fetch subscriber: ${sub.error}`, true);
|
|
58
58
|
if (!sub.data)
|
|
59
59
|
return result("Subscriber not found", true);
|
|
60
60
|
const status = String(sub.data.status ?? "").toUpperCase();
|
|
61
61
|
const balance = Number(sub.data.balance ?? 0);
|
|
62
|
+
// Check OCS status
|
|
62
63
|
if (status !== "ACTIVE") {
|
|
63
64
|
findings.push(`OCS status is ${status} (not ACTIVE)`);
|
|
64
|
-
actions.push(
|
|
65
|
+
actions.push(`Reactivate subscriber via modify_subscriber_status`);
|
|
65
66
|
severity = "critical";
|
|
66
67
|
}
|
|
68
|
+
// Check balance
|
|
67
69
|
if (balance <= 0) {
|
|
68
70
|
findings.push(`Balance is ${balance} — subscriber may be blocked from usage`);
|
|
69
|
-
actions.push(
|
|
71
|
+
actions.push(`Top up balance via modify_subscriber_balance`);
|
|
70
72
|
if (severity !== "critical")
|
|
71
73
|
severity = "warning";
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
75
|
+
// 2. Check SIM provider status — OCS expects bare simId (Long), resolve from subscriber record
|
|
76
|
+
const simId = sub.data.simId ?? sub.data.sim_id ?? sub.data.id;
|
|
77
|
+
const sim = simId !== undefined
|
|
78
|
+
? await safeCall(ctx.env, token, "getSimProviderStatus", Number(simId))
|
|
76
79
|
: { data: null, error: null };
|
|
77
80
|
if (sim.data) {
|
|
78
81
|
const simStatus = String(sim.data.simStatus ?? sim.data.status ?? "").toUpperCase();
|
|
79
82
|
if (simStatus && !["ENABLED", "ACTIVE", "ACTIVATED"].includes(simStatus)) {
|
|
80
83
|
findings.push(`SIM provider status is ${simStatus} — SIM may be disabled at network level`);
|
|
81
|
-
actions.push(
|
|
84
|
+
actions.push(`Enable SIM via change_sim_status`);
|
|
82
85
|
severity = "critical";
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
|
|
88
|
+
// 3. Check packages
|
|
89
|
+
const pkgs = await safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid });
|
|
86
90
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
87
91
|
const activePkgs = pkgs.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
88
92
|
if (activePkgs.length === 0) {
|
|
@@ -91,15 +95,18 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
91
95
|
severity = "critical";
|
|
92
96
|
}
|
|
93
97
|
else {
|
|
98
|
+
// Check for depleted packages
|
|
94
99
|
for (const pkg of activePkgs) {
|
|
95
100
|
const dataUsed = Number(pkg.dataUsed ?? pkg.dataConsumed ?? 0);
|
|
96
101
|
const dataLimit = Number(pkg.dataLimit ?? pkg.dataAllowance ?? 0);
|
|
97
102
|
if (dataLimit > 0 && dataUsed >= dataLimit) {
|
|
98
|
-
findings.push(`Package "${pkg.name ?? pkg.packageTemplateId}" data depleted:
|
|
103
|
+
findings.push(`Package "${pkg.name ?? pkg.packageTemplateId}" data depleted: ` +
|
|
104
|
+
`${formatBytes(dataUsed)} / ${formatBytes(dataLimit)}`);
|
|
99
105
|
actions.push("Assign additional package or increase limits via modify_package_limits");
|
|
100
106
|
if (severity !== "critical")
|
|
101
107
|
severity = "warning";
|
|
102
108
|
}
|
|
109
|
+
// Check expiry
|
|
103
110
|
const expiry = String(pkg.expirationDate ?? pkg.endDate ?? "");
|
|
104
111
|
if (expiry) {
|
|
105
112
|
const days = daysUntil(expiry);
|
|
@@ -119,9 +126,13 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
}
|
|
129
|
+
// 4. Check recent network events (last 2 days)
|
|
122
130
|
const now = new Date();
|
|
123
131
|
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
|
|
124
|
-
const events = await safeCall(
|
|
132
|
+
const events = await safeCall(ctx.env, token, "subscriberNetworkEventsOverPeriod", {
|
|
133
|
+
subscriber: { iccid },
|
|
134
|
+
period: { start: toISODate(twoDaysAgo), end: toISODate(now) },
|
|
135
|
+
});
|
|
125
136
|
if (events.data && Array.isArray(events.data)) {
|
|
126
137
|
if (events.data.length === 0) {
|
|
127
138
|
findings.push("No network events in last 48 hours — device may be powered off or out of coverage");
|
|
@@ -134,9 +145,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
134
145
|
findings.push(`Last network event: ${lastType} at ${lastEvent.timestamp ?? lastEvent.date ?? "unknown"}`);
|
|
135
146
|
}
|
|
136
147
|
}
|
|
148
|
+
// 5. Check HLR bitrate — OCS expects { imsi }, resolve from subscriber record
|
|
137
149
|
const imsiForBitrate = typeof sub.data.imsi === "string" ? sub.data.imsi : null;
|
|
138
150
|
const bitrate = imsiForBitrate
|
|
139
|
-
? await safeCall(
|
|
151
|
+
? await safeCall(ctx.env, token, "hlrGetBitrate", { imsi: imsiForBitrate })
|
|
140
152
|
: { data: null, error: null };
|
|
141
153
|
if (bitrate.data) {
|
|
142
154
|
const rate = Number(bitrate.data.bitrate ?? bitrate.data.maxBitrate ?? 0);
|
|
@@ -147,12 +159,11 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
147
159
|
severity = "info";
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
|
-
|
|
162
|
+
// Build report
|
|
163
|
+
if (findings.length === 0) {
|
|
151
164
|
findings.push("No issues detected — subscriber appears healthy");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
: "unknown";
|
|
155
|
-
return result([
|
|
165
|
+
}
|
|
166
|
+
const report = [
|
|
156
167
|
`# Subscriber Diagnosis: ${iccid}`,
|
|
157
168
|
``,
|
|
158
169
|
`## Severity: ${severity.toUpperCase()}`,
|
|
@@ -160,30 +171,39 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
160
171
|
`## Findings`,
|
|
161
172
|
...findings.map((f, i) => `${i + 1}. ${f}`),
|
|
162
173
|
``,
|
|
163
|
-
...(actions.length > 0 ? [
|
|
174
|
+
...(actions.length > 0 ? [
|
|
175
|
+
`## Recommended Actions`,
|
|
176
|
+
...actions.map((a, i) => `${i + 1}. ${a}`),
|
|
177
|
+
] : []),
|
|
164
178
|
``,
|
|
165
179
|
`## Raw Status`,
|
|
166
180
|
`- OCS Status: ${status}`,
|
|
167
181
|
`- Balance: ${balance}`,
|
|
168
|
-
`- Active Packages: ${
|
|
169
|
-
].join("\n")
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
182
|
+
`- Active Packages: ${pkgs.data && Array.isArray(pkgs.data) ? pkgs.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE").length : "unknown"}`,
|
|
183
|
+
].join("\n");
|
|
184
|
+
return result(report);
|
|
185
|
+
});
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// 2. FLEET HEALTH — single-call fleet overview
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
174
189
|
server.registerTool("fleet_health", {
|
|
175
190
|
title: "Fleet Health Dashboard",
|
|
176
|
-
description: "Aggregates eSIM status counts, low-balance accounts, and provides a
|
|
191
|
+
description: "Aggregates eSIM status counts, low-balance accounts, and provides a " +
|
|
192
|
+
"fleet-wide health summary in a single call. Identifies accounts that " +
|
|
193
|
+
"need attention.",
|
|
177
194
|
inputSchema: {
|
|
178
195
|
accountId: z.number().optional().describe("Filter to a specific account (omit for all)"),
|
|
179
196
|
},
|
|
180
197
|
annotations: { readOnlyHint: true },
|
|
181
|
-
},
|
|
198
|
+
}, async ({ accountId }) => {
|
|
199
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
200
|
+
// Parallel fetch
|
|
182
201
|
const [statusResult, accountsResult] = await Promise.all([
|
|
183
|
-
safeCall(
|
|
184
|
-
safeCall(
|
|
202
|
+
safeCall(ctx.env, token, "esimStatusPerAccount", accountId !== undefined ? { accountId } : {}),
|
|
203
|
+
safeCall(ctx.env, token, "listResellerAccount", {}),
|
|
185
204
|
]);
|
|
186
205
|
const sections = ["# Fleet Health Dashboard\n"];
|
|
206
|
+
// eSIM status breakdown
|
|
187
207
|
if (statusResult.data && Array.isArray(statusResult.data)) {
|
|
188
208
|
let totalActive = 0, totalSuspended = 0, totalInventory = 0, totalOther = 0;
|
|
189
209
|
for (const account of statusResult.data) {
|
|
@@ -204,9 +224,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
204
224
|
sections.push(`| **Total** | **${total}** | |`);
|
|
205
225
|
sections.push(`\n**Fleet Utilization: ${utilization}%**`);
|
|
206
226
|
if (totalSuspended > totalActive * 0.1) {
|
|
207
|
-
sections.push(`\
|
|
227
|
+
sections.push(`\n⚠ High suspension rate (${totalSuspended} suspended vs ${totalActive} active)`);
|
|
208
228
|
}
|
|
209
229
|
}
|
|
230
|
+
// Account balances
|
|
210
231
|
if (accountsResult.data && Array.isArray(accountsResult.data)) {
|
|
211
232
|
const lowBalance = accountsResult.data.filter((a) => Number(a.balance ?? 0) < 10);
|
|
212
233
|
if (lowBalance.length > 0) {
|
|
@@ -222,41 +243,54 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
222
243
|
sections.push(`- Low balance (< 10): ${lowBalance.length}`);
|
|
223
244
|
}
|
|
224
245
|
return result(sections.join("\n"));
|
|
225
|
-
})
|
|
226
|
-
//
|
|
227
|
-
// 3.
|
|
228
|
-
//
|
|
246
|
+
});
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// 3. USAGE ANOMALY DETECTION
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
229
250
|
server.registerTool("detect_usage_anomalies", {
|
|
230
251
|
title: "Detect Usage Anomalies",
|
|
231
|
-
description: "
|
|
232
|
-
|
|
252
|
+
description: "Analyzes a subscriber's recent usage patterns over the last 7 days to detect " +
|
|
253
|
+
"anomalies: sudden spikes, unusual off-hours activity, or consumption rates that " +
|
|
254
|
+
"would exhaust the package before expiry.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
iccid: z.string().describe("The subscriber ICCID to analyze"),
|
|
257
|
+
},
|
|
233
258
|
annotations: { readOnlyHint: true },
|
|
234
|
-
},
|
|
259
|
+
}, async ({ iccid }) => {
|
|
260
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
235
261
|
const now = new Date();
|
|
236
262
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
237
263
|
const [usageResult, pkgResult] = await Promise.all([
|
|
238
|
-
safeCall(
|
|
239
|
-
|
|
264
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
265
|
+
subscriber: { iccid },
|
|
266
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
267
|
+
}),
|
|
268
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
240
269
|
]);
|
|
241
270
|
if (usageResult.error)
|
|
242
271
|
return result(`Failed to fetch usage: ${usageResult.error}`, true);
|
|
243
272
|
const sections = [`# Usage Anomaly Report: ${iccid}\n`];
|
|
244
273
|
const anomalies = [];
|
|
245
274
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
275
|
+
// Extract daily data volumes
|
|
276
|
+
const dailyData = [];
|
|
277
|
+
for (const entry of usageResult.data) {
|
|
278
|
+
const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
|
|
279
|
+
const date = String(entry.date ?? entry.day ?? "?");
|
|
280
|
+
dailyData.push({ date, bytes });
|
|
281
|
+
}
|
|
250
282
|
if (dailyData.length >= 2) {
|
|
251
|
-
|
|
283
|
+
// Calculate stats
|
|
284
|
+
const volumes = dailyData.map(d => d.bytes);
|
|
252
285
|
const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
253
286
|
const stdDev = Math.sqrt(volumes.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / volumes.length);
|
|
254
287
|
sections.push(`## Daily Usage (Last 7 Days)`);
|
|
255
288
|
sections.push(`| Date | Data | vs Average |`);
|
|
256
289
|
sections.push(`|------|------|-----------|`);
|
|
257
290
|
for (const d of dailyData) {
|
|
258
|
-
const deviation = mean > 0 ? ((
|
|
259
|
-
const flag = d.bytes > mean + 2 * stdDev ? " SPIKE" :
|
|
291
|
+
const deviation = mean > 0 ? ((d.bytes - mean) / mean * 100).toFixed(0) : "0";
|
|
292
|
+
const flag = d.bytes > mean + 2 * stdDev ? " SPIKE" :
|
|
293
|
+
d.bytes > mean + stdDev ? " HIGH" : "";
|
|
260
294
|
sections.push(`| ${d.date} | ${formatBytes(d.bytes)} | ${deviation}%${flag} |`);
|
|
261
295
|
if (d.bytes > mean + 2 * stdDev) {
|
|
262
296
|
anomalies.push(`Spike on ${d.date}: ${formatBytes(d.bytes)} (${deviation}% above average)`);
|
|
@@ -264,6 +298,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
264
298
|
}
|
|
265
299
|
sections.push(`\n**Average daily usage: ${formatBytes(mean)}**`);
|
|
266
300
|
sections.push(`**Std deviation: ${formatBytes(stdDev)}**`);
|
|
301
|
+
// Burn rate analysis against active packages
|
|
267
302
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
268
303
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
269
304
|
for (const pkg of activePkgs) {
|
|
@@ -279,7 +314,8 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
279
314
|
sections.push(`- Days until expiry: ${daysLeft}`);
|
|
280
315
|
sections.push(`- At current rate, data exhausts in: ${daysToExhaust.toFixed(1)} days`);
|
|
281
316
|
if (daysToExhaust < daysLeft * 0.5) {
|
|
282
|
-
anomalies.push(`Package "${pkg.name ?? pkg.packageTemplateId}" will run out
|
|
317
|
+
anomalies.push(`Package "${pkg.name ?? pkg.packageTemplateId}" will run out ` +
|
|
318
|
+
`${(daysLeft - daysToExhaust).toFixed(0)} days before expiry at current consumption`);
|
|
283
319
|
}
|
|
284
320
|
}
|
|
285
321
|
}
|
|
@@ -297,24 +333,33 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
297
333
|
sections.push(`\n## No anomalies detected — usage appears normal.`);
|
|
298
334
|
}
|
|
299
335
|
return result(sections.join("\n"));
|
|
300
|
-
})
|
|
301
|
-
//
|
|
302
|
-
// 4.
|
|
303
|
-
//
|
|
336
|
+
});
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
// 4. PACKAGE OPTIMIZER — recommend better-fit packages
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
304
340
|
server.registerTool("optimize_package", {
|
|
305
341
|
title: "Package Optimization Advisor",
|
|
306
|
-
description: "Compares a subscriber's usage against their current package and all
|
|
307
|
-
|
|
342
|
+
description: "Compares a subscriber's actual usage against their current package and all " +
|
|
343
|
+
"available templates. Recommends better-fit packages to reduce waste or prevent " +
|
|
344
|
+
"overages. Calculates potential savings.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
iccid: z.string().describe("The subscriber ICCID to optimize"),
|
|
347
|
+
},
|
|
308
348
|
annotations: { readOnlyHint: true },
|
|
309
|
-
},
|
|
349
|
+
}, async ({ iccid }) => {
|
|
350
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
310
351
|
const now = new Date();
|
|
311
352
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
312
353
|
const [usageResult, pkgResult, templatesResult] = await Promise.all([
|
|
313
|
-
safeCall(
|
|
314
|
-
|
|
315
|
-
|
|
354
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
355
|
+
subscriber: { iccid },
|
|
356
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
357
|
+
}),
|
|
358
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
359
|
+
safeCall(ctx.env, token, "listPrepaidPackageTemplate", {}),
|
|
316
360
|
]);
|
|
317
361
|
const sections = [`# Package Optimization: ${iccid}\n`];
|
|
362
|
+
// Calculate average daily usage
|
|
318
363
|
let avgDailyData = 0;
|
|
319
364
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length > 0) {
|
|
320
365
|
const totalData = usageResult.data.reduce((sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0), 0);
|
|
@@ -323,6 +368,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
323
368
|
sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
|
|
324
369
|
sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
|
|
325
370
|
}
|
|
371
|
+
// Current packages
|
|
326
372
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
327
373
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
328
374
|
if (activePkgs.length > 0) {
|
|
@@ -339,6 +385,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
339
385
|
const expiry = String(pkg.expirationDate ?? pkg.endDate ?? "");
|
|
340
386
|
if (expiry)
|
|
341
387
|
sections.push(`- Expires: ${expiry} (${daysUntil(expiry)} days)`);
|
|
388
|
+
// Flag waste
|
|
342
389
|
if (dataLimit > 0 && Number(utilization) < 30) {
|
|
343
390
|
sections.push(`- LOW UTILIZATION — subscriber is using less than 30% of allowance`);
|
|
344
391
|
}
|
|
@@ -348,16 +395,21 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
348
395
|
}
|
|
349
396
|
}
|
|
350
397
|
}
|
|
398
|
+
// Recommend templates
|
|
351
399
|
if (templatesResult.data && Array.isArray(templatesResult.data) && avgDailyData > 0) {
|
|
400
|
+
// Score templates by fit
|
|
352
401
|
const scored = templatesResult.data
|
|
353
402
|
.map((t) => {
|
|
354
403
|
const limit = Number(t.dataLimit ?? t.dataAllowance ?? 0);
|
|
355
404
|
const validity = Number(t.validityDays ?? t.duration ?? 30);
|
|
356
405
|
const price = Number(t.price ?? t.cost ?? 0);
|
|
357
406
|
const projectedUsage = avgDailyData * validity;
|
|
407
|
+
// Fit score: penalize both waste (too much data) and shortage (too little)
|
|
358
408
|
const ratio = limit > 0 ? projectedUsage / limit : 0;
|
|
359
|
-
const fitScore = 1 - Math.abs(1 - ratio);
|
|
360
|
-
|
|
409
|
+
const fitScore = 1 - Math.abs(1 - ratio); // 1.0 = perfect fit, 0 = terrible
|
|
410
|
+
const costPerGB = limit > 0 && price > 0 ? price / (limit / (1024 * 1024 * 1024)) : Infinity;
|
|
411
|
+
const name = String(t.name ?? t.templateId ?? "?");
|
|
412
|
+
return { name, limit, validity, price, projectedUsage, fitScore, costPerGB, ratio };
|
|
361
413
|
})
|
|
362
414
|
.filter((t) => t.fitScore > 0.3 && t.limit > 0)
|
|
363
415
|
.sort((a, b) => b.fitScore - a.fitScore)
|
|
@@ -368,7 +420,9 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
368
420
|
sections.push(`|----------|------|----------|-------|-----|--------------|`);
|
|
369
421
|
for (const t of scored) {
|
|
370
422
|
const fitLabel = t.fitScore > 0.8 ? "GREAT" : t.fitScore > 0.6 ? "GOOD" : "OK";
|
|
371
|
-
sections.push(`| ${t.name} | ${formatBytes(t.limit)} |
|
|
423
|
+
sections.push(`| ${t.name} | ${formatBytes(t.limit)} | ` +
|
|
424
|
+
`${t.validity}d | ${t.price > 0 ? t.price.toFixed(2) : "?"} | ` +
|
|
425
|
+
`${fitLabel} (${(t.fitScore * 100).toFixed(0)}%) | ${formatBytes(t.projectedUsage)} |`);
|
|
372
426
|
}
|
|
373
427
|
}
|
|
374
428
|
}
|
|
@@ -376,26 +430,35 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
376
430
|
sections.push(`\n*No usage data available — cannot recommend packages without usage history.*`);
|
|
377
431
|
}
|
|
378
432
|
return result(sections.join("\n"));
|
|
379
|
-
})
|
|
380
|
-
//
|
|
381
|
-
// 5. CHURN RISK
|
|
382
|
-
//
|
|
433
|
+
});
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// 5. CHURN RISK SCORING
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
383
437
|
server.registerTool("churn_risk", {
|
|
384
438
|
title: "Churn Risk Assessment",
|
|
385
|
-
description: "
|
|
386
|
-
|
|
439
|
+
description: "Analyzes a subscriber's usage trends, package status, balance, and activity " +
|
|
440
|
+
"to produce a churn risk score (0-100) with contributing factors and retention " +
|
|
441
|
+
"recommendations.",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
iccid: z.string().describe("The subscriber ICCID to assess"),
|
|
444
|
+
},
|
|
387
445
|
annotations: { readOnlyHint: true },
|
|
388
|
-
},
|
|
446
|
+
}, async ({ iccid }) => {
|
|
447
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
389
448
|
const now = new Date();
|
|
390
449
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
391
450
|
const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
|
|
392
|
-
safeCall(
|
|
393
|
-
safeCall(
|
|
394
|
-
|
|
395
|
-
|
|
451
|
+
safeCall(ctx.env, token, "getSingleSubscriber", { iccid }),
|
|
452
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
453
|
+
subscriber: { iccid },
|
|
454
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
455
|
+
}),
|
|
456
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
457
|
+
safeCall(ctx.env, token, "getSubscriberActivePeriod", { iccid }),
|
|
396
458
|
]);
|
|
397
459
|
let riskScore = 0;
|
|
398
460
|
const factors = [];
|
|
461
|
+
// Factor 1: Usage trend (declining usage = higher risk)
|
|
399
462
|
if (usageResult.data && Array.isArray(usageResult.data) && usageResult.data.length >= 3) {
|
|
400
463
|
const volumes = usageResult.data.map((e) => Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0));
|
|
401
464
|
const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
|
|
@@ -405,12 +468,14 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
405
468
|
if (avgFirst > 0) {
|
|
406
469
|
const trend = (avgSecond - avgFirst) / avgFirst;
|
|
407
470
|
if (trend < -0.5) {
|
|
408
|
-
|
|
409
|
-
|
|
471
|
+
const impact = 30;
|
|
472
|
+
riskScore += impact;
|
|
473
|
+
factors.push({ factor: "Declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}% week-over-week` });
|
|
410
474
|
}
|
|
411
475
|
else if (trend < -0.2) {
|
|
412
|
-
|
|
413
|
-
|
|
476
|
+
const impact = 15;
|
|
477
|
+
riskScore += impact;
|
|
478
|
+
factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
|
|
414
479
|
}
|
|
415
480
|
}
|
|
416
481
|
}
|
|
@@ -418,6 +483,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
418
483
|
riskScore += 25;
|
|
419
484
|
factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
|
|
420
485
|
}
|
|
486
|
+
// Factor 2: Package status
|
|
421
487
|
if (pkgResult.data && Array.isArray(pkgResult.data)) {
|
|
422
488
|
const activePkgs = pkgResult.data.filter((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
423
489
|
if (activePkgs.length === 0) {
|
|
@@ -425,6 +491,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
425
491
|
factors.push({ factor: "No active packages", impact: 20, detail: "Subscriber has no active data packages" });
|
|
426
492
|
}
|
|
427
493
|
else {
|
|
494
|
+
// Check if all packages are near expiry
|
|
428
495
|
const allExpiringSoon = activePkgs.every((p) => {
|
|
429
496
|
const expiry = String(p.expirationDate ?? p.endDate ?? "");
|
|
430
497
|
return expiry && daysUntil(expiry) <= 5;
|
|
@@ -433,6 +500,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
433
500
|
riskScore += 15;
|
|
434
501
|
factors.push({ factor: "All packages expiring soon", impact: 15, detail: "No package renewal in sight" });
|
|
435
502
|
}
|
|
503
|
+
// Check if no recurring packages
|
|
436
504
|
const hasRecurring = activePkgs.some((p) => p.recurring === true || p.isRecurring === true);
|
|
437
505
|
if (!hasRecurring) {
|
|
438
506
|
riskScore += 10;
|
|
@@ -440,6 +508,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
440
508
|
}
|
|
441
509
|
}
|
|
442
510
|
}
|
|
511
|
+
// Factor 3: Balance
|
|
443
512
|
if (subResult.data) {
|
|
444
513
|
const balance = Number(subResult.data.balance ?? 0);
|
|
445
514
|
if (balance <= 0) {
|
|
@@ -447,6 +516,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
447
516
|
factors.push({ factor: "Zero balance", impact: 15, detail: "Cannot purchase new packages" });
|
|
448
517
|
}
|
|
449
518
|
}
|
|
519
|
+
// Factor 4: Subscriber age (newer = higher risk)
|
|
450
520
|
if (activeResult.data) {
|
|
451
521
|
const firstUse = String(activeResult.data.firstUseDate ?? activeResult.data.activationDate ?? "");
|
|
452
522
|
if (firstUse) {
|
|
@@ -457,14 +527,17 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
457
527
|
}
|
|
458
528
|
}
|
|
459
529
|
}
|
|
530
|
+
// Cap at 100
|
|
460
531
|
riskScore = Math.min(riskScore, 100);
|
|
532
|
+
// Risk level
|
|
461
533
|
const level = riskScore >= 70 ? "HIGH" : riskScore >= 40 ? "MEDIUM" : "LOW";
|
|
534
|
+
// Build report
|
|
462
535
|
const sections = [
|
|
463
536
|
`# Churn Risk Assessment: ${iccid}`,
|
|
464
537
|
``,
|
|
465
538
|
`## Risk Score: ${riskScore}/100 (${level})`,
|
|
466
539
|
``,
|
|
467
|
-
`${"
|
|
540
|
+
`${"█".repeat(Math.floor(riskScore / 5))}${"░".repeat(20 - Math.floor(riskScore / 5))}`,
|
|
468
541
|
``,
|
|
469
542
|
];
|
|
470
543
|
if (factors.length > 0) {
|
|
@@ -476,9 +549,10 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
476
549
|
sections.push(`| ${f.factor} | +${f.impact} | ${f.detail} |`);
|
|
477
550
|
}
|
|
478
551
|
}
|
|
552
|
+
// Retention recommendations
|
|
479
553
|
sections.push(`\n## Retention Recommendations`);
|
|
480
554
|
if (riskScore >= 70) {
|
|
481
|
-
sections.push("1. Immediate outreach — contact subscriber with special offer");
|
|
555
|
+
sections.push("1. **Immediate outreach** — contact subscriber with special offer");
|
|
482
556
|
sections.push("2. Assign a complimentary small data package to re-engage");
|
|
483
557
|
sections.push("3. Set up a recurring package to reduce renewal friction");
|
|
484
558
|
}
|
|
@@ -492,56 +566,74 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
492
566
|
sections.push("2. Continue monitoring via regular fleet_health checks");
|
|
493
567
|
}
|
|
494
568
|
return result(sections.join("\n"));
|
|
495
|
-
})
|
|
496
|
-
//
|
|
497
|
-
// 6. AUDIT
|
|
498
|
-
//
|
|
569
|
+
});
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// 6. NETWORK COVERAGE AUDIT — "am I on the right networks?"
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
499
573
|
server.registerTool("audit_network_coverage", {
|
|
500
574
|
title: "Network Coverage Audit",
|
|
501
|
-
description: "
|
|
575
|
+
description: "Analyzes which networks your subscribers are actually connecting to in a given " +
|
|
576
|
+
"country or across all countries. Compares against your steering lists to identify " +
|
|
577
|
+
"mismatches — subscribers roaming on expensive or non-preferred networks. " +
|
|
578
|
+
"Use this to answer: 'Am I using the right networks in country X?'",
|
|
502
579
|
inputSchema: {
|
|
503
580
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
504
581
|
limit: z.number().optional().describe("Max subscribers to sample (default 50)"),
|
|
505
582
|
},
|
|
506
583
|
annotations: { readOnlyHint: true },
|
|
507
|
-
},
|
|
584
|
+
}, async ({ accountId, limit: sampleLimit }) => {
|
|
585
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
508
586
|
const maxSample = sampleLimit ?? 50;
|
|
509
|
-
|
|
587
|
+
// Fetch subscribers and steering lists in parallel
|
|
588
|
+
const subParams = { status: "ACTIVE" };
|
|
510
589
|
if (accountId !== undefined)
|
|
511
590
|
subParams.accountId = accountId;
|
|
512
|
-
const [subsResult,
|
|
513
|
-
safeCall(
|
|
514
|
-
|
|
591
|
+
const [subsResult, resellerId] = await Promise.all([
|
|
592
|
+
safeCall(ctx.env, token, "listSubscriber", subParams),
|
|
593
|
+
getDefaultResellerId(ctx.env, token),
|
|
515
594
|
]);
|
|
595
|
+
const steeringResult = await safeCall(ctx.env, token, "listSteeringList", resellerId);
|
|
516
596
|
if (subsResult.error)
|
|
517
597
|
return result(`Failed to fetch subscribers: ${subsResult.error}`, true);
|
|
518
598
|
const sections = [`# Network Coverage Audit\n`];
|
|
599
|
+
// Build steering list lookup
|
|
600
|
+
const steeringMap = new Map();
|
|
519
601
|
if (steeringResult.data && Array.isArray(steeringResult.data)) {
|
|
520
|
-
|
|
602
|
+
for (const sl of steeringResult.data) {
|
|
603
|
+
steeringMap.set(Number(sl.steeringListId ?? sl.id), sl);
|
|
604
|
+
}
|
|
605
|
+
sections.push(`## Steering Lists: ${steeringMap.size} configured`);
|
|
521
606
|
}
|
|
607
|
+
// Sample subscriber locations and networks
|
|
522
608
|
const countryStats = new Map();
|
|
523
609
|
if (subsResult.data && Array.isArray(subsResult.data)) {
|
|
524
|
-
sections.push(`## Sampling ${subsResult.data.length} active subscribers\n`);
|
|
525
610
|
const subs = subsResult.data.slice(0, maxSample);
|
|
611
|
+
sections.push(`## Sampling ${subs.length} active subscribers\n`);
|
|
612
|
+
// Fetch locations in batches of 10
|
|
526
613
|
const batchSize = 10;
|
|
527
614
|
for (let i = 0; i < subs.length; i += batchSize) {
|
|
528
615
|
const batch = subs.slice(i, i + batchSize);
|
|
529
|
-
const locations = await Promise.all(batch.map((s) => safeCall(
|
|
616
|
+
const locations = await Promise.all(batch.map((s) => safeCall(ctx.env, token, "getSubscriberLocation", {
|
|
617
|
+
iccid: String(s.iccid ?? ""),
|
|
618
|
+
})));
|
|
530
619
|
for (let j = 0; j < batch.length; j++) {
|
|
531
620
|
const loc = locations[j];
|
|
532
621
|
if (loc.data) {
|
|
533
622
|
const country = String(loc.data.country ?? loc.data.countryCode ?? "Unknown");
|
|
534
623
|
const network = String(loc.data.network ?? loc.data.operator ?? loc.data.mccMnc ?? "Unknown");
|
|
624
|
+
const iccid = String(batch[j].iccid ?? "");
|
|
535
625
|
if (!countryStats.has(country)) {
|
|
536
|
-
countryStats.set(country, { count: 0, networks: new Map() });
|
|
626
|
+
countryStats.set(country, { count: 0, networks: new Map(), subscribers: [] });
|
|
537
627
|
}
|
|
538
628
|
const stat = countryStats.get(country);
|
|
539
629
|
stat.count++;
|
|
540
630
|
stat.networks.set(network, (stat.networks.get(network) ?? 0) + 1);
|
|
631
|
+
stat.subscribers.push(iccid);
|
|
541
632
|
}
|
|
542
633
|
}
|
|
543
634
|
}
|
|
544
635
|
}
|
|
636
|
+
// Report by country
|
|
545
637
|
if (countryStats.size > 0) {
|
|
546
638
|
sections.push(`## Network Distribution by Country`);
|
|
547
639
|
const sorted = [...countryStats.entries()].sort((a, b) => b[1].count - a[1].count);
|
|
@@ -562,44 +654,56 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
562
654
|
sections.push("No location data available for sampled subscribers.");
|
|
563
655
|
}
|
|
564
656
|
return result(sections.join("\n"));
|
|
565
|
-
})
|
|
566
|
-
//
|
|
567
|
-
// 7. MARKETING INTELLIGENCE
|
|
568
|
-
//
|
|
657
|
+
});
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
// 7. MARKETING INTELLIGENCE — "which countries should I target?"
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
569
661
|
server.registerTool("marketing_intelligence", {
|
|
570
662
|
title: "Marketing Intelligence Report",
|
|
571
|
-
description: "
|
|
663
|
+
description: "Analyzes your subscriber base to identify high-growth markets, underserved regions, " +
|
|
664
|
+
"and revenue concentration. Answers: 'Which countries should I target with marketing?' " +
|
|
665
|
+
"and 'Where are my most valuable subscribers?'",
|
|
572
666
|
inputSchema: {
|
|
573
667
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
574
668
|
},
|
|
575
669
|
annotations: { readOnlyHint: true },
|
|
576
|
-
},
|
|
577
|
-
const
|
|
670
|
+
}, async ({ accountId }) => {
|
|
671
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
672
|
+
const params = {};
|
|
578
673
|
if (accountId !== undefined)
|
|
579
674
|
params.accountId = accountId;
|
|
580
|
-
const [subsResult, templatesResult] = await Promise.all([
|
|
581
|
-
safeCall(
|
|
582
|
-
safeCall(
|
|
675
|
+
const [subsResult, templatesResult, resellerId] = await Promise.all([
|
|
676
|
+
safeCall(ctx.env, token, "listSubscriber", params),
|
|
677
|
+
safeCall(ctx.env, token, "listPrepaidPackageTemplate", {}),
|
|
678
|
+
getDefaultResellerId(ctx.env, token),
|
|
583
679
|
]);
|
|
680
|
+
const zonesResult = await safeCall(ctx.env, token, "listDetailedLocationZone", resellerId);
|
|
584
681
|
const sections = [`# Marketing Intelligence Report\n`];
|
|
682
|
+
// Subscriber distribution by location
|
|
585
683
|
if (subsResult.data && Array.isArray(subsResult.data)) {
|
|
586
684
|
const total = subsResult.data.length;
|
|
587
685
|
sections.push(`## Fleet Size: ${total} subscribers sampled\n`);
|
|
686
|
+
// Get locations for a sample
|
|
588
687
|
const sample = subsResult.data.slice(0, 100);
|
|
589
688
|
const countryData = new Map();
|
|
590
689
|
const batchSize = 10;
|
|
591
690
|
for (let i = 0; i < sample.length; i += batchSize) {
|
|
592
691
|
const batch = sample.slice(i, i + batchSize);
|
|
593
|
-
const locations = await Promise.all(batch.map((s) => safeCall(
|
|
692
|
+
const locations = await Promise.all(batch.map((s) => safeCall(ctx.env, token, "getSubscriberLocation", {
|
|
693
|
+
iccid: String(s.iccid ?? ""),
|
|
694
|
+
})));
|
|
594
695
|
for (let j = 0; j < batch.length; j++) {
|
|
595
696
|
const loc = locations[j];
|
|
596
|
-
const
|
|
597
|
-
const country = loc.data
|
|
598
|
-
|
|
599
|
-
|
|
697
|
+
const sub = batch[j];
|
|
698
|
+
const country = loc.data
|
|
699
|
+
? String(loc.data.country ?? loc.data.countryCode ?? "Unknown")
|
|
700
|
+
: "Unknown";
|
|
701
|
+
if (!countryData.has(country)) {
|
|
702
|
+
countryData.set(country, { subscribers: 0, activePackages: 0, totalBalance: 0 });
|
|
703
|
+
}
|
|
600
704
|
const cd = countryData.get(country);
|
|
601
705
|
cd.subscribers++;
|
|
602
|
-
cd.totalBalance += Number(
|
|
706
|
+
cd.totalBalance += Number(sub.balance ?? 0);
|
|
603
707
|
}
|
|
604
708
|
}
|
|
605
709
|
if (countryData.size > 0) {
|
|
@@ -612,29 +716,55 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
612
716
|
const avgBal = (data.totalBalance / data.subscribers).toFixed(2);
|
|
613
717
|
sections.push(`| ${country} | ${data.subscribers} | ${pct}% | ${avgBal} |`);
|
|
614
718
|
}
|
|
719
|
+
// Insights
|
|
615
720
|
sections.push(`\n## Market Insights`);
|
|
721
|
+
// Top market
|
|
616
722
|
const topMarket = sorted[0];
|
|
617
723
|
if (topMarket) {
|
|
618
|
-
sections.push(`- Strongest market
|
|
724
|
+
sections.push(`- **Strongest market**: ${topMarket[0]} (${topMarket[1].subscribers} subscribers)`);
|
|
619
725
|
if (topMarket[1].subscribers / sample.length > 0.5) {
|
|
620
726
|
sections.push(` Revenue concentration risk — >50% of fleet in one market`);
|
|
621
727
|
}
|
|
622
728
|
}
|
|
729
|
+
// High-value markets (high avg balance)
|
|
730
|
+
const highValue = sorted
|
|
731
|
+
.filter(([, d]) => d.subscribers >= 3)
|
|
732
|
+
.sort((a, b) => (b[1].totalBalance / b[1].subscribers) - (a[1].totalBalance / a[1].subscribers))
|
|
733
|
+
.slice(0, 3);
|
|
734
|
+
if (highValue.length > 0) {
|
|
735
|
+
sections.push(`\n### High-Value Markets (by avg balance)`);
|
|
736
|
+
for (const [country, data] of highValue) {
|
|
737
|
+
sections.push(`- **${country}**: avg balance ${(data.totalBalance / data.subscribers).toFixed(2)} (${data.subscribers} subs)`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// Underserved (1-2 subscribers — early traction)
|
|
623
741
|
const emerging = sorted.filter(([, d]) => d.subscribers >= 1 && d.subscribers <= 3);
|
|
624
742
|
if (emerging.length > 0) {
|
|
625
|
-
sections.push(`\n### Emerging Markets (1-3 subscribers)`);
|
|
743
|
+
sections.push(`\n### Emerging Markets (early traction, 1-3 subscribers)`);
|
|
744
|
+
sections.push(`These markets show initial demand — consider targeted campaigns:`);
|
|
626
745
|
for (const [country, data] of emerging) {
|
|
627
746
|
sections.push(`- ${country}: ${data.subscribers} subscriber(s)`);
|
|
628
747
|
}
|
|
629
748
|
}
|
|
630
749
|
}
|
|
631
750
|
}
|
|
751
|
+
// Available coverage vs actual usage
|
|
752
|
+
if (zonesResult.data && Array.isArray(zonesResult.data)) {
|
|
753
|
+
sections.push(`\n## Coverage Catalog`);
|
|
754
|
+
sections.push(`- Location zones available: ${zonesResult.data.length}`);
|
|
755
|
+
}
|
|
632
756
|
if (templatesResult.data && Array.isArray(templatesResult.data)) {
|
|
633
|
-
sections.push(
|
|
634
|
-
|
|
757
|
+
sections.push(`- Package templates available: ${templatesResult.data.length}`);
|
|
758
|
+
// Price analysis
|
|
759
|
+
const prices = templatesResult.data
|
|
760
|
+
.map((t) => Number(t.price ?? t.cost ?? 0))
|
|
761
|
+
.filter((p) => p > 0);
|
|
635
762
|
if (prices.length > 0) {
|
|
636
763
|
const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
|
|
637
|
-
|
|
764
|
+
const minPrice = Math.min(...prices);
|
|
765
|
+
const maxPrice = Math.max(...prices);
|
|
766
|
+
sections.push(`\n### Pricing Range`);
|
|
767
|
+
sections.push(`- Min: ${minPrice.toFixed(2)} | Avg: ${avgPrice.toFixed(2)} | Max: ${maxPrice.toFixed(2)}`);
|
|
638
768
|
}
|
|
639
769
|
}
|
|
640
770
|
sections.push(`\n## Recommended Actions`);
|
|
@@ -642,26 +772,29 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
642
772
|
sections.push(`2. Run \`high_cost_subscribers\` to identify margin pressure`);
|
|
643
773
|
sections.push(`3. Consider creating regional package templates for emerging markets`);
|
|
644
774
|
return result(sections.join("\n"));
|
|
645
|
-
})
|
|
646
|
-
//
|
|
647
|
-
// 8. HIGH COST SUBSCRIBERS
|
|
648
|
-
//
|
|
775
|
+
});
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// 8. HIGH COST SUBSCRIBERS — "who's costing me money?"
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
649
779
|
server.registerTool("high_cost_subscribers", {
|
|
650
780
|
title: "High Cost Subscriber Report",
|
|
651
|
-
description: "Identifies subscribers with disproportionately high data consumption relative
|
|
781
|
+
description: "Identifies subscribers with disproportionately high data consumption relative " +
|
|
782
|
+
"to their package value. Finds subscribers burning through data at rates that " +
|
|
783
|
+
"erode margins. Answers: 'Which subscribers are costing me money?'",
|
|
652
784
|
inputSchema: {
|
|
653
785
|
accountId: z.number().optional().describe("Filter to a specific account"),
|
|
654
|
-
limit: z.number().optional().describe("Max subscribers to
|
|
786
|
+
limit: z.number().optional().describe("Max subscribers to analyze (default 100)"),
|
|
655
787
|
thresholdPct: z.number().optional().describe("Usage % threshold to flag (default 80)"),
|
|
656
788
|
},
|
|
657
789
|
annotations: { readOnlyHint: true },
|
|
658
|
-
},
|
|
790
|
+
}, async ({ accountId, limit: maxLimit, thresholdPct }) => {
|
|
791
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
659
792
|
const sampleSize = maxLimit ?? 100;
|
|
660
793
|
const threshold = thresholdPct ?? 80;
|
|
661
|
-
const subParams = {
|
|
794
|
+
const subParams = { status: "ACTIVE" };
|
|
662
795
|
if (accountId !== undefined)
|
|
663
796
|
subParams.accountId = accountId;
|
|
664
|
-
const subsResult = await safeCall(
|
|
797
|
+
const subsResult = await safeCall(ctx.env, token, "listSubscriber", subParams);
|
|
665
798
|
if (subsResult.error)
|
|
666
799
|
return result(`Failed to fetch subscribers: ${subsResult.error}`, true);
|
|
667
800
|
if (!subsResult.data || !Array.isArray(subsResult.data))
|
|
@@ -670,6 +803,7 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
670
803
|
const now = new Date();
|
|
671
804
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
672
805
|
const highCostSubs = [];
|
|
806
|
+
// Analyze in batches
|
|
673
807
|
const batchSize = 5;
|
|
674
808
|
const subs = subsResult.data.slice(0, sampleSize);
|
|
675
809
|
for (let i = 0; i < subs.length; i += batchSize) {
|
|
@@ -677,15 +811,20 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
677
811
|
await Promise.all(batch.map(async (sub) => {
|
|
678
812
|
const iccid = String(sub.iccid ?? "");
|
|
679
813
|
const [usage, pkgs, loc] = await Promise.all([
|
|
680
|
-
safeCall(
|
|
681
|
-
|
|
682
|
-
|
|
814
|
+
safeCall(ctx.env, token, "subscriberUsageOverPeriod", {
|
|
815
|
+
subscriber: { iccid },
|
|
816
|
+
period: { start: toISODate(weekAgo), end: toISODate(now) },
|
|
817
|
+
}),
|
|
818
|
+
safeCall(ctx.env, token, "listSubscriberPrepaidPackages", { iccid }),
|
|
819
|
+
safeCall(ctx.env, token, "getSubscriberLocation", { iccid }),
|
|
683
820
|
]);
|
|
821
|
+
// Calculate daily avg
|
|
684
822
|
let dailyAvgBytes = 0;
|
|
685
823
|
if (usage.data && Array.isArray(usage.data) && usage.data.length > 0) {
|
|
686
824
|
const totalBytes = usage.data.reduce((sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0), 0);
|
|
687
825
|
dailyAvgBytes = totalBytes / usage.data.length;
|
|
688
826
|
}
|
|
827
|
+
// Get active package info
|
|
689
828
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
690
829
|
const activePkg = pkgs.data.find((p) => String(p.status ?? "").toUpperCase() === "ACTIVE");
|
|
691
830
|
if (activePkg && dailyAvgBytes > 0) {
|
|
@@ -695,17 +834,31 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
695
834
|
const utilizationPct = dataLimit > 0 ? (dataUsed / dataLimit) * 100 : 0;
|
|
696
835
|
const remaining = dataLimit - dataUsed;
|
|
697
836
|
const daysToExhaust = dailyAvgBytes > 0 ? remaining / dailyAvgBytes : Infinity;
|
|
698
|
-
const costPerGB = price > 0 && dataUsed > 0
|
|
699
|
-
|
|
837
|
+
const costPerGB = price > 0 && dataUsed > 0
|
|
838
|
+
? price / (dataUsed / (1024 * 1024 * 1024))
|
|
839
|
+
: 0;
|
|
840
|
+
const country = loc.data
|
|
841
|
+
? String(loc.data.country ?? loc.data.countryCode ?? "?")
|
|
842
|
+
: "?";
|
|
700
843
|
if (utilizationPct >= threshold || daysToExhaust < 3) {
|
|
701
|
-
highCostSubs.push({
|
|
844
|
+
highCostSubs.push({
|
|
845
|
+
iccid,
|
|
846
|
+
dailyAvgBytes,
|
|
847
|
+
packageDataLimit: dataLimit,
|
|
848
|
+
packagePrice: price,
|
|
849
|
+
utilizationPct,
|
|
850
|
+
costPerGB,
|
|
851
|
+
daysToExhaust,
|
|
852
|
+
country,
|
|
853
|
+
});
|
|
702
854
|
}
|
|
703
855
|
}
|
|
704
856
|
}
|
|
705
857
|
}));
|
|
706
858
|
}
|
|
859
|
+
// Sort by utilization (highest first)
|
|
707
860
|
highCostSubs.sort((a, b) => b.utilizationPct - a.utilizationPct);
|
|
708
|
-
sections.push(`
|
|
861
|
+
sections.push(`Analyzed ${subs.length} active subscribers (threshold: ${threshold}% usage)\n`);
|
|
709
862
|
if (highCostSubs.length === 0) {
|
|
710
863
|
sections.push(`No subscribers above ${threshold}% package utilization. Fleet margins look healthy.`);
|
|
711
864
|
}
|
|
@@ -716,17 +869,109 @@ export function registerAllIntelligenceTools(server, client) {
|
|
|
716
869
|
let totalDailyBytes = 0;
|
|
717
870
|
for (const s of highCostSubs) {
|
|
718
871
|
totalDailyBytes += s.dailyAvgBytes;
|
|
719
|
-
sections.push(`| ${s.iccid.slice(-8)}... | ${s.country} | ${formatBytes(s.dailyAvgBytes)} |
|
|
872
|
+
sections.push(`| ${s.iccid.slice(-8)}... | ${s.country} | ${formatBytes(s.dailyAvgBytes)} | ` +
|
|
873
|
+
`${s.utilizationPct.toFixed(0)}% | ${s.daysToExhaust === Infinity ? "inf" : s.daysToExhaust.toFixed(1)} | ` +
|
|
874
|
+
`${s.costPerGB > 0 ? s.costPerGB.toFixed(2) : "?"} |`);
|
|
720
875
|
}
|
|
721
876
|
sections.push(`\n## Summary`);
|
|
722
877
|
sections.push(`- High-cost subscribers: ${highCostSubs.length} / ${subs.length} (${((highCostSubs.length / subs.length) * 100).toFixed(1)}%)`);
|
|
723
878
|
sections.push(`- Combined daily data burn: ${formatBytes(totalDailyBytes)}`);
|
|
879
|
+
// Country breakdown
|
|
880
|
+
const byCountry = new Map();
|
|
881
|
+
for (const s of highCostSubs) {
|
|
882
|
+
byCountry.set(s.country, (byCountry.get(s.country) ?? 0) + 1);
|
|
883
|
+
}
|
|
884
|
+
const countrySorted = [...byCountry.entries()].sort((a, b) => b[1] - a[1]);
|
|
885
|
+
if (countrySorted.length > 0) {
|
|
886
|
+
sections.push(`\n### By Country`);
|
|
887
|
+
for (const [country, count] of countrySorted) {
|
|
888
|
+
sections.push(`- ${country}: ${count} high-cost subscriber(s)`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
724
891
|
sections.push(`\n## Recommended Actions`);
|
|
725
892
|
sections.push(`1. Review tariff rates for top countries via \`get_tariff\``);
|
|
726
893
|
sections.push(`2. Consider throttling heavy users via \`hlr_set_bitrate\``);
|
|
727
894
|
sections.push(`3. Run \`optimize_package\` on flagged ICCIDs to find better-fit plans`);
|
|
895
|
+
sections.push(`4. Negotiate better wholesale rates for high-volume countries`);
|
|
728
896
|
}
|
|
729
897
|
return result(sections.join("\n"));
|
|
730
|
-
})
|
|
898
|
+
});
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
// 9. DETECT COUNTRY ENTRY — cheap MCC-based location change detection
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
server.registerTool("detect_country_entry", {
|
|
903
|
+
title: "Detect Country Entry",
|
|
904
|
+
description: "Detects when a subscriber has entered a new country by reading " +
|
|
905
|
+
"networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call — " +
|
|
906
|
+
"avoids the per-call cost of getSubscriberLocationByCellId). Resolves " +
|
|
907
|
+
"MCC → ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied " +
|
|
908
|
+
"expectedCountry to return countryChanged. Designed for downstream " +
|
|
909
|
+
"country-entry upsell workflows (e.g. mango.talk SMS/push offers). " +
|
|
910
|
+
"COST NOTE: This tool makes exactly one OCS call per invocation. " +
|
|
911
|
+
"Consumers running polling crons MUST enforce their own rate floor — " +
|
|
912
|
+
"this layer provides no throttle.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
subscriber: z
|
|
915
|
+
.union([
|
|
916
|
+
z.object({ subscriberId: z.number() }).describe("Internal subscriber ID"),
|
|
917
|
+
z.object({ imsi: z.string() }).describe("IMSI"),
|
|
918
|
+
z.object({ iccid: z.string() }).describe("ICCID"),
|
|
919
|
+
z.object({ msisdn: z.string() }).describe("MSISDN / phone number"),
|
|
920
|
+
z.object({ multiImsi: z.string() }).describe("Multi-IMSI identifier"),
|
|
921
|
+
z.object({ activationCode: z.string() }).describe("eSIM activation code"),
|
|
922
|
+
])
|
|
923
|
+
.describe("Subscriber identifier (use exactly one field)"),
|
|
924
|
+
expectedCountry: z
|
|
925
|
+
.string()
|
|
926
|
+
.length(2)
|
|
927
|
+
.transform((code) => code.toUpperCase())
|
|
928
|
+
.optional()
|
|
929
|
+
.describe("Caller\u2019s last-known ISO 3166-1 alpha-2 country for this subscriber " +
|
|
930
|
+
"(e.g. \"RU\"). When provided, countryChanged is included in the response."),
|
|
931
|
+
},
|
|
932
|
+
annotations: { readOnlyHint: true },
|
|
933
|
+
}, async ({ subscriber, expectedCountry }) => {
|
|
934
|
+
const token = await ctx.getUserToken(ctx.props.sub);
|
|
935
|
+
// getSingleSubscriber accepts any of subscriberId | imsi | iccid | msisdn |
|
|
936
|
+
// multiImsi | activationCode — pass the discriminated union value directly.
|
|
937
|
+
const sub = await safeCall(ctx.env, token, "getSingleSubscriber", subscriber);
|
|
938
|
+
if (sub.error)
|
|
939
|
+
return result(`Failed to fetch subscriber: ${sub.error}`, true);
|
|
940
|
+
if (!sub.data)
|
|
941
|
+
return result("Subscriber not found", true);
|
|
942
|
+
const subscriberId = sub.data.subscriberId ??
|
|
943
|
+
sub.data.id ??
|
|
944
|
+
("subscriberId" in subscriber
|
|
945
|
+
? subscriber.subscriberId
|
|
946
|
+
: undefined);
|
|
947
|
+
const networkInfo = sub.data.networkInfo;
|
|
948
|
+
if (networkInfo === null || networkInfo === undefined || typeof networkInfo !== "object") {
|
|
949
|
+
return result(JSON.stringify({ status: "no_location_data", subscriberId: subscriberId ?? null }));
|
|
950
|
+
}
|
|
951
|
+
const lastMcc = networkInfo.lastMcc != null ? Number(networkInfo.lastMcc) : null;
|
|
952
|
+
const lastMnc = networkInfo.lastMnc != null ? Number(networkInfo.lastMnc) : null;
|
|
953
|
+
const lastSeenAtUtc = networkInfo.time != null ? String(networkInfo.time) : null;
|
|
954
|
+
if (lastMcc === null || isNaN(lastMcc)) {
|
|
955
|
+
return result(JSON.stringify({ status: "no_location_data", subscriberId: subscriberId ?? null }));
|
|
956
|
+
}
|
|
957
|
+
const currentCountry = mccToIso(lastMcc);
|
|
958
|
+
const mccUnresolved = currentCountry === null;
|
|
959
|
+
const response = {
|
|
960
|
+
subscriberId: subscriberId ?? null,
|
|
961
|
+
currentCountry,
|
|
962
|
+
currentMcc: lastMcc,
|
|
963
|
+
currentMnc: lastMnc,
|
|
964
|
+
lastSeenAtUtc,
|
|
965
|
+
...(mccUnresolved ? { mccUnresolved: true } : {}),
|
|
966
|
+
};
|
|
967
|
+
if (expectedCountry !== undefined) {
|
|
968
|
+
response.expectedCountry = expectedCountry;
|
|
969
|
+
// null = cannot determine (MCC unresolved); true/false = definitive diff
|
|
970
|
+
response.countryChanged = currentCountry !== null
|
|
971
|
+
? currentCountry !== expectedCountry
|
|
972
|
+
: null;
|
|
973
|
+
}
|
|
974
|
+
return result(JSON.stringify(response, null, 2));
|
|
975
|
+
});
|
|
731
976
|
}
|
|
732
977
|
//# sourceMappingURL=intelligence.js.map
|