@elizaos/plugin-shopify 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1802 @@
1
+ import { ModelType, Service, getConnectorAccountManager, logger, promoteSubactionsToActions } from "@elizaos/core";
2
+ //#region src/accounts.ts
3
+ const DEFAULT_SHOPIFY_ACCOUNT_ID = "default";
4
+ const DEFAULT_SHOPIFY_ACCOUNT_ROLE = "OWNER";
5
+ function nonEmptyString$1(value) {
6
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
7
+ }
8
+ function readSetting$1(runtime, key) {
9
+ return nonEmptyString$1(runtime.getSetting(key));
10
+ }
11
+ function normalizeShopifyAccountId(value) {
12
+ return nonEmptyString$1(value) ?? "default";
13
+ }
14
+ function resolveShopifyAccountId(runtime, options) {
15
+ const requested = nonEmptyString$1(options?.accountId) ?? nonEmptyString$1(options?.shopifyAccountId);
16
+ if (requested) return requested;
17
+ const configuredDefault = readSetting$1(runtime, "SHOPIFY_DEFAULT_ACCOUNT_ID") ?? readSetting$1(runtime, "SHOPIFY_ACCOUNT_ID");
18
+ return resolveShopifyDefaultAccount(readShopifyAccounts(runtime), configuredDefault)?.accountId ?? normalizeShopifyAccountId(configuredDefault);
19
+ }
20
+ function parseAccountsJson(raw) {
21
+ if (!raw) return [];
22
+ try {
23
+ const parsed = JSON.parse(raw);
24
+ if (Array.isArray(parsed)) return parsed.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
25
+ if (parsed && typeof parsed === "object") return Object.entries(parsed).filter(([, value]) => value && typeof value === "object").map(([id, value]) => ({
26
+ ...value,
27
+ accountId: value.accountId ?? id
28
+ }));
29
+ } catch {
30
+ return [];
31
+ }
32
+ return [];
33
+ }
34
+ function readRawField(record, keys) {
35
+ const credentials = record.credentials && typeof record.credentials === "object" ? record.credentials : {};
36
+ const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
37
+ const settings = record.settings && typeof record.settings === "object" ? record.settings : {};
38
+ for (const source of [
39
+ record,
40
+ credentials,
41
+ metadata,
42
+ settings
43
+ ]) for (const key of keys) {
44
+ const value = nonEmptyString$1(source[key]);
45
+ if (value) return value;
46
+ }
47
+ }
48
+ function accountFromRecord(record) {
49
+ const accountId = normalizeShopifyAccountId(record.accountId ?? record.id ?? record.name);
50
+ const storeDomain = readRawField(record, [
51
+ "SHOPIFY_STORE_DOMAIN",
52
+ "storeDomain",
53
+ "domain"
54
+ ]);
55
+ const accessToken = readRawField(record, [
56
+ "SHOPIFY_ACCESS_TOKEN",
57
+ "accessToken",
58
+ "token",
59
+ "access"
60
+ ]);
61
+ if (!storeDomain || !accessToken) return null;
62
+ return {
63
+ accountId,
64
+ role: DEFAULT_SHOPIFY_ACCOUNT_ROLE,
65
+ storeDomain,
66
+ accessToken,
67
+ label: nonEmptyString$1(record.label ?? record.displayName)
68
+ };
69
+ }
70
+ function addAccount(accounts, account) {
71
+ if (account) accounts.set(account.accountId, account);
72
+ }
73
+ function readShopifyAccounts(runtime) {
74
+ const accounts = /* @__PURE__ */ new Map();
75
+ const characterAccounts = (runtime.character?.settings?.shopify)?.accounts;
76
+ if (Array.isArray(characterAccounts)) {
77
+ for (const item of characterAccounts) if (item && typeof item === "object") addAccount(accounts, accountFromRecord(item));
78
+ } else if (characterAccounts && typeof characterAccounts === "object") {
79
+ for (const [id, value] of Object.entries(characterAccounts)) if (value && typeof value === "object") addAccount(accounts, accountFromRecord({
80
+ ...value,
81
+ accountId: value.accountId ?? id
82
+ }));
83
+ }
84
+ for (const record of parseAccountsJson(readSetting$1(runtime, "SHOPIFY_ACCOUNTS"))) addAccount(accounts, accountFromRecord(record));
85
+ const storeDomain = readSetting$1(runtime, "SHOPIFY_STORE_DOMAIN");
86
+ const accessToken = readSetting$1(runtime, "SHOPIFY_ACCESS_TOKEN");
87
+ if (storeDomain && accessToken) addAccount(accounts, {
88
+ accountId: normalizeShopifyAccountId(readSetting$1(runtime, "SHOPIFY_ACCOUNT_ID") ?? readSetting$1(runtime, "SHOPIFY_DEFAULT_ACCOUNT_ID")),
89
+ role: DEFAULT_SHOPIFY_ACCOUNT_ROLE,
90
+ storeDomain,
91
+ accessToken
92
+ });
93
+ return Array.from(accounts.values());
94
+ }
95
+ function resolveShopifyAccount(accounts, accountId) {
96
+ return accounts.find((account) => account.accountId === accountId) ?? null;
97
+ }
98
+ function resolveShopifyDefaultAccount(accounts, accountId) {
99
+ return resolveShopifyAccount(accounts, normalizeShopifyAccountId(accountId)) ?? resolveShopifyAccount(accounts, "default") ?? accounts.find((account) => account.role === "OWNER") ?? accounts[0] ?? null;
100
+ }
101
+ function hasShopifyAccountConfig(runtime, options) {
102
+ const accountId = resolveShopifyAccountId(runtime, options);
103
+ return Boolean(resolveShopifyAccount(readShopifyAccounts(runtime), accountId));
104
+ }
105
+ //#endregion
106
+ //#region src/actions/account-options.ts
107
+ function getShopifyActionOptions(options) {
108
+ const direct = options ?? {};
109
+ const parameters = direct.parameters && typeof direct.parameters === "object" ? direct.parameters : {};
110
+ return {
111
+ ...direct,
112
+ ...parameters
113
+ };
114
+ }
115
+ function getShopifyAccountId(runtime, options) {
116
+ return resolveShopifyAccountId(runtime, getShopifyActionOptions(options));
117
+ }
118
+ function hasShopifyConfig(runtime, options) {
119
+ return hasShopifyAccountConfig(runtime, getShopifyActionOptions(options));
120
+ }
121
+ const shopifyAccountIdParameter = {
122
+ name: "accountId",
123
+ description: "Optional Shopify account id from SHOPIFY_ACCOUNTS. Defaults to SHOPIFY_DEFAULT_ACCOUNT_ID or the legacy single store token.",
124
+ required: false,
125
+ schema: { type: "string" }
126
+ };
127
+ //#endregion
128
+ //#region src/shopify-client.ts
129
+ const API_VERSION = "2025-04";
130
+ /**
131
+ * Lightweight Shopify Admin GraphQL API client.
132
+ * Uses native fetch -- no external dependencies.
133
+ */
134
+ var ShopifyClient = class {
135
+ baseUrl;
136
+ accessToken;
137
+ constructor(storeDomain, accessToken) {
138
+ const domain = storeDomain.replace(/^https?:\/\//, "").replace(/\/$/, "");
139
+ this.baseUrl = `https://${domain}/admin/api/${API_VERSION}/graphql.json`;
140
+ this.accessToken = accessToken;
141
+ }
142
+ /**
143
+ * Execute a GraphQL query or mutation against the Shopify Admin API.
144
+ * Throws on HTTP errors and on GraphQL-level errors.
145
+ */
146
+ async query(graphql, variables) {
147
+ const resp = await fetch(this.baseUrl, {
148
+ method: "POST",
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ "X-Shopify-Access-Token": this.accessToken
152
+ },
153
+ body: JSON.stringify({
154
+ query: graphql,
155
+ variables
156
+ })
157
+ });
158
+ if (!resp.ok) {
159
+ const body = await resp.text().catch(() => "");
160
+ throw new Error(`Shopify API error: ${resp.status} ${resp.statusText}${body ? ` -- ${body.slice(0, 200)}` : ""}`);
161
+ }
162
+ const json = await resp.json();
163
+ if (json.errors?.length) throw new Error(`Shopify GraphQL error: ${json.errors.map((e) => e.message).join(", ")}`);
164
+ return json.data;
165
+ }
166
+ };
167
+ //#endregion
168
+ //#region src/services/ShopifyService.ts
169
+ const SHOPIFY_SERVICE_TYPE = "shopify";
170
+ const PRODUCT_FIELDS = `
171
+ id
172
+ title
173
+ handle
174
+ status
175
+ descriptionHtml
176
+ productType
177
+ vendor
178
+ totalInventory
179
+ featuredImage { url altText }
180
+ variants(first: 5) {
181
+ edges { node { id title price sku inventoryQuantity } }
182
+ }
183
+ `;
184
+ const ORDER_FIELDS = `
185
+ id
186
+ name
187
+ createdAt
188
+ displayFinancialStatus
189
+ displayFulfillmentStatus
190
+ totalPriceSet { shopMoney { amount currencyCode } }
191
+ customer { id displayName }
192
+ lineItems(first: 10) {
193
+ edges { node { title quantity originalUnitPriceSet { shopMoney { amount currencyCode } } } }
194
+ }
195
+ `;
196
+ const CUSTOMER_FIELDS = `
197
+ id
198
+ displayName
199
+ email
200
+ phone
201
+ ordersCount
202
+ totalSpentV2 { amount currencyCode }
203
+ createdAt
204
+ `;
205
+ function formatUserErrors(errors) {
206
+ return errors.map((e) => `${e.field?.join(".") ?? "?"}: ${e.message}`).join("; ");
207
+ }
208
+ var ShopifyService = class ShopifyService extends Service {
209
+ static serviceType = SHOPIFY_SERVICE_TYPE;
210
+ capabilityDescription = "Connects the agent to a Shopify store for managing products, orders, inventory, and customers through the Admin GraphQL API.";
211
+ clients = /* @__PURE__ */ new Map();
212
+ defaultAccountId = DEFAULT_SHOPIFY_ACCOUNT_ID;
213
+ constructor(runtime) {
214
+ super(runtime);
215
+ }
216
+ async stop() {
217
+ this.clients.clear();
218
+ }
219
+ static async start(runtime) {
220
+ const svc = new ShopifyService(runtime);
221
+ const accounts = readShopifyAccounts(runtime);
222
+ const defaultAccount = resolveShopifyDefaultAccount(accounts, normalizeShopifyAccountId(runtime.getSetting("SHOPIFY_DEFAULT_ACCOUNT_ID") ?? runtime.getSetting("SHOPIFY_ACCOUNT_ID")));
223
+ if (!defaultAccount) {
224
+ logger.warn({
225
+ src: "plugin:shopify",
226
+ agentId: runtime.agentId
227
+ }, "No Shopify account configured -- Shopify service inactive");
228
+ return svc;
229
+ }
230
+ svc.defaultAccountId = defaultAccount.accountId;
231
+ for (const account of accounts) svc.clients.set(account.accountId, {
232
+ accountId: account.accountId,
233
+ config: account,
234
+ client: new ShopifyClient(account.storeDomain, account.accessToken)
235
+ });
236
+ try {
237
+ const shop = await svc.getShop();
238
+ logger.info({
239
+ src: "plugin:shopify",
240
+ agentId: runtime.agentId,
241
+ accountId: defaultAccount.accountId,
242
+ store: shop.name
243
+ }, `Shopify connected to "${shop.name}" (${shop.myshopifyDomain})`);
244
+ } catch (err) {
245
+ logger.error({
246
+ src: "plugin:shopify",
247
+ agentId: runtime.agentId,
248
+ error: err instanceof Error ? err.message : String(err)
249
+ }, "Failed to connect to Shopify store");
250
+ svc.clients.delete(defaultAccount.accountId);
251
+ }
252
+ return svc;
253
+ }
254
+ isConnected(accountId) {
255
+ return Boolean(this.getClientState(accountId, false));
256
+ }
257
+ getClientState(accountId, throwOnMissing = true) {
258
+ const normalized = normalizeShopifyAccountId(accountId);
259
+ const state = accountId ? this.clients.get(normalized) ?? null : this.clients.get(this.defaultAccountId) ?? Array.from(this.clients.values())[0] ?? null;
260
+ if (!state && throwOnMissing) throw new Error("Shopify client is not initialised. Check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN.");
261
+ return state;
262
+ }
263
+ requireClient(accountId) {
264
+ return this.getClientState(accountId)?.client;
265
+ }
266
+ async getShop(accountId) {
267
+ return (await this.requireClient(accountId).query(`{
268
+ shop {
269
+ name
270
+ email
271
+ myshopifyDomain
272
+ plan { displayName }
273
+ currencyCode
274
+ primaryDomain { url }
275
+ }
276
+ }`)).shop;
277
+ }
278
+ async listProducts(opts = {}, accountId) {
279
+ const variables = { first: Math.min(opts.first ?? 10, 50) };
280
+ if (opts.after) variables.after = opts.after;
281
+ if (opts.query) variables.query = opts.query;
282
+ const gql = `query ListProducts($first: Int!, $after: String, $query: String) {
283
+ products(first: $first, after: $after, query: $query, sortKey: TITLE) {
284
+ edges { node { ${PRODUCT_FIELDS} } }
285
+ pageInfo { hasNextPage endCursor }
286
+ }
287
+ }`;
288
+ const data = await this.requireClient(accountId).query(gql, variables);
289
+ return {
290
+ products: data.products.edges.map((e) => e.node),
291
+ hasNextPage: data.products.pageInfo.hasNextPage,
292
+ endCursor: data.products.pageInfo.endCursor
293
+ };
294
+ }
295
+ async createProduct(input, accountId) {
296
+ const gql = `mutation CreateProduct($input: ProductInput!) {
297
+ productCreate(input: $input) {
298
+ product { ${PRODUCT_FIELDS} }
299
+ userErrors { field message }
300
+ }
301
+ }`;
302
+ const data = await this.requireClient(accountId).query(gql, { input: {
303
+ title: input.title,
304
+ descriptionHtml: input.descriptionHtml ?? "",
305
+ productType: input.productType ?? "",
306
+ vendor: input.vendor ?? "",
307
+ status: (input.status ?? "DRAFT").toUpperCase()
308
+ } });
309
+ if (data.productCreate.userErrors.length > 0) throw new Error(`Product create failed: ${formatUserErrors(data.productCreate.userErrors)}`);
310
+ if (!data.productCreate.product) throw new Error("Product create returned no product");
311
+ return data.productCreate.product;
312
+ }
313
+ async updateProduct(id, input, accountId) {
314
+ const gql = `mutation UpdateProduct($input: ProductInput!) {
315
+ productUpdate(input: $input) {
316
+ product { ${PRODUCT_FIELDS} }
317
+ userErrors { field message }
318
+ }
319
+ }`;
320
+ const data = await this.requireClient(accountId).query(gql, { input: {
321
+ id,
322
+ ...input
323
+ } });
324
+ if (data.productUpdate.userErrors.length > 0) throw new Error(`Product update failed: ${formatUserErrors(data.productUpdate.userErrors)}`);
325
+ if (!data.productUpdate.product) throw new Error("Product update returned no product");
326
+ return data.productUpdate.product;
327
+ }
328
+ async listOrders(opts = {}, accountId) {
329
+ const variables = { first: Math.min(opts.first ?? 10, 50) };
330
+ if (opts.after) variables.after = opts.after;
331
+ if (opts.query) variables.query = opts.query;
332
+ const gql = `query ListOrders($first: Int!, $after: String, $query: String) {
333
+ orders(first: $first, after: $after, query: $query, sortKey: CREATED_AT, reverse: true) {
334
+ edges { node { ${ORDER_FIELDS} } }
335
+ pageInfo { hasNextPage endCursor }
336
+ }
337
+ }`;
338
+ const data = await this.requireClient(accountId).query(gql, variables);
339
+ return {
340
+ orders: data.orders.edges.map((e) => e.node),
341
+ hasNextPage: data.orders.pageInfo.hasNextPage,
342
+ endCursor: data.orders.pageInfo.endCursor
343
+ };
344
+ }
345
+ async getOrder(id, accountId) {
346
+ const gql = `query GetOrder($id: ID!) {
347
+ order(id: $id) { ${ORDER_FIELDS} }
348
+ }`;
349
+ return (await this.requireClient(accountId).query(gql, { id })).order;
350
+ }
351
+ async fulfillOrder(orderId, accountId) {
352
+ const foData = await this.requireClient(accountId).query(`query FulfillmentOrders($id: ID!) {
353
+ order(id: $id) {
354
+ fulfillmentOrders(first: 5) {
355
+ edges {
356
+ node {
357
+ id
358
+ status
359
+ lineItems(first: 50) {
360
+ edges { node { id totalQuantity } }
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ }`, { id: orderId });
367
+ if (!foData.order) throw new Error(`Order ${orderId} not found`);
368
+ const openFOs = foData.order.fulfillmentOrders.edges.map((e) => e.node).filter((fo) => fo.status === "OPEN" || fo.status === "IN_PROGRESS");
369
+ if (openFOs.length === 0) throw new Error("No open fulfillment orders found for this order");
370
+ const fulfillGql = `mutation FulfillOrder($fulfillment: FulfillmentV2Input!) {
371
+ fulfillmentCreateV2(fulfillment: $fulfillment) {
372
+ fulfillment { id status }
373
+ userErrors { field message }
374
+ }
375
+ }`;
376
+ const lineItems = openFOs[0].lineItems.edges.map((e) => ({
377
+ fulfillmentOrderLineItemId: e.node.id,
378
+ quantity: e.node.totalQuantity
379
+ }));
380
+ const data = await this.requireClient(accountId).query(fulfillGql, { fulfillment: {
381
+ fulfillmentOrderId: openFOs[0].id,
382
+ lineItemsByFulfillmentOrder: [{
383
+ fulfillmentOrderId: openFOs[0].id,
384
+ fulfillmentOrderLineItems: lineItems
385
+ }]
386
+ } });
387
+ if (data.fulfillmentCreateV2.userErrors.length > 0) throw new Error(`Fulfillment failed: ${formatUserErrors(data.fulfillmentCreateV2.userErrors)}`);
388
+ if (!data.fulfillmentCreateV2.fulfillment) throw new Error("Fulfillment returned no result");
389
+ return data.fulfillmentCreateV2.fulfillment;
390
+ }
391
+ async listCustomers(opts = {}, accountId) {
392
+ const variables = { first: Math.min(opts.first ?? 10, 50) };
393
+ if (opts.after) variables.after = opts.after;
394
+ if (opts.query) variables.query = opts.query;
395
+ const gql = `query ListCustomers($first: Int!, $after: String, $query: String) {
396
+ customers(first: $first, after: $after, query: $query) {
397
+ edges { node { ${CUSTOMER_FIELDS} } }
398
+ pageInfo { hasNextPage endCursor }
399
+ }
400
+ }`;
401
+ const data = await this.requireClient(accountId).query(gql, variables);
402
+ return {
403
+ customers: data.customers.edges.map((e) => e.node),
404
+ hasNextPage: data.customers.pageInfo.hasNextPage,
405
+ endCursor: data.customers.pageInfo.endCursor
406
+ };
407
+ }
408
+ async checkInventory(inventoryItemId, accountId) {
409
+ const data = await this.requireClient(accountId).query(`query CheckInventory($id: ID!) {
410
+ inventoryItem(id: $id) {
411
+ id
412
+ tracked
413
+ inventoryLevels(first: 10) {
414
+ edges { node { id available location { id name } } }
415
+ }
416
+ }
417
+ }`, { id: inventoryItemId });
418
+ if (!data.inventoryItem) throw new Error(`Inventory item ${inventoryItemId} not found`);
419
+ return data.inventoryItem.inventoryLevels.edges.map((e) => e.node);
420
+ }
421
+ async adjustInventory(opts, accountId) {
422
+ const data = await this.requireClient(accountId).query(`mutation AdjustInventory($input: InventoryAdjustQuantitiesInput!) {
423
+ inventoryAdjustQuantities(input: $input) {
424
+ inventoryAdjustmentGroup { reason }
425
+ userErrors { field message }
426
+ }
427
+ }`, { input: {
428
+ reason: opts.reason ?? "correction",
429
+ name: "available",
430
+ changes: [{
431
+ inventoryItemId: opts.inventoryItemId,
432
+ locationId: opts.locationId,
433
+ delta: opts.delta
434
+ }]
435
+ } });
436
+ if (data.inventoryAdjustQuantities.userErrors.length > 0) throw new Error(`Inventory adjust failed: ${formatUserErrors(data.inventoryAdjustQuantities.userErrors)}`);
437
+ }
438
+ async listLocations(accountId) {
439
+ return (await this.requireClient(accountId).query(`{
440
+ locations(first: 20) {
441
+ edges { node { id name isActive } }
442
+ }
443
+ }`)).locations.edges.map((e) => e.node);
444
+ }
445
+ async getProductCount(accountId) {
446
+ return (await this.requireClient(accountId).query(`{
447
+ productsCount { count }
448
+ }`)).productsCount.count;
449
+ }
450
+ async getOrderCount(accountId) {
451
+ return (await this.requireClient(accountId).query(`{
452
+ ordersCount { count }
453
+ }`)).ordersCount.count;
454
+ }
455
+ };
456
+ //#endregion
457
+ //#region src/actions/confirmation.ts
458
+ function mergedOptions(options) {
459
+ const direct = options ?? {};
460
+ const parameters = direct.parameters && typeof direct.parameters === "object" ? direct.parameters : {};
461
+ return {
462
+ ...direct,
463
+ ...parameters
464
+ };
465
+ }
466
+ function getActionOptions(options) {
467
+ return mergedOptions(options);
468
+ }
469
+ function isConfirmed(options) {
470
+ const raw = mergedOptions(options).confirmed;
471
+ return raw === true || raw === "true";
472
+ }
473
+ function confirmationRequired(preview, data) {
474
+ return {
475
+ success: false,
476
+ text: preview,
477
+ data: {
478
+ requiresConfirmation: true,
479
+ preview,
480
+ ...data
481
+ }
482
+ };
483
+ }
484
+ //#endregion
485
+ //#region src/actions/json.ts
486
+ function parseJsonObject(value) {
487
+ try {
488
+ const parsed = JSON.parse(value.trim());
489
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+ //#endregion
495
+ //#region src/actions/manage-customers.ts
496
+ function formatCustomer(c) {
497
+ const email = c.email ?? "no email";
498
+ const spent = `${c.totalSpentV2.amount} ${c.totalSpentV2.currencyCode}`;
499
+ return `- **${c.displayName}** | ${email} | Orders: ${c.ordersCount} | Total spent: ${spent}`;
500
+ }
501
+ function readNullableString$3(value) {
502
+ return typeof value === "string" && value.trim().length > 0 && value.trim().toLowerCase() !== "null" ? value.trim() : null;
503
+ }
504
+ function readCustomerIntent(options) {
505
+ const params = getActionOptions(options);
506
+ const candidate = params.intent && typeof params.intent === "object" ? params.intent : params;
507
+ const action = candidate.action;
508
+ const query = readNullableString$3(candidate.query);
509
+ if (action === "list") return {
510
+ action,
511
+ query
512
+ };
513
+ if (action === "search" && query) return {
514
+ action,
515
+ query
516
+ };
517
+ return null;
518
+ }
519
+ async function classifyIntent$4(runtime, text) {
520
+ const prompt = `Analyze the user message and determine what customer action they want.
521
+ Respond with JSON only in one of these shapes:
522
+ {"action":"list","query":null}
523
+
524
+ {"action":"search","query":"customer name, email, or other search term"}
525
+
526
+ User message: "${text}"
527
+ `;
528
+ for (let i = 0; i < 2; i++) {
529
+ const parsed = parseJsonObject(await runtime.useModel(ModelType.TEXT_SMALL, { prompt }));
530
+ if (parsed?.action) return readCustomerIntent(parsed);
531
+ }
532
+ return null;
533
+ }
534
+ async function manageCustomersHandler(runtime, message, _state, options, callback) {
535
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
536
+ if (!svc?.isConnected()) {
537
+ await callback?.({ text: "Shopify is not connected. Please check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN." });
538
+ return {
539
+ success: false,
540
+ error: "Shopify not connected"
541
+ };
542
+ }
543
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
544
+ const intent = readCustomerIntent(options) ?? await classifyIntent$4(runtime, text);
545
+ if (!intent) {
546
+ await callback?.({ text: "I couldn't determine what customer action you want. Try: list customers or search for a specific customer." });
547
+ return {
548
+ success: false,
549
+ error: "Could not classify intent"
550
+ };
551
+ }
552
+ try {
553
+ const queryStr = intent.action === "search" ? intent.query : intent.query ?? void 0;
554
+ const result = await svc.listCustomers({
555
+ query: queryStr,
556
+ first: 15
557
+ });
558
+ if (result.customers.length === 0) {
559
+ const msg = queryStr ? `No customers found matching "${queryStr}".` : "No customers found in the store.";
560
+ await callback?.({ text: msg });
561
+ return {
562
+ success: true,
563
+ text: "No customers found"
564
+ };
565
+ }
566
+ const lines = result.customers.map(formatCustomer);
567
+ const more = result.hasNextPage ? "\n\n(More customers available)" : "";
568
+ if (intent.action === "search" && result.customers.length === 1) {
569
+ const c = result.customers[0];
570
+ const detail = [
571
+ `**${c.displayName}**`,
572
+ `Email: ${c.email ?? "not set"}`,
573
+ `Phone: ${c.phone ?? "not set"}`,
574
+ `Orders: ${c.ordersCount}`,
575
+ `Total spent: ${c.totalSpentV2.amount} ${c.totalSpentV2.currencyCode}`,
576
+ `Customer since: ${c.createdAt.slice(0, 10)}`
577
+ ].join("\n");
578
+ await callback?.({ text: detail });
579
+ } else await callback?.({ text: `Customers (${result.customers.length}):\n\n${lines.join("\n")}${more}` });
580
+ return {
581
+ success: true,
582
+ data: { customers: result.customers }
583
+ };
584
+ } catch (err) {
585
+ const msg = err instanceof Error ? err.message : String(err);
586
+ logger.error({
587
+ src: "plugin:shopify:manage-customers",
588
+ error: msg
589
+ }, "Customer action failed");
590
+ await callback?.({ text: `Shopify customer operation failed: ${msg}` });
591
+ return {
592
+ success: false,
593
+ error: msg
594
+ };
595
+ }
596
+ }
597
+ //#endregion
598
+ //#region src/actions/manage-inventory.ts
599
+ function formatInventoryLevel(level) {
600
+ const qty = level.available !== null ? String(level.available) : "untracked";
601
+ return `- ${level.location.name}: ${qty} available`;
602
+ }
603
+ function formatLocation(loc) {
604
+ return `- ${loc.name} (${loc.isActive ? "active" : "inactive"})`;
605
+ }
606
+ function readNullableString$2(value) {
607
+ return typeof value === "string" && value.trim().length > 0 && value.trim().toLowerCase() !== "null" ? value.trim() : null;
608
+ }
609
+ function readInventoryIntent(options) {
610
+ const params = getActionOptions(options);
611
+ const candidate = params.intent && typeof params.intent === "object" ? params.intent : params;
612
+ const action = candidate.action;
613
+ if (action === "locations") return { action };
614
+ if (action === "check") {
615
+ const productQuery = readNullableString$2(candidate.productQuery);
616
+ return productQuery ? {
617
+ action,
618
+ productQuery
619
+ } : null;
620
+ }
621
+ if (action === "adjust") {
622
+ const productQuery = readNullableString$2(candidate.productQuery);
623
+ const delta = typeof candidate.delta === "number" ? candidate.delta : Number.parseInt(String(candidate.delta ?? ""), 10);
624
+ if (!productQuery || !Number.isFinite(delta)) return null;
625
+ return {
626
+ action,
627
+ productQuery,
628
+ delta,
629
+ reason: readNullableString$2(candidate.reason)
630
+ };
631
+ }
632
+ return null;
633
+ }
634
+ async function classifyIntent$3(runtime, text) {
635
+ const prompt = `Analyze the user message and determine what inventory action they want.
636
+ Respond with JSON only in one of these shapes:
637
+ {"action":"check","productQuery":"product name or SKU to check"}
638
+
639
+ {"action":"adjust","productQuery":"product name","delta":5,"reason":"reason"}
640
+
641
+ {"action":"locations"}
642
+
643
+ For adjust, delta is positive to add stock and negative to remove stock.
644
+
645
+ User message: "${text}"
646
+ `;
647
+ for (let i = 0; i < 2; i++) {
648
+ const parsed = parseJsonObject(await runtime.useModel(ModelType.TEXT_SMALL, { prompt }));
649
+ if (parsed?.action) return readInventoryIntent(parsed);
650
+ }
651
+ return null;
652
+ }
653
+ async function manageInventoryHandler(runtime, message, _state, options, callback) {
654
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
655
+ if (!svc?.isConnected()) {
656
+ await callback?.({ text: "Shopify is not connected. Please check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN." });
657
+ return {
658
+ success: false,
659
+ error: "Shopify not connected"
660
+ };
661
+ }
662
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
663
+ const intent = readInventoryIntent(options) ?? await classifyIntent$3(runtime, text);
664
+ if (!intent) {
665
+ await callback?.({ text: "I couldn't determine what inventory action you want. Try: check stock, adjust inventory, or list locations." });
666
+ return {
667
+ success: false,
668
+ error: "Could not classify intent"
669
+ };
670
+ }
671
+ try {
672
+ if (intent.action === "locations") {
673
+ const locations = await svc.listLocations();
674
+ if (locations.length === 0) {
675
+ await callback?.({ text: "No locations found in the store." });
676
+ return {
677
+ success: true,
678
+ text: "No locations"
679
+ };
680
+ }
681
+ await callback?.({ text: `Store locations:\n\n${locations.map(formatLocation).join("\n")}` });
682
+ return {
683
+ success: true,
684
+ data: { locations }
685
+ };
686
+ }
687
+ if (intent.action === "check") {
688
+ const result = await svc.listProducts({
689
+ query: intent.productQuery,
690
+ first: 3
691
+ });
692
+ if (result.products.length === 0) {
693
+ await callback?.({ text: `No product found matching "${intent.productQuery}".` });
694
+ return {
695
+ success: false,
696
+ error: "Product not found"
697
+ };
698
+ }
699
+ const product = result.products[0];
700
+ const firstVariant = product.variants.edges[0]?.node;
701
+ if (!firstVariant) {
702
+ await callback?.({ text: `Product "${product.title}" has no variants.` });
703
+ return {
704
+ success: false,
705
+ error: "No variants"
706
+ };
707
+ }
708
+ const inventoryItemId = `gid://shopify/InventoryItem/${firstVariant.id.split("/").pop()}`;
709
+ const levels = await svc.checkInventory(inventoryItemId);
710
+ if (levels.length === 0) {
711
+ await callback?.({ text: `No inventory tracking found for "${product.title}".` });
712
+ return {
713
+ success: true,
714
+ text: "No inventory tracking"
715
+ };
716
+ }
717
+ await callback?.({ text: `Inventory for **${product.title}** (${firstVariant.title}):\n\n${levels.map(formatInventoryLevel).join("\n")}` });
718
+ return {
719
+ success: true,
720
+ data: {
721
+ product: product.title,
722
+ levels
723
+ }
724
+ };
725
+ }
726
+ if (intent.action === "adjust") {
727
+ const result = await svc.listProducts({
728
+ query: intent.productQuery,
729
+ first: 3
730
+ });
731
+ if (result.products.length === 0) {
732
+ await callback?.({ text: `No product found matching "${intent.productQuery}".` });
733
+ return {
734
+ success: false,
735
+ error: "Product not found"
736
+ };
737
+ }
738
+ const product = result.products[0];
739
+ const firstVariant = product.variants.edges[0]?.node;
740
+ if (!firstVariant) {
741
+ await callback?.({ text: `Product "${product.title}" has no variants.` });
742
+ return {
743
+ success: false,
744
+ error: "No variants"
745
+ };
746
+ }
747
+ const inventoryItemId = `gid://shopify/InventoryItem/${firstVariant.id.split("/").pop()}`;
748
+ const levels = await svc.checkInventory(inventoryItemId);
749
+ const locationId = levels[0]?.location.id ?? (await svc.listLocations())[0]?.id;
750
+ const locationName = levels[0]?.location.name ?? "first active location";
751
+ if (!locationId) {
752
+ await callback?.({ text: "No locations found in the store to adjust inventory against." });
753
+ return {
754
+ success: false,
755
+ error: "No locations"
756
+ };
757
+ }
758
+ const sign = intent.delta >= 0 ? "+" : "";
759
+ const preview = [
760
+ "Confirmation required before adjusting Shopify inventory:",
761
+ `Product: ${product.title}`,
762
+ `Variant: ${firstVariant.title}`,
763
+ `Location: ${locationName}`,
764
+ `Adjustment: ${sign}${intent.delta} units`,
765
+ `Reason: ${intent.reason ?? "correction"}`
766
+ ].join("\n");
767
+ if (!isConfirmed(options)) {
768
+ await callback?.({ text: preview });
769
+ return confirmationRequired(preview, {
770
+ intent,
771
+ productId: product.id,
772
+ inventoryItemId,
773
+ locationId
774
+ });
775
+ }
776
+ await svc.adjustInventory({
777
+ inventoryItemId,
778
+ locationId,
779
+ delta: intent.delta,
780
+ reason: intent.reason ?? "correction"
781
+ });
782
+ await callback?.({ text: `Inventory adjusted for **${product.title}**: ${sign}${intent.delta} units.` });
783
+ return {
784
+ success: true,
785
+ data: {
786
+ product: product.title,
787
+ delta: intent.delta
788
+ }
789
+ };
790
+ }
791
+ await callback?.({ text: "Unsupported inventory action." });
792
+ return {
793
+ success: false,
794
+ error: "Unknown action"
795
+ };
796
+ } catch (err) {
797
+ const msg = err instanceof Error ? err.message : String(err);
798
+ logger.error({
799
+ src: "plugin:shopify:manage-inventory",
800
+ error: msg
801
+ }, "Inventory action failed");
802
+ await callback?.({ text: `Shopify inventory operation failed: ${msg}` });
803
+ return {
804
+ success: false,
805
+ error: msg
806
+ };
807
+ }
808
+ }
809
+ //#endregion
810
+ //#region src/actions/manage-orders.ts
811
+ function formatOrder(o) {
812
+ const total = o.totalPriceSet.shopMoney;
813
+ const items = o.lineItems.edges.map((e) => `${e.node.title} x${e.node.quantity}`).join(", ");
814
+ const customer = o.customer?.displayName ?? "Guest";
815
+ return `- **${o.name}** | ${total.amount} ${total.currencyCode} | ${o.displayFulfillmentStatus} | ${customer} | Items: ${items} | ${o.createdAt.slice(0, 10)}`;
816
+ }
817
+ function readNullableString$1(value) {
818
+ return typeof value === "string" && value.trim().length > 0 && value.trim().toLowerCase() !== "null" ? value.trim() : null;
819
+ }
820
+ function readOrderIntent(options) {
821
+ const params = getActionOptions(options);
822
+ const candidate = params.intent && typeof params.intent === "object" ? params.intent : params;
823
+ const action = candidate.action;
824
+ if (action === "list") return {
825
+ action,
826
+ query: readNullableString$1(candidate.query)
827
+ };
828
+ if (action === "get" || action === "fulfill") {
829
+ const orderName = readNullableString$1(candidate.orderName);
830
+ return orderName ? {
831
+ action,
832
+ orderName
833
+ } : null;
834
+ }
835
+ return null;
836
+ }
837
+ async function classifyIntent$2(runtime, text) {
838
+ const prompt = `Analyze the user message and determine what order action they want.
839
+ Respond with JSON only in one of these shapes:
840
+ {"action":"list","query":"optional filter like unfulfilled or last week"}
841
+
842
+ {"action":"get","orderName":"order number like #1001 or 1001"}
843
+
844
+ {"action":"fulfill","orderName":"order number to fulfill"}
845
+
846
+ User message: "${text}"
847
+ `;
848
+ for (let i = 0; i < 2; i++) {
849
+ const parsed = parseJsonObject(await runtime.useModel(ModelType.TEXT_SMALL, { prompt }));
850
+ if (parsed?.action) return readOrderIntent(parsed);
851
+ }
852
+ return null;
853
+ }
854
+ async function manageOrdersHandler(runtime, message, _state, options, callback) {
855
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
856
+ if (!svc?.isConnected()) {
857
+ await callback?.({ text: "Shopify is not connected. Please check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN." });
858
+ return {
859
+ success: false,
860
+ error: "Shopify not connected"
861
+ };
862
+ }
863
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
864
+ const intent = readOrderIntent(options) ?? await classifyIntent$2(runtime, text);
865
+ if (!intent) {
866
+ await callback?.({ text: "I couldn't determine what order action you want. Try: list orders, check order status, or fulfill an order." });
867
+ return {
868
+ success: false,
869
+ error: "Could not classify intent"
870
+ };
871
+ }
872
+ try {
873
+ if (intent.action === "list") {
874
+ const queryStr = intent.query ?? void 0;
875
+ const result = await svc.listOrders({
876
+ query: queryStr,
877
+ first: 10
878
+ });
879
+ if (result.orders.length === 0) {
880
+ await callback?.({ text: queryStr ? `No orders found matching "${queryStr}".` : "No orders found." });
881
+ return {
882
+ success: true,
883
+ text: "No orders found"
884
+ };
885
+ }
886
+ const lines = result.orders.map(formatOrder);
887
+ const more = result.hasNextPage ? "\n\n(More orders available)" : "";
888
+ await callback?.({ text: `Recent orders (${result.orders.length}):\n\n${lines.join("\n")}${more}` });
889
+ return {
890
+ success: true,
891
+ data: { orders: result.orders }
892
+ };
893
+ }
894
+ if (intent.action === "get") {
895
+ const cleanName = intent.orderName.replace(/^#/, "").trim();
896
+ const result = await svc.listOrders({
897
+ query: `name:#${cleanName}`,
898
+ first: 1
899
+ });
900
+ if (result.orders.length === 0) {
901
+ await callback?.({ text: `Order #${cleanName} not found.` });
902
+ return {
903
+ success: false,
904
+ error: "Order not found"
905
+ };
906
+ }
907
+ const order = result.orders[0];
908
+ const total = order.totalPriceSet.shopMoney;
909
+ const lineItems = order.lineItems.edges.map((e) => ` - ${e.node.title} x${e.node.quantity} (${e.node.originalUnitPriceSet.shopMoney.amount} ${e.node.originalUnitPriceSet.shopMoney.currencyCode})`);
910
+ const detail = [
911
+ `**Order ${order.name}**`,
912
+ `Status: ${order.displayFulfillmentStatus} | Payment: ${order.displayFinancialStatus ?? "n/a"}`,
913
+ `Total: ${total.amount} ${total.currencyCode}`,
914
+ `Customer: ${order.customer?.displayName ?? "Guest"}`,
915
+ `Created: ${order.createdAt.slice(0, 10)}`,
916
+ `Items:`,
917
+ ...lineItems
918
+ ].join("\n");
919
+ await callback?.({ text: detail });
920
+ return {
921
+ success: true,
922
+ data: { order }
923
+ };
924
+ }
925
+ if (intent.action === "fulfill") {
926
+ const cleanName = intent.orderName.replace(/^#/, "").trim();
927
+ const result = await svc.listOrders({
928
+ query: `name:#${cleanName}`,
929
+ first: 1
930
+ });
931
+ if (result.orders.length === 0) {
932
+ await callback?.({ text: `Order #${cleanName} not found.` });
933
+ return {
934
+ success: false,
935
+ error: "Order not found"
936
+ };
937
+ }
938
+ const order = result.orders[0];
939
+ const preview = [
940
+ "Confirmation required before fulfilling Shopify order:",
941
+ `Order: ${order.name}`,
942
+ `Status: ${order.displayFulfillmentStatus}`,
943
+ `Customer: ${order.customer?.displayName ?? "Guest"}`
944
+ ].join("\n");
945
+ if (!isConfirmed(options)) {
946
+ await callback?.({ text: preview });
947
+ return confirmationRequired(preview, {
948
+ intent,
949
+ orderId: order.id
950
+ });
951
+ }
952
+ const fulfillment = await svc.fulfillOrder(order.id);
953
+ await callback?.({ text: `Order ${order.name} fulfilled (status: ${fulfillment.status}).` });
954
+ return {
955
+ success: true,
956
+ data: {
957
+ order: order.name,
958
+ fulfillment
959
+ }
960
+ };
961
+ }
962
+ await callback?.({ text: "Unsupported order action." });
963
+ return {
964
+ success: false,
965
+ error: "Unknown action"
966
+ };
967
+ } catch (err) {
968
+ const msg = err instanceof Error ? err.message : String(err);
969
+ logger.error({
970
+ src: "plugin:shopify:manage-orders",
971
+ error: msg
972
+ }, "Order action failed");
973
+ await callback?.({ text: `Shopify order operation failed: ${msg}` });
974
+ return {
975
+ success: false,
976
+ error: msg
977
+ };
978
+ }
979
+ }
980
+ //#endregion
981
+ //#region src/actions/manage-products.ts
982
+ function formatProduct(p) {
983
+ const variants = p.variants.edges.map((e) => e.node);
984
+ const priceRange = variants.length > 0 ? variants.map((v) => v.price).join(", ") : "n/a";
985
+ const inventory = p.totalInventory !== null ? String(p.totalInventory) : "untracked";
986
+ return `- **${p.title}** (${p.status}) | Price: ${priceRange} | Inventory: ${inventory} | Handle: ${p.handle}`;
987
+ }
988
+ function readNullableString(value) {
989
+ return typeof value === "string" && value.trim().length > 0 && value.trim().toLowerCase() !== "null" ? value.trim() : null;
990
+ }
991
+ function readProductIntent(options) {
992
+ const params = getActionOptions(options);
993
+ const candidate = params.intent && typeof params.intent === "object" ? params.intent : params;
994
+ const action = candidate.action;
995
+ if (action === "list") return {
996
+ action,
997
+ query: readNullableString(candidate.query)
998
+ };
999
+ if (action === "create") {
1000
+ const title = readNullableString(candidate.title);
1001
+ if (!title) return null;
1002
+ return {
1003
+ action,
1004
+ title,
1005
+ description: readNullableString(candidate.description),
1006
+ productType: readNullableString(candidate.productType),
1007
+ vendor: readNullableString(candidate.vendor),
1008
+ status: readNullableString(candidate.status)
1009
+ };
1010
+ }
1011
+ if (action === "update") {
1012
+ const identifier = readNullableString(candidate.identifier);
1013
+ if (!identifier) return null;
1014
+ return {
1015
+ action,
1016
+ identifier,
1017
+ title: readNullableString(candidate.title),
1018
+ description: readNullableString(candidate.description),
1019
+ status: readNullableString(candidate.status)
1020
+ };
1021
+ }
1022
+ return null;
1023
+ }
1024
+ async function classifyIntent$1(runtime, text) {
1025
+ const prompt = `Analyze the user message and determine what product action they want.
1026
+ Respond with JSON only in one of these shapes:
1027
+ {"action":"list","query":"search term"}
1028
+
1029
+ {"action":"create","title":"product title","description":"description","productType":"type","vendor":"vendor","status":"ACTIVE"}
1030
+
1031
+ {"action":"update","identifier":"product title or handle to find","title":"new title","description":"new description","status":"ACTIVE"}
1032
+
1033
+ User message: "${text}"
1034
+ `;
1035
+ for (let i = 0; i < 2; i++) {
1036
+ const parsed = parseJsonObject(await runtime.useModel(ModelType.TEXT_SMALL, { prompt }));
1037
+ if (parsed?.action) return readProductIntent(parsed);
1038
+ }
1039
+ return null;
1040
+ }
1041
+ async function manageProductsHandler(runtime, message, _state, options, callback) {
1042
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
1043
+ if (!svc?.isConnected()) {
1044
+ await callback?.({ text: "Shopify is not connected. Please check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN." });
1045
+ return {
1046
+ success: false,
1047
+ error: "Shopify not connected"
1048
+ };
1049
+ }
1050
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
1051
+ const intent = readProductIntent(options) ?? await classifyIntent$1(runtime, text);
1052
+ if (!intent) {
1053
+ await callback?.({ text: "I couldn't determine what product action you want. You can ask me to list, create, or update products." });
1054
+ return {
1055
+ success: false,
1056
+ error: "Could not classify intent"
1057
+ };
1058
+ }
1059
+ try {
1060
+ if (intent.action === "list") {
1061
+ const result = await svc.listProducts({
1062
+ query: intent.query,
1063
+ first: 10
1064
+ });
1065
+ if (result.products.length === 0) {
1066
+ await callback?.({ text: intent.query ? `No products found matching "${intent.query}".` : "The store has no products yet." });
1067
+ return {
1068
+ success: true,
1069
+ text: "No products found"
1070
+ };
1071
+ }
1072
+ const lines = result.products.map(formatProduct);
1073
+ const more = result.hasNextPage ? "\n\n(More products available -- ask to see more)" : "";
1074
+ await callback?.({ text: `Found ${result.products.length} product(s):\n\n${lines.join("\n")}${more}` });
1075
+ return {
1076
+ success: true,
1077
+ data: { products: result.products }
1078
+ };
1079
+ }
1080
+ if (intent.action === "create") {
1081
+ const status = intent.status ?? "DRAFT";
1082
+ const preview = [
1083
+ "Confirmation required before creating Shopify product:",
1084
+ `Title: ${intent.title}`,
1085
+ `Status: ${status}`,
1086
+ intent.vendor ? `Vendor: ${intent.vendor}` : null,
1087
+ intent.productType ? `Type: ${intent.productType}` : null
1088
+ ].filter((line) => line !== null).join("\n");
1089
+ if (!isConfirmed(options)) {
1090
+ await callback?.({ text: preview });
1091
+ return confirmationRequired(preview, { intent });
1092
+ }
1093
+ const product = await svc.createProduct({
1094
+ title: intent.title,
1095
+ descriptionHtml: intent.description ?? void 0,
1096
+ productType: intent.productType ?? void 0,
1097
+ vendor: intent.vendor ?? void 0,
1098
+ status
1099
+ });
1100
+ await callback?.({ text: `Product created: ${product.title} (${product.status}).` });
1101
+ return {
1102
+ success: true,
1103
+ data: { product }
1104
+ };
1105
+ }
1106
+ if (intent.action === "update") {
1107
+ const searchResult = await svc.listProducts({
1108
+ query: intent.identifier,
1109
+ first: 5
1110
+ });
1111
+ if (searchResult.products.length === 0) {
1112
+ await callback?.({ text: `Could not find a product matching "${intent.identifier}".` });
1113
+ return {
1114
+ success: false,
1115
+ error: "Product not found"
1116
+ };
1117
+ }
1118
+ const target = searchResult.products[0];
1119
+ const updateInput = {};
1120
+ if (intent.title) updateInput.title = intent.title;
1121
+ if (intent.description) updateInput.descriptionHtml = intent.description;
1122
+ if (intent.status) updateInput.status = intent.status.toUpperCase();
1123
+ const changeLines = [
1124
+ intent.title ? `Title: ${intent.title}` : null,
1125
+ intent.description ? "Description: updated" : null,
1126
+ intent.status ? `Status: ${intent.status.toUpperCase()}` : null
1127
+ ].filter((line) => line !== null);
1128
+ const preview = [
1129
+ "Confirmation required before updating Shopify product:",
1130
+ `Product: ${target.title}`,
1131
+ ...changeLines
1132
+ ].join("\n");
1133
+ if (!isConfirmed(options)) {
1134
+ await callback?.({ text: preview });
1135
+ return confirmationRequired(preview, {
1136
+ intent,
1137
+ productId: target.id
1138
+ });
1139
+ }
1140
+ const updated = await svc.updateProduct(target.id, updateInput);
1141
+ await callback?.({ text: `Product updated: ${updated.title} (${updated.status}).` });
1142
+ return {
1143
+ success: true,
1144
+ data: { product: updated }
1145
+ };
1146
+ }
1147
+ await callback?.({ text: "Unsupported product action." });
1148
+ return {
1149
+ success: false,
1150
+ error: "Unknown action"
1151
+ };
1152
+ } catch (err) {
1153
+ const msg = err instanceof Error ? err.message : String(err);
1154
+ logger.error({
1155
+ src: "plugin:shopify:manage-products",
1156
+ error: msg
1157
+ }, "Product action failed");
1158
+ await callback?.({ text: `Shopify product operation failed: ${msg}` });
1159
+ return {
1160
+ success: false,
1161
+ error: msg
1162
+ };
1163
+ }
1164
+ }
1165
+ //#endregion
1166
+ //#region src/actions/search-store.ts
1167
+ function formatProductBrief(p) {
1168
+ const price = p.variants.edges[0]?.node.price ?? "n/a";
1169
+ return `[Product] **${p.title}** -- ${p.status} -- ${price}`;
1170
+ }
1171
+ function formatOrderBrief(o) {
1172
+ const total = o.totalPriceSet.shopMoney;
1173
+ return `[Order] **${o.name}** -- ${total.amount} ${total.currencyCode} -- ${o.displayFulfillmentStatus}`;
1174
+ }
1175
+ function formatCustomerBrief(c) {
1176
+ return `[Customer] **${c.displayName}** -- ${c.email ?? "no email"} -- ${c.ordersCount} orders`;
1177
+ }
1178
+ function readSearchStoreParams(options) {
1179
+ const params = options?.parameters ?? {};
1180
+ const query = typeof params.query === "string" && params.query.trim().length > 0 ? params.query.trim() : null;
1181
+ const scope = params.scope === "products" || params.scope === "orders" || params.scope === "customers" || params.scope === "all" ? params.scope : "all";
1182
+ const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : 5;
1183
+ return {
1184
+ intent: query ? {
1185
+ query,
1186
+ scope
1187
+ } : null,
1188
+ limit
1189
+ };
1190
+ }
1191
+ async function classifyIntent(runtime, text) {
1192
+ const prompt = `Analyze the user message and determine what they want to search for in a Shopify store.
1193
+ Respond with JSON only:
1194
+ {"query":"the search term","scope":"all"}
1195
+
1196
+ Use "all" when the user does not specify a specific category, or mentions multiple.
1197
+
1198
+ User message: "${text}"
1199
+ `;
1200
+ for (let i = 0; i < 2; i++) {
1201
+ const parsed = parseJsonObject(await runtime.useModel(ModelType.TEXT_SMALL, { prompt }));
1202
+ const query = typeof parsed?.query === "string" && parsed.query.trim().length > 0 ? parsed.query.trim() : null;
1203
+ const scope = parsed?.scope === "products" || parsed?.scope === "orders" || parsed?.scope === "customers" || parsed?.scope === "all" ? parsed.scope : "all";
1204
+ if (query) return {
1205
+ query,
1206
+ scope
1207
+ };
1208
+ }
1209
+ return null;
1210
+ }
1211
+ async function searchStoreHandler(runtime, message, _state, _options, callback) {
1212
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
1213
+ const accountId = getShopifyAccountId(runtime, _options);
1214
+ if (!svc?.isConnected(accountId)) {
1215
+ await callback?.({ text: "Shopify is not connected. Please check SHOPIFY_STORE_DOMAIN and SHOPIFY_ACCESS_TOKEN." });
1216
+ return {
1217
+ success: false,
1218
+ error: "Shopify not connected"
1219
+ };
1220
+ }
1221
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
1222
+ const structured = readSearchStoreParams(_options);
1223
+ const intent = structured.intent ?? await classifyIntent(runtime, text);
1224
+ if (!intent) {
1225
+ await callback?.({ text: "I couldn't determine what to search for. Please provide a search term." });
1226
+ return {
1227
+ success: false,
1228
+ error: "Could not classify intent"
1229
+ };
1230
+ }
1231
+ try {
1232
+ const sections = [];
1233
+ const data = {};
1234
+ if (intent.scope === "all" || intent.scope === "products") {
1235
+ const result = await svc.listProducts({
1236
+ query: intent.query,
1237
+ first: structured.limit
1238
+ }, accountId);
1239
+ if (result.products.length > 0) {
1240
+ sections.push(`**Products** (${result.products.length}):\n${result.products.map(formatProductBrief).join("\n")}`);
1241
+ data.products = result.products;
1242
+ }
1243
+ }
1244
+ if (intent.scope === "all" || intent.scope === "orders") {
1245
+ const result = await svc.listOrders({
1246
+ query: intent.query,
1247
+ first: structured.limit
1248
+ }, accountId);
1249
+ if (result.orders.length > 0) {
1250
+ sections.push(`**Orders** (${result.orders.length}):\n${result.orders.map(formatOrderBrief).join("\n")}`);
1251
+ data.orders = result.orders;
1252
+ }
1253
+ }
1254
+ if (intent.scope === "all" || intent.scope === "customers") {
1255
+ const result = await svc.listCustomers({
1256
+ query: intent.query,
1257
+ first: structured.limit
1258
+ }, accountId);
1259
+ if (result.customers.length > 0) {
1260
+ sections.push(`**Customers** (${result.customers.length}):\n${result.customers.map(formatCustomerBrief).join("\n")}`);
1261
+ data.customers = result.customers;
1262
+ }
1263
+ }
1264
+ if (sections.length === 0) {
1265
+ await callback?.({ text: `No results found for "${intent.query}" in the store.` });
1266
+ return {
1267
+ success: true,
1268
+ text: "No results"
1269
+ };
1270
+ }
1271
+ await callback?.({ text: `Search results for "${intent.query}":\n\n${sections.join("\n\n")}` });
1272
+ return {
1273
+ success: true,
1274
+ data
1275
+ };
1276
+ } catch (err) {
1277
+ const msg = err instanceof Error ? err.message : String(err);
1278
+ logger.error({
1279
+ src: "plugin:shopify:search-store",
1280
+ error: msg
1281
+ }, "Store search failed");
1282
+ await callback?.({ text: `Shopify search failed: ${msg}` });
1283
+ return {
1284
+ success: false,
1285
+ error: msg
1286
+ };
1287
+ }
1288
+ }
1289
+ //#endregion
1290
+ //#region src/actions/shopify.ts
1291
+ const ALL_OPS = [
1292
+ "search",
1293
+ "products",
1294
+ "inventory",
1295
+ "orders",
1296
+ "customers"
1297
+ ];
1298
+ const ROUTES = [
1299
+ {
1300
+ op: "search",
1301
+ handler: searchStoreHandler,
1302
+ match: /\b(search|find|browse|look\s+up|catalog|store search)\b/i
1303
+ },
1304
+ {
1305
+ op: "inventory",
1306
+ handler: manageInventoryHandler,
1307
+ match: /\b(inventory|stock|quantity|on hand|in stock|out of stock|restock)\b/i
1308
+ },
1309
+ {
1310
+ op: "customers",
1311
+ handler: manageCustomersHandler,
1312
+ match: /\b(customer|buyer|shopper|client)s?\b/i
1313
+ },
1314
+ {
1315
+ op: "orders",
1316
+ handler: manageOrdersHandler,
1317
+ match: /\b(order|fulfill|ship|refund|return)s?\b/i
1318
+ },
1319
+ {
1320
+ op: "products",
1321
+ handler: manageProductsHandler,
1322
+ match: /\b(product|sku|variant|listing|item)s?\b/i
1323
+ }
1324
+ ];
1325
+ function readOptions(options) {
1326
+ const direct = options ?? {};
1327
+ const parameters = direct.parameters && typeof direct.parameters === "object" ? direct.parameters : {};
1328
+ return {
1329
+ ...direct,
1330
+ ...parameters
1331
+ };
1332
+ }
1333
+ function normalizeOp(value) {
1334
+ if (typeof value !== "string") return null;
1335
+ const trimmed = value.trim().toLowerCase();
1336
+ return ALL_OPS.includes(trimmed) ? trimmed : null;
1337
+ }
1338
+ function selectRoute(message, options) {
1339
+ const opts = readOptions(options);
1340
+ const requested = normalizeOp(opts.action ?? opts.op ?? opts.entity ?? opts.subaction);
1341
+ if (requested) {
1342
+ const route = ROUTES.find((candidate) => candidate.op === requested);
1343
+ if (route) return route;
1344
+ }
1345
+ const text = typeof message.content?.text === "string" ? message.content.text : "";
1346
+ return ROUTES.find((route) => route.match.test(text)) ?? null;
1347
+ }
1348
+ const shopifyAction = {
1349
+ name: "SHOPIFY",
1350
+ description: "Manage a Shopify store. Actions: search (read-only catalog browsing across products, orders, and customers), products (CRUD on products), inventory (stock adjustments), orders (list/update orders), customers (CRUD on customers). Action is inferred from the message text when not explicitly provided.",
1351
+ descriptionCompressed: "Shopify: search, products, inventory, orders, customers.",
1352
+ similes: [
1353
+ "MANAGE_SHOPIFY_PRODUCTS",
1354
+ "MANAGE_SHOPIFY_INVENTORY",
1355
+ "MANAGE_SHOPIFY_ORDERS",
1356
+ "MANAGE_SHOPIFY_CUSTOMERS",
1357
+ "LIST_PRODUCTS",
1358
+ "CREATE_PRODUCT",
1359
+ "UPDATE_PRODUCT",
1360
+ "SEARCH_PRODUCTS",
1361
+ "CHECK_INVENTORY",
1362
+ "ADJUST_INVENTORY",
1363
+ "CHECK_STOCK",
1364
+ "UPDATE_STOCK",
1365
+ "LIST_ORDERS",
1366
+ "CHECK_ORDERS",
1367
+ "FULFILL_ORDER",
1368
+ "ORDER_STATUS",
1369
+ "LIST_CUSTOMERS",
1370
+ "FIND_CUSTOMER",
1371
+ "SEARCH_CUSTOMERS"
1372
+ ],
1373
+ contexts: [
1374
+ "payments",
1375
+ "connectors",
1376
+ "automation",
1377
+ "knowledge"
1378
+ ],
1379
+ contextGate: { anyOf: [
1380
+ "payments",
1381
+ "connectors",
1382
+ "automation",
1383
+ "knowledge"
1384
+ ] },
1385
+ roleGate: { minRole: "USER" },
1386
+ parameters: [
1387
+ {
1388
+ name: "action",
1389
+ description: "Operation to perform. One of: search, products, inventory, orders, customers. Inferred from message text when omitted.",
1390
+ required: false,
1391
+ schema: {
1392
+ type: "string",
1393
+ enum: [...ALL_OPS]
1394
+ }
1395
+ },
1396
+ {
1397
+ name: "subaction",
1398
+ description: "Legacy alias for action.",
1399
+ required: false,
1400
+ schema: { type: "string" }
1401
+ },
1402
+ {
1403
+ name: "query",
1404
+ description: "Search term for action=search.",
1405
+ required: false,
1406
+ schema: { type: "string" }
1407
+ },
1408
+ {
1409
+ name: "scope",
1410
+ description: "Search scope for action=search: all, products, orders, or customers.",
1411
+ required: false,
1412
+ schema: {
1413
+ type: "string",
1414
+ enum: [
1415
+ "all",
1416
+ "products",
1417
+ "orders",
1418
+ "customers"
1419
+ ]
1420
+ }
1421
+ },
1422
+ {
1423
+ name: "limit",
1424
+ description: "Maximum results per searched Shopify category.",
1425
+ required: false,
1426
+ schema: { type: "number" }
1427
+ },
1428
+ shopifyAccountIdParameter
1429
+ ],
1430
+ validate: async (runtime) => {
1431
+ if (!hasShopifyConfig(runtime)) return false;
1432
+ return true;
1433
+ },
1434
+ handler: async (runtime, message, state, options, callback) => {
1435
+ const route = selectRoute(message, options);
1436
+ if (!route) {
1437
+ const ops = ALL_OPS.join(", ");
1438
+ const text = `SHOPIFY could not determine the operation. Specify one of: ${ops}.`;
1439
+ await callback?.({
1440
+ text,
1441
+ source: message.content?.source
1442
+ });
1443
+ return {
1444
+ success: false,
1445
+ text,
1446
+ values: { error: "MISSING" },
1447
+ data: {
1448
+ actionName: "SHOPIFY",
1449
+ availableOps: ops
1450
+ }
1451
+ };
1452
+ }
1453
+ const result = await route.handler(runtime, message, state, options, callback) ?? { success: true };
1454
+ return {
1455
+ ...result,
1456
+ data: {
1457
+ ...typeof result.data === "object" && result.data ? result.data : {},
1458
+ actionName: "SHOPIFY",
1459
+ action: route.op,
1460
+ op: route.op
1461
+ }
1462
+ };
1463
+ },
1464
+ examples: [
1465
+ [{
1466
+ name: "{{user1}}",
1467
+ content: { text: "Show me my Shopify orders from this week" }
1468
+ }, {
1469
+ name: "{{agentName}}",
1470
+ content: {
1471
+ text: "Pulling recent Shopify orders.",
1472
+ actions: ["SHOPIFY"]
1473
+ }
1474
+ }],
1475
+ [{
1476
+ name: "{{user1}}",
1477
+ content: { text: "Search my Shopify store for hat" }
1478
+ }, {
1479
+ name: "{{agentName}}",
1480
+ content: {
1481
+ text: "Searching the Shopify store.",
1482
+ actions: ["SHOPIFY"]
1483
+ }
1484
+ }],
1485
+ [{
1486
+ name: "{{user1}}",
1487
+ content: { text: "Adjust inventory for SKU ABC-123 to 50 units" }
1488
+ }, {
1489
+ name: "{{agentName}}",
1490
+ content: {
1491
+ text: "Updating inventory.",
1492
+ actions: ["SHOPIFY"]
1493
+ }
1494
+ }],
1495
+ [{
1496
+ name: "{{user1}}",
1497
+ content: { text: "Create a new product: red t-shirt, $25" }
1498
+ }, {
1499
+ name: "{{agentName}}",
1500
+ content: {
1501
+ text: "Creating that product.",
1502
+ actions: ["SHOPIFY"]
1503
+ }
1504
+ }]
1505
+ ]
1506
+ };
1507
+ //#endregion
1508
+ //#region src/connector-account-provider.ts
1509
+ /**
1510
+ * Shopify ConnectorAccountManager provider.
1511
+ *
1512
+ * Bridges plugin-shopify to the @elizaos/core ConnectorAccountManager so the
1513
+ * generic HTTP CRUD + OAuth surface can list, create, patch, delete, and run
1514
+ * the OAuth flow for Shopify stores.
1515
+ *
1516
+ * Account model:
1517
+ * - role "OWNER" — store admin (Shopify Admin API access token)
1518
+ * - accountKey — store domain (e.g. mystore.myshopify.com)
1519
+ * - purpose — ["admin"]
1520
+ */
1521
+ const SHOPIFY_PROVIDER_NAME = "shopify";
1522
+ const DEFAULT_PURPOSES = ["admin"];
1523
+ function nonEmptyString(value) {
1524
+ if (typeof value !== "string") return void 0;
1525
+ const trimmed = value.trim();
1526
+ return trimmed.length > 0 ? trimmed : void 0;
1527
+ }
1528
+ function readSetting(runtime, key) {
1529
+ return nonEmptyString(runtime.getSetting?.(key));
1530
+ }
1531
+ function readClientConfig(runtime) {
1532
+ const clientId = readSetting(runtime, "SHOPIFY_OAUTH_CLIENT_ID");
1533
+ const clientSecret = readSetting(runtime, "SHOPIFY_OAUTH_CLIENT_SECRET");
1534
+ const redirectUri = readSetting(runtime, "SHOPIFY_OAUTH_REDIRECT_URI");
1535
+ if (!clientId || !clientSecret || !redirectUri) throw new Error("Shopify OAuth requires SHOPIFY_OAUTH_CLIENT_ID, SHOPIFY_OAUTH_CLIENT_SECRET, and SHOPIFY_OAUTH_REDIRECT_URI to be configured.");
1536
+ return {
1537
+ clientId,
1538
+ clientSecret,
1539
+ redirectUri
1540
+ };
1541
+ }
1542
+ function parseScopes(value) {
1543
+ if (!value) return [];
1544
+ return value.split(/[,\s]+/).map((scope) => scope.trim()).filter(Boolean);
1545
+ }
1546
+ function normalizeStoreDomain(value) {
1547
+ const trimmed = value.trim().toLowerCase();
1548
+ if (trimmed.endsWith(".myshopify.com")) return trimmed;
1549
+ if (trimmed.includes(".")) return trimmed;
1550
+ return `${trimmed}.myshopify.com`;
1551
+ }
1552
+ async function exchangeCodeForToken(args) {
1553
+ const url = `https://${args.storeDomain}/admin/oauth/access_token`;
1554
+ const response = await fetch(url, {
1555
+ method: "POST",
1556
+ headers: {
1557
+ "Content-Type": "application/json",
1558
+ Accept: "application/json"
1559
+ },
1560
+ body: JSON.stringify({
1561
+ client_id: args.clientId,
1562
+ client_secret: args.clientSecret,
1563
+ code: args.code
1564
+ })
1565
+ });
1566
+ if (!response.ok) {
1567
+ const body = await response.text();
1568
+ throw new Error(`Shopify token exchange failed with ${response.status}: ${body}`);
1569
+ }
1570
+ const parsed = await response.json();
1571
+ if (parsed.error) throw new Error(`Shopify token exchange returned error ${parsed.error}: ${parsed.error_description ?? "no description"}`);
1572
+ if (!parsed.access_token) throw new Error("Shopify token exchange returned no access_token.");
1573
+ return parsed;
1574
+ }
1575
+ async function fetchShopInfo(storeDomain, accessToken) {
1576
+ const url = `https://${storeDomain}/admin/api/2024-10/shop.json`;
1577
+ const response = await fetch(url, { headers: {
1578
+ "X-Shopify-Access-Token": accessToken,
1579
+ Accept: "application/json"
1580
+ } });
1581
+ if (!response.ok) throw new Error(`Shopify shop.json query failed with ${response.status}`);
1582
+ return await response.json();
1583
+ }
1584
+ function synthesizeEnvAccounts(runtime) {
1585
+ const now = Date.now();
1586
+ return readShopifyAccounts(runtime).map((account) => ({
1587
+ id: account.accountId,
1588
+ provider: SHOPIFY_PROVIDER_NAME,
1589
+ label: account.label ?? `Shopify (${account.storeDomain})`,
1590
+ role: "OWNER",
1591
+ purpose: DEFAULT_PURPOSES,
1592
+ accessGate: "open",
1593
+ status: "connected",
1594
+ externalId: account.storeDomain,
1595
+ displayHandle: account.storeDomain,
1596
+ createdAt: now,
1597
+ updatedAt: now,
1598
+ metadata: {
1599
+ authMethod: "access_token",
1600
+ source: "env",
1601
+ storeDomain: account.storeDomain
1602
+ }
1603
+ }));
1604
+ }
1605
+ /**
1606
+ * Build the Shopify ConnectorAccountManager provider.
1607
+ */
1608
+ function createShopifyConnectorAccountProvider(runtime) {
1609
+ return {
1610
+ provider: SHOPIFY_PROVIDER_NAME,
1611
+ label: "Shopify",
1612
+ listAccounts: async (manager) => {
1613
+ const stored = await manager.getStorage().listAccounts(SHOPIFY_PROVIDER_NAME);
1614
+ if (stored.length > 0) return stored;
1615
+ return synthesizeEnvAccounts(runtime);
1616
+ },
1617
+ createAccount: async (input, _manager) => {
1618
+ return {
1619
+ ...input,
1620
+ provider: SHOPIFY_PROVIDER_NAME,
1621
+ role: input.role ?? "OWNER",
1622
+ purpose: input.purpose ?? DEFAULT_PURPOSES,
1623
+ accessGate: input.accessGate ?? "open",
1624
+ status: input.status ?? "pending"
1625
+ };
1626
+ },
1627
+ patchAccount: async (_accountId, patch, _manager) => {
1628
+ return {
1629
+ ...patch,
1630
+ provider: SHOPIFY_PROVIDER_NAME
1631
+ };
1632
+ },
1633
+ deleteAccount: async (_accountId, _manager) => {},
1634
+ startOAuth: async (request, _manager) => {
1635
+ const config = readClientConfig(runtime);
1636
+ const redirectUri = request.redirectUri ?? config.redirectUri;
1637
+ const metadataInput = request.metadata ?? {};
1638
+ const storeDomainRaw = nonEmptyString(metadataInput.storeDomain) ?? nonEmptyString(metadataInput.shopDomain) ?? nonEmptyString(metadataInput.shop);
1639
+ if (!storeDomainRaw) throw new Error("Shopify OAuth requires a storeDomain (e.g. mystore.myshopify.com) in startOAuth metadata.");
1640
+ const storeDomain = normalizeStoreDomain(storeDomainRaw);
1641
+ const scopes = request.scopes && request.scopes.length > 0 ? request.scopes : [
1642
+ "read_products",
1643
+ "write_products",
1644
+ "read_orders",
1645
+ "write_orders",
1646
+ "read_customers",
1647
+ "read_inventory",
1648
+ "write_inventory",
1649
+ "read_locations"
1650
+ ];
1651
+ return {
1652
+ authUrl: `https://${storeDomain}/admin/oauth/authorize?${new URLSearchParams({
1653
+ client_id: config.clientId,
1654
+ scope: scopes.join(","),
1655
+ redirect_uri: redirectUri,
1656
+ state: request.flow.state
1657
+ }).toString()}`,
1658
+ metadata: {
1659
+ ...request.metadata,
1660
+ requestedScopes: scopes,
1661
+ redirectUri,
1662
+ storeDomain
1663
+ }
1664
+ };
1665
+ },
1666
+ completeOAuth: async (request, _manager) => {
1667
+ const code = nonEmptyString(request.code);
1668
+ if (!code) throw new Error("Shopify OAuth callback is missing an authorization code.");
1669
+ const storeDomainRaw = nonEmptyString((request.flow.metadata ?? {}).storeDomain) ?? nonEmptyString(request.query.shop);
1670
+ if (!storeDomainRaw) throw new Error("Shopify OAuth callback could not resolve a storeDomain.");
1671
+ const storeDomain = normalizeStoreDomain(storeDomainRaw);
1672
+ const config = readClientConfig(runtime);
1673
+ const tokens = await exchangeCodeForToken({
1674
+ storeDomain,
1675
+ clientId: config.clientId,
1676
+ clientSecret: config.clientSecret,
1677
+ code
1678
+ });
1679
+ if (!tokens.access_token) throw new Error("Shopify token exchange returned no access_token.");
1680
+ const shop = (await fetchShopInfo(storeDomain, tokens.access_token)).shop;
1681
+ const externalId = nonEmptyString(shop?.myshopify_domain ?? shop?.domain ?? storeDomain);
1682
+ if (!externalId) throw new Error("Shopify shop payload did not include a usable domain.");
1683
+ const accountPatch = {
1684
+ provider: SHOPIFY_PROVIDER_NAME,
1685
+ role: "OWNER",
1686
+ purpose: DEFAULT_PURPOSES,
1687
+ accessGate: "open",
1688
+ status: "connected",
1689
+ externalId,
1690
+ displayHandle: externalId,
1691
+ label: nonEmptyString(shop?.name) ?? externalId,
1692
+ metadata: {
1693
+ authMethod: "oauth",
1694
+ storeDomain,
1695
+ shopId: shop?.id ?? null,
1696
+ shopName: nonEmptyString(shop?.name) ?? null,
1697
+ shopEmail: nonEmptyString(shop?.email) ?? null,
1698
+ planName: nonEmptyString(shop?.plan_name) ?? null,
1699
+ currency: nonEmptyString(shop?.currency) ?? null,
1700
+ countryCode: nonEmptyString(shop?.country_code) ?? null,
1701
+ grantedScopes: parseScopes(tokens.scope)
1702
+ }
1703
+ };
1704
+ logger.info({
1705
+ src: "plugin:shopify:connector",
1706
+ storeDomain
1707
+ }, "Shopify OAuth completed");
1708
+ return {
1709
+ account: accountPatch,
1710
+ flow: { status: "completed" }
1711
+ };
1712
+ }
1713
+ };
1714
+ }
1715
+ //#endregion
1716
+ //#region src/providers/store-context.ts
1717
+ const MAX_SHOPIFY_DOMAIN_CHARS = 200;
1718
+ const storeContextProvider = {
1719
+ name: "shopifyStoreContext",
1720
+ description: "Provides context about the connected Shopify store -- name, domain, plan, product count, and order count.",
1721
+ descriptionCompressed: "Shopify store: name, domain, plan, product/order counts.",
1722
+ dynamic: true,
1723
+ contexts: ["connectors", "finance"],
1724
+ contextGate: { anyOf: ["connectors", "finance"] },
1725
+ cacheStable: false,
1726
+ cacheScope: "turn",
1727
+ get: async (runtime, _message, _state) => {
1728
+ const svc = runtime.getService(SHOPIFY_SERVICE_TYPE);
1729
+ if (!svc?.isConnected()) return {
1730
+ text: "",
1731
+ values: { shopifyConnected: false },
1732
+ data: { shopifyConnected: false }
1733
+ };
1734
+ try {
1735
+ const [shop, productCount, orderCount] = await Promise.all([
1736
+ svc.getShop(),
1737
+ svc.getProductCount().catch(() => null),
1738
+ svc.getOrderCount().catch(() => null)
1739
+ ]);
1740
+ return {
1741
+ text: [
1742
+ `Connected Shopify store: ${shop.name}`,
1743
+ `Domain: ${shop.primaryDomain.url}`,
1744
+ `Plan: ${shop.plan.displayName}`,
1745
+ `Currency: ${shop.currencyCode}`,
1746
+ productCount !== null ? `Products: ${productCount}` : null,
1747
+ orderCount !== null ? `Orders: ${orderCount}` : null
1748
+ ].filter(Boolean).join("\n"),
1749
+ values: {
1750
+ shopifyConnected: true,
1751
+ shopifyStoreName: shop.name,
1752
+ shopifyDomain: shop.myshopifyDomain.slice(0, MAX_SHOPIFY_DOMAIN_CHARS),
1753
+ shopifyPlan: shop.plan.displayName,
1754
+ shopifyCurrency: shop.currencyCode,
1755
+ shopifyProductCount: productCount ?? 0,
1756
+ shopifyOrderCount: orderCount ?? 0
1757
+ },
1758
+ data: {
1759
+ shopifyConnected: true,
1760
+ shop,
1761
+ productCount: productCount ?? 0,
1762
+ orderCount: orderCount ?? 0,
1763
+ truncated: false
1764
+ }
1765
+ };
1766
+ } catch (err) {
1767
+ logger.error({
1768
+ src: "plugin:shopify:store-context",
1769
+ error: err instanceof Error ? err.message : String(err)
1770
+ }, "Failed to fetch Shopify store context");
1771
+ return {
1772
+ text: "Shopify store context unavailable.",
1773
+ values: { shopifyConnected: false },
1774
+ data: { shopifyConnected: false }
1775
+ };
1776
+ }
1777
+ }
1778
+ };
1779
+ //#endregion
1780
+ //#region src/index.ts
1781
+ const shopifyPlugin = {
1782
+ name: "shopify",
1783
+ description: "Manage Shopify stores -- products, orders, inventory, customers",
1784
+ actions: [...promoteSubactionsToActions(shopifyAction)],
1785
+ providers: [storeContextProvider],
1786
+ services: [ShopifyService],
1787
+ autoEnable: { envKeys: ["SHOPIFY_ACCESS_TOKEN", "SHOPIFY_ACCOUNTS"] },
1788
+ init: async (_config, runtime) => {
1789
+ try {
1790
+ getConnectorAccountManager(runtime).registerProvider(createShopifyConnectorAccountProvider(runtime));
1791
+ } catch (err) {
1792
+ logger.warn({
1793
+ src: "plugin:shopify",
1794
+ err: err instanceof Error ? err.message : String(err)
1795
+ }, "Failed to register Shopify provider with ConnectorAccountManager");
1796
+ }
1797
+ }
1798
+ };
1799
+ //#endregion
1800
+ export { DEFAULT_SHOPIFY_ACCOUNT_ID, DEFAULT_SHOPIFY_ACCOUNT_ROLE, ShopifyService, createShopifyConnectorAccountProvider, shopifyPlugin as default, hasShopifyAccountConfig, normalizeShopifyAccountId, readShopifyAccounts, resolveShopifyAccount, resolveShopifyAccountId, resolveShopifyDefaultAccount };
1801
+
1802
+ //# sourceMappingURL=index.js.map