@elizaos/plugin-shopify 2.0.0-beta.1 → 2.0.11-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/LICENSE +21 -0
- package/README.md +91 -0
- package/package.json +25 -7
- package/dist/index.d.ts +0 -213
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -1802
- package/dist/index.js.map +0 -1
package/dist/index.js
DELETED
|
@@ -1,1802 +0,0 @@
|
|
|
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
|