@sealab/mcp-server 1.0.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/PROPOSED-CHANGES-INSERTION-POINTS.md +220 -0
- package/SEALAB_MCP_DOCUMENTATION.md +1136 -0
- package/dist/client/api-client.js +44 -0
- package/dist/index.js +42 -0
- package/dist/tools/canvas.js +446 -0
- package/dist/tools/catalog.js +95 -0
- package/dist/tools/configuration-info.js +299 -0
- package/dist/tools/configuration.js +32 -0
- package/dist/tools/orders.js +1267 -0
- package/dist/tools/saved-settings.js +271 -0
- package/package.json +32 -0
- package/resources/tooltips/backPanel.txt +17 -0
- package/resources/tooltips/backPanelMaterial.txt +29 -0
- package/resources/tooltips/caseEdge.txt +18 -0
- package/resources/tooltips/caseMaterial.txt +31 -0
- package/resources/tooltips/depth.txt +11 -0
- package/resources/tooltips/drawerType.txt +12 -0
- package/resources/tooltips/edgeBandingType.txt +18 -0
- package/resources/tooltips/excludeFronts.txt +5 -0
- package/resources/tooltips/frontEdge.txt +18 -0
- package/resources/tooltips/frontMaterial.txt +35 -0
- package/resources/tooltips/gapBottom.txt +2 -0
- package/resources/tooltips/gapCenter.txt +2 -0
- package/resources/tooltips/gapLeft.txt +15 -0
- package/resources/tooltips/gapRight.txt +15 -0
- package/resources/tooltips/gapTop.txt +2 -0
- package/resources/tooltips/height.txt +6 -0
- package/resources/tooltips/hingePlate.txt +11 -0
- package/resources/tooltips/includeLegLevelers.txt +8 -0
- package/resources/tooltips/jointMethod.txt +7 -0
- package/resources/tooltips/leftCornerWidth.txt +2 -0
- package/resources/tooltips/numOfShelves.txt +6 -0
- package/resources/tooltips/positionName.txt +3 -0
- package/resources/tooltips/rightCornerDepth.txt +2 -0
- package/resources/tooltips/topDrwrHeight.txt +8 -0
- package/resources/tooltips/width.txt +5 -0
- package/src/client/api-client.ts +37 -0
- package/src/index.ts +52 -0
- package/src/tools/canvas.ts +442 -0
- package/src/tools/catalog.test.ts +61 -0
- package/src/tools/catalog.ts +80 -0
- package/src/tools/configuration-info.ts +274 -0
- package/src/tools/configuration.test.ts +43 -0
- package/src/tools/configuration.ts +25 -0
- package/src/tools/orders.test.ts +260 -0
- package/src/tools/orders.ts +1229 -0
- package/src/tools/saved-settings.ts +241 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { client, handleAxiosError } from '../client/api-client';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helper: flexible array schema that accepts both native arrays and JSON strings.
|
|
6
|
+
//
|
|
7
|
+
// WHY: MCP clients (e.g. Claude Code) validate tool arguments against the
|
|
8
|
+
// advertised JSON schema BEFORE sending the request to the server. A plain
|
|
9
|
+
// z.preprocess(..., z.array(...)) generates { type: "array" }, so the client
|
|
10
|
+
// rejects string values immediately with "params/<field> must be array" and
|
|
11
|
+
// the request never reaches the server's preprocess rescue logic.
|
|
12
|
+
//
|
|
13
|
+
// This helper generates { anyOf: [{ type: "array", ... }, { type: "string" }] }
|
|
14
|
+
// so the client validator passes strings through, while runtime validation in
|
|
15
|
+
// the string branch parses and re-validates the JSON before the handler runs.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function flexArray<T extends z.ZodTypeAny>(itemSchema: T, minItems = 1) {
|
|
18
|
+
return z.union([
|
|
19
|
+
z.array(itemSchema).min(minItems),
|
|
20
|
+
z.string().transform((str, ctx): z.infer<T>[] => {
|
|
21
|
+
let parsed: unknown;
|
|
22
|
+
try { parsed = JSON.parse(str); } catch {
|
|
23
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Value is not valid JSON' });
|
|
24
|
+
return z.NEVER as never;
|
|
25
|
+
}
|
|
26
|
+
const result = z.array(itemSchema).min(minItems).safeParse(parsed);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
for (const issue of result.error.issues) ctx.addIssue(issue);
|
|
29
|
+
return z.NEVER as never;
|
|
30
|
+
}
|
|
31
|
+
return result.data;
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Helper: Apply default gaps when gapControl = true
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if an article has gapControl = true and applies default gap values ("0.125" = 1/8")
|
|
42
|
+
* when gaps are not provided. Returns a new article object with defaults applied.
|
|
43
|
+
*/
|
|
44
|
+
async function applyGapDefaults(article: z.infer<typeof ArticleItemSchema>): Promise<z.infer<typeof ArticleItemSchema>> {
|
|
45
|
+
try {
|
|
46
|
+
// Fetch article configuration to check gapControl flag
|
|
47
|
+
const { data: config } = await client.get(`/catalog/${article.serialNumber}/configuration`);
|
|
48
|
+
|
|
49
|
+
// Check if gapControl is enabled for this article
|
|
50
|
+
if (config.gapControl === true) {
|
|
51
|
+
// Create a copy of the article to avoid mutating the original
|
|
52
|
+
const articleWithDefaults = { ...article };
|
|
53
|
+
|
|
54
|
+
// Apply default "0.125" (1/8") to any gap fields that are not provided
|
|
55
|
+
if (articleWithDefaults.gapTop === undefined) articleWithDefaults.gapTop = "0.125";
|
|
56
|
+
if (articleWithDefaults.gapBottom === undefined) articleWithDefaults.gapBottom = "0.125";
|
|
57
|
+
if (articleWithDefaults.gapLeft === undefined) articleWithDefaults.gapLeft = "0.125";
|
|
58
|
+
if (articleWithDefaults.gapRight === undefined) articleWithDefaults.gapRight = "0.125";
|
|
59
|
+
if (articleWithDefaults.gapCenter === undefined) articleWithDefaults.gapCenter = "0.125";
|
|
60
|
+
|
|
61
|
+
return articleWithDefaults;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// If we can't fetch configuration, proceed without applying defaults
|
|
65
|
+
// The API will handle any validation errors
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// No gapControl or error fetching config - return article as-is
|
|
69
|
+
return article;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Process an array of articles, applying gap defaults to each one.
|
|
74
|
+
* Returns a new array with defaults applied where applicable.
|
|
75
|
+
*/
|
|
76
|
+
async function applyGapDefaultsToArticles(
|
|
77
|
+
articles: z.infer<typeof ArticleItemSchema>[]
|
|
78
|
+
): Promise<z.infer<typeof ArticleItemSchema>[]> {
|
|
79
|
+
return Promise.all(articles.map(applyGapDefaults));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Helper: Apply saved setting preset values to articles
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cache for fetched saved settings to avoid redundant API calls.
|
|
88
|
+
* Each entry stores the data alongside a fetchedAt timestamp so entries
|
|
89
|
+
* older than SETTINGS_CACHE_TTL_MS are treated as stale and re-fetched.
|
|
90
|
+
* This handles updates made from the web portal (not just via MCP tools).
|
|
91
|
+
*/
|
|
92
|
+
const SETTINGS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
93
|
+
const savedSettingsCache = new Map<string, { data: any; fetchedAt: number }>();
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch a saved setting by name (from user settings or presets)
|
|
97
|
+
*/
|
|
98
|
+
async function fetchSavedSettingByName(name: string): Promise<any | null> {
|
|
99
|
+
// Return cached entry only if it is still within the TTL window
|
|
100
|
+
const cached = savedSettingsCache.get(name);
|
|
101
|
+
if (cached && Date.now() - cached.fetchedAt < SETTINGS_CACHE_TTL_MS) {
|
|
102
|
+
return cached.data;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Try fetching from user's saved settings first
|
|
107
|
+
const { data: userSettings } = await client.get('/saved-settings/user');
|
|
108
|
+
const userSetting = (userSettings as any[]).find((s: any) => s.name === name);
|
|
109
|
+
if (userSetting) {
|
|
110
|
+
savedSettingsCache.set(name, { data: userSetting, fetchedAt: Date.now() });
|
|
111
|
+
return userSetting;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try fetching from public presets
|
|
115
|
+
const { data: presets } = await client.get('/saved-settings/presets');
|
|
116
|
+
const preset = (presets as any[]).find((s: any) => s.name === name);
|
|
117
|
+
if (preset) {
|
|
118
|
+
savedSettingsCache.set(name, { data: preset, fetchedAt: Date.now() });
|
|
119
|
+
return preset;
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// If we can't fetch settings, proceed without applying preset values
|
|
123
|
+
// The API will handle validation
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Applies saved setting preset values to an article when settingsName is specified.
|
|
131
|
+
* This ensures ALL preset values are applied to the article fields, not just the
|
|
132
|
+
* settingsName reference. Fields explicitly set by the user take precedence over preset values.
|
|
133
|
+
*
|
|
134
|
+
* @param article - The article to apply preset values to
|
|
135
|
+
* @returns The article with preset values applied (or original if no preset)
|
|
136
|
+
*/
|
|
137
|
+
async function applySavedSettingToArticle<T extends { settingsName?: string; [key: string]: any }>(
|
|
138
|
+
article: T
|
|
139
|
+
): Promise<T> {
|
|
140
|
+
// If no settingsName is specified, return article as-is
|
|
141
|
+
if (!article.settingsName) {
|
|
142
|
+
return article;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Fetch the saved setting by name
|
|
146
|
+
const savedSetting = await fetchSavedSettingByName(article.settingsName);
|
|
147
|
+
if (!savedSetting) {
|
|
148
|
+
// Could not find the saved setting - return article as-is
|
|
149
|
+
// The API will return an error if the settingsName is invalid
|
|
150
|
+
return article;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create a copy of the article to avoid mutating the original
|
|
154
|
+
const articleWithPreset = { ...article } as any;
|
|
155
|
+
|
|
156
|
+
// Mapping of saved setting fields to article fields
|
|
157
|
+
// Only apply preset values if the article field is not already set (user values take precedence)
|
|
158
|
+
if (savedSetting.caseMaterial && !articleWithPreset.caseMaterial) {
|
|
159
|
+
articleWithPreset.caseMaterial = savedSetting.caseMaterial;
|
|
160
|
+
}
|
|
161
|
+
if (savedSetting.frontMaterial && !articleWithPreset.frontMaterial) {
|
|
162
|
+
articleWithPreset.frontMaterial = savedSetting.frontMaterial;
|
|
163
|
+
}
|
|
164
|
+
if (savedSetting.backPanelMaterial && !articleWithPreset.backPanelMaterial) {
|
|
165
|
+
articleWithPreset.backPanelMaterial = savedSetting.backPanelMaterial;
|
|
166
|
+
}
|
|
167
|
+
if (savedSetting.backPanel && !articleWithPreset.backPanel) {
|
|
168
|
+
articleWithPreset.backPanel = savedSetting.backPanel;
|
|
169
|
+
}
|
|
170
|
+
if (savedSetting.drawerType && !articleWithPreset.drawerType) {
|
|
171
|
+
articleWithPreset.drawerType = savedSetting.drawerType;
|
|
172
|
+
}
|
|
173
|
+
if (savedSetting.jointMethod && !articleWithPreset.jointMethod) {
|
|
174
|
+
articleWithPreset.jointMethod = savedSetting.jointMethod;
|
|
175
|
+
}
|
|
176
|
+
if (savedSetting.caseEdge && !articleWithPreset.caseEdge) {
|
|
177
|
+
articleWithPreset.caseEdge = savedSetting.caseEdge;
|
|
178
|
+
}
|
|
179
|
+
if (savedSetting.frontEdge && !articleWithPreset.frontEdge) {
|
|
180
|
+
articleWithPreset.frontEdge = savedSetting.frontEdge;
|
|
181
|
+
}
|
|
182
|
+
if (savedSetting.hingePlate && !articleWithPreset.hingePlate) {
|
|
183
|
+
articleWithPreset.hingePlate = savedSetting.hingePlate;
|
|
184
|
+
}
|
|
185
|
+
if (savedSetting.numOfShelves && !articleWithPreset.numOfShelves) {
|
|
186
|
+
articleWithPreset.numOfShelves = savedSetting.numOfShelves;
|
|
187
|
+
}
|
|
188
|
+
if (savedSetting.topDrwrHeightValue && !articleWithPreset.topDrwrHeightValue) {
|
|
189
|
+
articleWithPreset.topDrwrHeightValue = savedSetting.topDrwrHeightValue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Handle includeLegLevelers - convert string to boolean if needed
|
|
193
|
+
if (savedSetting.includeLegLevelers !== undefined && articleWithPreset.includeLegLevelers === undefined) {
|
|
194
|
+
// The saved setting stores this as a string ("true"/"false" or "1"/"0")
|
|
195
|
+
const val = savedSetting.includeLegLevelers;
|
|
196
|
+
articleWithPreset.includeLegLevelers = val === true || val === 'true' || val === '1' || val === 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Note: bottomPanelConnector maps to includeLegLevelers logic in some cases
|
|
200
|
+
// but we don't have a direct field mapping for it in ArticleItemSchema
|
|
201
|
+
|
|
202
|
+
return articleWithPreset as T;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Process an array of articles, applying saved setting preset values to each one.
|
|
207
|
+
* Returns a new array with preset values applied where settingsName is specified.
|
|
208
|
+
*/
|
|
209
|
+
async function applySavedSettingsToArticles<T extends { settingsName?: string; [key: string]: any }>(
|
|
210
|
+
articles: T[]
|
|
211
|
+
): Promise<T[]> {
|
|
212
|
+
return Promise.all(articles.map(applySavedSettingToArticle));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const ArticleItemSchema = z.object({
|
|
216
|
+
serialNumber: z.string(),
|
|
217
|
+
positionName: z.string().max(25).describe(
|
|
218
|
+
'Unique label for this cabinet within the order. Must be unique across ALL articles in the order. ' +
|
|
219
|
+
'Derive from the cabinet display name: "Upper 1125" → "Upper", "Base Cabinet" → "Base". ' +
|
|
220
|
+
'First of a type: just the prefix ("Upper"). Additional same-type cabinets: dot-number suffix ("Upper.1", "Upper.2"). ' +
|
|
221
|
+
'MULTI-QUANTITY NAMING (qty > 1): You MUST include the "_01" suffix in the positionName you provide. ' +
|
|
222
|
+
'The server starts from the number in your positionName and increments it for each unit. ' +
|
|
223
|
+
'Example: positionName "DR_B2_01" with quantity=3 → server creates "DR_B2_01", "DR_B2_02", "DR_B2_03". ' +
|
|
224
|
+
'Do NOT omit the suffix — a bare name like "DR_B2" triggers incorrect fallback parsing that produces wrong names (e.g. "DR_B_02"). ' +
|
|
225
|
+
'COORDINATE CONSEQUENCE: When you later call update_cabinet_coordinates for a qty>1 article, ' +
|
|
226
|
+
'you must supply a separate coordinate entry for EVERY expanded positionName (e.g. all three of "DR_B2_01", "DR_B2_02", "DR_B2_03"). ' +
|
|
227
|
+
'Max 25 characters, no dashes (use underscores).'
|
|
228
|
+
),
|
|
229
|
+
quantity: z.number().int().min(1),
|
|
230
|
+
height: z.number().describe('Height in inches'),
|
|
231
|
+
width: z.number().describe('Width in inches'),
|
|
232
|
+
depth: z.number().describe('Depth in inches'),
|
|
233
|
+
|
|
234
|
+
// Materials — use exact description strings returned by get_article_options
|
|
235
|
+
caseMaterial: z.string().optional().describe('Case/box material. Use exact string from get_article_options materials list.'),
|
|
236
|
+
frontMaterial: z.string().optional().describe('Door/drawer front material. Use exact string from get_article_options. Omit when excludeFronts is true.'),
|
|
237
|
+
innerCaseMaterial: z.string().optional().describe('Inner case material (only for articles with hasMatCaseThin=true). Use exact string from get_article_options materials list.'),
|
|
238
|
+
backPanelMaterial: z.string().optional().describe('Back panel material. Use exact string from get_article_options materials list.'),
|
|
239
|
+
|
|
240
|
+
// Edgebanding — standard articles
|
|
241
|
+
caseEdge: z.string().optional().describe('Case edgebanding. Use exact string from get_article_options edgebanding list.'),
|
|
242
|
+
frontEdge: z.string().optional().describe('Front edgebanding. Use exact string from get_article_options edgebanding list.'),
|
|
243
|
+
|
|
244
|
+
// Edgebanding — LP_SP/LP_GP series only
|
|
245
|
+
edgeBandingType: z.string().optional().describe('Edgebanding type for LP_SP/LP_GP series articles. Applied to all four edge positions. Use exact string from get_article_options edgebanding list.'),
|
|
246
|
+
|
|
247
|
+
// Configuration options — use exact description strings returned by get_article_options
|
|
248
|
+
drawerType: z.string().optional().describe('Drawer box construction type. Use exact string from get_article_options drawerType list.'),
|
|
249
|
+
jointMethod: z.string().optional().describe('Case joinery method. Use exact string from get_article_options jointMethod list.'),
|
|
250
|
+
hingePlate: z.string().optional().describe('Hinge plate type. Use exact string from get_article_options hingePlate list.'),
|
|
251
|
+
backPanel: z.string().optional().describe('Back panel attachment method. Use exact string from get_article_options backPanel list.'),
|
|
252
|
+
numOfShelves: z.string().optional().describe('Number of adjustable shelves. Use exact string from get_article_options numOfShelves list (e.g. "3", "0", "Parametric Shelves").'),
|
|
253
|
+
|
|
254
|
+
// Gap control — decimal inch strings
|
|
255
|
+
gapTop: z.string().optional().describe('Top reveal gap in inches. Valid values: "0" (flush), "0.0625" (1/16"), "0.125" (1/8").'),
|
|
256
|
+
gapBottom: z.string().optional().describe('Bottom reveal gap in inches. Valid values: "0", "0.0625", "0.125".'),
|
|
257
|
+
gapLeft: z.string().optional().describe('Left reveal gap in inches. Valid values: "0", "0.0625", "0.125".'),
|
|
258
|
+
gapRight: z.string().optional().describe('Right reveal gap in inches. Valid values: "0", "0.0625", "0.125".'),
|
|
259
|
+
gapCenter: z.string().optional().describe('Center gap between adjacent doors/drawers in inches. Valid values: "0", "0.0625", "0.125".'),
|
|
260
|
+
|
|
261
|
+
// Top drawer height (only for articles with topDrwrHeight feature)
|
|
262
|
+
topDrwrHeightValue: z.string().optional().describe('Custom top drawer height in inches (e.g. "12"). Only for articles that support topDrwrHeight. Valid range: 4–28.'),
|
|
263
|
+
|
|
264
|
+
// Corner cabinet dimensions
|
|
265
|
+
leftCornerWidth: z.number().optional().describe('Left corner width in inches. Only for corner cabinet articles with cornerVariables=true.'),
|
|
266
|
+
rightCornerDepth: z.number().optional().describe('Right corner depth in inches. Only for corner cabinet articles with cornerVariables=true.'),
|
|
267
|
+
|
|
268
|
+
// Boolean flags
|
|
269
|
+
excludeFronts: z.boolean().optional().describe('When true, fronts are excluded from this order (supplied by others). Sets frontMaterial to "Fronts By Others". Do not also set frontMaterial.'),
|
|
270
|
+
includeLegLevelers: z.boolean().optional().describe('When true, leg levelers are included. Only valid for articles with the Leg_Levelers filter tag.'),
|
|
271
|
+
|
|
272
|
+
// INSERTION POINT COORDINATE CONVENTION
|
|
273
|
+
// x, y, z, rotation = 2D plan coordinates + height + orientation, all in decimal inches / degrees.
|
|
274
|
+
// Pass CENTER X and CENTER Y directly — server stores as-is, no conversion applied.
|
|
275
|
+
//
|
|
276
|
+
// x: CENTER X of the cabinet on the plan in decimal inches.
|
|
277
|
+
// North wall (rotation=0) : x = left_edge + width/2 (left_edge = sum of widths of cabinets to the left + any filler)
|
|
278
|
+
// South wall (rotation=180): x = left_edge + width/2 (same — reading left-to-right on plan)
|
|
279
|
+
// East wall (rotation=90) : x = room_width - depth/2 (fixed value for all cabinets on this wall)
|
|
280
|
+
// West wall (rotation=270): x = depth/2 (fixed value for all cabinets on this wall)
|
|
281
|
+
//
|
|
282
|
+
// y: CENTER Y of the cabinet on the plan in decimal inches. REQUIRED for multi-wall kitchens.
|
|
283
|
+
// South wall (rotation=180): y = depth / 2
|
|
284
|
+
// North wall (rotation=0) : y = room_depth - (depth / 2)
|
|
285
|
+
// East wall (rotation=90) : y = bottom_edge + width/2 (bottom_edge = sum of widths of cabinets south of this one + any filler from corner)
|
|
286
|
+
// West wall (rotation=270): y = bottom_edge + width/2 (same method)
|
|
287
|
+
// If omitted, server defaults to depth/2 — correct only for south-wall single-wall layouts.
|
|
288
|
+
//
|
|
289
|
+
// z: height from floor. 0 for base cabinets. Stacked units: sum heights of all cabinets below.
|
|
290
|
+
//
|
|
291
|
+
// rotation: plan orientation in degrees (clockwise). 0 = north wall, 90 = east wall, 180 = south wall, 270 = west wall.
|
|
292
|
+
// If no compass rose, assume top=north, bottom=south, left=west, right=east.
|
|
293
|
+
// Determines which direction the cabinet face points in CAD.
|
|
294
|
+
// Omit or use 0 if all cabinets are on a single north wall.
|
|
295
|
+
x: z.number().optional().describe(
|
|
296
|
+
'Center X in decimal inches. See the coordinate formulas in the tool description — same rules apply here. ' +
|
|
297
|
+
'North/South wall: x = D_adj_west + cumulative_widths_left + this_width/2. ' +
|
|
298
|
+
'West wall base: x = depth/2 (NEVER negative). West wall upper front-flush: x = base_depth - upper_depth/2. ' +
|
|
299
|
+
'East wall base: x = room_width - depth/2. East wall upper front-flush: x = room_width - base_depth + upper_depth/2.'
|
|
300
|
+
),
|
|
301
|
+
y: z.number().optional().describe(
|
|
302
|
+
'Center Y in decimal inches. See the coordinate formulas in the tool description — same rules apply here. ' +
|
|
303
|
+
'South wall base: y = depth/2. South wall upper front-flush: y = base_depth - upper_depth/2. ' +
|
|
304
|
+
'North wall base: y = room_depth - depth/2. North wall upper front-flush: y = room_depth - base_depth + upper_depth/2. ' +
|
|
305
|
+
'East/West wall: cabinets run north-to-south (y decreases going south). List them northernmost first. ' +
|
|
306
|
+
'D_adj_north = north wall cabinet depth if present, else 0. ' +
|
|
307
|
+
'Northernmost cabinet: y = room_depth - D_adj_north - this_width/2. ' +
|
|
308
|
+
'Each next going south: y = room_depth - D_adj_north - (sum of widths of all preceding cabinets) - this_width/2. ' +
|
|
309
|
+
'NEVER add widths — adding goes north (wrong direction). ' +
|
|
310
|
+
'UPPER cabinets on east/west walls: y = same as the base cabinet below — do NOT change y for front-flush. Front-flush = x change only.'
|
|
311
|
+
),
|
|
312
|
+
z: z.number().optional().describe(
|
|
313
|
+
'Height from floor in decimal inches. ' +
|
|
314
|
+
'Base cabinets on the floor = 0. ' +
|
|
315
|
+
'Stacked units: sum the heights of all cabinets directly below this one. ' +
|
|
316
|
+
'Example: niche above a 28.375" base cabinet has z=28.375; upper above niche (14.125" tall) has z=42.5.'
|
|
317
|
+
),
|
|
318
|
+
rotation: z.number().optional().describe(
|
|
319
|
+
'Plan orientation in degrees (clockwise). ' +
|
|
320
|
+
'0 = north wall, 90 = east wall, 180 = south wall, 270 = west wall. ' +
|
|
321
|
+
'If the drawing has no compass rose, assume: top wall = north, bottom wall = south, left wall = west, right wall = east. ' +
|
|
322
|
+
'Determines which direction the cabinet face points on the plan and in CAD. ' +
|
|
323
|
+
'Default 0 for single-wall or north-wall installations.'
|
|
324
|
+
),
|
|
325
|
+
|
|
326
|
+
// Saved setting preset name — references a saved configuration preset applied to this article
|
|
327
|
+
// CRITICAL: If you are using values from a saved setting (obtained via get_my_saved_settings or get_saved_settings_presets),
|
|
328
|
+
// you MUST also set settingsName to the exact name of that saved setting.
|
|
329
|
+
// This ensures the preset_setting column is populated in the database for traceability.
|
|
330
|
+
// Example: If using "Zach's Kitchen" preset, set settingsName: "Zach's Kitchen"
|
|
331
|
+
settingsName: z.string().optional().describe(
|
|
332
|
+
'REQUIRED when using a saved setting preset: The exact name of the saved setting (e.g., "Zach\'s Kitchen", "Modern Euro"). ' +
|
|
333
|
+
'Obtain this from get_my_saved_settings or get_saved_settings_presets. ' +
|
|
334
|
+
'This field MUST be set when any configuration values (caseMaterial, frontMaterial, edgebanding, etc.) are taken from a saved setting.'
|
|
335
|
+
),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const ProjectAddressSchema = z.object({
|
|
339
|
+
projectAddress1: z.string().optional().describe('Project address line 1'),
|
|
340
|
+
projectAddress2: z.string().nullable().optional().describe('Project address line 2'),
|
|
341
|
+
projectCity: z.string().optional().describe('Project city'),
|
|
342
|
+
projectState: z.string().optional().describe('Project state'),
|
|
343
|
+
projectZipcode: z.string().optional().describe('Project zipcode'),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const ListOrdersSchema = z.object({
|
|
347
|
+
limit: z.number().int().min(1).max(100).default(20).describe('Max results (default 20, max 100)'),
|
|
348
|
+
offset: z.number().int().min(0).default(0).describe('Pagination offset'),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const GetOrderSchema = z.object({
|
|
352
|
+
orderId: z.string().describe('Order ID, e.g. ORD-001'),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const GetOrderBreakdownSchema = z.object({
|
|
356
|
+
orderId: z.string().describe('Order ID, e.g. ORD-001'),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const GetPositionBreakdownSchema = z.object({
|
|
360
|
+
orderId: z.string().describe('Order ID, e.g. ORD-001'),
|
|
361
|
+
positionName: z.string().describe('Position name of the cabinet, e.g. "Base", "Upper.1"'),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const UpdateOrderOptionsSchema = z.object({
|
|
365
|
+
orderId: z.string().describe('Order ID to update, e.g. ORD-001'),
|
|
366
|
+
includeDrawerboxes: z.boolean().optional().describe('Include drawer boxes in the order. Omit to leave unchanged.'),
|
|
367
|
+
includeAssembly: z.boolean().optional().describe('Include assembly in the order. Omit to leave unchanged.'),
|
|
368
|
+
includeHardware: z.boolean().optional().describe('Include hardware in the order. Omit to leave unchanged.'),
|
|
369
|
+
includeFinishing: z.boolean().optional().describe('Include finishing in the order. Omit to leave unchanged.'),
|
|
370
|
+
finishingType: z.enum(['Matte', 'Satin', 'Primed']).optional().describe('Finishing type. Required when setting includeFinishing to true. Options: Matte, Satin, Primed.'),
|
|
371
|
+
finishingColor: z.string().optional().describe('Finishing color (e.g. "White", "Black", "Natural"). Required when setting includeFinishing to true.'),
|
|
372
|
+
specialInstructions: z.string().optional().describe('Special instructions for the order. Pass an empty string to clear.'),
|
|
373
|
+
notes: z.string().optional().describe('Internal notes for the order. Pass an empty string to clear.'),
|
|
374
|
+
projectName: z.string().optional().describe('Project name for the order.'),
|
|
375
|
+
purchaseOrder: z.string().optional().describe('Purchase order number for the order.'),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const ListSavedCartsSchema = z.object({});
|
|
379
|
+
|
|
380
|
+
const CreateEditCartSchema = z.object({
|
|
381
|
+
orderId: z.string().describe('Order ID to create edit cart from'),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const SubmitCartAsVersionSchema = z.object({
|
|
385
|
+
tempOrderId: z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
|
|
386
|
+
projectName: z.string().optional().describe('Project name for the order (optional, uses saved cart value if not provided)'),
|
|
387
|
+
purchaseOrder: z.string().optional().describe('Purchase order number (optional, uses saved cart value if not provided)'),
|
|
388
|
+
includeDrawerboxes: z.boolean().optional().describe('Include drawer boxes in the order'),
|
|
389
|
+
includeAssembly: z.boolean().optional().describe('Include assembly in the order'),
|
|
390
|
+
includeHardware: z.boolean().optional().describe('Include hardware in the order'),
|
|
391
|
+
includeFinishing: z.boolean().optional().describe('Include finishing in the order'),
|
|
392
|
+
finishingType: z.enum(['Matte', 'Satin', 'Primed']).optional().describe('Finishing type (required if includeFinishing is true). Options: Matte, Satin, Primed'),
|
|
393
|
+
finishingColor: z.string().optional().describe('Finishing color (required if includeFinishing is true)'),
|
|
394
|
+
specialInstructions: z.string().optional().describe('Special instructions for the order'),
|
|
395
|
+
projectAddress: ProjectAddressSchema.optional().describe('Project address (optional, uses saved cart value if not provided)'),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const AddArticlesToCartSchema = z.object({
|
|
399
|
+
tempOrderId: z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
|
|
400
|
+
articles: flexArray(ArticleItemSchema).describe('Articles to add to the cart.'),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const GetSavedCartSchema = z.object({
|
|
404
|
+
tempOrderId: z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const UpdateArticleItemSchema = z.object({
|
|
408
|
+
positionName: z.string(),
|
|
409
|
+
serialNumber: z.string().optional(),
|
|
410
|
+
height: z.number().optional(),
|
|
411
|
+
width: z.number().optional(),
|
|
412
|
+
depth: z.number().optional(),
|
|
413
|
+
caseMaterial: z.string().optional(),
|
|
414
|
+
frontMaterial: z.string().optional(),
|
|
415
|
+
innerCaseMaterial: z.string().optional(),
|
|
416
|
+
backPanelMaterial: z.string().optional(),
|
|
417
|
+
caseEdge: z.string().optional(),
|
|
418
|
+
frontEdge: z.string().optional(),
|
|
419
|
+
edgeBandingType: z.string().optional(),
|
|
420
|
+
excludeFronts: z.boolean().optional(),
|
|
421
|
+
drawerType: z.string().optional(),
|
|
422
|
+
jointMethod: z.string().optional(),
|
|
423
|
+
hingePlate: z.string().optional(),
|
|
424
|
+
backPanel: z.string().optional(),
|
|
425
|
+
numOfShelves: z.string().optional(),
|
|
426
|
+
gapTop: z.string().optional(),
|
|
427
|
+
gapBottom: z.string().optional(),
|
|
428
|
+
gapLeft: z.string().optional(),
|
|
429
|
+
gapRight: z.string().optional(),
|
|
430
|
+
gapCenter: z.string().optional(),
|
|
431
|
+
topDrwrHeightValue: z.string().optional(),
|
|
432
|
+
leftCornerWidth: z.number().optional(),
|
|
433
|
+
rightCornerDepth: z.number().optional(),
|
|
434
|
+
includeLegLevelers: z.boolean().optional(),
|
|
435
|
+
// CRITICAL: If updating article to use a saved setting, MUST include settingsName
|
|
436
|
+
settingsName: z.string().optional().describe(
|
|
437
|
+
'REQUIRED when using a saved setting preset: The exact name of the saved setting. ' +
|
|
438
|
+
'Must be set when any configuration values are taken from a saved setting.'
|
|
439
|
+
),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const UpdateSavedCartArticlesSchema = z.object({
|
|
443
|
+
tempOrderId: z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
|
|
444
|
+
// x, z, rotation omitted — PATCH endpoint updates article configuration only, not CAD coordinates
|
|
445
|
+
articles: flexArray(UpdateArticleItemSchema).describe('Articles to update. positionName is required to identify each article; all other fields are optional.'),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const DeleteSavedCartArticlesSchema = z.object({
|
|
449
|
+
tempOrderId: z.string().describe('Saved cart reference ID — a plain numeric string returned by create_saved_cart or list_saved_carts (e.g. "901787"). Do NOT add any prefix such as "SavedCart_".'),
|
|
450
|
+
positionNames: flexArray(z.string()).describe('Position names of the articles to delete.'),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const CreateOrderSchema = z.object({
|
|
454
|
+
articles: flexArray(ArticleItemSchema).describe('Line items to order.'),
|
|
455
|
+
projectName: z.string().describe('Project name for this order'),
|
|
456
|
+
purchaseOrder: z.string().describe('Purchase order number'),
|
|
457
|
+
projectAddress: ProjectAddressSchema.optional().describe('Project address (where cabinets will be installed)'),
|
|
458
|
+
includeDrawerboxes: z.boolean().optional(),
|
|
459
|
+
includeAssembly: z.boolean().optional(),
|
|
460
|
+
includeHardware: z.boolean().optional(),
|
|
461
|
+
includeFinishing: z.boolean().optional(),
|
|
462
|
+
finishingType: z.enum(['Matte', 'Satin', 'Primed']).optional().describe('Finishing type (required if includeFinishing is true). Options: Matte, Satin, Primed'),
|
|
463
|
+
finishingColor: z.string().optional().describe('Finishing color (required if includeFinishing is true)'),
|
|
464
|
+
specialInstructions: z.string().optional(),
|
|
465
|
+
savedCartId: z.string().optional().describe(
|
|
466
|
+
'Optional: the tempOrderId of a saved cart whose canvas metadata (background image, scale calibration, ' +
|
|
467
|
+
'elevation setup) should be copied to the new order. Provide this when the user set up a drawing tool ' +
|
|
468
|
+
'canvas on a saved cart and wants that calibration carried over to the placed order.'
|
|
469
|
+
),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
function formatOrderBreakdown(data: any): string {
|
|
473
|
+
const lines: string[] = [];
|
|
474
|
+
const info = data.orderInfo ?? {};
|
|
475
|
+
|
|
476
|
+
lines.push(`Order Breakdown: ${info.orderId ?? ''}`);
|
|
477
|
+
lines.push(`Project: ${info.projectName ?? ''} | PO: ${info.purchaseOrder ?? ''}`);
|
|
478
|
+
lines.push(`Cabinets: ${data.numOfCabinets ?? 0}`);
|
|
479
|
+
|
|
480
|
+
const matEntries = Object.entries(data.materialSqft ?? {}) as [string, number][];
|
|
481
|
+
if (matEntries.length > 0) {
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push('MATERIALS');
|
|
484
|
+
for (const [name, sqft] of matEntries) {
|
|
485
|
+
const sheets = Math.max(1, Math.ceil((sqft / 32) * 1.2));
|
|
486
|
+
lines.push(` ${name}: ${sqft.toFixed(2)} sqft (est. ${sheets} sheet${sheets !== 1 ? 's' : ''})`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const prfEntries = Object.entries(data.prfFt ?? {}) as [string, number][];
|
|
491
|
+
if (prfEntries.length > 0) {
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push('EDGEBANDING');
|
|
494
|
+
for (const [name, footage] of prfEntries) {
|
|
495
|
+
lines.push(` ${name}: ${footage.toFixed(2)} ft`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const hwEntries = Object.entries(data.hardwareQuantity ?? {}) as [string, number][];
|
|
500
|
+
if (hwEntries.length > 0) {
|
|
501
|
+
lines.push('');
|
|
502
|
+
lines.push('HARDWARE');
|
|
503
|
+
for (const [name, qty] of hwEntries) {
|
|
504
|
+
lines.push(` ${name}: ${qty}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const drawers: any[] = data.drawers ?? [];
|
|
509
|
+
if (drawers.length > 0) {
|
|
510
|
+
lines.push('');
|
|
511
|
+
lines.push(`DRAWER BOXES (${drawers.length})`);
|
|
512
|
+
for (const d of drawers) {
|
|
513
|
+
const parts = [d.pos];
|
|
514
|
+
if (d.cpId) parts.push(d.cpId);
|
|
515
|
+
if (d.width != null && d.height != null && d.depth != null) {
|
|
516
|
+
parts.push(`${d.width}" W × ${d.height}" H × ${d.depth}" D`);
|
|
517
|
+
}
|
|
518
|
+
lines.push(` ${parts.join(' — ')}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const spp: any[] = data.spp ?? [];
|
|
523
|
+
if (spp.length > 0) {
|
|
524
|
+
lines.push('');
|
|
525
|
+
lines.push(`STRETCHABLE PARTS (${spp.length})`);
|
|
526
|
+
for (const s of spp) {
|
|
527
|
+
lines.push(` ${s.sppName}: ${s.sppLength} ft`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const doors: any[] = data.doorDimensions ?? [];
|
|
532
|
+
if (doors.length > 0) {
|
|
533
|
+
lines.push('');
|
|
534
|
+
lines.push(`DOOR PANELS (${doors.length})`);
|
|
535
|
+
for (const d of doors) {
|
|
536
|
+
const parts = [d.pos];
|
|
537
|
+
if (d.name1) parts.push(d.name1);
|
|
538
|
+
if (d.matId) parts.push(d.matId);
|
|
539
|
+
if (d.width != null && d.height != null) parts.push(`${d.width}" W × ${d.height}" H`);
|
|
540
|
+
lines.push(` ${parts.join(' — ')}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const drawerFronts: any[] = data.drawerFronts ?? [];
|
|
545
|
+
if (drawerFronts.length > 0) {
|
|
546
|
+
lines.push('');
|
|
547
|
+
lines.push(`DRAWER FRONTS (${drawerFronts.length})`);
|
|
548
|
+
for (const d of drawerFronts) {
|
|
549
|
+
const parts = [d.pos];
|
|
550
|
+
if (d.matId) parts.push(d.matId);
|
|
551
|
+
if (d.width != null && d.height != null) parts.push(`${d.width}" W × ${d.height}" H`);
|
|
552
|
+
lines.push(` ${parts.join(' — ')}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const blindFronts: any[] = data.blindFronts ?? [];
|
|
557
|
+
if (blindFronts.length > 0) {
|
|
558
|
+
lines.push('');
|
|
559
|
+
lines.push(`BLIND FRONTS (${blindFronts.length})`);
|
|
560
|
+
for (const d of blindFronts) {
|
|
561
|
+
const parts = [d.pos];
|
|
562
|
+
if (d.matId) parts.push(d.matId);
|
|
563
|
+
if (d.width != null && d.height != null) parts.push(`${d.width}" W × ${d.height}" H`);
|
|
564
|
+
lines.push(` ${parts.join(' — ')}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return lines.join('\n');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export async function getOrderBreakdown(input: z.infer<typeof GetOrderBreakdownSchema>): Promise<string> {
|
|
572
|
+
try {
|
|
573
|
+
const { data } = await client.get(`/orders/${input.orderId}/breakdown`);
|
|
574
|
+
return formatOrderBreakdown(data);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
577
|
+
return 'Unexpected error fetching order breakdown.';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function getPositionBreakdown(input: z.infer<typeof GetPositionBreakdownSchema>): Promise<string> {
|
|
582
|
+
try {
|
|
583
|
+
const { data } = await client.get(`/orders/${input.orderId}/position/${input.positionName}/breakdown`);
|
|
584
|
+
return JSON.stringify(data, null, 2);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
587
|
+
return 'Unexpected error fetching position breakdown.';
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export async function createEditCart(input: z.infer<typeof CreateEditCartSchema>): Promise<string> {
|
|
592
|
+
try {
|
|
593
|
+
const { data } = await client.post(`/saved-carts/${input.orderId}/create-edit-cart`);
|
|
594
|
+
const tempOrderId: string = data.tempOrderId ?? 'unknown';
|
|
595
|
+
const sourceOrderId: string = data.sourceOrderId ?? input.orderId;
|
|
596
|
+
return `Edit cart created successfully. Reference ID: ${tempOrderId}. Source order: ${sourceOrderId}. Inform the user that they can log in to their Sealab account at thesealab.com and navigate to Saved Carts to review their cabinet configuration before submitting the revised order.`;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
599
|
+
return 'Unexpected error creating edit cart.';
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function submitCartAsVersion(input: z.infer<typeof SubmitCartAsVersionSchema>): Promise<string> {
|
|
604
|
+
try {
|
|
605
|
+
const { data } = await client.post(`/saved-carts/${input.tempOrderId}/submit-as-version`, input);
|
|
606
|
+
const newOrderId: string = data.newOrderId ?? 'unknown';
|
|
607
|
+
const versionNumber: number = data.versionNumber ?? 0;
|
|
608
|
+
return `Order version created successfully. New order ID: ${newOrderId} (version ${versionNumber}). The saved cart has been deleted. Inform the user that they will receive an email notification and must log in to complete payment for the revised order.`;
|
|
609
|
+
} catch (error) {
|
|
610
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
611
|
+
return 'Unexpected error submitting cart as version.';
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export async function listOrders(input: z.infer<typeof ListOrdersSchema>): Promise<string> {
|
|
616
|
+
try {
|
|
617
|
+
const { data } = await client.get('/orders', { params: { limit: input.limit, offset: input.offset } });
|
|
618
|
+
if (!data || data.length === 0) return 'No orders found.';
|
|
619
|
+
return JSON.stringify(data, null, 2);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
622
|
+
return 'Unexpected error listing orders.';
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export async function getOrder(input: z.infer<typeof GetOrderSchema>): Promise<string> {
|
|
627
|
+
try {
|
|
628
|
+
const { data } = await client.get(`/orders/${input.orderId}`);
|
|
629
|
+
return JSON.stringify(data, null, 2);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
632
|
+
return 'Unexpected error fetching order.';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export async function createOrder(input: z.infer<typeof CreateOrderSchema>): Promise<string> {
|
|
637
|
+
// Enforce unique positionNames before sending to server
|
|
638
|
+
const names = input.articles.map((a) => a.positionName);
|
|
639
|
+
const seen = new Set<string>();
|
|
640
|
+
const duplicates = names.filter((n) => {
|
|
641
|
+
if (seen.has(n)) return true;
|
|
642
|
+
seen.add(n);
|
|
643
|
+
return false;
|
|
644
|
+
});
|
|
645
|
+
if (duplicates.length > 0) {
|
|
646
|
+
return `Cannot create order: duplicate positionName(s) detected: ${[...new Set(duplicates)].join(', ')}. Each article must have a unique positionName.`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Step 1: Apply saved setting preset values to articles (user values take precedence)
|
|
650
|
+
const articlesWithPresets = await applySavedSettingsToArticles(input.articles);
|
|
651
|
+
|
|
652
|
+
// Step 2: Apply gap defaults for articles with gapControl = true
|
|
653
|
+
const articlesWithDefaults = await applyGapDefaultsToArticles(articlesWithPresets);
|
|
654
|
+
const inputWithDefaults = { ...input, articles: articlesWithDefaults };
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
const { data } = await client.post('/orders', inputWithDefaults);
|
|
658
|
+
return `Order created successfully. Order ID: ${data.orderId}`;
|
|
659
|
+
} catch (error) {
|
|
660
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
661
|
+
return 'Unexpected error creating order.';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function formatSavedCart(data: any): string {
|
|
666
|
+
const lines: string[] = [];
|
|
667
|
+
lines.push(`Saved cart: ${data.tempOrderId}`);
|
|
668
|
+
lines.push(`Project: ${data.projectName ?? ''} | PO: ${data.purchaseOrder ?? ''}`);
|
|
669
|
+
lines.push('');
|
|
670
|
+
lines.push(`Articles (${data.articles?.length ?? 0} total):`);
|
|
671
|
+
|
|
672
|
+
for (let i = 0; i < (data.articles?.length ?? 0); i++) {
|
|
673
|
+
const a = data.articles[i];
|
|
674
|
+
lines.push(`${i + 1}. ${a.positionName} — ${a.displayName ?? ''} (${a.serialNumber ?? ''})`);
|
|
675
|
+
|
|
676
|
+
const sizeParts: string[] = [];
|
|
677
|
+
if (a.height != null) sizeParts.push(`${a.height}" H`);
|
|
678
|
+
if (a.width != null) sizeParts.push(`${a.width}" W`);
|
|
679
|
+
if (a.depth != null) sizeParts.push(`${a.depth}" D`);
|
|
680
|
+
if (sizeParts.length > 0) lines.push(` Size: ${sizeParts.join(' × ')}`);
|
|
681
|
+
|
|
682
|
+
const matParts: string[] = [];
|
|
683
|
+
if (a.caseMaterial != null) matParts.push(`Case: ${a.caseMaterial}`);
|
|
684
|
+
if (a.frontMaterial != null) matParts.push(`Front: ${a.frontMaterial}`);
|
|
685
|
+
if (a.excludeFronts === true) matParts.push('Exclude Fronts: true');
|
|
686
|
+
if (matParts.length > 0) lines.push(` ${matParts.join(' | ')}`);
|
|
687
|
+
|
|
688
|
+
const innerParts: string[] = [];
|
|
689
|
+
if (a.innerCaseMaterial != null) innerParts.push(`Inner Case: ${a.innerCaseMaterial}`);
|
|
690
|
+
if (a.backPanelMaterial != null) innerParts.push(`Back Panel Material: ${a.backPanelMaterial}`);
|
|
691
|
+
if (innerParts.length > 0) lines.push(` ${innerParts.join(' | ')}`);
|
|
692
|
+
|
|
693
|
+
const edgeParts: string[] = [];
|
|
694
|
+
if (a.caseEdge != null) edgeParts.push(`Case Edge: ${a.caseEdge}`);
|
|
695
|
+
if (a.frontEdge != null) edgeParts.push(`Front Edge: ${a.frontEdge}`);
|
|
696
|
+
if (a.edgeBandingType != null) edgeParts.push(`Edge Banding: ${a.edgeBandingType}`);
|
|
697
|
+
if (edgeParts.length > 0) lines.push(` ${edgeParts.join(' | ')}`);
|
|
698
|
+
|
|
699
|
+
const configParts: string[] = [];
|
|
700
|
+
if (a.drawerType != null) configParts.push(`Drawer Type: ${a.drawerType}`);
|
|
701
|
+
if (a.jointMethod != null) configParts.push(`Joint Method: ${a.jointMethod}`);
|
|
702
|
+
if (a.hingePlate != null) configParts.push(`Hinge Plate: ${a.hingePlate}`);
|
|
703
|
+
if (configParts.length > 0) lines.push(` ${configParts.join(' | ')}`);
|
|
704
|
+
|
|
705
|
+
const panelParts: string[] = [];
|
|
706
|
+
if (a.backPanel != null) panelParts.push(`Back Panel: ${a.backPanel}`);
|
|
707
|
+
if (a.numOfShelves != null) panelParts.push(`Shelves: ${a.numOfShelves}`);
|
|
708
|
+
if (a.includeLegLevelers === true) panelParts.push('Leg Levelers: true');
|
|
709
|
+
if (panelParts.length > 0) lines.push(` ${panelParts.join(' | ')}`);
|
|
710
|
+
|
|
711
|
+
const gapKeys = ['gapTop', 'gapBottom', 'gapLeft', 'gapRight', 'gapCenter'] as const;
|
|
712
|
+
const gapLabels: Record<string, string> = { gapTop: 'Top', gapBottom: 'Bottom', gapLeft: 'Left', gapRight: 'Right', gapCenter: 'Center' };
|
|
713
|
+
const gapParts = gapKeys.filter(k => a[k] != null).map(k => `${gapLabels[k]}: ${a[k]}`);
|
|
714
|
+
if (gapParts.length > 0) lines.push(` Gaps — ${gapParts.join(' | ')}`);
|
|
715
|
+
|
|
716
|
+
if (a.topDrwrHeightValue != null) lines.push(` Top Drawer Height: ${a.topDrwrHeightValue}`);
|
|
717
|
+
|
|
718
|
+
const cornerParts: string[] = [];
|
|
719
|
+
if (a.leftCornerWidth != null) cornerParts.push(`Left Width: ${a.leftCornerWidth}`);
|
|
720
|
+
if (a.rightCornerDepth != null) cornerParts.push(`Right Depth: ${a.rightCornerDepth}`);
|
|
721
|
+
if (cornerParts.length > 0) lines.push(` Corner: ${cornerParts.join(' | ')}`);
|
|
722
|
+
|
|
723
|
+
if (a.settingsName != null) lines.push(` Preset: ${a.settingsName}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return lines.join('\n');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function listSavedCarts(_input: z.infer<typeof ListSavedCartsSchema>): Promise<string> {
|
|
730
|
+
try {
|
|
731
|
+
const { data } = await client.get('/saved-carts');
|
|
732
|
+
const carts = data as any[];
|
|
733
|
+
if (!carts || carts.length === 0) return 'No saved carts found.';
|
|
734
|
+
|
|
735
|
+
const lines = carts.map((cart) => {
|
|
736
|
+
const parts: string[] = [`• ${cart.tempOrderId}`];
|
|
737
|
+
if (cart.projectName) parts.push(` Project: ${cart.projectName}`);
|
|
738
|
+
if (cart.purchaseOrder) parts.push(` PO: ${cart.purchaseOrder}`);
|
|
739
|
+
parts.push(` Articles: ${cart.articleCount}`);
|
|
740
|
+
if (cart.isEditCart && cart.sourceOrderId) parts.push(` Edit of order: ${cart.sourceOrderId}`);
|
|
741
|
+
if (cart.date) parts.push(` Date: ${cart.date}`);
|
|
742
|
+
return parts.join('\n');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
return `Saved carts (${carts.length}):\n\n${lines.join('\n\n')}`;
|
|
746
|
+
} catch (error) {
|
|
747
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
748
|
+
return 'Unexpected error listing saved carts.';
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export async function getSavedCart(input: z.infer<typeof GetSavedCartSchema>): Promise<string> {
|
|
753
|
+
try {
|
|
754
|
+
const { data } = await client.get(`/saved-carts/${input.tempOrderId}`);
|
|
755
|
+
return formatSavedCart(data);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
758
|
+
return 'Unexpected error fetching saved cart.';
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export async function createSavedCart(input: z.infer<typeof CreateOrderSchema>): Promise<string> {
|
|
763
|
+
// Client-side duplicate positionName guard
|
|
764
|
+
const names = input.articles.map((a) => a.positionName);
|
|
765
|
+
const seen = new Set<string>();
|
|
766
|
+
const duplicates = names.filter((n) => {
|
|
767
|
+
if (seen.has(n)) return true;
|
|
768
|
+
seen.add(n);
|
|
769
|
+
return false;
|
|
770
|
+
});
|
|
771
|
+
if (duplicates.length > 0) {
|
|
772
|
+
return `Cannot create saved cart: duplicate positionName(s) detected: ${[...new Set(duplicates)].join(', ')}. Each article must have a unique positionName.`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Step 1: Apply saved setting preset values to articles (user values take precedence)
|
|
776
|
+
const articlesWithPresets = await applySavedSettingsToArticles(input.articles);
|
|
777
|
+
|
|
778
|
+
// Step 2: Apply gap defaults for articles with gapControl = true
|
|
779
|
+
const articlesWithDefaults = await applyGapDefaultsToArticles(articlesWithPresets);
|
|
780
|
+
const inputWithDefaults = { ...input, articles: articlesWithDefaults };
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
const { data } = await client.post('/saved-carts', inputWithDefaults);
|
|
784
|
+
const tempOrderId: string = data.tempOrderId ?? 'unknown';
|
|
785
|
+
return `Saved cart created successfully. Reference ID: ${tempOrderId}. Please inform the user that they can log in to their Sealab account at thesealab.com and navigate to Saved Carts to review their cabinet configuration before placing the order.`;
|
|
786
|
+
} catch (error) {
|
|
787
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
788
|
+
return 'Unexpected error creating saved cart.';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export async function updateSavedCartArticles(input: z.infer<typeof UpdateSavedCartArticlesSchema>): Promise<string> {
|
|
793
|
+
// Client-side duplicate positionName guard (same pattern as createSavedCart)
|
|
794
|
+
const names = input.articles.map((a) => a.positionName);
|
|
795
|
+
const seen = new Set<string>();
|
|
796
|
+
const duplicates = names.filter((n) => {
|
|
797
|
+
if (seen.has(n)) return true;
|
|
798
|
+
seen.add(n);
|
|
799
|
+
return false;
|
|
800
|
+
});
|
|
801
|
+
if (duplicates.length > 0) {
|
|
802
|
+
return `Cannot update saved cart: duplicate positionName(s) in request: ${[...new Set(duplicates)].join(', ')}. Each article must appear only once per update call.`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Step 1: Apply saved setting preset values to articles (user values take precedence)
|
|
806
|
+
const articlesWithPresets = await applySavedSettingsToArticles(input.articles);
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const { data } = await client.patch(`/saved-carts/${input.tempOrderId}/articles`, {
|
|
810
|
+
articles: articlesWithPresets,
|
|
811
|
+
});
|
|
812
|
+
return `Updated ${data.updated} article(s) in saved cart ${input.tempOrderId}.`;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
815
|
+
return 'Unexpected error updating saved cart articles.';
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function addArticlesToCart(input: z.infer<typeof AddArticlesToCartSchema>): Promise<string> {
|
|
820
|
+
// Client-side duplicate positionName guard
|
|
821
|
+
const names = input.articles.map((a) => a.positionName);
|
|
822
|
+
const seen = new Set<string>();
|
|
823
|
+
const duplicates = names.filter((n) => {
|
|
824
|
+
if (seen.has(n)) return true;
|
|
825
|
+
seen.add(n);
|
|
826
|
+
return false;
|
|
827
|
+
});
|
|
828
|
+
if (duplicates.length > 0) {
|
|
829
|
+
return `Cannot add articles: duplicate positionName(s) in request: ${[...new Set(duplicates)].join(', ')}. Each article must have a unique positionName.`;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Step 1: Apply saved setting preset values to articles (user values take precedence)
|
|
833
|
+
const articlesWithPresets = await applySavedSettingsToArticles(input.articles);
|
|
834
|
+
|
|
835
|
+
// Step 2: Apply gap defaults for articles with gapControl = true
|
|
836
|
+
const articlesWithDefaults = await applyGapDefaultsToArticles(articlesWithPresets);
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const { data } = await client.post(`/saved-carts/${input.tempOrderId}/articles`, { articles: articlesWithDefaults });
|
|
840
|
+
return `Added ${data.added} article(s) to saved cart ${input.tempOrderId}. Total cart ID: ${data.tempOrderId}`;
|
|
841
|
+
} catch (error) {
|
|
842
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
843
|
+
return 'Unexpected error adding articles to saved cart.';
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export async function deleteSavedCartArticles(input: z.infer<typeof DeleteSavedCartArticlesSchema>): Promise<string> {
|
|
848
|
+
try {
|
|
849
|
+
const { data } = await client.delete(`/saved-carts/${input.tempOrderId}/articles`, {
|
|
850
|
+
data: { positionNames: input.positionNames },
|
|
851
|
+
});
|
|
852
|
+
return `Deleted ${data.deleted} article(s) from saved cart ${input.tempOrderId}: [${input.positionNames.join(', ')}].`;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
855
|
+
return 'Unexpected error deleting saved cart articles.';
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function updateOrderOptions(input: z.infer<typeof UpdateOrderOptionsSchema>): Promise<string> {
|
|
860
|
+
const { orderId, ...fields } = input;
|
|
861
|
+
// Only send keys the caller explicitly provided (strip undefined values)
|
|
862
|
+
const payload: Record<string, unknown> = {};
|
|
863
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
864
|
+
if (val !== undefined) payload[key] = val;
|
|
865
|
+
}
|
|
866
|
+
if (Object.keys(payload).length === 0) {
|
|
867
|
+
return 'No options provided. Supply at least one of: includeDrawerboxes, includeAssembly, includeHardware, includeFinishing, finishingType, finishingColor.';
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
const { data } = await client.patch(`/orders/${orderId}/options`, payload);
|
|
871
|
+
return JSON.stringify(data, null, 2);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
try { handleAxiosError(error); } catch (e: any) { return e.message; }
|
|
874
|
+
return 'Unexpected error updating order options.';
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export const orderTools = [
|
|
879
|
+
{
|
|
880
|
+
name: 'list_orders',
|
|
881
|
+
description: 'List orders on this account. Supports pagination via limit (default 20, max 100) and offset.',
|
|
882
|
+
inputSchema: ListOrdersSchema,
|
|
883
|
+
handler: listOrders,
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'get_order',
|
|
887
|
+
description: `Get full details and current status for a specific order by its order ID.
|
|
888
|
+
|
|
889
|
+
Returns order information including:
|
|
890
|
+
- orderId, status, date, price, projectName, purchaseOrder
|
|
891
|
+
- articles: Array of cabinet articles, each with:
|
|
892
|
+
- positionName: UNIQUE identifier for the cabinet (e.g., "Base", "Upper.1", "SouthU_009") — USE THIS to identify cabinets
|
|
893
|
+
- serialNumber: Cabinet type/model number (e.g., "BC_O_1048")
|
|
894
|
+
- quantity, height, width, depth
|
|
895
|
+
|
|
896
|
+
IMPORTANT: When referencing a specific cabinet in an order, ALWAYS use positionName (not serialNumber), as multiple cabinets can have the same serialNumber but different positionNames.`,
|
|
897
|
+
inputSchema: GetOrderSchema,
|
|
898
|
+
handler: getOrder,
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: 'get_order_breakdown',
|
|
902
|
+
description: 'Get the full Bill of Materials (BOM) breakdown for a placed order. Returns aggregated materials with square footage and estimated sheet counts, edgebanding footage, hardware quantities, drawer boxes with dimensions, drawer fronts, door panels, blind fronts, and stretchable purchased parts. Call this after get_order when the user needs component-level detail for an order.',
|
|
903
|
+
inputSchema: GetOrderBreakdownSchema,
|
|
904
|
+
handler: getOrderBreakdown,
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: 'get_position_breakdown',
|
|
908
|
+
description: 'Get detailed breakdown for a single cabinet position within an order. Returns materials (sqft), edgebanding, hardware, and drawer boxes for the specified positionName. Use this when the user asks about a specific cabinet\'s construction, materials, or components.',
|
|
909
|
+
inputSchema: GetPositionBreakdownSchema,
|
|
910
|
+
handler: getPositionBreakdown,
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: 'update_order_options',
|
|
914
|
+
description: `Update order-level options on a placed order WITHOUT creating a new version or going through the cart/edit workflow. Changes are persisted immediately and broadcast to the frontend in real time.
|
|
915
|
+
|
|
916
|
+
Use this tool when the user wants to:
|
|
917
|
+
- Add or remove an add-on service: "Add drawer boxes to order ORD-123", "Remove hardware from my order"
|
|
918
|
+
- Enable finishing: "Enable finishing on order ORD-456, Matte, White"
|
|
919
|
+
- Update special instructions: "Change the special instructions on ORD-123 to 'deliver to back entrance'"
|
|
920
|
+
- Update notes: "Add a note to order ORD-456"
|
|
921
|
+
- Rename the project: "Change the project name on ORD-123 to 'Smith Kitchen'"
|
|
922
|
+
- Update the PO number: "Set purchase order to PO-9981 on ORD-456"
|
|
923
|
+
|
|
924
|
+
Only the fields you pass are updated. Omitted fields are left exactly as they are on the order.
|
|
925
|
+
|
|
926
|
+
If includeFinishing is being set to true, you MUST also provide finishingType and finishingColor.
|
|
927
|
+
|
|
928
|
+
Returns the full updated state of all options after the change.`,
|
|
929
|
+
inputSchema: UpdateOrderOptionsSchema,
|
|
930
|
+
handler: updateOrderOptions,
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: 'create_edit_cart',
|
|
934
|
+
description: `Create a saved cart from an EXISTING order for editing. Use this tool when the user wants to edit or revise a previously placed order.
|
|
935
|
+
|
|
936
|
+
⚠️ USE THIS TOOL when:
|
|
937
|
+
- User says "edit order 429308" or "revise order ORD-123"
|
|
938
|
+
- User wants to modify cabinets in an existing order
|
|
939
|
+
- You have an existing order ID to work with
|
|
940
|
+
|
|
941
|
+
DO NOT use this tool when:
|
|
942
|
+
- User is creating a BRAND NEW order (use create_saved_cart instead)
|
|
943
|
+
- There is no existing order ID
|
|
944
|
+
|
|
945
|
+
This tool:
|
|
946
|
+
1. Creates a saved cart with ALL articles from the source order
|
|
947
|
+
2. Sets the sourceOrderId and isEditCart flags (required for versioned submission)
|
|
948
|
+
3. Returns a tempOrderId for the edit cart
|
|
949
|
+
|
|
950
|
+
After calling this tool:
|
|
951
|
+
- The user can review the cart at thesealab.com in Saved Carts
|
|
952
|
+
- Use update_saved_cart_articles to modify cabinet configurations
|
|
953
|
+
- Use submit_cart_as_version to create a versioned order (ORDER123_v2)
|
|
954
|
+
|
|
955
|
+
The saved cart preserves all order settings (project name, PO, special instructions) from the source order.`,
|
|
956
|
+
inputSchema: CreateEditCartSchema,
|
|
957
|
+
handler: createEditCart,
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
name: 'submit_cart_as_version',
|
|
961
|
+
description: `Submit a saved cart as a VERSIONED ORDER (revision of an existing order). This creates a new version like ORDER123_v2.
|
|
962
|
+
|
|
963
|
+
⚠️ USE THIS TOOL ONLY WHEN EDITING AN EXISTING ORDER ⚠️
|
|
964
|
+
|
|
965
|
+
This tool:
|
|
966
|
+
1. Creates a new VERSION of an existing order (e.g., ORDER123_v2)
|
|
967
|
+
2. Copies canvas data, coordinates, and permissions from the source order
|
|
968
|
+
3. Applies the modified articles from the saved cart
|
|
969
|
+
4. Deletes the saved cart after successful submission
|
|
970
|
+
|
|
971
|
+
Use submit_cart_as_version ONLY when:
|
|
972
|
+
- User explicitly says "edit order XXX" or "revise order XXX"
|
|
973
|
+
- The saved cart was created using create_edit_cart (which sets sourceOrderId)
|
|
974
|
+
- You are submitting changes to a previously placed order
|
|
975
|
+
|
|
976
|
+
DO NOT use this tool for:
|
|
977
|
+
- New orders (use create_order instead)
|
|
978
|
+
- Saved carts created via create_saved_cart (use create_order instead)
|
|
979
|
+
|
|
980
|
+
BEFORE calling this tool, you MUST explicitly ask the user to confirm ALL of the following:
|
|
981
|
+
- includeDrawerboxes: should drawer boxes be included? (true or false)
|
|
982
|
+
- includeAssembly: should assembly be included? (true or false)
|
|
983
|
+
- includeHardware: should hardware be included? (true or false)
|
|
984
|
+
- includeFinishing: should finishing be included? (true or false)
|
|
985
|
+
- If includeFinishing is true, you MUST also ask:
|
|
986
|
+
- finishingType: select from Matte, Satin, or Primed
|
|
987
|
+
- finishingColor: the desired color name (e.g., "White", "Black", "Natural")
|
|
988
|
+
- specialInstructions: any special notes or instructions? (optional)
|
|
989
|
+
- projectAddress: project address where cabinets will be installed (optional)
|
|
990
|
+
- projectAddress1, projectAddress2, projectCity, projectState, projectZipcode
|
|
991
|
+
|
|
992
|
+
After submission:
|
|
993
|
+
- The user will receive an email notification
|
|
994
|
+
- Payment must be completed by the user via the web portal
|
|
995
|
+
- The saved cart is deleted`,
|
|
996
|
+
inputSchema: SubmitCartAsVersionSchema,
|
|
997
|
+
handler: submitCartAsVersion,
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: 'create_order',
|
|
1001
|
+
description: `Place a NEW cabinet order. Use this tool when the user wants to submit a brand new order (not an edit/revision of an existing order).
|
|
1002
|
+
|
|
1003
|
+
This tool creates a completely new order with a new order ID.
|
|
1004
|
+
|
|
1005
|
+
Use create_order when:
|
|
1006
|
+
- User is placing a first-time order
|
|
1007
|
+
- User has a saved cart created via create_saved_cart (not create_edit_cart)
|
|
1008
|
+
- User says "submit my order" or "place the order" for a new order
|
|
1009
|
+
|
|
1010
|
+
DO NOT use this tool when editing an existing order — use submit_cart_as_version instead.
|
|
1011
|
+
|
|
1012
|
+
BEFORE calling this tool, you MUST explicitly ask the user to confirm ALL of the following — do not assume or skip any:
|
|
1013
|
+
|
|
1014
|
+
ORDER-LEVEL (ask once for the whole order):
|
|
1015
|
+
- projectName: the name for this project/order
|
|
1016
|
+
- purchaseOrder: their PO or reference number
|
|
1017
|
+
- includeDrawerboxes: should drawer boxes be included? (true or false)
|
|
1018
|
+
- includeAssembly: should assembly be included? (true or false)
|
|
1019
|
+
- includeHardware: should hardware be included? (true or false)
|
|
1020
|
+
- includeFinishing: should finishing be included? (true or false)
|
|
1021
|
+
- If includeFinishing is true, you MUST also ask:
|
|
1022
|
+
- finishingType: select from Matte, Satin, or Primed
|
|
1023
|
+
- finishingColor: the desired color name (e.g., "White", "Black", "Natural")
|
|
1024
|
+
- specialInstructions: any special notes or instructions? (optional — ask if they have any)
|
|
1025
|
+
- projectAddress: project address where cabinets will be installed (optional)
|
|
1026
|
+
- projectAddress1, projectAddress2, projectCity, projectState, projectZipcode
|
|
1027
|
+
|
|
1028
|
+
ARTICLE-LEVEL (must be confirmed per article, or as a shared spec across articles):
|
|
1029
|
+
- All applicable configuration fields: materials, edgebanding, joint method, drawer type, hinge plate, back panel, shelves, gaps, leg levelers, etc.
|
|
1030
|
+
- positionName for each article (unique, derived from cabinet type/location)
|
|
1031
|
+
- x, y, z, rotation: if an architectural drawing is provided, derive from the layout.
|
|
1032
|
+
Pass back-edge X and back-edge Y directly — server stores as-is, no conversion applied.
|
|
1033
|
+
x (back-edge X, decimal inches):
|
|
1034
|
+
North/South wall (rotation=0/180):
|
|
1035
|
+
x = D_adj_west + (sum of widths of all cabinets to the left on that wall + any filler) + width/2.
|
|
1036
|
+
D_adj_west = depth of west-wall cabinets if west wall cabinets exist in this layout, else 0.
|
|
1037
|
+
CORNER CLEARANCE: north/south wall cabinets must start at x=D_adj_west, NOT x=0.
|
|
1038
|
+
D_adj_west applies ONLY here — it does NOT affect east/west wall cabinet x.
|
|
1039
|
+
Similarly, the rightmost north/south cabinet must end no further right than x = room_width - D_adj_east.
|
|
1040
|
+
West wall (rotation=270): back-edge coordinate (distance from west wall to cabinet BACK).
|
|
1041
|
+
Base cabinet: x = 0 (back edge against west wall).
|
|
1042
|
+
Shallower cabinet (e.g. upper) front-flush with base: x = base_depth - upper_depth.
|
|
1043
|
+
Example: 24" base + 15" upper → x = 24 - 15 = 9".
|
|
1044
|
+
Verification: base front at 0+24=24", upper front at 9+15=24" ✅ MATCH.
|
|
1045
|
+
East wall (rotation=90): back-edge coordinate (distance from west wall to cabinet BACK).
|
|
1046
|
+
Base cabinet: x = room_width (back edge against east wall). Requires room_width from drawing.
|
|
1047
|
+
Shallower cabinet front-flush: x = base_x - (base_depth - upper_depth).
|
|
1048
|
+
Example: base x=120 (24" deep) + 15" upper → upper x = 120 - 9 = 111".
|
|
1049
|
+
Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.
|
|
1050
|
+
CRITICAL: Upper x = base_x - (base_depth - upper_depth). Just offset from base — no room dimension math.
|
|
1051
|
+
IF room_width IS UNKNOWN: ask the user for room dimensions from the drawing to set base x.
|
|
1052
|
+
y (back-edge Y, decimal inches) — ABSOLUTE room coordinate (0 = south wall face, room_depth = north wall face).
|
|
1053
|
+
CRITICAL: Y is the BACK-EDGE coordinate (distance from south wall face to cabinet BACK), NOT center.
|
|
1054
|
+
North wall and all east/west y values require room_depth — ask the user if unknown.
|
|
1055
|
+
South wall (rotation=180):
|
|
1056
|
+
Base cabinet: y = 0 (back edge against wall).
|
|
1057
|
+
Shallower cabinet (e.g. upper) front-flush with base: y = base_depth - upper_depth.
|
|
1058
|
+
Example: 24" base + 15" upper front-flush → upper y = 24 - 15 = 9".
|
|
1059
|
+
BASE cabinet y is NEVER modified. Only the shallower cabinet y shifts north (away from wall).
|
|
1060
|
+
North wall (rotation=0):
|
|
1061
|
+
Base cabinet: y = room_depth (back edge against north wall).
|
|
1062
|
+
Shallower cabinet front-flush: y = base_y - (base_depth - upper_depth).
|
|
1063
|
+
Example: base y=120 (24" deep) + 15" upper → upper y = 120 - 9 = 111".
|
|
1064
|
+
Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.
|
|
1065
|
+
CRITICAL: Upper y = base_y - (base_depth - upper_depth). Just offset from base — no room dimension math.
|
|
1066
|
+
East wall (rotation=90):
|
|
1067
|
+
"Width" = this cabinet's own width dimension, running north-south after 90° rotation.
|
|
1068
|
+
East/west wall cabinets run from the north wall SOUTHWARD (decreasing y). List them north-to-south.
|
|
1069
|
+
D_adj_north = the catalog DEPTH of the north-wall BASE cabinet (the "depth" used in north wall y = room_depth).
|
|
1070
|
+
Example: north wall has 24"-deep bases → D_adj_north = 24.
|
|
1071
|
+
NEVER set D_adj_north = 0 when north wall cabinets exist — that places east/west cabinet inside the north wall footprint.
|
|
1072
|
+
Northernmost cabinet (1st in list): y = room_depth - D_adj_north - width/2.
|
|
1073
|
+
Each next cabinet going south: y = room_depth - D_adj_north - (sum of widths of preceding cabinets) - width/2.
|
|
1074
|
+
y DECREASES with each cabinet going south. NEVER add widths — adding goes north (wrong direction).
|
|
1075
|
+
IF room_depth IS UNKNOWN: ask the user for room dimensions before computing east/west wall y.
|
|
1076
|
+
UPPER cabinets: DO NOT compute y — copy the base cabinet y value exactly. No formula. No D_adj_north.
|
|
1077
|
+
Do NOT apply north/south front-flush y formulas to east/west wall cabinets.
|
|
1078
|
+
West wall (rotation=270): y = same formula as east wall.
|
|
1079
|
+
z = height from floor in inches. 0 for base cabinets on the floor. Stacked units: sum heights of all cabinets below.
|
|
1080
|
+
rotation: 0 = north wall, 90 = east wall, 180 = south wall, 270 = west wall.
|
|
1081
|
+
If no compass rose, assume top = north, bottom = south, left = west, right = east.
|
|
1082
|
+
If no drawing is provided, omit coordinates (cabinets placed in fallback row in CAD file).
|
|
1083
|
+
|
|
1084
|
+
Only call this tool once ALL of the above have been explicitly confirmed by the user. Never auto-fill, assume, or default any of these fields.`,
|
|
1085
|
+
inputSchema: CreateOrderSchema,
|
|
1086
|
+
handler: createOrder,
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: 'create_saved_cart',
|
|
1090
|
+
description: `Save a NEW cabinet configuration as a saved cart for the user to review before committing to an order.
|
|
1091
|
+
|
|
1092
|
+
⚠️ For NEW orders ONLY — DO NOT use when editing an existing order ⚠️
|
|
1093
|
+
|
|
1094
|
+
Use create_saved_cart ONLY when:
|
|
1095
|
+
- User is creating a BRAND NEW order from scratch
|
|
1096
|
+
- User is configuring cabinets for the first time
|
|
1097
|
+
- There is NO existing order ID to edit
|
|
1098
|
+
|
|
1099
|
+
Use create_edit_cart when:
|
|
1100
|
+
- User says "edit order 429308" or "revise order ORD-123"
|
|
1101
|
+
- User wants to modify a previously placed order
|
|
1102
|
+
- You have an existing order ID to work with
|
|
1103
|
+
|
|
1104
|
+
BEFORE calling this tool, you MUST explicitly ask the user to confirm ALL of the following — do not assume or skip any:
|
|
1105
|
+
|
|
1106
|
+
ORDER-LEVEL (ask once for the whole order):
|
|
1107
|
+
- projectName: the name for this project/order
|
|
1108
|
+
- purchaseOrder: their PO or reference number
|
|
1109
|
+
- includeDrawerboxes: should drawer boxes be included? (true or false)
|
|
1110
|
+
- includeAssembly: should assembly be included? (true or false)
|
|
1111
|
+
- includeHardware: should hardware be included? (true or false)
|
|
1112
|
+
- includeFinishing: should finishing be included? (true or false)
|
|
1113
|
+
- specialInstructions: any special notes or instructions? (optional — ask if they have any)
|
|
1114
|
+
- projectAddress: project address where cabinets will be installed (optional)
|
|
1115
|
+
- projectAddress1, projectAddress2, projectCity, projectState, projectZipcode
|
|
1116
|
+
|
|
1117
|
+
ARTICLE-LEVEL (must be confirmed per article, or as a shared spec across articles):
|
|
1118
|
+
- All applicable configuration fields: materials, edgebanding, joint method, drawer type, hinge plate, back panel, shelves, gaps, leg levelers, etc.
|
|
1119
|
+
- positionName for each article (unique, derived from cabinet type/location)
|
|
1120
|
+
- x, y, z, rotation: if an architectural drawing is provided, derive from the layout.
|
|
1121
|
+
Pass back-edge X and back-edge Y directly — server stores as-is, no conversion applied.
|
|
1122
|
+
x (back-edge X, decimal inches):
|
|
1123
|
+
North/South wall (rotation=0/180):
|
|
1124
|
+
x = D_adj_west + (sum of widths of all cabinets to the left on that wall + any filler) + width/2.
|
|
1125
|
+
D_adj_west = depth of west-wall cabinets if west wall cabinets exist in this layout, else 0.
|
|
1126
|
+
CORNER CLEARANCE: north/south wall cabinets must start at x=D_adj_west, NOT x=0.
|
|
1127
|
+
D_adj_west applies ONLY here — it does NOT affect east/west wall cabinet x.
|
|
1128
|
+
Similarly, the rightmost north/south cabinet must end no further right than x = room_width - D_adj_east.
|
|
1129
|
+
West wall (rotation=270): back-edge coordinate (distance from west wall to cabinet BACK).
|
|
1130
|
+
Base cabinet: x = 0 (back edge against west wall).
|
|
1131
|
+
Shallower cabinet (e.g. upper) front-flush with base: x = base_depth - upper_depth.
|
|
1132
|
+
Example: 24" base + 15" upper → x = 24 - 15 = 9".
|
|
1133
|
+
Verification: base front at 0+24=24", upper front at 9+15=24" ✅ MATCH.
|
|
1134
|
+
East wall (rotation=90): back-edge coordinate (distance from west wall to cabinet BACK).
|
|
1135
|
+
Base cabinet: x = room_width (back edge against east wall). Requires room_width from drawing.
|
|
1136
|
+
Shallower cabinet front-flush: x = base_x - (base_depth - upper_depth).
|
|
1137
|
+
Example: base x=120 (24" deep) + 15" upper → upper x = 120 - 9 = 111".
|
|
1138
|
+
Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.
|
|
1139
|
+
CRITICAL: Upper x = base_x - (base_depth - upper_depth). Just offset from base — no room dimension math.
|
|
1140
|
+
IF room_width IS UNKNOWN: ask the user for room dimensions from the drawing to set base x.
|
|
1141
|
+
y (back-edge Y, decimal inches) — ABSOLUTE room coordinate (0 = south wall face, room_depth = north wall face).
|
|
1142
|
+
CRITICAL: Y is the BACK-EDGE coordinate (distance from south wall face to cabinet BACK), NOT center.
|
|
1143
|
+
North wall and all east/west y values require room_depth — ask the user if unknown.
|
|
1144
|
+
South wall (rotation=180):
|
|
1145
|
+
Base cabinet: y = 0 (back edge against wall).
|
|
1146
|
+
Shallower cabinet (e.g. upper) front-flush with base: y = base_depth - upper_depth.
|
|
1147
|
+
Example: 24" base + 15" upper front-flush → upper y = 24 - 15 = 9".
|
|
1148
|
+
BASE cabinet y is NEVER modified. Only the shallower cabinet y shifts north (away from wall).
|
|
1149
|
+
North wall (rotation=0):
|
|
1150
|
+
Base cabinet: y = room_depth (back edge against north wall).
|
|
1151
|
+
Shallower cabinet front-flush: y = base_y - (base_depth - upper_depth).
|
|
1152
|
+
Example: base y=120 (24" deep) + 15" upper → upper y = 120 - 9 = 111".
|
|
1153
|
+
Verification: base front at 120-24=96", upper front at 111-15=96" ✅ MATCH.
|
|
1154
|
+
CRITICAL: Upper y = base_y - (base_depth - upper_depth). Just offset from base — no room dimension math.
|
|
1155
|
+
East wall (rotation=90):
|
|
1156
|
+
"Width" = this cabinet's own width dimension, running north-south after 90° rotation.
|
|
1157
|
+
East/west wall cabinets run from the north wall SOUTHWARD (decreasing y). List them north-to-south.
|
|
1158
|
+
D_adj_north = the catalog DEPTH of the north-wall BASE cabinet (the "depth" used in north wall y = room_depth).
|
|
1159
|
+
Example: north wall has 24"-deep bases → D_adj_north = 24.
|
|
1160
|
+
NEVER set D_adj_north = 0 when north wall cabinets exist — that places east/west cabinet inside the north wall footprint.
|
|
1161
|
+
Northernmost cabinet (1st in list): y = room_depth - D_adj_north - width/2.
|
|
1162
|
+
Each next cabinet going south: y = room_depth - D_adj_north - (sum of widths of preceding cabinets) - width/2.
|
|
1163
|
+
y DECREASES with each cabinet going south. NEVER add widths — adding goes north (wrong direction).
|
|
1164
|
+
IF room_depth IS UNKNOWN: ask the user for room dimensions before computing east/west wall y.
|
|
1165
|
+
UPPER cabinets: DO NOT compute y — copy the base cabinet y value exactly. No formula. No D_adj_north.
|
|
1166
|
+
Do NOT apply north/south front-flush y formulas to east/west wall cabinets.
|
|
1167
|
+
West wall (rotation=270): y = same formula as east wall.
|
|
1168
|
+
z = height from floor in inches. 0 for base cabinets on the floor. Stacked units: sum heights of all cabinets below.
|
|
1169
|
+
rotation: 0 = north wall, 90 = east wall, 180 = south wall, 270 = west wall.
|
|
1170
|
+
If no compass rose, assume top = north, bottom = south, left = west, right = east.
|
|
1171
|
+
If no drawing is provided, omit coordinates (cabinets placed in fallback row in CAD file).
|
|
1172
|
+
|
|
1173
|
+
Only call this tool once ALL of the above have been explicitly confirmed by the user. Never auto-fill, assume, or default any of these fields.
|
|
1174
|
+
|
|
1175
|
+
After the cart is created, inform the user of the Reference ID and direct them to log in at thesealab.com and navigate to Saved Carts to review their configuration before placing the order.`,
|
|
1176
|
+
inputSchema: CreateOrderSchema,
|
|
1177
|
+
handler: createSavedCart,
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: 'list_saved_carts',
|
|
1181
|
+
description: `List all saved carts for the authenticated user. Returns a summary of each cart: tempOrderId, project name, purchase order, article count, date, and whether it is an edit cart (revision of an existing order).
|
|
1182
|
+
|
|
1183
|
+
Use this tool when:
|
|
1184
|
+
- The user asks "what saved carts do I have?" or "show my saved carts"
|
|
1185
|
+
- You need to find a tempOrderId before calling get_saved_cart, update_saved_cart_articles, or submit_cart_as_version
|
|
1186
|
+
- You want to confirm whether a cart already exists before creating a new one`,
|
|
1187
|
+
inputSchema: ListSavedCartsSchema,
|
|
1188
|
+
handler: listSavedCarts,
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
name: 'get_saved_cart',
|
|
1192
|
+
description: `Read the current state of a saved cart including all articles and their configurations.
|
|
1193
|
+
Use this tool before making any edits to a saved cart — the user may have updated
|
|
1194
|
+
the cart directly in the Sealab web portal since it was created.`,
|
|
1195
|
+
inputSchema: GetSavedCartSchema,
|
|
1196
|
+
handler: getSavedCart,
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
name: 'update_saved_cart_articles',
|
|
1200
|
+
description: `Update the configuration of one or more articles in a saved cart. Only send the fields that have changed — positionName is required to identify each article, all other fields are optional. You can also change the cabinet type by patching serialNumber. Call get_saved_cart first to read current state before making edits.`,
|
|
1201
|
+
inputSchema: UpdateSavedCartArticlesSchema,
|
|
1202
|
+
handler: updateSavedCartArticles,
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
name: 'add_articles_to_cart',
|
|
1206
|
+
description: `Add NEW articles to an existing saved cart. Use this when the user wants to add more cabinets to a saved cart without recreating it.
|
|
1207
|
+
|
|
1208
|
+
This tool:
|
|
1209
|
+
- Validates that new article positionNames don't conflict with existing articles
|
|
1210
|
+
- Enriches articles with catalog data (same as create_saved_cart)
|
|
1211
|
+
- Returns the number of articles added
|
|
1212
|
+
|
|
1213
|
+
Use add_articles_to_cart when:
|
|
1214
|
+
- User has an existing saved cart and wants to add more items
|
|
1215
|
+
- User says "add another cabinet to my cart" or "add this to my saved cart"
|
|
1216
|
+
|
|
1217
|
+
DO NOT use this tool to:
|
|
1218
|
+
- Modify existing articles (use update_saved_cart_articles instead)
|
|
1219
|
+
- Create a new saved cart (use create_saved_cart instead)`,
|
|
1220
|
+
inputSchema: AddArticlesToCartSchema,
|
|
1221
|
+
handler: addArticlesToCart,
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: 'delete_saved_cart_articles',
|
|
1225
|
+
description: `Remove one or more articles from a saved cart by positionName. Use this when the user wants to remove an article entirely. To replace an article with a different cabinet type, use update_saved_cart_articles with the new serialNumber instead of deleting and re-adding.`,
|
|
1226
|
+
inputSchema: DeleteSavedCartArticlesSchema,
|
|
1227
|
+
handler: deleteSavedCartArticles,
|
|
1228
|
+
},
|
|
1229
|
+
];
|