@carrierllc/mcp 0.1.0 → 0.2.0

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