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