@alexkroman1/aai-cli 5.3.0 → 5.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/{_agent-D8zBb3M5.mjs → _agent-DMyOab9_.mjs} +2 -2
- package/dist/{_bundler-DFS4xxqE.mjs → _bundler-Cjaxa2wi.mjs} +1 -1
- package/dist/{_config-D3F9km8X.mjs → _config-5AEqhh-O.mjs} +2 -2
- package/dist/{_dev-server-BBUWo0sb.mjs → _dev-server-vV05Fnki.mjs} +3 -3
- package/dist/{_init-DU-sXH6S.mjs → _init-BZ9t_Kz-.mjs} +3 -56
- package/dist/{_slug-api-19R5kZ0U.mjs → _slug-api-DaqQJHk8.mjs} +1 -1
- package/dist/_templates-Bv8CR800.mjs +69 -0
- package/dist/_templates.d.ts +6 -0
- package/dist/{build-Xq1xxoSg.mjs → build-D_PgQOD4.mjs} +1 -1
- package/dist/cli.mjs +59 -17
- package/dist/{client-bundler-BWDkUeEP.mjs → client-bundler-yiWoXrgb.mjs} +3 -0
- package/dist/client-bundler.d.ts +9 -0
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-kSXFLxek.mjs → delete-DXilFBb1.mjs} +1 -1
- package/dist/{deploy-Ci0X1gp2.mjs → deploy-1eaXcfUw.mjs} +3 -3
- package/dist/{dev-g7sGt8qX.mjs → dev-gVNdGFYY.mjs} +1 -1
- package/dist/{init-BjeK8crW.mjs → init-DoU4_txp.mjs} +3 -3
- package/dist/login-C59ZHzuO.mjs +109 -0
- package/dist/login.d.ts +34 -0
- package/dist/scaffold/CLAUDE.md +190 -120
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-DOva9OGk.mjs → secret-CGAIAbUx.mjs} +1 -1
- package/dist/{storage-vM6HjDZZ.mjs → storage-CnhOayhm.mjs} +1 -1
- package/dist/templates/code-interpreter/agent.ts +1 -2
- package/dist/templates/dispatch-center/agent.test.ts +3 -3
- package/dist/templates/dispatch-center/agent.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +7 -2
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_create.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_get.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_triage.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +1 -1
- package/dist/templates/embedded-assets/agent.ts +2 -3
- package/dist/templates/health-assistant/agent.ts +3 -4
- package/dist/templates/infocom-adventure/agent.ts +10 -12
- package/dist/templates/infocom-adventure/client.tsx +5 -5
- package/dist/templates/math-buddy/agent.ts +7 -6
- package/dist/templates/night-owl/agent.ts +2 -3
- package/dist/templates/personal-finance/agent.ts +1 -2
- package/dist/templates/pipeline-simple/agent.test.ts +13 -13
- package/dist/templates/pipeline-simple/agent.ts +5 -6
- package/dist/templates/pizza-ordering/agent.test.ts +13 -11
- package/dist/templates/pizza-ordering/agent.ts +5 -6
- package/dist/templates/retail/address.ts +29 -0
- package/dist/templates/retail/agent.test.ts +1030 -0
- package/dist/templates/retail/agent.ts +71 -0
- package/dist/templates/retail/authenticate.ts +34 -0
- package/dist/templates/retail/client.tsx +459 -0
- package/dist/templates/retail/refund.ts +19 -0
- package/dist/templates/retail/registry.test.ts +182 -0
- package/dist/templates/retail/resolve.test.ts +158 -0
- package/dist/templates/retail/resolve.ts +158 -0
- package/dist/templates/retail/seed.json +4260 -0
- package/dist/templates/retail/seed.test.ts +224 -0
- package/dist/templates/retail/shared.test.ts +192 -0
- package/dist/templates/retail/shared.ts +430 -0
- package/dist/templates/retail/store.test.ts +256 -0
- package/dist/templates/retail/store.ts +243 -0
- package/dist/templates/retail/swap.test.ts +186 -0
- package/dist/templates/retail/swap.ts +138 -0
- package/dist/templates/retail/system-prompt.md +111 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +82 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +83 -0
- package/dist/templates/retail/tools/find_user_id_by_email.ts +32 -0
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +38 -0
- package/dist/templates/retail/tools/get_item_details.ts +29 -0
- package/dist/templates/retail/tools/get_order_details.ts +41 -0
- package/dist/templates/retail/tools/get_product_details.ts +33 -0
- package/dist/templates/retail/tools/get_user_details.ts +44 -0
- package/dist/templates/retail/tools/list_all_product_types.ts +24 -0
- package/dist/templates/retail/tools/modify_pending_order_address.ts +46 -0
- package/dist/templates/retail/tools/modify_pending_order_items.ts +102 -0
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +103 -0
- package/dist/templates/retail/tools/modify_user_address.ts +38 -0
- package/dist/templates/retail/tools/return_delivered_order_items.ts +101 -0
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +22 -0
- package/dist/templates/simple/agent.test.ts +18 -0
- package/dist/templates/simple/agent.ts +3 -0
- package/dist/templates/solo-rpg/agent.test.ts +2 -2
- package/dist/templates/solo-rpg/agent.ts +1 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +1 -1
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +1 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +1 -1
- package/dist/templates/web-researcher/agent.ts +1 -2
- package/dist/typecheck.d.ts +18 -0
- package/dist/typecheck.mjs +8 -0
- package/dist/worker-bundler.d.ts +9 -1
- package/dist/worker-bundler.mjs +8 -0
- package/package.json +3 -3
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { creditRefund, REFUND_DELAY_NOTE, REFUND_IMMEDIATE_NOTE } from "../refund.ts";
|
|
3
|
+
import { resolveOrder } from "../resolve.ts";
|
|
4
|
+
import { authenticatedUser, getState, isError, retailTool, setFocus } from "../store.ts";
|
|
5
|
+
|
|
6
|
+
/** tau2 accepts exactly these two. Anything else is refused. */
|
|
7
|
+
const CANCEL_REASONS = ["no longer needed", "ordered by mistake"] as const;
|
|
8
|
+
|
|
9
|
+
export const cancelPendingOrder = retailTool({
|
|
10
|
+
name: "cancel_pending_order",
|
|
11
|
+
description:
|
|
12
|
+
"Cancel a pending order. Only an order whose status is exactly 'pending' can be cancelled — " +
|
|
13
|
+
"check the status first. The reason must be either 'no longer needed' or 'ordered by mistake'. " +
|
|
14
|
+
"State the order, its total and the refund destination to the caller and get an explicit yes " +
|
|
15
|
+
"before calling this.",
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
order_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.max(120)
|
|
20
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
21
|
+
reason: z
|
|
22
|
+
.enum(CANCEL_REASONS)
|
|
23
|
+
.describe("Either 'no longer needed' or 'ordered by mistake' — no other reason is accepted"),
|
|
24
|
+
}),
|
|
25
|
+
// `execute` before `summary`: TS infers the wrapper's generic `R` from
|
|
26
|
+
// `execute`'s return type, and processes object literal properties in
|
|
27
|
+
// source order — with `summary` first, `result` in its signature can't be
|
|
28
|
+
// inferred and silently falls back to `unknown`.
|
|
29
|
+
execute: (args, ctx) => {
|
|
30
|
+
const state = getState(ctx);
|
|
31
|
+
const user = authenticatedUser(state);
|
|
32
|
+
if (isError(user)) return user;
|
|
33
|
+
|
|
34
|
+
const order = resolveOrder(state, args.order_id);
|
|
35
|
+
if (isError(order)) return order;
|
|
36
|
+
setFocus(state, { orderId: order.order_id });
|
|
37
|
+
|
|
38
|
+
if (order.status !== "pending") {
|
|
39
|
+
return {
|
|
40
|
+
error: `Order ${order.order_id} is ${order.status}, and only a pending order can be cancelled.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// The enum makes this unreachable from a well-formed call; it stays because
|
|
44
|
+
// the reason is the one field a caller supplies in free speech.
|
|
45
|
+
if (!CANCEL_REASONS.includes(args.reason)) {
|
|
46
|
+
return {
|
|
47
|
+
error: `'${args.reason}' is not an accepted cancellation reason. It must be 'no longer needed' or 'ordered by mistake'.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let immediate = false;
|
|
52
|
+
const refunds = order.payment_history
|
|
53
|
+
.filter((payment) => payment.transaction_type === "payment")
|
|
54
|
+
.map((payment) => {
|
|
55
|
+
const credited = creditRefund(user, payment.payment_method_id, payment.amount);
|
|
56
|
+
immediate = immediate || credited.immediate;
|
|
57
|
+
return {
|
|
58
|
+
transaction_type: "refund" as const,
|
|
59
|
+
amount: payment.amount,
|
|
60
|
+
payment_method_id: payment.payment_method_id,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
order.status = "cancelled";
|
|
65
|
+
order.cancel_reason = args.reason;
|
|
66
|
+
order.payment_history.push(...refunds);
|
|
67
|
+
|
|
68
|
+
const total = refunds.reduce((sum, refund) => sum + refund.amount, 0);
|
|
69
|
+
return {
|
|
70
|
+
order_id: order.order_id,
|
|
71
|
+
status: order.status,
|
|
72
|
+
cancel_reason: order.cancel_reason,
|
|
73
|
+
refunded: total,
|
|
74
|
+
refund_immediate: immediate,
|
|
75
|
+
message: `Order ${order.order_id} is cancelled and $${total.toFixed(2)} is being refunded. ${
|
|
76
|
+
immediate ? REFUND_IMMEDIATE_NOTE : REFUND_DELAY_NOTE
|
|
77
|
+
}`,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
summary: (_args, result) =>
|
|
81
|
+
"error" in result ? "cancel failed" : `cancelled ${result.order_id}`,
|
|
82
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveOrder } from "../resolve.ts";
|
|
3
|
+
import { authenticatedUser, getState, isError, retailTool, setFocus } from "../store.ts";
|
|
4
|
+
import { assertCanCoverDiff, planItemSwap } from "../swap.ts";
|
|
5
|
+
|
|
6
|
+
export const exchangeDeliveredOrderItems = retailTool({
|
|
7
|
+
name: "exchange_delivered_order_items",
|
|
8
|
+
description:
|
|
9
|
+
"Request an exchange of items in a delivered order for different options of the SAME products. " +
|
|
10
|
+
"Only a 'delivered' order can be exchanged, and only once. Remind the caller to name EVERY " +
|
|
11
|
+
"item they want exchanged before you call this, read the whole list and the price difference " +
|
|
12
|
+
"back, and get an explicit yes. No new order is needed — the customer gets an email explaining " +
|
|
13
|
+
"how to send the originals back.",
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
order_id: z
|
|
16
|
+
.string()
|
|
17
|
+
.max(120)
|
|
18
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
19
|
+
item_ids: z
|
|
20
|
+
.array(z.string().max(60))
|
|
21
|
+
.max(20)
|
|
22
|
+
.describe("Item ids to exchange. May repeat if the order holds duplicates"),
|
|
23
|
+
new_item_ids: z
|
|
24
|
+
.array(z.string().max(60))
|
|
25
|
+
.max(20)
|
|
26
|
+
.describe("Replacement item ids, each matching the item in the same position"),
|
|
27
|
+
payment_method_id: z
|
|
28
|
+
.string()
|
|
29
|
+
.max(80)
|
|
30
|
+
.describe("Method to charge or refund the price difference"),
|
|
31
|
+
}),
|
|
32
|
+
// `execute` before `summary`: TS infers the wrapper's generic `R` from
|
|
33
|
+
// `execute`'s return type, and processes object literal properties in
|
|
34
|
+
// source order — with `summary` first, `result` in its signature can't be
|
|
35
|
+
// inferred and silently falls back to `unknown`.
|
|
36
|
+
execute: (args, ctx) => {
|
|
37
|
+
const state = getState(ctx);
|
|
38
|
+
const user = authenticatedUser(state);
|
|
39
|
+
if (isError(user)) return user;
|
|
40
|
+
|
|
41
|
+
const order = resolveOrder(state, args.order_id);
|
|
42
|
+
if (isError(order)) return order;
|
|
43
|
+
setFocus(state, { orderId: order.order_id });
|
|
44
|
+
|
|
45
|
+
if (order.status !== "delivered") {
|
|
46
|
+
return {
|
|
47
|
+
error: `Order ${order.order_id} is ${order.status}. Only a delivered order can be exchanged, and only once.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// requireDifferent is false: tau2 permits a same-item entry here, and a
|
|
52
|
+
// zero-difference exchange is harmless on a delivered order.
|
|
53
|
+
const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
|
|
54
|
+
requireDifferent: false,
|
|
55
|
+
});
|
|
56
|
+
if (isError(plan)) return plan;
|
|
57
|
+
|
|
58
|
+
const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
|
|
59
|
+
if (blocked) return blocked;
|
|
60
|
+
|
|
61
|
+
// No balance moves and no items change: this records a REQUEST. The money
|
|
62
|
+
// and the item swap settle when the originals come back.
|
|
63
|
+
order.status = "exchange requested";
|
|
64
|
+
order.exchange_items = [...args.item_ids].sort();
|
|
65
|
+
order.exchange_new_items = [...args.new_item_ids].sort();
|
|
66
|
+
order.exchange_payment_method_id = args.payment_method_id;
|
|
67
|
+
order.exchange_price_difference = plan.diff;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
order_id: order.order_id,
|
|
71
|
+
status: order.status,
|
|
72
|
+
price_difference: plan.diff,
|
|
73
|
+
exchange_items: order.exchange_items,
|
|
74
|
+
exchange_new_items: order.exchange_new_items,
|
|
75
|
+
message:
|
|
76
|
+
plan.diff > 0
|
|
77
|
+
? `Exchange requested on ${order.order_id}. $${plan.diff.toFixed(2)} will be charged to ${args.payment_method_id}. An email with return instructions is on its way.`
|
|
78
|
+
: `Exchange requested on ${order.order_id}. $${Math.abs(plan.diff).toFixed(2)} will be refunded to ${args.payment_method_id}. An email with return instructions is on its way.`,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
summary: (_args, result) =>
|
|
82
|
+
"error" in result ? "exchange failed" : `exchange requested on ${result.order_id}`,
|
|
83
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { authenticateAs } from "../authenticate.ts";
|
|
3
|
+
import { getState, retailTool } from "../store.ts";
|
|
4
|
+
|
|
5
|
+
export const findUserIdByEmail = retailTool({
|
|
6
|
+
name: "find_user_id_by_email",
|
|
7
|
+
description:
|
|
8
|
+
"Find a customer's user id by their email address. This is how you authenticate the caller — " +
|
|
9
|
+
"do it before anything else, even if they volunteer their user id. Prefer this over " +
|
|
10
|
+
"find_user_id_by_name_zip unless they cannot remember the email.",
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
email: z.string().max(200).describe("The customer's email, e.g. 'something@example.com'"),
|
|
13
|
+
}),
|
|
14
|
+
requiresAuth: false,
|
|
15
|
+
// `execute` must come before `summary` in this object literal: TS infers the
|
|
16
|
+
// wrapper's generic `R` from `execute`'s return type, and processes object
|
|
17
|
+
// literal properties in source order — with `summary` first, `result` in its
|
|
18
|
+
// signature can't be inferred and silently falls back to `unknown`.
|
|
19
|
+
execute: (args, ctx) => {
|
|
20
|
+
const state = getState(ctx);
|
|
21
|
+
const target = args.email.trim().toLowerCase();
|
|
22
|
+
const match = Object.values(state.store.users).find(
|
|
23
|
+
(user) => user.email.toLowerCase() === target,
|
|
24
|
+
);
|
|
25
|
+
if (!match) {
|
|
26
|
+
return { error: `No customer found with email ${args.email}.` };
|
|
27
|
+
}
|
|
28
|
+
return authenticateAs(state, match);
|
|
29
|
+
},
|
|
30
|
+
summary: (_args, result) =>
|
|
31
|
+
"error" in result ? "lookup failed" : `identified ${result.user_id}`,
|
|
32
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { authenticateAs } from "../authenticate.ts";
|
|
3
|
+
import { getState, retailTool } from "../store.ts";
|
|
4
|
+
|
|
5
|
+
export const findUserIdByNameZip = retailTool({
|
|
6
|
+
name: "find_user_id_by_name_zip",
|
|
7
|
+
description:
|
|
8
|
+
"Find a customer's user id by first name, last name and zip code. Use this only when the " +
|
|
9
|
+
"caller cannot remember the email address on the account.",
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
first_name: z.string().max(100).describe("First name, e.g. 'John'"),
|
|
12
|
+
last_name: z.string().max(100).describe("Last name, e.g. 'Doe'"),
|
|
13
|
+
zip: z.string().max(20).describe("Zip code, e.g. '12345'"),
|
|
14
|
+
}),
|
|
15
|
+
requiresAuth: false,
|
|
16
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
17
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
18
|
+
execute: (args, ctx) => {
|
|
19
|
+
const state = getState(ctx);
|
|
20
|
+
const first = args.first_name.trim().toLowerCase();
|
|
21
|
+
const last = args.last_name.trim().toLowerCase();
|
|
22
|
+
const zip = args.zip.replace(/\D/g, "");
|
|
23
|
+
const match = Object.values(state.store.users).find(
|
|
24
|
+
(user) =>
|
|
25
|
+
user.name.first_name.toLowerCase() === first &&
|
|
26
|
+
user.name.last_name.toLowerCase() === last &&
|
|
27
|
+
user.address.zip === zip,
|
|
28
|
+
);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return {
|
|
31
|
+
error: `No customer found for ${args.first_name} ${args.last_name} at zip ${args.zip}.`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return authenticateAs(state, match);
|
|
35
|
+
},
|
|
36
|
+
summary: (_args, result) =>
|
|
37
|
+
"error" in result ? "lookup failed" : `identified ${result.user_id}`,
|
|
38
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findItem, getState, isError, retailTool } from "../store.ts";
|
|
3
|
+
|
|
4
|
+
export const getItemDetails = retailTool({
|
|
5
|
+
name: "get_item_details",
|
|
6
|
+
description:
|
|
7
|
+
"Get one specific item's options, price and availability. Takes an ITEM id, which is not the " +
|
|
8
|
+
"same as a product id.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
item_id: z.string().max(60).describe("The item id, e.g. '1008292230'"),
|
|
11
|
+
}),
|
|
12
|
+
requiresAuth: false,
|
|
13
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
15
|
+
execute: (args, ctx) => {
|
|
16
|
+
const found = findItem(getState(ctx), args.item_id);
|
|
17
|
+
if (isError(found)) return found;
|
|
18
|
+
return {
|
|
19
|
+
item_id: found.variant.item_id,
|
|
20
|
+
product_id: found.product.product_id,
|
|
21
|
+
product_name: found.product.name,
|
|
22
|
+
options: found.variant.options,
|
|
23
|
+
price: found.variant.price,
|
|
24
|
+
available: found.variant.available,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
summary: (_args, result) =>
|
|
28
|
+
"error" in result ? "item read failed" : `read item ${result.item_id}`,
|
|
29
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveOrder } from "../resolve.ts";
|
|
3
|
+
import { getState, isError, retailTool, setFocus } from "../store.ts";
|
|
4
|
+
|
|
5
|
+
export const getOrderDetails = retailTool({
|
|
6
|
+
name: "get_order_details",
|
|
7
|
+
description:
|
|
8
|
+
"Get the status and full details of one of the authenticated customer's orders. Accepts the " +
|
|
9
|
+
"order id (e.g. '#W0000000' — note the leading '#'), or a spoken reference such as " +
|
|
10
|
+
"'my pending order', 'the delivered one', or 'the second pending order'.",
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
order_id: z
|
|
13
|
+
.string()
|
|
14
|
+
.max(120)
|
|
15
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
16
|
+
}),
|
|
17
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
18
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
19
|
+
execute: (args, ctx) => {
|
|
20
|
+
const state = getState(ctx);
|
|
21
|
+
const order = resolveOrder(state, args.order_id);
|
|
22
|
+
if (isError(order)) return order;
|
|
23
|
+
setFocus(state, { orderId: order.order_id });
|
|
24
|
+
return {
|
|
25
|
+
order_id: order.order_id,
|
|
26
|
+
status: order.status,
|
|
27
|
+
address: order.address,
|
|
28
|
+
items: order.items.map((item) => ({
|
|
29
|
+
name: item.name,
|
|
30
|
+
product_id: item.product_id,
|
|
31
|
+
item_id: item.item_id,
|
|
32
|
+
price: item.price,
|
|
33
|
+
options: item.options,
|
|
34
|
+
})),
|
|
35
|
+
payment_history: order.payment_history,
|
|
36
|
+
...(order.fulfillments ? { fulfillments: order.fulfillments } : {}),
|
|
37
|
+
...(order.cancel_reason ? { cancel_reason: order.cancel_reason } : {}),
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
summary: (_args, result) => ("error" in result ? "order read failed" : `read ${result.order_id}`),
|
|
41
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { findProduct, getState, isError, retailTool, setFocus } from "../store.ts";
|
|
3
|
+
|
|
4
|
+
export const getProductDetails = retailTool({
|
|
5
|
+
name: "get_product_details",
|
|
6
|
+
description:
|
|
7
|
+
"Get a product's variants — each with its item id, options, price and availability. Takes a " +
|
|
8
|
+
"PRODUCT id, which is not the same as an item id. Use list_all_product_types to find one.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
product_id: z.string().max(60).describe("The product id, e.g. '6086499569'"),
|
|
11
|
+
}),
|
|
12
|
+
requiresAuth: false,
|
|
13
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
15
|
+
execute: (args, ctx) => {
|
|
16
|
+
const state = getState(ctx);
|
|
17
|
+
const product = findProduct(state, args.product_id);
|
|
18
|
+
if (isError(product)) return product;
|
|
19
|
+
setFocus(state, { productId: product.product_id });
|
|
20
|
+
return {
|
|
21
|
+
name: product.name,
|
|
22
|
+
product_id: product.product_id,
|
|
23
|
+
variants: Object.values(product.variants).map((variant) => ({
|
|
24
|
+
item_id: variant.item_id,
|
|
25
|
+
options: variant.options,
|
|
26
|
+
price: variant.price,
|
|
27
|
+
available: variant.available,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
summary: (_args, result) =>
|
|
32
|
+
"error" in result ? "product read failed" : `read product ${result.name}`,
|
|
33
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { authenticatedUser, getState, isError, retailTool } from "../store.ts";
|
|
3
|
+
|
|
4
|
+
export const getUserDetails = retailTool({
|
|
5
|
+
name: "get_user_details",
|
|
6
|
+
description:
|
|
7
|
+
"Get the authenticated customer's profile: name, email, default address, payment methods " +
|
|
8
|
+
"(with gift-card balances) and their order ids with each order's status.",
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
user_id: z.string().max(100).describe("The user id, e.g. 'sara_doe_496'"),
|
|
11
|
+
}),
|
|
12
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
13
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
14
|
+
execute: (args, ctx) => {
|
|
15
|
+
const state = getState(ctx);
|
|
16
|
+
const user = authenticatedUser(state);
|
|
17
|
+
if (isError(user)) return user;
|
|
18
|
+
if (user.user_id !== args.user_id) {
|
|
19
|
+
return {
|
|
20
|
+
error: `${args.user_id} is not the customer on this call. You can help only one customer per conversation.`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
user_id: user.user_id,
|
|
25
|
+
name: `${user.name.first_name} ${user.name.last_name}`,
|
|
26
|
+
email: user.email,
|
|
27
|
+
address: user.address,
|
|
28
|
+
payment_methods: Object.values(user.payment_methods).map((method) => ({
|
|
29
|
+
payment_method_id: method.id,
|
|
30
|
+
source: method.source,
|
|
31
|
+
...(method.source === "gift_card" ? { balance: method.balance } : {}),
|
|
32
|
+
...(method.source === "credit_card"
|
|
33
|
+
? { brand: method.brand, last_four: method.last_four }
|
|
34
|
+
: {}),
|
|
35
|
+
})),
|
|
36
|
+
orders: user.orders.map((id) => ({
|
|
37
|
+
order_id: id,
|
|
38
|
+
status: state.store.orders[id]?.status ?? "unknown",
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
summary: (_args, result) =>
|
|
43
|
+
"error" in result ? "profile read failed" : `read profile ${result.user_id}`,
|
|
44
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getState, retailTool } from "../store.ts";
|
|
3
|
+
|
|
4
|
+
export const listAllProductTypes = retailTool({
|
|
5
|
+
name: "list_all_product_types",
|
|
6
|
+
description:
|
|
7
|
+
"List every product type the store carries, as a name-to-product-id map. Each product type " +
|
|
8
|
+
"has variant items with their own item ids and options.",
|
|
9
|
+
// Empty schema rather than omitting it: the wrapper has one code path, and
|
|
10
|
+
// that is where the per-call UI-update invariant lives.
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
requiresAuth: false,
|
|
13
|
+
// `execute` before `summary`: see find_user_id_by_email.ts for why the order
|
|
14
|
+
// is load-bearing for the generic `result` type in `summary`.
|
|
15
|
+
execute: (_args, ctx) => {
|
|
16
|
+
const state = getState(ctx);
|
|
17
|
+
const entries = Object.values(state.store.products)
|
|
18
|
+
.map((product) => [product.name, product.product_id] as const)
|
|
19
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
20
|
+
return { count: entries.length, products: Object.fromEntries(entries) };
|
|
21
|
+
},
|
|
22
|
+
summary: (_args, result) =>
|
|
23
|
+
"error" in result ? "catalog read failed" : `listed ${result.count} product types`,
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AddressFields, formatAddress, toAddress } from "../address.ts";
|
|
3
|
+
import { resolveOrder } from "../resolve.ts";
|
|
4
|
+
import { getState, isError, retailTool, setFocus } from "../store.ts";
|
|
5
|
+
|
|
6
|
+
export const modifyPendingOrderAddress = retailTool({
|
|
7
|
+
name: "modify_pending_order_address",
|
|
8
|
+
description:
|
|
9
|
+
"Change the shipping address of a pending order. Read the new address back to the caller and " +
|
|
10
|
+
"get an explicit yes before calling this. This does not change the customer's default address.",
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
order_id: z
|
|
13
|
+
.string()
|
|
14
|
+
.max(120)
|
|
15
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
16
|
+
...AddressFields,
|
|
17
|
+
}),
|
|
18
|
+
// `execute` before `summary`: TS infers the wrapper's generic `R` from
|
|
19
|
+
// `execute`'s return type, and processes object literal properties in
|
|
20
|
+
// source order — with `summary` first, `result` in its signature can't be
|
|
21
|
+
// inferred and silently falls back to `unknown`.
|
|
22
|
+
execute: (args, ctx) => {
|
|
23
|
+
const state = getState(ctx);
|
|
24
|
+
const order = resolveOrder(state, args.order_id);
|
|
25
|
+
if (isError(order)) return order;
|
|
26
|
+
setFocus(state, { orderId: order.order_id });
|
|
27
|
+
|
|
28
|
+
// Any pending variant is fine here — unlike cancel and modify-items, which
|
|
29
|
+
// require exactly 'pending'. Re-addressing a modified order is harmless.
|
|
30
|
+
if (!order.status.startsWith("pending")) {
|
|
31
|
+
return {
|
|
32
|
+
error: `Order ${order.order_id} is ${order.status}, and only a pending order's address can be changed.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
order.address = toAddress(args);
|
|
37
|
+
return {
|
|
38
|
+
order_id: order.order_id,
|
|
39
|
+
status: order.status,
|
|
40
|
+
address: order.address,
|
|
41
|
+
message: `Order ${order.order_id} now ships to ${formatAddress(order.address)}.`,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
summary: (_args, result) =>
|
|
45
|
+
"error" in result ? "address change failed" : `re-addressed ${result.order_id}`,
|
|
46
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveOrder } from "../resolve.ts";
|
|
3
|
+
import {
|
|
4
|
+
authenticatedUser,
|
|
5
|
+
getState,
|
|
6
|
+
isError,
|
|
7
|
+
isGiftCard,
|
|
8
|
+
money,
|
|
9
|
+
retailTool,
|
|
10
|
+
setFocus,
|
|
11
|
+
} from "../store.ts";
|
|
12
|
+
import { applySwap, assertCanCoverDiff, planItemSwap } from "../swap.ts";
|
|
13
|
+
|
|
14
|
+
export const modifyPendingOrderItems = retailTool({
|
|
15
|
+
name: "modify_pending_order_items",
|
|
16
|
+
description:
|
|
17
|
+
"Change items in a pending order to different options of the SAME products. This can be done " +
|
|
18
|
+
"ONCE per order and is irreversible — afterwards the order can no longer be cancelled or " +
|
|
19
|
+
"modified at all. Collect EVERY item the caller wants changed into one call, read the whole " +
|
|
20
|
+
"list and the price difference back, and get an explicit yes first. The item and replacement " +
|
|
21
|
+
"lists are positional and must be the same length.",
|
|
22
|
+
inputSchema: z.object({
|
|
23
|
+
order_id: z
|
|
24
|
+
.string()
|
|
25
|
+
.max(120)
|
|
26
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
27
|
+
item_ids: z
|
|
28
|
+
.array(z.string().max(60))
|
|
29
|
+
.max(20)
|
|
30
|
+
.describe("Item ids to change. May repeat if the order holds duplicates"),
|
|
31
|
+
new_item_ids: z
|
|
32
|
+
.array(z.string().max(60))
|
|
33
|
+
.max(20)
|
|
34
|
+
.describe("Replacement item ids, each matching the item in the same position"),
|
|
35
|
+
payment_method_id: z
|
|
36
|
+
.string()
|
|
37
|
+
.max(80)
|
|
38
|
+
.describe("Method to charge or refund the price difference, e.g. 'gift_card_0000000'"),
|
|
39
|
+
}),
|
|
40
|
+
// `execute` before `summary`: TS infers the wrapper's generic `R` from
|
|
41
|
+
// `execute`'s return type, and processes object literal properties in
|
|
42
|
+
// source order — with `summary` first, `result` in its signature can't be
|
|
43
|
+
// inferred and silently falls back to `unknown`.
|
|
44
|
+
execute: (args, ctx) => {
|
|
45
|
+
const state = getState(ctx);
|
|
46
|
+
const user = authenticatedUser(state);
|
|
47
|
+
if (isError(user)) return user;
|
|
48
|
+
|
|
49
|
+
const order = resolveOrder(state, args.order_id);
|
|
50
|
+
if (isError(order)) return order;
|
|
51
|
+
setFocus(state, { orderId: order.order_id });
|
|
52
|
+
|
|
53
|
+
// Exactly 'pending'. A 'pending (item modified)' order has already used its
|
|
54
|
+
// one modification, which is what makes this action terminal.
|
|
55
|
+
if (order.status !== "pending") {
|
|
56
|
+
return {
|
|
57
|
+
error: `Order ${order.order_id} is ${order.status}. Items can only be changed while an order is exactly 'pending', and only once.`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const plan = planItemSwap(state, order, args.item_ids, args.new_item_ids, {
|
|
62
|
+
requireDifferent: true,
|
|
63
|
+
});
|
|
64
|
+
if (isError(plan)) return plan;
|
|
65
|
+
|
|
66
|
+
const blocked = assertCanCoverDiff(user, args.payment_method_id, plan.diff);
|
|
67
|
+
if (blocked) return blocked;
|
|
68
|
+
|
|
69
|
+
const method = user.payment_methods[args.payment_method_id];
|
|
70
|
+
if (method && isGiftCard(method)) {
|
|
71
|
+
method.balance = money(method.balance - plan.diff);
|
|
72
|
+
}
|
|
73
|
+
order.payment_history.push({
|
|
74
|
+
transaction_type: plan.diff > 0 ? "payment" : "refund",
|
|
75
|
+
amount: money(Math.abs(plan.diff)),
|
|
76
|
+
payment_method_id: args.payment_method_id,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
applySwap(order, plan);
|
|
80
|
+
order.status = "pending (item modified)";
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
order_id: order.order_id,
|
|
84
|
+
status: order.status,
|
|
85
|
+
price_difference: plan.diff,
|
|
86
|
+
items: order.items.map((item) => ({
|
|
87
|
+
name: item.name,
|
|
88
|
+
item_id: item.item_id,
|
|
89
|
+
options: item.options,
|
|
90
|
+
price: item.price,
|
|
91
|
+
})),
|
|
92
|
+
message:
|
|
93
|
+
plan.diff > 0
|
|
94
|
+
? `Done. $${plan.diff.toFixed(2)} was charged to ${args.payment_method_id}. This order can no longer be modified or cancelled.`
|
|
95
|
+
: `Done. $${Math.abs(plan.diff).toFixed(2)} is being refunded to ${args.payment_method_id}. This order can no longer be modified or cancelled.`,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
summary: (_args, result) =>
|
|
99
|
+
"error" in result
|
|
100
|
+
? "item change failed"
|
|
101
|
+
: `modified ${result.order_id} (${result.price_difference >= 0 ? "+" : ""}${result.price_difference})`,
|
|
102
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveOrder } from "../resolve.ts";
|
|
3
|
+
import {
|
|
4
|
+
authenticatedUser,
|
|
5
|
+
findPaymentMethod,
|
|
6
|
+
getState,
|
|
7
|
+
isError,
|
|
8
|
+
isGiftCard,
|
|
9
|
+
money,
|
|
10
|
+
retailTool,
|
|
11
|
+
setFocus,
|
|
12
|
+
} from "../store.ts";
|
|
13
|
+
|
|
14
|
+
export const modifyPendingOrderPayment = retailTool({
|
|
15
|
+
name: "modify_pending_order_payment",
|
|
16
|
+
description:
|
|
17
|
+
"Change which payment method a pending order is charged to. The new method must be different " +
|
|
18
|
+
"from the current one, and a gift card must hold enough to cover the whole order. The original " +
|
|
19
|
+
"method is refunded. Read the change back and get an explicit yes before calling this.",
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
order_id: z
|
|
22
|
+
.string()
|
|
23
|
+
.max(120)
|
|
24
|
+
.describe("Order id such as '#W0000000', or a spoken reference to one of their orders"),
|
|
25
|
+
payment_method_id: z
|
|
26
|
+
.string()
|
|
27
|
+
.max(80)
|
|
28
|
+
.describe("The new payment method id, e.g. 'gift_card_0000000'"),
|
|
29
|
+
}),
|
|
30
|
+
// `execute` before `summary`: TS infers the wrapper's generic `R` from
|
|
31
|
+
// `execute`'s return type, and processes object literal properties in
|
|
32
|
+
// source order — with `summary` first, `result` in its signature can't be
|
|
33
|
+
// inferred and silently falls back to `unknown`.
|
|
34
|
+
execute: (args, ctx) => {
|
|
35
|
+
const state = getState(ctx);
|
|
36
|
+
const user = authenticatedUser(state);
|
|
37
|
+
if (isError(user)) return user;
|
|
38
|
+
|
|
39
|
+
const order = resolveOrder(state, args.order_id);
|
|
40
|
+
if (isError(order)) return order;
|
|
41
|
+
setFocus(state, { orderId: order.order_id });
|
|
42
|
+
|
|
43
|
+
if (!order.status.startsWith("pending")) {
|
|
44
|
+
return {
|
|
45
|
+
error: `Order ${order.order_id} is ${order.status}, and only a pending order's payment method can be changed.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const newMethod = findPaymentMethod(user, args.payment_method_id);
|
|
50
|
+
if (isError(newMethod)) return newMethod;
|
|
51
|
+
|
|
52
|
+
const original = order.payment_history[0];
|
|
53
|
+
if (order.payment_history.length !== 1 || original?.transaction_type !== "payment") {
|
|
54
|
+
return {
|
|
55
|
+
error: `Order ${order.order_id} does not have exactly one payment on record, so its payment method cannot be changed.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (original.payment_method_id === args.payment_method_id) {
|
|
59
|
+
return {
|
|
60
|
+
error: `Order ${order.order_id} is already paid with ${args.payment_method_id}. The new method must be different.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const amount = original.amount;
|
|
65
|
+
if (isGiftCard(newMethod) && newMethod.balance < amount) {
|
|
66
|
+
return {
|
|
67
|
+
error: `Gift card ${args.payment_method_id}'s balance ($${newMethod.balance.toFixed(2)}) does not cover the $${amount.toFixed(2)} order total.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
order.payment_history.push(
|
|
72
|
+
{
|
|
73
|
+
transaction_type: "payment",
|
|
74
|
+
amount,
|
|
75
|
+
payment_method_id: args.payment_method_id,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
transaction_type: "refund",
|
|
79
|
+
amount,
|
|
80
|
+
payment_method_id: original.payment_method_id,
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (isGiftCard(newMethod)) {
|
|
85
|
+
newMethod.balance = money(newMethod.balance - amount);
|
|
86
|
+
}
|
|
87
|
+
const oldMethod = user.payment_methods[original.payment_method_id];
|
|
88
|
+
if (oldMethod && isGiftCard(oldMethod)) {
|
|
89
|
+
oldMethod.balance = money(oldMethod.balance + amount);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
order_id: order.order_id,
|
|
94
|
+
status: order.status,
|
|
95
|
+
amount,
|
|
96
|
+
paid_with: args.payment_method_id,
|
|
97
|
+
refunded_to: original.payment_method_id,
|
|
98
|
+
message: `Order ${order.order_id} is now charged to ${args.payment_method_id}, and $${amount.toFixed(2)} is going back to ${original.payment_method_id}.`,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
summary: (_args, result) =>
|
|
102
|
+
"error" in result ? "payment change failed" : `re-paid ${result.order_id}`,
|
|
103
|
+
});
|