@elizaos/plugin-finances 2.0.3-beta.6 → 2.0.3-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/finances.d.ts +38 -0
- package/dist/actions/finances.d.ts.map +1 -0
- package/dist/actions/finances.js +368 -0
- package/dist/actions/finances.js.map +1 -0
- package/dist/components/finances/FinancesSpatialView.d.ts +80 -0
- package/dist/components/finances/FinancesSpatialView.d.ts.map +1 -0
- package/dist/components/finances/FinancesSpatialView.js +157 -0
- package/dist/components/finances/FinancesSpatialView.js.map +1 -0
- package/dist/components/finances/FinancesView.d.ts +97 -0
- package/dist/components/finances/FinancesView.d.ts.map +1 -0
- package/dist/components/finances/FinancesView.js +231 -0
- package/dist/components/finances/FinancesView.js.map +1 -0
- package/dist/components/finances/finances-view-bundle.d.ts +10 -0
- package/dist/components/finances/finances-view-bundle.d.ts.map +1 -0
- package/dist/components/finances/finances-view-bundle.js +5 -0
- package/dist/components/finances/finances-view-bundle.js.map +1 -0
- package/dist/db/finances-repository.d.ts +51 -0
- package/dist/db/finances-repository.d.ts.map +1 -0
- package/dist/db/finances-repository.js +521 -0
- package/dist/db/finances-repository.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +6 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +2615 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +133 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/sql.d.ts +65 -0
- package/dist/db/sql.d.ts.map +1 -0
- package/dist/db/sql.js +182 -0
- package/dist/db/sql.js.map +1 -0
- package/dist/finance-normalize.d.ts +24 -0
- package/dist/finance-normalize.d.ts.map +1 -0
- package/dist/finance-normalize.js +66 -0
- package/dist/finance-normalize.js.map +1 -0
- package/dist/finances-service.d.ts +179 -0
- package/dist/finances-service.d.ts.map +1 -0
- package/dist/finances-service.js +1122 -0
- package/dist/finances-service.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/dist/payment-csv-import.d.ts +23 -0
- package/dist/payment-csv-import.d.ts.map +1 -0
- package/dist/payment-csv-import.js +271 -0
- package/dist/payment-csv-import.js.map +1 -0
- package/dist/payment-recurrence.d.ts +14 -0
- package/dist/payment-recurrence.d.ts.map +1 -0
- package/dist/payment-recurrence.js +190 -0
- package/dist/payment-recurrence.js.map +1 -0
- package/dist/payment-types.d.ts +158 -0
- package/dist/payment-types.d.ts.map +1 -0
- package/dist/payment-types.js +1 -0
- package/dist/payment-types.js.map +1 -0
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +31 -0
- package/dist/plugin.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +21 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/register.d.ts +9 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +5 -0
- package/dist/register.js.map +1 -0
- package/dist/services/browser-bridge-seam.d.ts +40 -0
- package/dist/services/browser-bridge-seam.d.ts.map +1 -0
- package/dist/services/browser-bridge-seam.js +39 -0
- package/dist/services/browser-bridge-seam.js.map +1 -0
- package/dist/services/gmail-seam.d.ts +40 -0
- package/dist/services/gmail-seam.d.ts.map +1 -0
- package/dist/services/gmail-seam.js +208 -0
- package/dist/services/gmail-seam.js.map +1 -0
- package/dist/services/migration.d.ts +65 -0
- package/dist/services/migration.d.ts.map +1 -0
- package/dist/services/migration.js +116 -0
- package/dist/services/migration.js.map +1 -0
- package/dist/services/subscriptions-service.d.ts +76 -0
- package/dist/services/subscriptions-service.d.ts.map +1 -0
- package/dist/services/subscriptions-service.js +1002 -0
- package/dist/services/subscriptions-service.js.map +1 -0
- package/dist/subscriptions-playbooks.d.ts +79 -0
- package/dist/subscriptions-playbooks.d.ts.map +1 -0
- package/dist/subscriptions-playbooks.js +871 -0
- package/dist/subscriptions-playbooks.js.map +1 -0
- package/dist/subscriptions-types.d.ts +80 -0
- package/dist/subscriptions-types.d.ts.map +1 -0
- package/dist/subscriptions-types.js +1 -0
- package/dist/subscriptions-types.js.map +1 -0
- package/dist/token-encryption.d.ts +42 -0
- package/dist/token-encryption.d.ts.map +1 -0
- package/dist/token-encryption.js +96 -0
- package/dist/token-encryption.js.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/views/bundle.js +411 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +11 -11
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import { logger } from "@elizaos/core";
|
|
2
|
+
import {
|
|
3
|
+
BROWSER_SERVICE_TYPE
|
|
4
|
+
} from "@elizaos/plugin-browser";
|
|
5
|
+
import {
|
|
6
|
+
createLifeOpsSubscriptionAudit,
|
|
7
|
+
createLifeOpsSubscriptionCancellation,
|
|
8
|
+
createLifeOpsSubscriptionCandidate,
|
|
9
|
+
FinancesRepository
|
|
10
|
+
} from "../db/finances-repository.js";
|
|
11
|
+
import {
|
|
12
|
+
fail,
|
|
13
|
+
normalizeOptionalBoolean,
|
|
14
|
+
normalizeOptionalString,
|
|
15
|
+
requireAgentId,
|
|
16
|
+
requireNonEmptyString
|
|
17
|
+
} from "../finance-normalize.js";
|
|
18
|
+
import {
|
|
19
|
+
findLifeOpsSubscriptionPlaybook,
|
|
20
|
+
listLifeOpsSubscriptionPlaybooks,
|
|
21
|
+
PLAYBOOK_UNSUPPORTED_FLOW_ERROR
|
|
22
|
+
} from "../subscriptions-playbooks.js";
|
|
23
|
+
import {
|
|
24
|
+
createSubscriptionsBrowserGateway
|
|
25
|
+
} from "./browser-bridge-seam.js";
|
|
26
|
+
import {
|
|
27
|
+
createSubscriptionsGmailGateway
|
|
28
|
+
} from "./gmail-seam.js";
|
|
29
|
+
const DEFAULT_SUBSCRIPTION_BROWSER_WAIT_MS = 5e3;
|
|
30
|
+
function isComputerUseBrowserService(service) {
|
|
31
|
+
return Boolean(service) && typeof service === "object" && typeof service.executeBrowserAction === "function";
|
|
32
|
+
}
|
|
33
|
+
function isBrowserService(service) {
|
|
34
|
+
return Boolean(service) && typeof service === "object" && typeof service.execute === "function";
|
|
35
|
+
}
|
|
36
|
+
function resultRecord(value) {
|
|
37
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
38
|
+
}
|
|
39
|
+
function resultStringField(value, field) {
|
|
40
|
+
const record = resultRecord(value);
|
|
41
|
+
const candidate = record?.[field];
|
|
42
|
+
return typeof candidate === "string" && candidate.trim() ? candidate : null;
|
|
43
|
+
}
|
|
44
|
+
function workspaceResultContent(result) {
|
|
45
|
+
if (typeof result.value === "string") {
|
|
46
|
+
return result.value;
|
|
47
|
+
}
|
|
48
|
+
if (result.snapshot?.data) {
|
|
49
|
+
return result.snapshot.data;
|
|
50
|
+
}
|
|
51
|
+
if (result.value !== void 0) {
|
|
52
|
+
return JSON.stringify(result.value);
|
|
53
|
+
}
|
|
54
|
+
if (result.elements) {
|
|
55
|
+
return JSON.stringify(result.elements);
|
|
56
|
+
}
|
|
57
|
+
if (result.tab) {
|
|
58
|
+
return `${result.tab.title} ${result.tab.url}`.trim();
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function workspaceResultToBrowserActionResult(result) {
|
|
63
|
+
const content = workspaceResultContent(result);
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
message: `browser workspace ${result.subaction} completed`,
|
|
67
|
+
content,
|
|
68
|
+
url: result.tab?.url ?? resultStringField(result.value, "url"),
|
|
69
|
+
title: result.tab?.title ?? resultStringField(result.value, "title"),
|
|
70
|
+
data: {
|
|
71
|
+
mode: result.mode,
|
|
72
|
+
subaction: result.subaction,
|
|
73
|
+
value: result.value,
|
|
74
|
+
tab: result.tab,
|
|
75
|
+
elements: result.elements
|
|
76
|
+
},
|
|
77
|
+
screenshot: result.snapshot?.data ?? null
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
class BrowserServiceActionExecutor {
|
|
81
|
+
constructor(browser) {
|
|
82
|
+
this.browser = browser;
|
|
83
|
+
}
|
|
84
|
+
browser;
|
|
85
|
+
currentTabId = null;
|
|
86
|
+
async executeBrowserAction(params) {
|
|
87
|
+
try {
|
|
88
|
+
const command = await this.toWorkspaceCommand(params);
|
|
89
|
+
const result = await this.browser.execute(command, "workspace");
|
|
90
|
+
if (result.tab?.id) {
|
|
91
|
+
this.currentTabId = result.tab.id;
|
|
92
|
+
}
|
|
93
|
+
return workspaceResultToBrowserActionResult(result);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
message,
|
|
99
|
+
error: message
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async toWorkspaceCommand(params) {
|
|
104
|
+
const id = this.currentTabId ?? void 0;
|
|
105
|
+
switch (params.action) {
|
|
106
|
+
case "open": {
|
|
107
|
+
const reusableId = await this.findReusableBlankWorkspaceTabId();
|
|
108
|
+
if (reusableId) {
|
|
109
|
+
return { id: reusableId, subaction: "navigate", url: params.url };
|
|
110
|
+
}
|
|
111
|
+
return { show: true, subaction: "open", url: params.url };
|
|
112
|
+
}
|
|
113
|
+
case "navigate":
|
|
114
|
+
return id ? { id, subaction: "navigate", url: params.url } : { show: true, subaction: "open", url: params.url };
|
|
115
|
+
case "wait":
|
|
116
|
+
return {
|
|
117
|
+
...id ? { id } : {},
|
|
118
|
+
...params.selector ? { selector: params.selector } : {},
|
|
119
|
+
...params.text ? { text: params.text } : {},
|
|
120
|
+
subaction: "wait",
|
|
121
|
+
timeoutMs: params.timeout ?? DEFAULT_SUBSCRIPTION_BROWSER_WAIT_MS
|
|
122
|
+
};
|
|
123
|
+
case "click":
|
|
124
|
+
return {
|
|
125
|
+
...id ? { id } : {},
|
|
126
|
+
...params.selector ? { selector: params.selector } : { findBy: "text", text: params.text },
|
|
127
|
+
subaction: "click"
|
|
128
|
+
};
|
|
129
|
+
case "get_dom":
|
|
130
|
+
return {
|
|
131
|
+
...id ? { id } : {},
|
|
132
|
+
getMode: "html",
|
|
133
|
+
selector: "body",
|
|
134
|
+
subaction: "get"
|
|
135
|
+
};
|
|
136
|
+
case "screenshot":
|
|
137
|
+
return {
|
|
138
|
+
...id ? { id } : {},
|
|
139
|
+
fullPage: true,
|
|
140
|
+
subaction: "screenshot"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async findReusableBlankWorkspaceTabId() {
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.browser.execute(
|
|
147
|
+
{ subaction: "list" },
|
|
148
|
+
"workspace"
|
|
149
|
+
);
|
|
150
|
+
const tabs = result.tabs ?? [];
|
|
151
|
+
const current = this.currentTabId ? tabs.find(
|
|
152
|
+
(candidate) => candidate.id === this.currentTabId && candidate.visible && candidate.url.trim() === "about:blank"
|
|
153
|
+
) : null;
|
|
154
|
+
const tab = current ?? tabs.find(
|
|
155
|
+
(candidate) => candidate.visible && candidate.url.trim() === "about:blank"
|
|
156
|
+
);
|
|
157
|
+
return tab?.id ?? null;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function resolveAgentBrowserExecutor(runtime) {
|
|
164
|
+
const computerUseService = runtime.getService("computeruse");
|
|
165
|
+
if (isComputerUseBrowserService(computerUseService)) {
|
|
166
|
+
return computerUseService;
|
|
167
|
+
}
|
|
168
|
+
const browserService = runtime.getService(BROWSER_SERVICE_TYPE);
|
|
169
|
+
return isBrowserService(browserService) ? new BrowserServiceActionExecutor(browserService) : null;
|
|
170
|
+
}
|
|
171
|
+
const MAX_AUDIT_MESSAGES = 80;
|
|
172
|
+
const DEFAULT_AUDIT_WINDOW_DAYS = 180;
|
|
173
|
+
function normalizeSubscriptionLookup(value) {
|
|
174
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
|
|
175
|
+
}
|
|
176
|
+
function slugifySubscriptionValue(value) {
|
|
177
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
178
|
+
}
|
|
179
|
+
function guessCadence(message) {
|
|
180
|
+
const blob = `${message.subject} ${message.snippet}`.toLowerCase();
|
|
181
|
+
if (/\bannual\b|\byearly\b|\byear\b|\b12 month\b|\b12-month\b/.test(blob)) {
|
|
182
|
+
return "annual";
|
|
183
|
+
}
|
|
184
|
+
if (/\bmonth\b|\bmonthly\b|\brenewal\b|\bsubscription\b|\bbilling\b/.test(blob)) {
|
|
185
|
+
return "monthly";
|
|
186
|
+
}
|
|
187
|
+
return "unknown";
|
|
188
|
+
}
|
|
189
|
+
function guessState(message) {
|
|
190
|
+
const blob = `${message.subject} ${message.snippet}`.toLowerCase();
|
|
191
|
+
if (/\bcancelled\b|\bcanceled\b|\bended\b|\bexpires on\b|\bexpired\b/.test(blob)) {
|
|
192
|
+
return "canceled";
|
|
193
|
+
}
|
|
194
|
+
if (/\brenewal\b|\breceipt\b|\bbilled\b|\bpayment\b/.test(blob)) {
|
|
195
|
+
return "active";
|
|
196
|
+
}
|
|
197
|
+
return "uncertain";
|
|
198
|
+
}
|
|
199
|
+
function parseUsdAmount(message) {
|
|
200
|
+
const blob = `${message.subject} ${message.snippet}`;
|
|
201
|
+
const match = blob.match(/\$([0-9]+(?:\.[0-9]{1,2})?)/);
|
|
202
|
+
if (!match) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const value = Number(match[1]);
|
|
206
|
+
return Number.isFinite(value) ? value : null;
|
|
207
|
+
}
|
|
208
|
+
function annualizeAmount(amount, cadence) {
|
|
209
|
+
if (amount === null) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (cadence === "monthly") {
|
|
213
|
+
return Number((amount * 12).toFixed(2));
|
|
214
|
+
}
|
|
215
|
+
if (cadence === "annual") {
|
|
216
|
+
return Number(amount.toFixed(2));
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function summarizeEvidence(serviceName, evidence) {
|
|
221
|
+
const latest = evidence[0];
|
|
222
|
+
if (!latest) {
|
|
223
|
+
return `No recent email evidence found for ${serviceName}.`;
|
|
224
|
+
}
|
|
225
|
+
return `${serviceName}: ${evidence.length} matching email${evidence.length === 1 ? "" : "s"}, latest "${latest.subject}" on ${latest.receivedAt}.`;
|
|
226
|
+
}
|
|
227
|
+
function messageBlob(message) {
|
|
228
|
+
return [
|
|
229
|
+
message.subject,
|
|
230
|
+
message.snippet,
|
|
231
|
+
message.from,
|
|
232
|
+
message.fromEmail ?? ""
|
|
233
|
+
].join(" ").toLowerCase();
|
|
234
|
+
}
|
|
235
|
+
function scoreMessageAgainstPlaybook(message, playbook) {
|
|
236
|
+
const blob = messageBlob(message);
|
|
237
|
+
let score = 0;
|
|
238
|
+
for (const alias of [playbook.serviceName, ...playbook.aliases]) {
|
|
239
|
+
if (blob.includes(alias.toLowerCase())) {
|
|
240
|
+
score += 2;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const keyword of playbook.auditSubjectKeywords) {
|
|
244
|
+
if (blob.includes(keyword.toLowerCase())) {
|
|
245
|
+
score += 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const domain of playbook.auditDomains) {
|
|
249
|
+
if (blob.includes(domain.toLowerCase())) {
|
|
250
|
+
score += 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return score;
|
|
254
|
+
}
|
|
255
|
+
function resolvePlaybookFromMessage(text) {
|
|
256
|
+
return findLifeOpsSubscriptionPlaybook(text);
|
|
257
|
+
}
|
|
258
|
+
function resolvePlaybookFromCandidate(candidate) {
|
|
259
|
+
return findLifeOpsSubscriptionPlaybook(candidate.serviceSlug) ?? findLifeOpsSubscriptionPlaybook(candidate.serviceName);
|
|
260
|
+
}
|
|
261
|
+
function companionSelectorForClickTextStep(playbook, step) {
|
|
262
|
+
const clickText = step.text.trim().toLowerCase();
|
|
263
|
+
if (clickText === "cancel subscription") {
|
|
264
|
+
const selector = playbook.companionSelectors?.cancel;
|
|
265
|
+
if (selector) {
|
|
266
|
+
return selector;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (clickText === "confirm cancellation") {
|
|
270
|
+
const selector = playbook.companionSelectors?.confirm;
|
|
271
|
+
if (selector) {
|
|
272
|
+
return selector;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
fail(
|
|
276
|
+
400,
|
|
277
|
+
`${playbook.serviceName} companion playbook is missing a selector for "${step.text}"`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
function toUserBrowserActions(playbook) {
|
|
281
|
+
const actions = [];
|
|
282
|
+
for (const step of playbook.steps ?? []) {
|
|
283
|
+
switch (step.kind) {
|
|
284
|
+
case "open":
|
|
285
|
+
case "navigate":
|
|
286
|
+
actions.push({
|
|
287
|
+
kind: step.kind,
|
|
288
|
+
label: `${playbook.serviceName}: ${step.kind}`,
|
|
289
|
+
url: step.url,
|
|
290
|
+
selector: null,
|
|
291
|
+
text: null,
|
|
292
|
+
accountAffecting: false,
|
|
293
|
+
requiresConfirmation: false,
|
|
294
|
+
metadata: { playbookKey: playbook.key }
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
case "click_text":
|
|
298
|
+
actions.push({
|
|
299
|
+
kind: "click",
|
|
300
|
+
label: `${playbook.serviceName}: click ${step.text}`,
|
|
301
|
+
url: null,
|
|
302
|
+
selector: companionSelectorForClickTextStep(playbook, step),
|
|
303
|
+
text: step.text,
|
|
304
|
+
accountAffecting: true,
|
|
305
|
+
requiresConfirmation: step.destructive ?? false,
|
|
306
|
+
metadata: { playbookKey: playbook.key }
|
|
307
|
+
});
|
|
308
|
+
break;
|
|
309
|
+
case "click_selector":
|
|
310
|
+
actions.push({
|
|
311
|
+
kind: "click",
|
|
312
|
+
label: `${playbook.serviceName}: click selector`,
|
|
313
|
+
url: null,
|
|
314
|
+
selector: step.selector,
|
|
315
|
+
text: null,
|
|
316
|
+
accountAffecting: true,
|
|
317
|
+
requiresConfirmation: step.destructive ?? false,
|
|
318
|
+
metadata: { playbookKey: playbook.key }
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
case "wait_text":
|
|
322
|
+
case "assert_text":
|
|
323
|
+
case "wait_selector":
|
|
324
|
+
case "screenshot":
|
|
325
|
+
actions.push({
|
|
326
|
+
kind: "read_page",
|
|
327
|
+
label: `${playbook.serviceName}: inspect page`,
|
|
328
|
+
url: null,
|
|
329
|
+
selector: null,
|
|
330
|
+
text: null,
|
|
331
|
+
accountAffecting: false,
|
|
332
|
+
requiresConfirmation: false,
|
|
333
|
+
metadata: {
|
|
334
|
+
playbookKey: playbook.key,
|
|
335
|
+
expected: step.kind === "wait_selector" ? step.selector : "text" in step ? step.text : step.label
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return actions;
|
|
342
|
+
}
|
|
343
|
+
function browserResultText(result) {
|
|
344
|
+
return [
|
|
345
|
+
result.message ?? "",
|
|
346
|
+
typeof result.content === "string" ? result.content : "",
|
|
347
|
+
typeof result.url === "string" ? result.url : "",
|
|
348
|
+
typeof result.title === "string" ? result.title : "",
|
|
349
|
+
result.error ?? "",
|
|
350
|
+
result.data ? JSON.stringify(result.data) : ""
|
|
351
|
+
].join(" ").toLowerCase();
|
|
352
|
+
}
|
|
353
|
+
function summarizeCancellationStatus(cancellation) {
|
|
354
|
+
switch (cancellation.status) {
|
|
355
|
+
case "completed":
|
|
356
|
+
return `${cancellation.serviceName} cancellation completed.`;
|
|
357
|
+
case "awaiting_confirmation":
|
|
358
|
+
return `Cancellation for ${cancellation.serviceName} is ready for final confirmation.`;
|
|
359
|
+
case "needs_login":
|
|
360
|
+
return `${cancellation.serviceName} needs the user to sign in before cancellation can continue.`;
|
|
361
|
+
case "needs_mfa":
|
|
362
|
+
return `${cancellation.serviceName} needs multi-factor verification before cancellation can continue.`;
|
|
363
|
+
case "phone_only":
|
|
364
|
+
return `${cancellation.serviceName} can only be canceled by phone.`;
|
|
365
|
+
case "chat_only":
|
|
366
|
+
return `${cancellation.serviceName} can only be canceled through support chat.`;
|
|
367
|
+
case "already_canceled":
|
|
368
|
+
return `${cancellation.serviceName} already appears to be canceled.`;
|
|
369
|
+
case "unsupported_surface":
|
|
370
|
+
if (typeof cancellation.error === "string" && cancellation.error.startsWith(PLAYBOOK_UNSUPPORTED_FLOW_ERROR)) {
|
|
371
|
+
return cancellation.evidenceSummary ?? `I can open the ${cancellation.serviceName} cancel page for you, but I haven't learned the exact click-flow yet. Want me to open the page and you finish the cancel?`;
|
|
372
|
+
}
|
|
373
|
+
return `I don't have a cancellation surface for ${cancellation.serviceName} yet${cancellation.error ? `: ${cancellation.error}` : "."}`;
|
|
374
|
+
case "failed":
|
|
375
|
+
return `Cancellation for ${cancellation.serviceName} failed${cancellation.error ? `: ${cancellation.error}` : "."}`;
|
|
376
|
+
default:
|
|
377
|
+
return `${cancellation.serviceName} cancellation status: ${cancellation.status}.`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function extractEvidenceMessages(messages) {
|
|
381
|
+
return messages.slice(0, 5).map((message) => ({
|
|
382
|
+
messageId: message.id,
|
|
383
|
+
subject: message.subject,
|
|
384
|
+
from: message.from,
|
|
385
|
+
receivedAt: message.receivedAt,
|
|
386
|
+
snippet: message.snippet,
|
|
387
|
+
htmlLink: message.htmlLink
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
async function probeBrowserSignals(browser, playbook) {
|
|
391
|
+
const dom = await browser.executeBrowserAction({ action: "get_dom" });
|
|
392
|
+
const blob = browserResultText(dom);
|
|
393
|
+
for (const marker of playbook.cancellationMarkers) {
|
|
394
|
+
if (blob.includes(marker.toLowerCase())) {
|
|
395
|
+
return { status: "completed", detail: marker };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const marker of playbook.phoneOnlyMarkers) {
|
|
399
|
+
if (blob.includes(marker.toLowerCase())) {
|
|
400
|
+
return { status: "phone_only", detail: marker };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const marker of playbook.chatOnlyMarkers) {
|
|
404
|
+
if (blob.includes(marker.toLowerCase())) {
|
|
405
|
+
return { status: "chat_only", detail: marker };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for (const marker of playbook.mfaMarkers) {
|
|
409
|
+
if (blob.includes(marker.toLowerCase())) {
|
|
410
|
+
return { status: "needs_mfa", detail: marker };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const marker of playbook.loginMarkers) {
|
|
414
|
+
if (blob.includes(marker.toLowerCase())) {
|
|
415
|
+
return { status: "needs_login", detail: marker };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return { status: "clear", detail: null };
|
|
419
|
+
}
|
|
420
|
+
function selectorForBrowserClickTextStep(playbook, step) {
|
|
421
|
+
const clickText = step.text.trim().toLowerCase();
|
|
422
|
+
if (clickText === "cancel subscription") {
|
|
423
|
+
return playbook.companionSelectors?.cancel;
|
|
424
|
+
}
|
|
425
|
+
if (clickText === "confirm cancellation") {
|
|
426
|
+
return playbook.companionSelectors?.confirm;
|
|
427
|
+
}
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
async function executeBrowserStep(browser, playbook, step) {
|
|
431
|
+
const params = (() => {
|
|
432
|
+
switch (step.kind) {
|
|
433
|
+
case "open":
|
|
434
|
+
return { action: "open", url: step.url };
|
|
435
|
+
case "navigate":
|
|
436
|
+
return { action: "navigate", url: step.url };
|
|
437
|
+
case "wait_text":
|
|
438
|
+
return { action: "wait", text: step.text, timeout: step.timeoutMs };
|
|
439
|
+
case "wait_selector":
|
|
440
|
+
return {
|
|
441
|
+
action: "wait",
|
|
442
|
+
selector: step.selector,
|
|
443
|
+
timeout: step.timeoutMs
|
|
444
|
+
};
|
|
445
|
+
case "click_text": {
|
|
446
|
+
const selector = selectorForBrowserClickTextStep(playbook, step);
|
|
447
|
+
return { action: "click", selector, text: step.text };
|
|
448
|
+
}
|
|
449
|
+
case "click_selector":
|
|
450
|
+
return { action: "click", selector: step.selector };
|
|
451
|
+
case "assert_text":
|
|
452
|
+
return { action: "get_dom" };
|
|
453
|
+
case "screenshot":
|
|
454
|
+
return { action: "screenshot" };
|
|
455
|
+
}
|
|
456
|
+
})();
|
|
457
|
+
return browser.executeBrowserAction(params);
|
|
458
|
+
}
|
|
459
|
+
function findServiceInText(text) {
|
|
460
|
+
const playbook = resolvePlaybookFromMessage(text);
|
|
461
|
+
if (!playbook) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
serviceName: playbook.serviceName,
|
|
466
|
+
serviceSlug: playbook.key
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
class SubscriptionsService {
|
|
470
|
+
constructor(runtime, options = {}) {
|
|
471
|
+
this.runtime = runtime;
|
|
472
|
+
this.repository = new FinancesRepository(runtime);
|
|
473
|
+
this.ownerEntityId = normalizeOptionalString(options.ownerEntityId) ?? null;
|
|
474
|
+
this.gmail = options.gmailGateway ?? createSubscriptionsGmailGateway(runtime, requireAgentId(runtime));
|
|
475
|
+
this.browser = options.browserGateway ?? createSubscriptionsBrowserGateway(runtime, this.ownerEntityId);
|
|
476
|
+
}
|
|
477
|
+
runtime;
|
|
478
|
+
repository;
|
|
479
|
+
ownerEntityId;
|
|
480
|
+
gmail;
|
|
481
|
+
browser;
|
|
482
|
+
agentId() {
|
|
483
|
+
return requireAgentId(this.runtime);
|
|
484
|
+
}
|
|
485
|
+
logSubscriptionsWarn(operation, message) {
|
|
486
|
+
logger.warn(
|
|
487
|
+
{
|
|
488
|
+
boundary: "finances",
|
|
489
|
+
operation,
|
|
490
|
+
agentId: this.agentId()
|
|
491
|
+
},
|
|
492
|
+
message
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
async listSubscriptionPlaybooks() {
|
|
496
|
+
return [...listLifeOpsSubscriptionPlaybooks()];
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Best-effort merchant→playbook lookup used by the Payments dashboard to
|
|
500
|
+
* deep-link from a recurring charge row to the cancellation flow. Returns
|
|
501
|
+
* a *trimmed* playbook descriptor (no `steps`) so callers don't render
|
|
502
|
+
* automation internals.
|
|
503
|
+
*/
|
|
504
|
+
findSubscriptionPlaybookForMerchant(merchant) {
|
|
505
|
+
const playbook = findLifeOpsSubscriptionPlaybook(merchant);
|
|
506
|
+
if (!playbook) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
key: playbook.key,
|
|
511
|
+
serviceName: playbook.serviceName,
|
|
512
|
+
managementUrl: playbook.managementUrl,
|
|
513
|
+
executorPreference: playbook.executorPreference
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async getLatestSubscriptionAudit() {
|
|
517
|
+
const audit = await this.repository.getLatestSubscriptionAudit(
|
|
518
|
+
this.agentId()
|
|
519
|
+
);
|
|
520
|
+
if (!audit) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const candidates = await this.repository.listSubscriptionCandidatesForAudit(
|
|
524
|
+
this.agentId(),
|
|
525
|
+
audit.id
|
|
526
|
+
);
|
|
527
|
+
return { audit, candidates };
|
|
528
|
+
}
|
|
529
|
+
async auditSubscriptions(request = {}) {
|
|
530
|
+
const queryWindowDays = Math.max(
|
|
531
|
+
1,
|
|
532
|
+
Math.min(
|
|
533
|
+
365,
|
|
534
|
+
Number.isFinite(request.queryWindowDays) ? Math.trunc(request.queryWindowDays) : DEFAULT_AUDIT_WINDOW_DAYS
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
const serviceQuery = normalizeOptionalString(request.serviceQuery) ?? null;
|
|
538
|
+
let messages = [];
|
|
539
|
+
let source = "gmail";
|
|
540
|
+
try {
|
|
541
|
+
const found = await this.gmail.searchSubscriptionMessages({
|
|
542
|
+
windowDays: queryWindowDays,
|
|
543
|
+
maxResults: MAX_AUDIT_MESSAGES
|
|
544
|
+
});
|
|
545
|
+
const sinceMs = Date.now() - queryWindowDays * 864e5;
|
|
546
|
+
messages = found.filter((message) => {
|
|
547
|
+
const receivedMs = Date.parse(message.receivedAt);
|
|
548
|
+
return !Number.isNaN(receivedMs) && receivedMs >= sinceMs;
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
source = serviceQuery ? "manual" : "gmail";
|
|
552
|
+
this.logSubscriptionsWarn(
|
|
553
|
+
"subscriptions_audit",
|
|
554
|
+
`gmail discovery unavailable: ${error instanceof Error ? error.message : String(error)}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const playbooks = serviceQuery ? listLifeOpsSubscriptionPlaybooks().filter((playbook) => {
|
|
558
|
+
const lookup = normalizeSubscriptionLookup(serviceQuery);
|
|
559
|
+
return normalizeSubscriptionLookup(playbook.serviceName) === lookup || playbook.aliases.some(
|
|
560
|
+
(alias) => normalizeSubscriptionLookup(alias) === lookup
|
|
561
|
+
) || normalizeSubscriptionLookup(playbook.key) === lookup;
|
|
562
|
+
}) : listLifeOpsSubscriptionPlaybooks();
|
|
563
|
+
const candidates = [];
|
|
564
|
+
for (const playbook of playbooks) {
|
|
565
|
+
const evidence = messages.map((message) => ({
|
|
566
|
+
message,
|
|
567
|
+
score: scoreMessageAgainstPlaybook(message, playbook)
|
|
568
|
+
})).filter((candidate2) => candidate2.score > 0).sort((left, right) => right.score - left.score);
|
|
569
|
+
if (evidence.length === 0 && source !== "manual") {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
const bestEvidence = evidence[0] ?? null;
|
|
573
|
+
const bestMessage = bestEvidence?.message;
|
|
574
|
+
const cadence = bestMessage ? guessCadence(bestMessage) : "unknown";
|
|
575
|
+
const state = bestMessage ? guessState(bestMessage) : "uncertain";
|
|
576
|
+
const amount = bestMessage ? parseUsdAmount(bestMessage) : null;
|
|
577
|
+
const confidence = bestEvidence ? Math.min(0.98, 0.45 + bestEvidence.score * 0.12) : 0.4;
|
|
578
|
+
const candidate = createLifeOpsSubscriptionCandidate({
|
|
579
|
+
agentId: this.agentId(),
|
|
580
|
+
auditId: "",
|
|
581
|
+
serviceSlug: playbook.key,
|
|
582
|
+
serviceName: playbook.serviceName,
|
|
583
|
+
provider: bestMessage ? bestMessage.fromEmail ?? bestMessage.from : playbook.serviceName,
|
|
584
|
+
cadence,
|
|
585
|
+
state,
|
|
586
|
+
confidence,
|
|
587
|
+
annualCostEstimateUsd: annualizeAmount(amount, cadence),
|
|
588
|
+
managementUrl: playbook.managementUrl,
|
|
589
|
+
latestEvidenceAt: bestMessage ? bestMessage.receivedAt : null,
|
|
590
|
+
evidenceJson: extractEvidenceMessages(
|
|
591
|
+
evidence.map((item) => item.message)
|
|
592
|
+
),
|
|
593
|
+
metadata: {
|
|
594
|
+
playbookKey: playbook.key,
|
|
595
|
+
evidenceCount: evidence.length,
|
|
596
|
+
source
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
candidates.push(candidate);
|
|
600
|
+
}
|
|
601
|
+
const audit = createLifeOpsSubscriptionAudit({
|
|
602
|
+
agentId: this.agentId(),
|
|
603
|
+
source,
|
|
604
|
+
queryWindowDays,
|
|
605
|
+
status: "completed",
|
|
606
|
+
totalCandidates: candidates.length,
|
|
607
|
+
activeCandidates: candidates.filter(
|
|
608
|
+
(candidate) => candidate.state === "active"
|
|
609
|
+
).length,
|
|
610
|
+
canceledCandidates: candidates.filter(
|
|
611
|
+
(candidate) => candidate.state === "canceled"
|
|
612
|
+
).length,
|
|
613
|
+
uncertainCandidates: candidates.filter(
|
|
614
|
+
(candidate) => candidate.state === "uncertain"
|
|
615
|
+
).length,
|
|
616
|
+
summary: candidates.length === 0 ? source === "manual" ? "No matching subscription playbooks were found for the requested service." : "No subscription evidence was found in recent Gmail receipts." : `Found ${candidates.length} likely subscription${candidates.length === 1 ? "" : "s"} from recent LifeOps signals.`,
|
|
617
|
+
metadata: {
|
|
618
|
+
serviceQuery,
|
|
619
|
+
scannedMessageCount: messages.length,
|
|
620
|
+
playbookCount: playbooks.length
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
await this.repository.createSubscriptionAudit(audit);
|
|
624
|
+
for (const candidate of candidates) {
|
|
625
|
+
const persisted = {
|
|
626
|
+
...candidate,
|
|
627
|
+
auditId: audit.id
|
|
628
|
+
};
|
|
629
|
+
await this.repository.createSubscriptionCandidate(persisted);
|
|
630
|
+
}
|
|
631
|
+
const persistedCandidates = await this.repository.listSubscriptionCandidatesForAudit(
|
|
632
|
+
this.agentId(),
|
|
633
|
+
audit.id
|
|
634
|
+
);
|
|
635
|
+
return { audit, candidates: persistedCandidates };
|
|
636
|
+
}
|
|
637
|
+
async getSubscriptionCancellationStatus(args) {
|
|
638
|
+
const serviceSlug = normalizeOptionalString(args.serviceSlug);
|
|
639
|
+
let cancellation = normalizeOptionalString(args.cancellationId) !== void 0 ? await this.repository.getSubscriptionCancellation(
|
|
640
|
+
this.agentId(),
|
|
641
|
+
requireNonEmptyString(args.cancellationId, "cancellationId")
|
|
642
|
+
) : await this.repository.getLatestSubscriptionCancellation(
|
|
643
|
+
this.agentId(),
|
|
644
|
+
serviceSlug
|
|
645
|
+
);
|
|
646
|
+
if (!cancellation && normalizeOptionalString(args.serviceName)) {
|
|
647
|
+
const playbook = resolvePlaybookFromMessage(
|
|
648
|
+
requireNonEmptyString(args.serviceName, "serviceName")
|
|
649
|
+
);
|
|
650
|
+
cancellation = await this.repository.getLatestSubscriptionCancellation(
|
|
651
|
+
this.agentId(),
|
|
652
|
+
playbook?.key
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
if (!cancellation) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
if (cancellation.browserSessionId) {
|
|
659
|
+
const session = await this.browser.getBrowserSession(
|
|
660
|
+
cancellation.browserSessionId
|
|
661
|
+
);
|
|
662
|
+
if (session) {
|
|
663
|
+
const nextStatus = session.status === "done" ? "completed" : session.status === "failed" ? "failed" : session.status === "awaiting_confirmation" ? "awaiting_confirmation" : "running";
|
|
664
|
+
if (nextStatus !== cancellation.status) {
|
|
665
|
+
cancellation = {
|
|
666
|
+
...cancellation,
|
|
667
|
+
status: nextStatus,
|
|
668
|
+
evidenceSummary: cancellation.evidenceSummary ?? `Agent Browser Bridge session ${session.status}.`,
|
|
669
|
+
error: nextStatus === "failed" ? JSON.stringify(session.result) : cancellation.error,
|
|
670
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
671
|
+
finishedAt: nextStatus === "completed" || nextStatus === "failed" ? (/* @__PURE__ */ new Date()).toISOString() : cancellation.finishedAt
|
|
672
|
+
};
|
|
673
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const candidate = cancellation.candidateId ? await this.repository.getSubscriptionCandidate(
|
|
678
|
+
this.agentId(),
|
|
679
|
+
cancellation.candidateId
|
|
680
|
+
) : null;
|
|
681
|
+
return { cancellation, candidate };
|
|
682
|
+
}
|
|
683
|
+
async cancelSubscription(request) {
|
|
684
|
+
const candidate = request.candidateId ? await this.repository.getSubscriptionCandidate(
|
|
685
|
+
this.agentId(),
|
|
686
|
+
request.candidateId
|
|
687
|
+
) : null;
|
|
688
|
+
const requestedServiceName = normalizeOptionalString(request.serviceName);
|
|
689
|
+
const requestedServiceSlug = normalizeOptionalString(request.serviceSlug);
|
|
690
|
+
const playbook = (candidate ? resolvePlaybookFromCandidate(candidate) : null) ?? (requestedServiceSlug ? resolvePlaybookFromMessage(requestedServiceSlug) : null) ?? (requestedServiceName ? resolvePlaybookFromMessage(requestedServiceName) : null);
|
|
691
|
+
if (!candidate && !playbook && !requestedServiceName) {
|
|
692
|
+
fail(
|
|
693
|
+
400,
|
|
694
|
+
"cancelSubscription requires a known candidateId or recognizable serviceName/serviceSlug"
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const serviceName = candidate?.serviceName ?? playbook?.serviceName ?? requestedServiceName;
|
|
698
|
+
if (!serviceName) {
|
|
699
|
+
fail(
|
|
700
|
+
400,
|
|
701
|
+
"cancelSubscription requires a known candidateId or recognizable serviceName/serviceSlug"
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const serviceSlug = candidate?.serviceSlug ?? playbook?.key ?? requestedServiceSlug ?? slugifySubscriptionValue(serviceName);
|
|
705
|
+
const connectedCompanions = await this.browser.listBrowserCompanions();
|
|
706
|
+
const explicitExecutor = normalizeOptionalString(request.executor);
|
|
707
|
+
const executor = explicitExecutor ?? (connectedCompanions.some(
|
|
708
|
+
(companion) => companion.connectionState === "connected"
|
|
709
|
+
) ? "user_browser" : playbook?.executorPreference ?? "agent_browser");
|
|
710
|
+
const confirmed = normalizeOptionalBoolean(request.confirmed, "confirmed") ?? false;
|
|
711
|
+
let cancellation = createLifeOpsSubscriptionCancellation({
|
|
712
|
+
agentId: this.agentId(),
|
|
713
|
+
auditId: candidate?.auditId ?? null,
|
|
714
|
+
candidateId: candidate?.id ?? null,
|
|
715
|
+
serviceSlug,
|
|
716
|
+
serviceName,
|
|
717
|
+
executor,
|
|
718
|
+
status: "draft",
|
|
719
|
+
confirmed,
|
|
720
|
+
currentStep: null,
|
|
721
|
+
browserSessionId: null,
|
|
722
|
+
evidenceSummary: null,
|
|
723
|
+
artifactCount: 0,
|
|
724
|
+
managementUrl: candidate?.managementUrl ?? playbook?.managementUrl ?? null,
|
|
725
|
+
error: null,
|
|
726
|
+
metadata: {
|
|
727
|
+
playbookKey: playbook?.key ?? null,
|
|
728
|
+
candidateState: candidate?.state ?? null
|
|
729
|
+
},
|
|
730
|
+
finishedAt: null
|
|
731
|
+
});
|
|
732
|
+
await this.repository.createSubscriptionCancellation(cancellation);
|
|
733
|
+
if (!playbook) {
|
|
734
|
+
cancellation = {
|
|
735
|
+
...cancellation,
|
|
736
|
+
status: "unsupported_surface",
|
|
737
|
+
error: "No known cancellation playbook for this service.",
|
|
738
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
739
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
740
|
+
};
|
|
741
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
742
|
+
return { cancellation, candidate };
|
|
743
|
+
}
|
|
744
|
+
if (!playbook.steps || playbook.steps.length === 0) {
|
|
745
|
+
cancellation = {
|
|
746
|
+
...cancellation,
|
|
747
|
+
status: "unsupported_surface",
|
|
748
|
+
error: `${PLAYBOOK_UNSUPPORTED_FLOW_ERROR}:${playbook.key}`,
|
|
749
|
+
evidenceSummary: `I can open the ${playbook.serviceName} cancel page for you, but I haven't learned the exact click-flow yet. Want me to open the page and you finish the cancel? Management URL: ${playbook.managementUrl}`,
|
|
750
|
+
managementUrl: playbook.managementUrl,
|
|
751
|
+
metadata: {
|
|
752
|
+
...cancellation.metadata,
|
|
753
|
+
playbookUnsupportedFlow: true,
|
|
754
|
+
managementUrl: playbook.managementUrl
|
|
755
|
+
},
|
|
756
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
757
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
758
|
+
};
|
|
759
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
760
|
+
return { cancellation, candidate };
|
|
761
|
+
}
|
|
762
|
+
if (candidate?.state === "canceled") {
|
|
763
|
+
cancellation = {
|
|
764
|
+
...cancellation,
|
|
765
|
+
status: "already_canceled",
|
|
766
|
+
evidenceSummary: summarizeEvidence(serviceName, []),
|
|
767
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
768
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
769
|
+
};
|
|
770
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
771
|
+
return { cancellation, candidate };
|
|
772
|
+
}
|
|
773
|
+
if (executor === "user_browser") {
|
|
774
|
+
const companion = connectedCompanions.find(
|
|
775
|
+
(entry) => entry.connectionState === "connected"
|
|
776
|
+
);
|
|
777
|
+
if (!companion) {
|
|
778
|
+
cancellation = {
|
|
779
|
+
...cancellation,
|
|
780
|
+
status: "blocked",
|
|
781
|
+
error: "No connected Agent Browser Bridge companion is available.",
|
|
782
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
783
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
784
|
+
};
|
|
785
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
786
|
+
return { cancellation, candidate };
|
|
787
|
+
}
|
|
788
|
+
const session = await this.browser.createBrowserSession({
|
|
789
|
+
title: `Manage ${serviceName} subscription`,
|
|
790
|
+
browser: companion.browser,
|
|
791
|
+
companionId: companion.id,
|
|
792
|
+
profileId: companion.profileId,
|
|
793
|
+
actions: toUserBrowserActions(playbook)
|
|
794
|
+
});
|
|
795
|
+
cancellation = {
|
|
796
|
+
...cancellation,
|
|
797
|
+
status: session.status === "awaiting_confirmation" ? "awaiting_confirmation" : "running",
|
|
798
|
+
currentStep: "browser_session_created",
|
|
799
|
+
browserSessionId: session.id,
|
|
800
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
801
|
+
metadata: {
|
|
802
|
+
...cancellation.metadata,
|
|
803
|
+
browserSessionStatus: session.status
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
807
|
+
return { cancellation, candidate };
|
|
808
|
+
}
|
|
809
|
+
const browser = resolveAgentBrowserExecutor(this.runtime);
|
|
810
|
+
if (!browser) {
|
|
811
|
+
cancellation = {
|
|
812
|
+
...cancellation,
|
|
813
|
+
status: "failed",
|
|
814
|
+
error: "Agent browser service is not available.",
|
|
815
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
816
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
817
|
+
};
|
|
818
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
819
|
+
return { cancellation, candidate };
|
|
820
|
+
}
|
|
821
|
+
const artifacts = [];
|
|
822
|
+
cancellation = {
|
|
823
|
+
...cancellation,
|
|
824
|
+
status: "running",
|
|
825
|
+
currentStep: "starting_playbook",
|
|
826
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
827
|
+
};
|
|
828
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
829
|
+
for (const step of playbook.steps) {
|
|
830
|
+
if ("destructive" in step && step.destructive && !confirmed) {
|
|
831
|
+
cancellation = {
|
|
832
|
+
...cancellation,
|
|
833
|
+
status: "awaiting_confirmation",
|
|
834
|
+
currentStep: step.kind === "click_text" ? step.text : step.kind === "click_selector" ? step.selector : "destructive_step",
|
|
835
|
+
evidenceSummary: cancellation.evidenceSummary ?? `Ready to confirm ${serviceName} cancellation.`,
|
|
836
|
+
artifactCount: artifacts.length,
|
|
837
|
+
metadata: {
|
|
838
|
+
...cancellation.metadata,
|
|
839
|
+
artifacts
|
|
840
|
+
},
|
|
841
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
842
|
+
};
|
|
843
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
844
|
+
return { cancellation, candidate };
|
|
845
|
+
}
|
|
846
|
+
const result = await executeBrowserStep(browser, playbook, step);
|
|
847
|
+
if (!result.success) {
|
|
848
|
+
cancellation = {
|
|
849
|
+
...cancellation,
|
|
850
|
+
status: "failed",
|
|
851
|
+
currentStep: step.kind,
|
|
852
|
+
error: result.error ?? result.message ?? "browser step failed",
|
|
853
|
+
artifactCount: artifacts.length,
|
|
854
|
+
metadata: {
|
|
855
|
+
...cancellation.metadata,
|
|
856
|
+
artifacts,
|
|
857
|
+
lastBrowserResult: result
|
|
858
|
+
},
|
|
859
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
860
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
861
|
+
};
|
|
862
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
863
|
+
return { cancellation, candidate };
|
|
864
|
+
}
|
|
865
|
+
if (step.kind === "screenshot" && result.screenshot) {
|
|
866
|
+
artifacts.push({
|
|
867
|
+
kind: "screenshot",
|
|
868
|
+
label: step.label,
|
|
869
|
+
detail: `screenshot:${result.screenshot.length}`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const probe = await probeBrowserSignals(browser, playbook);
|
|
873
|
+
if (probe.status === "needs_login") {
|
|
874
|
+
cancellation = {
|
|
875
|
+
...cancellation,
|
|
876
|
+
status: "needs_login",
|
|
877
|
+
currentStep: step.kind,
|
|
878
|
+
evidenceSummary: probe.detail,
|
|
879
|
+
artifactCount: artifacts.length,
|
|
880
|
+
metadata: {
|
|
881
|
+
...cancellation.metadata,
|
|
882
|
+
artifacts
|
|
883
|
+
},
|
|
884
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
885
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
886
|
+
};
|
|
887
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
888
|
+
return { cancellation, candidate };
|
|
889
|
+
}
|
|
890
|
+
if (probe.status === "needs_mfa") {
|
|
891
|
+
cancellation = {
|
|
892
|
+
...cancellation,
|
|
893
|
+
status: "needs_mfa",
|
|
894
|
+
currentStep: step.kind,
|
|
895
|
+
evidenceSummary: probe.detail,
|
|
896
|
+
artifactCount: artifacts.length,
|
|
897
|
+
metadata: {
|
|
898
|
+
...cancellation.metadata,
|
|
899
|
+
artifacts
|
|
900
|
+
},
|
|
901
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
902
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
903
|
+
};
|
|
904
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
905
|
+
return { cancellation, candidate };
|
|
906
|
+
}
|
|
907
|
+
if (probe.status === "phone_only" || probe.status === "chat_only") {
|
|
908
|
+
cancellation = {
|
|
909
|
+
...cancellation,
|
|
910
|
+
status: probe.status,
|
|
911
|
+
currentStep: step.kind,
|
|
912
|
+
evidenceSummary: probe.detail,
|
|
913
|
+
artifactCount: artifacts.length,
|
|
914
|
+
metadata: {
|
|
915
|
+
...cancellation.metadata,
|
|
916
|
+
artifacts
|
|
917
|
+
},
|
|
918
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
919
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
920
|
+
};
|
|
921
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
922
|
+
return { cancellation, candidate };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const finalProbe = await probeBrowserSignals(browser, playbook);
|
|
926
|
+
cancellation = {
|
|
927
|
+
...cancellation,
|
|
928
|
+
status: finalProbe.status === "completed" ? "completed" : "blocked",
|
|
929
|
+
currentStep: "done",
|
|
930
|
+
evidenceSummary: finalProbe.detail ?? `${serviceName} flow finished in the local browser.`,
|
|
931
|
+
artifactCount: artifacts.length,
|
|
932
|
+
metadata: {
|
|
933
|
+
...cancellation.metadata,
|
|
934
|
+
artifacts
|
|
935
|
+
},
|
|
936
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
937
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
938
|
+
};
|
|
939
|
+
await this.repository.updateSubscriptionCancellation(cancellation);
|
|
940
|
+
return { cancellation, candidate };
|
|
941
|
+
}
|
|
942
|
+
summarizeSubscriptionAudit(summary) {
|
|
943
|
+
if (summary.candidates.length === 0) {
|
|
944
|
+
return summary.audit.summary;
|
|
945
|
+
}
|
|
946
|
+
return [
|
|
947
|
+
summary.audit.summary,
|
|
948
|
+
...summary.candidates.slice(0, 5).map((candidate) => {
|
|
949
|
+
const annual = candidate.annualCostEstimateUsd === null ? "" : `, est $${candidate.annualCostEstimateUsd.toFixed(2)}/yr`;
|
|
950
|
+
return `- ${candidate.serviceName} (${candidate.state}, ${candidate.cadence}${annual})`;
|
|
951
|
+
})
|
|
952
|
+
].join("\n");
|
|
953
|
+
}
|
|
954
|
+
summarizeSubscriptionCancellation(summary) {
|
|
955
|
+
const status = summarizeCancellationStatus(summary.cancellation);
|
|
956
|
+
const lines = [status];
|
|
957
|
+
if (summary.cancellation.evidenceSummary && summary.cancellation.evidenceSummary !== status) {
|
|
958
|
+
lines.push(summary.cancellation.evidenceSummary);
|
|
959
|
+
}
|
|
960
|
+
if (summary.candidate) {
|
|
961
|
+
lines.push(
|
|
962
|
+
`Candidate confidence ${summary.candidate.confidence.toFixed(2)} from ${summary.candidate.provider}.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
return lines.join(" ");
|
|
966
|
+
}
|
|
967
|
+
resolveSubscriptionIntent(text) {
|
|
968
|
+
const normalized = text.trim().toLowerCase();
|
|
969
|
+
if (!normalized) {
|
|
970
|
+
return { mode: null };
|
|
971
|
+
}
|
|
972
|
+
const matchedService = findServiceInText(text);
|
|
973
|
+
if (/\baudit\b|\breport\b|\breview\b|\bfind\b.*\bsubscription\b|\bwhat subscriptions\b/.test(
|
|
974
|
+
normalized
|
|
975
|
+
)) {
|
|
976
|
+
return {
|
|
977
|
+
mode: "audit",
|
|
978
|
+
...matchedService
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
if (/\bcancel\b|\bunsubscribe\b|\bend\b.*\bsubscription\b/.test(normalized)) {
|
|
982
|
+
return {
|
|
983
|
+
mode: "cancel",
|
|
984
|
+
...matchedService,
|
|
985
|
+
executor: /\bin my browser\b|\bpersonal browser\b/.test(normalized) ? "user_browser" : "agent_browser"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (/\bstatus\b|\bwhat happened\b|\bupdate\b.*\bsubscription\b/.test(
|
|
989
|
+
normalized
|
|
990
|
+
)) {
|
|
991
|
+
return {
|
|
992
|
+
mode: "status",
|
|
993
|
+
...matchedService
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return { mode: null, ...matchedService };
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
export {
|
|
1000
|
+
SubscriptionsService
|
|
1001
|
+
};
|
|
1002
|
+
//# sourceMappingURL=subscriptions-service.js.map
|