@growrk/cli 4.9.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/dist/cli.mjs ADDED
@@ -0,0 +1,1568 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { config } from "dotenv";
5
+ import cac from "cac";
6
+
7
+ // src/commands/auth.ts
8
+ import chalk2 from "chalk";
9
+
10
+ // src/config.ts
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+ import { parse, stringify } from "yaml";
15
+ var CONFIG_DIR = join(homedir(), ".growrk");
16
+ var CONFIG_FILE = join(CONFIG_DIR, "config.yml");
17
+ function loadConfig() {
18
+ if (!existsSync(CONFIG_FILE)) {
19
+ return { defaultAccount: "", accounts: {} };
20
+ }
21
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
22
+ return parse(raw) || { defaultAccount: "", accounts: {} };
23
+ }
24
+ function saveConfig(config2) {
25
+ mkdirSync(CONFIG_DIR, { recursive: true });
26
+ writeFileSync(CONFIG_FILE, stringify(config2), "utf-8");
27
+ }
28
+ function getActiveAccount() {
29
+ const config2 = loadConfig();
30
+ if (!config2.defaultAccount || !config2.accounts[config2.defaultAccount]) {
31
+ return null;
32
+ }
33
+ return config2.accounts[config2.defaultAccount];
34
+ }
35
+ function removeAccount(name) {
36
+ const config2 = loadConfig();
37
+ if (!config2.accounts[name]) return false;
38
+ const { [name]: _, ...rest } = config2.accounts;
39
+ config2.accounts = rest;
40
+ if (config2.defaultAccount === name) {
41
+ config2.defaultAccount = Object.keys(config2.accounts)[0] || "";
42
+ }
43
+ saveConfig(config2);
44
+ return true;
45
+ }
46
+
47
+ // src/environments.ts
48
+ var ENVIRONMENTS = {
49
+ prod: "https://ai.growrk.com",
50
+ // io.growrk.com is the same host behind the ai.growrk.com alias — kept for
51
+ // anyone who pinned it before the cutover.
52
+ io: "https://io.growrk.com",
53
+ // The `next` branch deploys to the growrk-staging project, whose io hosting
54
+ // site is `growrk-staging-io` (the pipeline's io job ends with
55
+ // `hosting[growrk-staging-io]: release complete`). growrk-next-io was never
56
+ // created and 404s, so this pointed at nothing.
57
+ next: "https://growrk-staging-io.web.app",
58
+ danielgdev: "https://growrk-danielgdev-io.web.app",
59
+ elsayedfarahat: "https://growrk-elsayedfarahat-io-test.web.app",
60
+ // `localhost`, not `127.0.0.1`: the io dev server binds IPv6 (`[::1]:3001`)
61
+ // on macOS, so the IPv4 literal fails with an opaque
62
+ // "NETWORK_ERROR: <no response> fetch failed". `localhost` resolves on both
63
+ // stacks.
64
+ local: "http://localhost:3001"
65
+ };
66
+ function resolveEnvironment(env, opts) {
67
+ const url = ENVIRONMENTS[env];
68
+ if (!url) {
69
+ const allowed = Object.keys(ENVIRONMENTS).join(", ");
70
+ throw new Error(`Unknown environment '${env}'. Allowed: ${allowed}`);
71
+ }
72
+ if (env === "local" && !opts?.allowLocalhost) {
73
+ throw new Error("Local environment requires --allow-localhost flag.");
74
+ }
75
+ return url;
76
+ }
77
+ function environmentLabel(ctx) {
78
+ if (ctx.environment) return ctx.environment;
79
+ const match = Object.entries(ENVIRONMENTS).find(([, url]) => url === ctx.apiUrl);
80
+ return match ? match[0] : "custom";
81
+ }
82
+ function validateApiUrl(url, opts) {
83
+ let parsed;
84
+ try {
85
+ parsed = new URL(url);
86
+ } catch {
87
+ throw new Error(`Invalid API URL '${url}'. Must use https:// (or http://localhost / http://127.0.0.1 with --allow-localhost).`);
88
+ }
89
+ if (parsed.protocol === "https:") return;
90
+ if (opts?.allowLocalhost && parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1")) return;
91
+ throw new Error(`Invalid API URL '${url}'. Must use https:// (or http://localhost / http://127.0.0.1 with --allow-localhost).`);
92
+ }
93
+
94
+ // src/auth.ts
95
+ var VALID_FORMATS = ["json", "table", "text"];
96
+ function resolveContext(argv) {
97
+ const account = getActiveAccount();
98
+ const apiKey = argv.apiKey || process.env.GROWRK_API_KEY || account?.apiKey;
99
+ if (!apiKey) {
100
+ throw new Error("No API key configured. Run `growrk auth login` or set GROWRK_API_KEY.");
101
+ }
102
+ const localhostOpts = { allowLocalhost: argv.allowLocalhost };
103
+ let apiUrl;
104
+ let environment;
105
+ if (argv.apiUrl) {
106
+ validateApiUrl(argv.apiUrl, localhostOpts);
107
+ apiUrl = argv.apiUrl;
108
+ } else if (argv.environment) {
109
+ environment = argv.environment;
110
+ apiUrl = resolveEnvironment(argv.environment, localhostOpts);
111
+ } else if (process.env.GROWRK_ENVIRONMENT) {
112
+ environment = process.env.GROWRK_ENVIRONMENT;
113
+ apiUrl = resolveEnvironment(process.env.GROWRK_ENVIRONMENT, localhostOpts);
114
+ } else if (account?.environment) {
115
+ environment = account.environment;
116
+ apiUrl = resolveEnvironment(account.environment, localhostOpts);
117
+ } else {
118
+ apiUrl = process.env.GROWRK_API_URL || account?.apiUrl || "https://ai.growrk.com";
119
+ }
120
+ const format = argv.format || "table";
121
+ if (!VALID_FORMATS.includes(format)) {
122
+ throw new Error(`Invalid format "${argv.format}". Valid formats: ${VALID_FORMATS.join(", ")}`);
123
+ }
124
+ return { apiKey, apiUrl, format, environment };
125
+ }
126
+
127
+ // src/v4/client.ts
128
+ import { ofetch } from "ofetch";
129
+ import { randomUUID } from "crypto";
130
+ var meta = (correlationId) => ({ correlationId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v4" });
131
+ function httpStatusToCode(status) {
132
+ switch (status) {
133
+ case 401:
134
+ return "UNAUTHORIZED";
135
+ case 403:
136
+ return "FORBIDDEN";
137
+ case 404:
138
+ return "NOT_FOUND";
139
+ case 400:
140
+ case 409:
141
+ return "VALIDATION_ERROR";
142
+ case 429:
143
+ return "RATE_LIMITED";
144
+ default:
145
+ return status && status >= 500 ? "INTERNAL_ERROR" : "API_ERROR";
146
+ }
147
+ }
148
+ var REQUEST_TIMEOUT_MS = 6e4;
149
+ function createV4Client(ctx) {
150
+ const baseURL = `${ctx.apiUrl}/v4`;
151
+ const request = async (method, path, opts = {}) => {
152
+ const correlationId = randomUUID();
153
+ try {
154
+ const data = await ofetch(path, {
155
+ baseURL,
156
+ method,
157
+ query: opts.query,
158
+ body: opts.body,
159
+ // Without this a server that accepts the connection and never answers
160
+ // leaves the CLI hanging with no output and no way to tell whether the
161
+ // write landed. ofetch (1.5.x) aborts the request itself.
162
+ timeout: REQUEST_TIMEOUT_MS,
163
+ headers: { "X-API-KEY": ctx.apiKey, "X-Correlation-Id": correlationId }
164
+ });
165
+ return { ok: true, data, meta: meta(correlationId) };
166
+ } catch (e) {
167
+ const err = e;
168
+ const status = err.response?.status ?? err.data?.statusCode;
169
+ if (status) {
170
+ const message2 = err.data?.statusMessage || err.data?.message || err.message || "Request failed";
171
+ return { ok: false, error: { code: httpStatusToCode(status), message: message2 }, meta: meta(correlationId) };
172
+ }
173
+ const message = e instanceof Error ? e.message : "Failed to connect to GroWrk API";
174
+ return { ok: false, error: { code: "NETWORK_ERROR", message }, meta: meta(correlationId) };
175
+ }
176
+ };
177
+ return {
178
+ get: (path, query) => request("GET", path, { query }),
179
+ post: (path, body) => request("POST", path, { body }),
180
+ patch: (path, body) => request("PATCH", path, { body })
181
+ };
182
+ }
183
+
184
+ // src/output.ts
185
+ import chalk from "chalk";
186
+ function stringifyValue(v) {
187
+ if (v === null || v === void 0) return "";
188
+ if (Array.isArray(v)) return v.length ? `[${v.length} items]` : "(empty)";
189
+ if (typeof v === "object") return JSON.stringify(v);
190
+ return String(v);
191
+ }
192
+ function formatOutput(response, format) {
193
+ if (format === "json") {
194
+ return JSON.stringify(response, null, 2);
195
+ }
196
+ if (!response.ok) {
197
+ return chalk.red(`Error [${response.error?.code}]: ${response.error?.message}`);
198
+ }
199
+ if (format === "text") {
200
+ return typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2);
201
+ }
202
+ const data = response.data;
203
+ if (Array.isArray(data)) {
204
+ return formatTable(data);
205
+ }
206
+ if (typeof data === "object" && data !== null) {
207
+ const obj = data;
208
+ const arrayKey = Object.keys(obj).find((k) => Array.isArray(obj[k]));
209
+ if (arrayKey) {
210
+ const arr = obj[arrayKey];
211
+ if (arr.length === 0) {
212
+ const footer2 = Object.entries(obj).filter(([k]) => k !== arrayKey).map(([k, v]) => `${k}: ${v}`).join(" ");
213
+ return footer2 ? `${chalk.dim("No results found.")}
214
+ ${chalk.dim(footer2)}` : chalk.dim("No results found.");
215
+ }
216
+ const tableOutput = formatTable(arr);
217
+ const footer = Object.entries(obj).filter(([k]) => k !== arrayKey).map(([k, v]) => `${k}: ${v}`).join(" ");
218
+ return footer ? `${tableOutput}
219
+ ${chalk.dim(footer)}` : tableOutput;
220
+ }
221
+ return formatKeyValue(obj);
222
+ }
223
+ return String(data);
224
+ }
225
+ function formatTable(rows) {
226
+ if (rows.length === 0) return chalk.dim("No results found.");
227
+ const keys = Object.keys(rows[0]);
228
+ const widths = keys.map(
229
+ (k) => Math.max(k.length, ...rows.map((r) => stringifyValue(r[k]).length))
230
+ );
231
+ const header = keys.map((k, i) => chalk.bold(k.padEnd(widths[i]))).join(" ");
232
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
233
+ const body = rows.map(
234
+ (row) => keys.map((k, i) => stringifyValue(row[k]).padEnd(widths[i])).join(" ")
235
+ ).join("\n");
236
+ return `${header}
237
+ ${separator}
238
+ ${body}`;
239
+ }
240
+ function formatKeyValue(obj) {
241
+ const keys = Object.keys(obj);
242
+ if (keys.length === 0) return chalk.dim("(empty)");
243
+ const maxKeyLen = Math.max(...keys.map((k) => k.length));
244
+ return Object.entries(obj).map(([k, v]) => `${chalk.bold(k.padEnd(maxKeyLen))} ${stringifyValue(v)}`).join("\n");
245
+ }
246
+ function printResponse(response, format) {
247
+ console.log(formatOutput(response, format));
248
+ if (!response.ok) {
249
+ process.exitCode = 1;
250
+ }
251
+ }
252
+
253
+ // src/commands/auth.ts
254
+ import { ofetch as ofetch2 } from "ofetch";
255
+ async function authLogin(options) {
256
+ const inquirer = await import("inquirer");
257
+ const apiKey = options.apiKey || (await inquirer.default.prompt([{
258
+ type: "password",
259
+ name: "apiKey",
260
+ message: "Enter your GroWrk API key:",
261
+ mask: "*"
262
+ }])).apiKey;
263
+ let apiUrl;
264
+ let environment;
265
+ if (options.apiUrl) {
266
+ validateApiUrl(options.apiUrl, { allowLocalhost: !!options.allowLocalhost });
267
+ apiUrl = options.apiUrl;
268
+ } else if (options.environment) {
269
+ environment = options.environment;
270
+ apiUrl = resolveEnvironment(options.environment, { allowLocalhost: !!options.allowLocalhost });
271
+ } else {
272
+ environment = "prod";
273
+ apiUrl = process.env.GROWRK_API_URL || "https://ai.growrk.com";
274
+ }
275
+ if (environment === "local") {
276
+ console.log(chalk2.yellow(`\u26A0 WARNING: Using local API at ${apiUrl}`));
277
+ }
278
+ console.log(chalk2.dim("Testing connection..."));
279
+ try {
280
+ const health = await ofetch2(`${apiUrl}/cli/health`);
281
+ if (!health.ok) throw new Error("Health check failed");
282
+ } catch {
283
+ console.log(chalk2.red("Failed to connect to GroWrk API. Check your API URL."));
284
+ process.exitCode = 1;
285
+ return;
286
+ }
287
+ let companyName = "Unknown";
288
+ try {
289
+ await ofetch2(`${apiUrl}/v4/me`, { headers: { "X-API-KEY": apiKey } });
290
+ companyName = "Verified";
291
+ } catch {
292
+ }
293
+ const accountName = options.name || (await inquirer.default.prompt([{
294
+ type: "input",
295
+ name: "name",
296
+ message: "Account name (for reference):",
297
+ default: "default"
298
+ }])).name;
299
+ const config2 = loadConfig();
300
+ config2.accounts[accountName] = { apiKey, apiUrl, companyName, environment };
301
+ const previous = config2.defaultAccount;
302
+ config2.defaultAccount = accountName;
303
+ saveConfig(config2);
304
+ console.log(chalk2.green(`
305
+ Authenticated successfully as "${accountName}".`));
306
+ console.log(chalk2.dim("Config saved to ~/.growrk/config.yml"));
307
+ console.log(chalk2.bold("Active account:"), accountName, chalk2.dim(`(${environmentLabel({ apiUrl, environment })})`));
308
+ if (previous && previous !== accountName) {
309
+ console.log(chalk2.dim(`Switched from "${previous}". Run \`growrk auth login --name ${previous}\` to switch back.`));
310
+ }
311
+ console.log(chalk2.dim("\nTry: growrk orders list"));
312
+ }
313
+ async function authLogout(options) {
314
+ const name = options.name || loadConfig().defaultAccount;
315
+ if (!name) {
316
+ console.log(chalk2.yellow("No account configured."));
317
+ return;
318
+ }
319
+ if (removeAccount(name)) {
320
+ console.log(chalk2.green(`Removed account "${name}".`));
321
+ } else {
322
+ console.log(chalk2.yellow(`Account "${name}" not found.`));
323
+ }
324
+ }
325
+ async function authStatus(options) {
326
+ const account = getActiveAccount();
327
+ const config2 = loadConfig();
328
+ let ctx;
329
+ try {
330
+ ctx = resolveContext(options);
331
+ } catch (error) {
332
+ const hasKey = Boolean(options.apiKey || process.env.GROWRK_API_KEY || account);
333
+ console.log(chalk2.yellow(hasKey ? error.message : "Not authenticated. Run `growrk auth login`."));
334
+ return;
335
+ }
336
+ console.log(chalk2.bold("Active account:"), config2.defaultAccount || chalk2.dim("(none \u2014 using flags/env)"));
337
+ console.log(chalk2.bold("Environment:"), environmentLabel(ctx));
338
+ console.log(chalk2.bold("API URL:"), ctx.apiUrl);
339
+ console.log(chalk2.bold("API Key:"), ctx.apiKey.slice(0, 8) + "...");
340
+ console.log(chalk2.bold("Company:"), account?.companyName ?? "Unknown");
341
+ const resp = await createV4Client(ctx).get("/me");
342
+ if (resp.ok) {
343
+ console.log(chalk2.bold("\nIdentity (whoami):"));
344
+ printResponse(resp, ctx.format);
345
+ } else {
346
+ console.log(chalk2.dim(`
347
+ whoami: ${resp.error?.message ?? "unavailable"} (this key may be a service/monitoring key, not a per-user key)`));
348
+ }
349
+ }
350
+ function registerAuthCommands(cli2) {
351
+ cli2.command("auth <subcommand>", "Authentication (login | logout | status)").allowUnknownOptions().option("--api-key <key>", "API key").option("--api-url <url>", "Link API URL").option("--environment <env>", "Target environment (prod, next, danielgdev, local)").option("--allow-localhost", "Allow local environment (http://localhost)").option("--name <name>", "Account name").action(async (subcommand, options) => {
352
+ switch (subcommand) {
353
+ case "login":
354
+ await authLogin(options);
355
+ break;
356
+ case "logout":
357
+ await authLogout(options);
358
+ break;
359
+ case "status":
360
+ await authStatus(options);
361
+ break;
362
+ default:
363
+ console.error(`Unknown auth subcommand: ${subcommand}`);
364
+ console.error("Available: login, logout, status");
365
+ process.exitCode = 1;
366
+ }
367
+ });
368
+ }
369
+
370
+ // ../../dist/runtime/mcp/order-types.js
371
+ var VALIDATABLE_ORDER_TYPES = [
372
+ "Deployment",
373
+ "Offboarding",
374
+ "Collection",
375
+ "Collect for Maintenance",
376
+ "Maintenance",
377
+ "Swap from Inventory"
378
+ ];
379
+
380
+ // ../../dist/runtime/mcp/schemas.js
381
+ import { z } from "zod";
382
+ var countryCode = z.string().regex(/^[A-Za-z]{2}$/, "Use an ISO 3166-1 alpha-2 code").transform((c) => c.toUpperCase());
383
+ var getOrderInput = {
384
+ orderId: z.string().describe("The order ID to retrieve"),
385
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
386
+ };
387
+ var listOrdersInput = {
388
+ query: z.string().optional().describe("Search query (employee email, product name, order ID, etc.)"),
389
+ status: z.array(z.string()).optional().describe('Filter by order status. Accepts one or more values (e.g. ["processing"], ["shipped", "delivered"]). Valid values: "order placed", "in progress", "processing", "shipped", "delivered", "on hold", "pending payment", "awaiting client", "awaiting recipient", "pending approval", "delayed", "closed", "cancelled"'),
390
+ type: z.array(z.string()).optional().describe('Filter by order type. Accepts one or more values (e.g. ["Offboarding"], ["Purchase for Employee", "Deployment"]). Valid values: "Purchase for Employee", "Deployment", "Offboarding", "Collection", "Maintenance" ("Assign to Employee" is the legacy name for "Deployment" and may appear on older orders)'),
391
+ page: z.number().int().min(0).default(0).describe("Page number for pagination (0-indexed)"),
392
+ limit: z.number().int().min(1).max(50).default(20).describe("Maximum number of results per page"),
393
+ sort: z.string().optional().describe('Field to sort by (e.g. "createdAt", "status", "type")'),
394
+ sortDirection: z.enum(["asc", "desc"]).optional().describe('Sort direction (default: "desc")'),
395
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
396
+ };
397
+ var validateOrderInput = {
398
+ orderType: z.enum(VALIDATABLE_ORDER_TYPES).describe("Order type to validate"),
399
+ employeeId: z.string().optional().describe("Employee whose devices the order acts on \u2014 required for every type except Maintenance (device-only)"),
400
+ productIds: z.array(z.string()).min(1).describe("Inventory product IDs. For Swap from Inventory: exactly one id \u2014 the device being replaced."),
401
+ replacementProductId: z.string().optional().describe("Swap from Inventory: the replacement device from company inventory"),
402
+ country: countryCode.optional().describe("Optional cross-check country (ISO 3166-1 alpha-2, case-insensitive). The order country always comes from the employee's stored delivery address; providing a different country here fails validation."),
403
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
404
+ };
405
+ var createOrderInput = {
406
+ orderType: z.enum(VALIDATABLE_ORDER_TYPES).describe("Order type to create"),
407
+ employeeId: z.string().optional().describe("Employee the order acts on \u2014 required for every type except Maintenance (device-only). The destination/collection country and address are taken from the employee's stored delivery address."),
408
+ productIds: z.array(z.string()).min(1).describe("Inventory product IDs to assign/collect/offboard/maintain. For Swap from Inventory: exactly one id \u2014 the device being replaced."),
409
+ notes: z.array(z.string()).optional().describe("Optional order notes"),
410
+ reason: z.string().optional().describe("Collection / Collect for Maintenance / Maintenance / Swap: manager reason for the request"),
411
+ replacementProductId: z.string().optional().describe("Swap from Inventory: the replacement device from company inventory (must be Ready to Use, unassigned, same region and product type as the old device)"),
412
+ shippingType: z.enum(["standard", "expedited", "overnight"]).optional().describe("Deployment / Swap from Inventory: shipping speed (default: standard)"),
413
+ neededBy: z.string().optional().describe("Deployment: desired arrival date (ISO 8601), stored as the order's desired time of arrival"),
414
+ includeCharger: z.boolean().optional().describe("Deployment / Swap from Inventory: purchase replacement chargers (per-country power-accessory fee applies). ONLY devices whose charger status is 'Damaged' or 'Not Included' are eligible \u2014 devices that already include a charger are skipped, and the result's `charger` field reports what was requested vs skipped"),
415
+ termination: z.enum(["voluntary", "involuntary"]).optional().describe("Offboarding: termination type (required for Offboarding)"),
416
+ dispositions: z.record(z.string(), z.enum(["keep", "clientRecover", "growrkRecover"])).optional().describe("Offboarding: per-productId device disposition (required for every product on Offboarding orders)"),
417
+ offboardingTime: z.string().optional().describe("Offboarding: optional scheduled offboarding time (ISO 8601)"),
418
+ scheduledTimeZone: z.string().optional().describe('Offboarding: human-readable timezone label that offboardingTime was given in (e.g. "America/Mexico_City")'),
419
+ legalHold: z.record(z.string(), z.object({ instructions: z.string().optional() })).optional().describe('Offboarding: per-productId legal hold request, with optional preservation instructions (e.g. {"prod1": {"instructions": "Preserve mailbox"}})'),
420
+ pickupWindow: z.object({
421
+ fromDate: z.string().describe("Pickup window start (ISO 8601)"),
422
+ toDate: z.string().describe("Pickup window end (ISO 8601)"),
423
+ instructions: z.string().optional().describe("Pickup instructions")
424
+ }).optional().describe("Offboarding: scheduled pickup window for device recovery"),
425
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
426
+ };
427
+ var getOrderHistoryInput = {
428
+ orderId: z.string().describe("The order ID to get the history for (parent order ID also works for compound orders)"),
429
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
430
+ };
431
+ var addPowerAccessoryInput = {
432
+ orderId: z.string().describe("The order the device belongs to (parent order ID also works for compound orders)"),
433
+ productId: z.string().optional().describe("The inventory product (device) to request a replacement charger for. Provide this OR serialNumber."),
434
+ serialNumber: z.string().optional().describe("The device's serial number \u2014 resolved to the product automatically. Provide this OR productId."),
435
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
436
+ };
437
+ var getEmployeeInput = {
438
+ employeeId: z.string().describe("The employee ID to retrieve"),
439
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
440
+ };
441
+ var listEmployeesInput = {
442
+ query: z.string().optional().describe("Search query (name, email, department, etc.)"),
443
+ email: z.string().optional().describe("Exact email address match (case-insensitive). Unlike `query`, this is an equality filter, not a free-text search \u2014 use it to resolve one known employee."),
444
+ status: z.string().optional().describe('Filter by status (e.g. "Active", "Offboarded")'),
445
+ country: countryCode.optional().describe('Filter by country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US", "BR", "MX")'),
446
+ page: z.number().int().min(0).default(0).describe("Page number for pagination (0-indexed)"),
447
+ limit: z.number().int().min(1).max(50).default(20).describe("Maximum number of results per page"),
448
+ sort: z.string().optional().describe('Field to sort by (e.g. "displayName", "createdAt", "startDate", "status")'),
449
+ sortDirection: z.enum(["asc", "desc"]).optional().describe('Sort direction (default: "asc")'),
450
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
451
+ };
452
+ var createEmployeeInput = {
453
+ displayName: z.string().min(1).describe("Employee full name"),
454
+ email: z.string().email().describe("Employee email address"),
455
+ companyId: z.string().optional().describe("Company ID"),
456
+ teamName: z.string().optional().describe("Team name the employee belongs to"),
457
+ department: z.string().optional().describe("Department the employee belongs to"),
458
+ division: z.string().optional().describe("Division the employee belongs to"),
459
+ jobTitle: z.string().optional().describe("Employee job title"),
460
+ personalEmail: z.string().email().optional().describe("Employee personal email address"),
461
+ taxId: z.string().optional().describe("Employee tax identification number"),
462
+ address: z.string().optional().describe("Street address"),
463
+ city: z.string().optional().describe("City"),
464
+ state: z.string().optional().describe("State or province"),
465
+ zipCode: z.string().optional().describe("ZIP or postal code"),
466
+ country: countryCode.optional().describe('Country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US", "BR")')
467
+ };
468
+ var updateEmployeeInput = {
469
+ employeeId: z.string().describe("The employee ID to update"),
470
+ displayName: z.string().min(1).optional().describe("New employee full name"),
471
+ email: z.string().email().optional().describe("New employee email address (must be unique within the company). Only allowed for employees WITHOUT an active login."),
472
+ // Stored as the employee's `secondaryEmail` — the same field `create_employee`
473
+ // writes from its own `personalEmail`. Nullable because clearing it is a real
474
+ // operation (v2 `PATCH /employees/:id` accepted null or '' to unset).
475
+ personalEmail: z.union([z.string().email(), z.literal("")]).nullable().optional().describe('New personal / secondary email address (must be unique within the company). Pass null or "" to clear it.'),
476
+ department: z.string().optional().describe("New department"),
477
+ division: z.string().optional().describe("New division"),
478
+ jobTitle: z.string().optional().describe("New job title"),
479
+ taxId: z.string().optional().describe("New tax identification number (e.g. CPF \u2014 required for collections in Brazil)"),
480
+ teamName: z.string().optional().describe("Team name to move the employee to (created if it does not exist)"),
481
+ address: z.string().optional().describe("New street address (affects future orders only)"),
482
+ addressTwo: z.string().optional().describe("New address line two"),
483
+ city: z.string().optional().describe("New city"),
484
+ state: z.string().optional().describe("New state or province"),
485
+ zipCode: z.string().optional().describe("New ZIP or postal code"),
486
+ country: countryCode.optional().describe('New country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US", "BR")'),
487
+ phone: z.string().optional().describe("New contact phone for deliveries"),
488
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
489
+ };
490
+ var getTrackingInput = {
491
+ orderId: z.string().optional().describe("Order ID to get tracking for (returns all shipments on the order)"),
492
+ trackingNumber: z.string().optional().describe("A specific tracking number to look up"),
493
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
494
+ };
495
+ var getOrderSlaInput = {
496
+ orderId: z.string().describe("The order ID to get SLA / delay status for"),
497
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
498
+ };
499
+ var searchInventoryInput = {
500
+ query: z.string().optional().describe("Free-text search (model, serial, etc.)"),
501
+ category: z.string().optional().describe("Product type / category filter. Known values: Accessories, Adapters, Cables, Desktops, Headsets, Keyboards, Laptops, Mice, Mobile, Monitors, Network Gear A, Network Gear B, Power Protection, Tablet, VR Headsets, Webcams, Wifi"),
502
+ inStock: z.boolean().optional().describe("Only return available (in-stock) devices"),
503
+ country: countryCode.optional().describe('Filter by country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US", "BR")'),
504
+ status: z.string().optional().describe("Filter by inventory status"),
505
+ companyId: z.string().optional().describe("Company ID (optional for staff)"),
506
+ limit: z.number().int().min(1).max(50).default(20).describe("Maximum results")
507
+ };
508
+ var getAssignedDevicesInput = {
509
+ employeeId: z.string().describe("Employee ID to list assigned devices for"),
510
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
511
+ };
512
+ var getDeviceOptionsInput = {
513
+ productType: z.string().optional().describe("Desired product type / category. Known values: accessories, adapters, cables, desktops, headsets, keyboards, laptops, mice, mobile, monitors, network gear a, network gear b, power protection, tablet, vr headsets, webcams, wifi. Matched case- and plural-insensitively"),
514
+ location: z.string().optional().describe('Location to filter by (e.g. "GroWrk Warehouse", "Employee Address", "Client Location", "In transit")'),
515
+ specs: z.string().optional().describe('Free-text desired specs (e.g. "16GB 512GB M3")'),
516
+ companyId: z.string().optional().describe("Company ID (optional for staff)"),
517
+ limit: z.number().int().min(1).max(25).default(10).describe("Maximum options")
518
+ };
519
+ var suggestAlternativeInput = {
520
+ country: countryCode.describe('Destination country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US") \u2014 catalog availability is per region'),
521
+ productType: z.string().optional().describe("Product type / category. Known values: accessories, adapters, cables, desktops, headsets, keyboards, laptops, mice, mobile, monitors, network gear a, network gear b, power protection, tablet, vr headsets, webcams, wifi. Matched case- and plural-insensitively; an unknown value returns an error listing the region's valid options"),
522
+ manufacturer: z.string().optional().describe(`Preferred manufacturer (e.g. "Apple", "Lenovo", "Dell") \u2014 matched against the catalog's manufacturers; an unknown value returns an error listing the valid options`),
523
+ specs: z.string().optional().describe('Free-text desired specs (e.g. "16GB 512GB M3")'),
524
+ targetPrice: z.number().positive().optional().describe("Rank alternatives by proximity to this price (USD)"),
525
+ excludeItemId: z.string().optional().describe("Catalog item ID to exclude (the device being replaced)"),
526
+ companyId: z.string().optional().describe("Company ID (optional for staff)"),
527
+ limit: z.number().int().min(1).max(15).default(5).describe("Maximum suggestions")
528
+ };
529
+ var listTeamsInput = {
530
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
531
+ };
532
+ var getTeamInput = {
533
+ teamId: z.string().describe("The team ID to retrieve"),
534
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
535
+ };
536
+ var createTeamInput = {
537
+ name: z.string().min(1).describe("Team name. Must be unique within the company \u2014 employee tools address teams by name, so a duplicate would make that lookup ambiguous."),
538
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
539
+ };
540
+ var updateTeamInput = {
541
+ teamId: z.string().describe("The team ID to update"),
542
+ name: z.string().min(1).describe("New team name. Must be unique within the company."),
543
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
544
+ };
545
+ var getProductInput = {
546
+ productId: z.string().describe("The inventory product ID to retrieve"),
547
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
548
+ };
549
+ var addProductPinCodeInput = {
550
+ productId: z.string().optional().describe("The inventory product to set the PIN on. Provide this OR serialNumber."),
551
+ serialNumber: z.string().optional().describe("The device's serial number \u2014 resolved to the product automatically. Provide this OR productId."),
552
+ pinCode: z.string().min(1).describe("The device PIN code to store"),
553
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
554
+ };
555
+ var listCompanyAddressesInput = {
556
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
557
+ };
558
+ var createCompanyAddressInput = {
559
+ alias: z.string().min(1).describe('Short label for the location (e.g. "HQ", "Madrid office")'),
560
+ address: z.string().min(1).describe("Street address"),
561
+ addressTwo: z.string().optional().describe("Address line two"),
562
+ city: z.string().min(1).describe("City"),
563
+ state: z.string().optional().describe("State or province"),
564
+ zipCode: z.string().min(1).describe("ZIP or postal code"),
565
+ country: countryCode.describe('Country (ISO 3166-1 alpha-2, case-insensitive, e.g. "US", "ES"). Must be in the GroWrk regional catalog.'),
566
+ contactName: z.string().min(1).describe("Name of the on-site contact"),
567
+ contactEmail: z.string().email().describe("Email of the on-site contact"),
568
+ phone: z.string().min(1).describe("Contact phone number for deliveries and collections"),
569
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
570
+ };
571
+ var listAvailableCountriesInput = {
572
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
573
+ };
574
+ var createPurchaseOrderInput = {
575
+ orderType: z.enum(["Purchase for Employee", "Purchase for Inventory"]).describe("Purchase for Employee ships to an employee's stored delivery address; Purchase for Inventory stocks the company pool."),
576
+ employeeId: z.string().optional().describe('Employee to purchase for \u2014 required for "Purchase for Employee", ignored otherwise.'),
577
+ items: z.array(z.object({
578
+ itemId: z.string().describe("GroWrk catalog item ID (items/{id}) \u2014 NOT the legacy v2 companyItem id"),
579
+ quantity: z.number().int().min(1).describe("How many of this item to purchase"),
580
+ country: countryCode.describe("Country the item ships from (ISO 3166-1 alpha-2). Quotes are per region, resolved from this country.")
581
+ })).min(1).describe("Catalog items to purchase"),
582
+ shippingType: z.enum(["standard", "expedited", "overnight"]).optional().describe("Shipping speed (default: standard)"),
583
+ notes: z.array(z.string()).optional().describe("Optional order notes"),
584
+ companyId: z.string().optional().describe("Company ID (optional for staff)")
585
+ };
586
+
587
+ // ../../dist/runtime/mcp/descriptors.js
588
+ import { z as z2 } from "zod";
589
+ var descriptors = {
590
+ whoami: {
591
+ name: "whoami",
592
+ group: "identity",
593
+ title: "Who Am I",
594
+ description: "Get information about the authenticated user and their access level.",
595
+ inputSchema: {}
596
+ },
597
+ get_order: {
598
+ name: "get_order",
599
+ group: "orders",
600
+ title: "Get Order",
601
+ description: "Retrieve a single order by ID with full hierarchy (parent + sub-orders, device requests, tracking). Staff may omit companyId \u2014 it is resolved from the order id. Requires mcp:orders:read scope.",
602
+ inputSchema: getOrderInput,
603
+ scope: "mcp:orders:read",
604
+ permission: { group: "orders", page: "ordersPage", value: 1 }
605
+ // VIEW
606
+ },
607
+ list_orders: {
608
+ name: "list_orders",
609
+ group: "orders",
610
+ title: "List Orders",
611
+ description: "Search and list orders for a company. Supports free-text search, status/type filtering, sorting, and pagination. Staff may omit companyId to search across all companies. Requires mcp:orders:read scope.",
612
+ inputSchema: listOrdersInput,
613
+ scope: "mcp:orders:read",
614
+ permission: { group: "orders", page: "ordersPage", value: 1 }
615
+ // VIEW
616
+ },
617
+ get_order_history: {
618
+ name: "get_order_history",
619
+ group: "orders",
620
+ title: "Get Order History",
621
+ description: "Get an order's full event history: order logs (status updates, comments) plus a per-device timeline merging product logs and courier tracking events. Works for compound (parent) orders \u2014 child-order devices are included and labeled. Staff may omit companyId \u2014 it is resolved from the order id. Requires mcp:orders:read scope.",
622
+ inputSchema: getOrderHistoryInput,
623
+ scope: "mcp:orders:read",
624
+ permission: { group: "orders", page: "ordersPage", value: 1 }
625
+ // VIEW
626
+ },
627
+ validate_order_constraints: {
628
+ name: "validate_order_constraints",
629
+ group: "orders",
630
+ title: "Validate Order Constraints",
631
+ description: "Check whether an order type (Deployment, Offboarding, Collection, Collect for Maintenance, Maintenance, Swap from Inventory) can be placed for an employee and their devices \u2014 validates region availability, delivery address, and device eligibility (Deployment also requires the device to be stocked in the employee's region). The order country is derived from the employee's stored delivery address (same as create_order). Read-only; run this before create_order. Requires mcp:orders:read scope.",
632
+ inputSchema: validateOrderInput,
633
+ scope: "mcp:orders:read",
634
+ permission: { group: "orders", page: "ordersPage", value: 1 }
635
+ // VIEW
636
+ },
637
+ create_order: {
638
+ name: "create_order",
639
+ group: "orders",
640
+ title: "Create Order",
641
+ description: "Place an order (Deployment, Offboarding, Collection, Collect for Maintenance, device-only Maintenance, or Swap from Inventory) for a company's devices. Validates constraints first and refuses to place if invalid (call validate_order_constraints to preview). The destination/collection address is the employee's stored delivery address. Requires mcp:orders:write scope and explicit approval.",
642
+ inputSchema: createOrderInput,
643
+ scope: "mcp:orders:write",
644
+ // the dashboard gates order placement on `teams.makeRequest` CREATE (HomeActions.vue, EmployeeProfileProductOptions.vue) — NOT `orders.orders`.
645
+ permission: { group: "teams", page: "makeRequest", value: 2 },
646
+ // CREATE
647
+ write: true
648
+ },
649
+ add_power_accessory: {
650
+ name: "add_power_accessory",
651
+ group: "orders",
652
+ title: "Add Power Accessory",
653
+ description: "Request a replacement charger (power accessory) for a device on an EXISTING order \u2014 same flow as the hub dashboard. Only allowed within 24 hours of placing the order, while the order is still 'order placed', and for devices whose charger status is 'Damaged' or 'Not Included'. The per-country power-accessory fee is added to the order cost. Requires mcp:orders:write scope and explicit approval.",
654
+ inputSchema: addPowerAccessoryInput,
655
+ scope: "mcp:orders:write",
656
+ // amending a placed order.
657
+ permission: { group: "orders", page: "orders", value: 4 },
658
+ // EDIT
659
+ write: true
660
+ },
661
+ get_employee: {
662
+ name: "get_employee",
663
+ group: "employees",
664
+ title: "Get Employee",
665
+ description: "Retrieve a single employee by ID, including their stored delivery address (deliveryAddress \u2014 null when none is set; set it with update_employee). Requires mcp:employees:read scope.",
666
+ inputSchema: getEmployeeInput,
667
+ scope: "mcp:employees:read",
668
+ permission: { group: "teams", page: "employees", value: 1 }
669
+ // VIEW
670
+ },
671
+ list_employees: {
672
+ name: "list_employees",
673
+ group: "employees",
674
+ title: "List Employees",
675
+ description: "Search and list employees for a company. Supports free-text search, status filtering, sorting, and pagination. Requires mcp:employees:read scope.",
676
+ inputSchema: listEmployeesInput,
677
+ scope: "mcp:employees:read",
678
+ permission: { group: "teams", page: "employees", value: 1 }
679
+ // VIEW
680
+ },
681
+ create_employee: {
682
+ name: "create_employee",
683
+ group: "employees",
684
+ title: "Create Employee",
685
+ description: "Create a new employee record. Requires name and email. Requires mcp:employees:write scope.",
686
+ inputSchema: createEmployeeInput,
687
+ scope: "mcp:employees:write",
688
+ permission: { group: "teams", page: "employees", value: 2 },
689
+ // CREATE
690
+ write: true
691
+ },
692
+ update_employee: {
693
+ name: "update_employee",
694
+ group: "employees",
695
+ title: "Update Employee",
696
+ description: "Update an existing employee's editable fields (name, email, department, division, job title, team, delivery address). Email changes are checked for uniqueness; address changes affect future orders only. Requires mcp:employees:write scope.",
697
+ inputSchema: updateEmployeeInput,
698
+ scope: "mcp:employees:write",
699
+ permission: { group: "teams", page: "employees", value: 4 },
700
+ // EDIT
701
+ write: true
702
+ },
703
+ get_tracking: {
704
+ name: "get_tracking",
705
+ group: "tracking",
706
+ title: "Get Tracking",
707
+ description: "Get shipment tracking for an order, merging GroWrk records with live TrackingMore courier data. Works for compound (parent) orders too \u2014 child-order shipments are included and labeled. Provide orderId (optionally with trackingNumber to filter to one shipment). Bare trackingNumber lookups are staff-only. Requires mcp:orders:read scope.",
708
+ inputSchema: getTrackingInput,
709
+ scope: "mcp:orders:read",
710
+ permission: { group: "orders", page: "ordersPage", value: 1 }
711
+ // VIEW
712
+ },
713
+ get_order_sla: {
714
+ name: "get_order_sla",
715
+ group: "orders",
716
+ title: "Get Order SLA",
717
+ description: "Get the SLA / delay status for a single order: days elapsed vs the expected SLA days for its order type, whether it is delayed and by how much, the specific reasons for any delay (paused-SLA periods, blocking device statuses), and recent activity. Staff may omit companyId \u2014 it is resolved from the order id. Requires mcp:orders:read scope.",
718
+ inputSchema: getOrderSlaInput,
719
+ scope: "mcp:orders:read",
720
+ permission: { group: "orders", page: "ordersPage", value: 1 }
721
+ // VIEW
722
+ },
723
+ search_inventory: {
724
+ name: "search_inventory",
725
+ group: "inventory",
726
+ title: "Search Inventory",
727
+ description: "Search a company's device inventory with optional filters (category, country, status, in-stock). Requires mcp:inventory:read scope.",
728
+ inputSchema: searchInventoryInput,
729
+ scope: "mcp:inventory:read",
730
+ permission: { group: "inventory", page: "inventoryManagementPage", value: 1 }
731
+ // VIEW
732
+ },
733
+ get_assigned_devices: {
734
+ name: "get_assigned_devices",
735
+ group: "inventory",
736
+ title: "Get Assigned Devices",
737
+ description: "List the devices currently assigned to an employee. Requires mcp:inventory:read scope.",
738
+ inputSchema: getAssignedDevicesInput,
739
+ scope: "mcp:inventory:read",
740
+ // reading an employee's devices is the read side of the `teams.employeeProducts` gate the assign flow uses (TeamsEmployeeAppData.vue).
741
+ permission: { group: "teams", page: "employeeProducts", value: 1 }
742
+ // VIEW
743
+ },
744
+ get_device_options: {
745
+ name: "get_device_options",
746
+ group: "inventory",
747
+ title: "Get Device Options",
748
+ description: "List available device options (anonymized model + availability), ranked by spec proximity and availability \u2014 use this both for initial device choices and for suggesting alternatives. Requires mcp:inventory:read scope.",
749
+ inputSchema: getDeviceOptionsInput,
750
+ scope: "mcp:inventory:read",
751
+ permission: { group: "inventory", page: "inventoryManagementPage", value: 1 }
752
+ // VIEW
753
+ },
754
+ suggest_alternative_device: {
755
+ name: "suggest_alternative_device",
756
+ group: "inventory",
757
+ title: "Suggest Alternative Device",
758
+ description: "Suggest alternative PURCHASABLE devices from the GroWrk master catalog for a destination country, ranked by manufacturer/spec/price proximity. Use when a desired device is unavailable and a purchase is being considered (vs get_device_options, which lists existing warehouse stock for redeployment). Requires mcp:inventory:read scope.",
759
+ inputSchema: suggestAlternativeInput,
760
+ scope: "mcp:inventory:read",
761
+ permission: { group: "inventory", page: "inventoryManagementPage", value: 1 }
762
+ // VIEW
763
+ },
764
+ list_teams: {
765
+ name: "list_teams",
766
+ group: "teams",
767
+ title: "List Teams",
768
+ description: "List the company's teams. Managers see only the teams their role scopes them to. Requires mcp:teams:read scope.",
769
+ inputSchema: listTeamsInput,
770
+ scope: "mcp:teams:read",
771
+ permission: { group: "teams", page: "teamsPage", value: 1 }
772
+ // VIEW
773
+ },
774
+ get_team: {
775
+ name: "get_team",
776
+ group: "teams",
777
+ title: "Get Team",
778
+ description: "Get a single team by ID, including its employee count. Requires mcp:teams:read scope.",
779
+ inputSchema: getTeamInput,
780
+ scope: "mcp:teams:read",
781
+ permission: { group: "teams", page: "teamsPage", value: 1 }
782
+ // VIEW
783
+ },
784
+ create_team: {
785
+ name: "create_team",
786
+ group: "teams",
787
+ title: "Create Team",
788
+ description: "Create a new team in the company. The name must be unique within the company, because the employee tools resolve teams by name. Requires mcp:teams:write scope.",
789
+ inputSchema: createTeamInput,
790
+ scope: "mcp:teams:write",
791
+ permission: { group: "teams", page: "teams", value: 2 },
792
+ // CREATE
793
+ write: true
794
+ },
795
+ update_team: {
796
+ name: "update_team",
797
+ group: "teams",
798
+ title: "Update Team",
799
+ description: "Rename an existing team. The new name must be unique within the company. Requires mcp:teams:write scope.",
800
+ inputSchema: updateTeamInput,
801
+ scope: "mcp:teams:write",
802
+ permission: { group: "teams", page: "teams", value: 4 },
803
+ // EDIT
804
+ write: true
805
+ },
806
+ get_product: {
807
+ name: "get_product",
808
+ group: "products",
809
+ title: "Get Product",
810
+ description: "Get a single inventory product (device) by ID, including its assignment, condition, location and serial number. Employees may only read their own devices. Requires mcp:inventory:read scope.",
811
+ inputSchema: getProductInput,
812
+ scope: "mcp:inventory:read",
813
+ permission: { group: "inventory", page: "inventoryManagementPage", value: 1 }
814
+ // VIEW
815
+ },
816
+ add_product_pin_code: {
817
+ name: "add_product_pin_code",
818
+ group: "products",
819
+ title: "Add Product PIN Code",
820
+ description: "Store the unlock PIN code for a device, identified by productId or serialNumber. Only available for device types that support a PIN (Desktops, Laptops, Mobile, Tablet). Requires mcp:inventory:write scope.",
821
+ inputSchema: addProductPinCodeInput,
822
+ scope: "mcp:inventory:write",
823
+ // ModalDevicePin.vue gates the PIN action on `orders.orders` EDIT.
824
+ permission: { group: "orders", page: "orders", value: 4 },
825
+ // EDIT
826
+ write: true
827
+ },
828
+ list_company_addresses: {
829
+ name: "list_company_addresses",
830
+ group: "company",
831
+ title: "List Company Addresses",
832
+ description: "List the company's saved office / warehouse addresses \u2014 the locations devices can be shipped to or collected from. Requires mcp:company:read scope.",
833
+ inputSchema: listCompanyAddressesInput,
834
+ scope: "mcp:company:read",
835
+ permission: { group: "offices", page: "officesPage", value: 1 }
836
+ // VIEW
837
+ },
838
+ create_company_address: {
839
+ name: "create_company_address",
840
+ group: "company",
841
+ title: "Create Company Address",
842
+ description: "Save a new office / warehouse address for the company. The country must exist in the GroWrk regional catalog. Requires mcp:company:write scope.",
843
+ inputSchema: createCompanyAddressInput,
844
+ scope: "mcp:company:write",
845
+ // `offices` exposes only `officesPage`, so the CREATE bit on that key is the closest faithful expression of "may add an office".
846
+ permission: { group: "offices", page: "officesPage", value: 2 },
847
+ // CREATE
848
+ write: true
849
+ },
850
+ list_available_countries: {
851
+ name: "list_available_countries",
852
+ group: "employees",
853
+ title: "List Available Countries",
854
+ description: "List the countries the company already has employees in, derived from stored employee delivery addresses. Returns each region as { value, label }. Requires mcp:employees:read scope.",
855
+ inputSchema: listAvailableCountriesInput,
856
+ scope: "mcp:employees:read",
857
+ permission: { group: "teams", page: "employees", value: 1 }
858
+ // VIEW
859
+ },
860
+ create_purchase_order: {
861
+ name: "create_purchase_order",
862
+ group: "orders",
863
+ title: "Create Purchase Order",
864
+ description: "Purchase devices from the GroWrk catalog, either for an employee (shipped to their stored delivery address) or into the company inventory pool. Items are priced from their selected regional quote; configure/build-to-order devices cannot be purchased this way. Requires mcp:orders:write scope and explicit approval.",
865
+ inputSchema: createPurchaseOrderInput,
866
+ scope: "mcp:orders:write",
867
+ // buying from the catalog is the `inventory.purchaseProduct` CREATE gate (HomeInventoryPool.vue).
868
+ permission: { group: "inventory", page: "purchaseProduct", value: 2 },
869
+ // CREATE
870
+ write: true
871
+ }
872
+ };
873
+ var allToolDescriptors = Object.assign(/* @__PURE__ */ Object.create(null), descriptors);
874
+ var toolInputJsonSchemas = Object.fromEntries(
875
+ Object.values(allToolDescriptors).map((d) => [d.name, z2.toJSONSchema(z2.object(d.inputSchema), { io: "input" })])
876
+ );
877
+
878
+ // src/tools/render.ts
879
+ var camelToKebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
880
+ function parseJsonObjectArg(json) {
881
+ let parsed;
882
+ try {
883
+ parsed = JSON.parse(json);
884
+ } catch {
885
+ throw new Error("must be a JSON object");
886
+ }
887
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
888
+ throw new Error("must be a JSON object");
889
+ }
890
+ return parsed;
891
+ }
892
+ function classify(prop) {
893
+ if (prop.enum) return "enum";
894
+ const t = Array.isArray(prop.type) ? prop.type.find((x) => x !== "null") : prop.type;
895
+ if (t === "string") return "string";
896
+ if (t === "number" || t === "integer") return "number";
897
+ if (t === "boolean") return "boolean";
898
+ if (t === "array") {
899
+ const item = prop.items?.type;
900
+ if (item === "number" || item === "integer") return "number-array";
901
+ if (item === "string") return "array";
902
+ return "json";
903
+ }
904
+ return "json";
905
+ }
906
+ function toolFlags(schema) {
907
+ const props = schema.properties ?? {};
908
+ const flags = [];
909
+ const jsonOnly = [];
910
+ for (const [name, prop] of Object.entries(props)) {
911
+ const kind = classify(prop);
912
+ if (kind === "json") {
913
+ jsonOnly.push(name);
914
+ continue;
915
+ }
916
+ const kebab = camelToKebab(name);
917
+ const flag = kind === "boolean" ? `--${kebab}` : kind === "array" || kind === "number-array" ? `--${kebab} <comma,separated>` : `--${kebab} <value>`;
918
+ flags.push({ name, flag, description: prop.description ?? "", kind });
919
+ }
920
+ return { flags, jsonOnly };
921
+ }
922
+ function buildArgs(schema, options) {
923
+ const { flags } = toolFlags(schema);
924
+ const args = {};
925
+ for (const f of flags) {
926
+ const raw = options[f.name];
927
+ if (raw === void 0 || raw === null) continue;
928
+ if (f.kind === "number") args[f.name] = Number(raw);
929
+ else if (f.kind === "boolean") args[f.name] = Boolean(raw);
930
+ else if (f.kind === "array") args[f.name] = String(raw).split(",").map((s) => s.trim()).filter(Boolean);
931
+ else if (f.kind === "number-array") args[f.name] = String(raw).split(",").map((s) => s.trim()).filter(Boolean).map(Number);
932
+ else if (f.kind === "string" || f.kind === "enum") args[f.name] = String(raw);
933
+ else args[f.name] = raw;
934
+ }
935
+ if (typeof options.json === "string" && options.json.trim()) {
936
+ Object.assign(args, parseJsonObjectArg(options.json));
937
+ }
938
+ return args;
939
+ }
940
+
941
+ // src/v4/manifest.ts
942
+ var MANIFEST = {
943
+ orders: [
944
+ { tool: "list_orders", action: "list", method: "GET", path: "/orders" },
945
+ { tool: "get_order", action: "get", method: "GET", path: "/orders/:id", pathParam: "orderId" },
946
+ { tool: "create_order", action: "create", method: "POST", path: "/orders" },
947
+ { tool: "get_order_history", action: "history", method: "GET", path: "/orders/:id/history", pathParam: "orderId" },
948
+ { tool: "get_tracking", action: "tracking", method: "GET", path: "/orders/:id/tracking", pathParam: "orderId" },
949
+ { tool: "validate_order_constraints", action: "validate", method: "POST", path: "/orders/validate" },
950
+ { tool: "add_power_accessory", action: "add-accessory", method: "POST", path: "/orders/:id/accessories", pathParam: "orderId" },
951
+ { tool: "get_order_sla", action: "sla", method: "GET", path: "/orders/:id/sla", pathParam: "orderId" },
952
+ { tool: "create_purchase_order", action: "purchase", method: "POST", path: "/orders/purchase" }
953
+ ],
954
+ employees: [
955
+ { tool: "list_employees", action: "list", method: "GET", path: "/employees" },
956
+ { tool: "get_employee", action: "get", method: "GET", path: "/employees/:id", pathParam: "employeeId" },
957
+ { tool: "create_employee", action: "create", method: "POST", path: "/employees" },
958
+ { tool: "update_employee", action: "update", method: "PATCH", path: "/employees/:id", pathParam: "employeeId" },
959
+ { tool: "get_assigned_devices", action: "devices", method: "GET", path: "/employees/:id/devices", pathParam: "employeeId" },
960
+ { tool: "list_available_countries", action: "countries", method: "GET", path: "/employees/countries" }
961
+ ],
962
+ teams: [
963
+ { tool: "list_teams", action: "list", method: "GET", path: "/teams" },
964
+ { tool: "get_team", action: "get", method: "GET", path: "/teams/:id", pathParam: "teamId" },
965
+ { tool: "create_team", action: "create", method: "POST", path: "/teams" },
966
+ { tool: "update_team", action: "update", method: "PATCH", path: "/teams/:id", pathParam: "teamId" }
967
+ ],
968
+ products: [
969
+ { tool: "get_product", action: "get", method: "GET", path: "/products/:id", pathParam: "productId" },
970
+ { tool: "add_product_pin_code", action: "add-pin", method: "POST", path: "/products/pin-code" }
971
+ ],
972
+ company: [
973
+ { tool: "list_company_addresses", action: "addresses", method: "GET", path: "/company/addresses" },
974
+ { tool: "create_company_address", action: "add-address", method: "POST", path: "/company/addresses" }
975
+ ],
976
+ inventory: [
977
+ { tool: "search_inventory", action: "search", method: "GET", path: "/inventory" }
978
+ ],
979
+ devices: [
980
+ { tool: "get_device_options", action: "options", method: "GET", path: "/devices/options" },
981
+ { tool: "suggest_alternative_device", action: "suggestions", method: "GET", path: "/devices/suggestions" }
982
+ ]
983
+ };
984
+
985
+ // src/v4/resources.ts
986
+ var COMMON_OPTIONS = [
987
+ ["--json <json>", "Extra arguments as a JSON object (merged over flags; for complex fields)"],
988
+ ["--format <format>", "Output format (json | table | text)"],
989
+ ["--api-key <key>", "API key (overrides config)"],
990
+ ["--api-url <url>", "API URL (overrides config)"],
991
+ ["--environment <env>", "Target environment (prod, next, danielgdev, local)"],
992
+ ["--allow-localhost", "Allow local environment (http://localhost)"]
993
+ ];
994
+ var nowMeta = () => ({ correlationId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v4" });
995
+ var schemaFor = (tool) => toolInputJsonSchemas[tool] ?? {};
996
+ async function callResource(entry, args, options) {
997
+ let ctx;
998
+ try {
999
+ ctx = resolveContext(options);
1000
+ } catch (e) {
1001
+ const message = e instanceof Error ? e.message : String(e);
1002
+ printResponse({ ok: false, error: { code: "CONFIG_ERROR", message }, meta: nowMeta() }, "text");
1003
+ return;
1004
+ }
1005
+ const input = buildArgs(schemaFor(entry.tool), options);
1006
+ let path = entry.path;
1007
+ if (entry.pathParam) {
1008
+ const id = args[0];
1009
+ if (!id) {
1010
+ printResponse({ ok: false, error: { code: "VALIDATION_ERROR", message: `Missing required <id> for "${entry.action}"` }, meta: nowMeta() }, ctx.format);
1011
+ return;
1012
+ }
1013
+ path = path.replace(":id", encodeURIComponent(id));
1014
+ Reflect.deleteProperty(input, entry.pathParam);
1015
+ }
1016
+ const client = createV4Client(ctx);
1017
+ const resp = entry.method === "GET" ? await client.get(path, input) : entry.method === "PATCH" ? await client.patch(path, input) : await client.post(path, input);
1018
+ printResponse(resp, ctx.format);
1019
+ }
1020
+ function registerResourceCommands(cli2) {
1021
+ for (const [resource, entries] of Object.entries(MANIFEST)) {
1022
+ const actions = entries.map((e) => e.action).join(" | ");
1023
+ const cmd = cli2.command(`${resource} <action> [...args]`, `Manage ${resource} (${actions})`).allowUnknownOptions();
1024
+ const seen = /* @__PURE__ */ new Set();
1025
+ for (const entry of entries) {
1026
+ for (const f of toolFlags(schemaFor(entry.tool)).flags) {
1027
+ if (f.name === entry.pathParam || seen.has(f.name)) continue;
1028
+ seen.add(f.name);
1029
+ cmd.option(f.flag, f.description);
1030
+ }
1031
+ }
1032
+ for (const [flag, desc] of COMMON_OPTIONS) cmd.option(flag, desc);
1033
+ cmd.action(async (action, args, options) => {
1034
+ const entry = entries.find((e) => e.action === action);
1035
+ if (!entry) {
1036
+ console.error(`Unknown ${resource} action: ${action}. Available: ${actions}`);
1037
+ process.exitCode = 1;
1038
+ return;
1039
+ }
1040
+ await callResource(entry, args, options);
1041
+ });
1042
+ }
1043
+ const me = cli2.command("me", "Show your identity + access (whoami)").allowUnknownOptions();
1044
+ for (const [flag, desc] of COMMON_OPTIONS) me.option(flag, desc);
1045
+ me.action(async (options) => {
1046
+ let ctx;
1047
+ try {
1048
+ ctx = resolveContext(options);
1049
+ } catch (e) {
1050
+ printResponse({ ok: false, error: { code: "CONFIG_ERROR", message: e instanceof Error ? e.message : String(e) }, meta: nowMeta() }, "text");
1051
+ return;
1052
+ }
1053
+ printResponse(await createV4Client(ctx).get("/me"), ctx.format);
1054
+ });
1055
+ }
1056
+
1057
+ // src/http.ts
1058
+ import { ofetch as ofetch3 } from "ofetch";
1059
+ import { randomUUID as randomUUID2 } from "crypto";
1060
+ function createClient(ctx, base = "/cli") {
1061
+ const baseURL = `${ctx.apiUrl}${base}`;
1062
+ const request = async (path, options = {}) => {
1063
+ const correlationId = randomUUID2();
1064
+ try {
1065
+ return await ofetch3(path, {
1066
+ baseURL,
1067
+ method: options.method,
1068
+ query: options.query,
1069
+ body: options.body,
1070
+ headers: {
1071
+ "X-API-KEY": ctx.apiKey,
1072
+ "X-Correlation-Id": correlationId
1073
+ }
1074
+ });
1075
+ } catch (e) {
1076
+ const err = e;
1077
+ if (err.data && typeof err.data === "object" && "ok" in err.data) {
1078
+ return err.data;
1079
+ }
1080
+ if (err.data && typeof err.data === "object" && (err.data.statusCode || err.data.error === true)) {
1081
+ const status = err.response?.status ?? err.data.statusCode;
1082
+ return {
1083
+ ok: false,
1084
+ error: {
1085
+ code: status === 401 ? "UNAUTHORIZED" : status === 403 ? "FORBIDDEN" : "API_ERROR",
1086
+ message: err.data.message || err.data.statusMessage || "API error"
1087
+ },
1088
+ meta: { correlationId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v1" }
1089
+ };
1090
+ }
1091
+ const message = e instanceof Error ? e.message : "Failed to connect to GroWrk API";
1092
+ return {
1093
+ ok: false,
1094
+ error: {
1095
+ code: "NETWORK_ERROR",
1096
+ message
1097
+ },
1098
+ meta: { correlationId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v1" }
1099
+ };
1100
+ }
1101
+ };
1102
+ return {
1103
+ get: (path, query) => request(path, { method: "GET", query }),
1104
+ patch: (path, body) => request(path, { method: "PATCH", body }),
1105
+ post: (path, body) => request(path, { method: "POST", body })
1106
+ };
1107
+ }
1108
+ function resolveOrFail(options) {
1109
+ try {
1110
+ return resolveContext(options);
1111
+ } catch (e) {
1112
+ const message = e instanceof Error ? e.message : String(e);
1113
+ printResponse({ ok: false, error: { code: "CONFIG_ERROR", message }, meta: { correlationId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v1" } }, "text");
1114
+ return null;
1115
+ }
1116
+ }
1117
+ async function executeGet(options, path, query) {
1118
+ const ctx = resolveOrFail(options);
1119
+ if (!ctx) return;
1120
+ const client = createClient(ctx);
1121
+ const resp = await client.get(path, query);
1122
+ printResponse(resp, ctx.format);
1123
+ }
1124
+ async function executePatch(options, path, body) {
1125
+ const ctx = resolveOrFail(options);
1126
+ if (!ctx) return;
1127
+ const client = createClient(ctx);
1128
+ const resp = await client.patch(path, body);
1129
+ printResponse(resp, ctx.format);
1130
+ }
1131
+
1132
+ // src/commands/orders.ts
1133
+ async function runStaffOrders(subcommand, args, options) {
1134
+ switch (subcommand) {
1135
+ case "get": {
1136
+ const id = args[0];
1137
+ if (!id) {
1138
+ console.error("Usage: growrk staff orders get <id>");
1139
+ process.exitCode = 1;
1140
+ return;
1141
+ }
1142
+ const query = {};
1143
+ if (options.include) query.include = options.include;
1144
+ if (options.companyId) query.companyId = options.companyId;
1145
+ await executeGet(options, `/orders/${id}`, query);
1146
+ break;
1147
+ }
1148
+ case "search": {
1149
+ const query = {};
1150
+ if (options.status) query.status = options.status.split(",").map((s) => s.trim());
1151
+ if (options.type) query.type = options.type.split(",").map((s) => s.trim());
1152
+ if (options.region) query.region = options.region;
1153
+ if (options.search) query.q = options.search;
1154
+ if (options.sort) query.sort = options.sort;
1155
+ if (options.sortDirection) query.sortDirection = options.sortDirection;
1156
+ if (options.include) query.include = options.include;
1157
+ if (options.companyId) query.companyId = options.companyId;
1158
+ if (options.logStatus) query.logStatus = options.logStatus;
1159
+ if (options.logLimit) query.logLimit = Number(options.logLimit);
1160
+ if (options.logCommentsOnly) query.logCommentsOnly = true;
1161
+ if (options.logAuthorRole) query.logAuthorRole = options.logAuthorRole;
1162
+ query.page = options.page || 0;
1163
+ query.pageSize = options.pageSize || 25;
1164
+ await executeGet(options, "/orders", query);
1165
+ break;
1166
+ }
1167
+ case "by-employee": {
1168
+ const query = {};
1169
+ if (options.search) query.q = options.search;
1170
+ if (options.name) query.name = options.name;
1171
+ if (options.email) query.email = options.email;
1172
+ if (options.include) query.include = options.include;
1173
+ if (options.companyId) query.companyId = options.companyId;
1174
+ query.page = options.page || 0;
1175
+ query.pageSize = options.pageSize || 50;
1176
+ await executeGet(options, "/orders/by-employee", query);
1177
+ break;
1178
+ }
1179
+ case "history": {
1180
+ const id = args[0];
1181
+ if (!id) {
1182
+ console.error("Usage: growrk staff orders history <id>");
1183
+ process.exitCode = 1;
1184
+ return;
1185
+ }
1186
+ await executeGet(options, `/orders/${id}/history`);
1187
+ break;
1188
+ }
1189
+ case "dri-edit": {
1190
+ const id = args[0];
1191
+ if (!id) {
1192
+ console.error("Usage: growrk staff orders dri-edit <id> --dri-email <email>");
1193
+ process.exitCode = 1;
1194
+ return;
1195
+ }
1196
+ if (!options.driEmail) {
1197
+ console.error("--dri-email is required. Usage: growrk staff orders dri-edit <id> --dri-email user@growrk.com");
1198
+ process.exitCode = 1;
1199
+ return;
1200
+ }
1201
+ await executePatch(options, `/orders/${id}/dri`, { email: options.driEmail });
1202
+ break;
1203
+ }
1204
+ default:
1205
+ console.error(`Unknown "staff orders" subcommand: ${subcommand}`);
1206
+ console.error("Available: get, search, by-employee, history, dri-edit");
1207
+ process.exitCode = 1;
1208
+ }
1209
+ }
1210
+
1211
+ // src/commands/tracking.ts
1212
+ async function runStaffTracking(subcommand, args, options) {
1213
+ switch (subcommand) {
1214
+ case "get": {
1215
+ const orderId = args[0];
1216
+ if (!orderId) {
1217
+ console.error("Usage: growrk staff tracking get <orderId>");
1218
+ process.exitCode = 1;
1219
+ return;
1220
+ }
1221
+ await executeGet(options, `/tracking/${orderId}`);
1222
+ break;
1223
+ }
1224
+ case "live": {
1225
+ const trackingNumber = args[0];
1226
+ if (!trackingNumber) {
1227
+ console.error("Usage: growrk staff tracking live <trackingNumber>");
1228
+ process.exitCode = 1;
1229
+ return;
1230
+ }
1231
+ await executeGet(options, `/tracking/live/${trackingNumber}`);
1232
+ break;
1233
+ }
1234
+ default:
1235
+ console.error(`Unknown "staff tracking" subcommand: ${subcommand}`);
1236
+ console.error("Available: get, live");
1237
+ process.exitCode = 1;
1238
+ }
1239
+ }
1240
+
1241
+ // src/commands/sla.ts
1242
+ async function runStaffSla(subcommand, args, options) {
1243
+ switch (subcommand) {
1244
+ case "delay": {
1245
+ const orderId = args[0];
1246
+ if (!orderId) {
1247
+ console.error("Usage: growrk staff sla delay <orderId>");
1248
+ process.exitCode = 1;
1249
+ return;
1250
+ }
1251
+ await executeGet(options, `/sla/${orderId}/delay`);
1252
+ break;
1253
+ }
1254
+ case "global":
1255
+ if (!options.type) {
1256
+ console.error("Usage: growrk staff sla global --type <orderType>");
1257
+ process.exitCode = 1;
1258
+ return;
1259
+ }
1260
+ await executeGet(options, "/sla/global", { type: options.type });
1261
+ break;
1262
+ default:
1263
+ console.error(`Unknown "staff sla" subcommand: ${subcommand}`);
1264
+ console.error("Available: delay, global");
1265
+ process.exitCode = 1;
1266
+ }
1267
+ }
1268
+
1269
+ // src/commands/dri.ts
1270
+ async function runStaffDri(subcommand, _args, options) {
1271
+ switch (subcommand) {
1272
+ case "list":
1273
+ await executeGet(options, "/dri");
1274
+ break;
1275
+ default:
1276
+ console.error(`Unknown "staff dri" subcommand: ${subcommand}`);
1277
+ console.error("Available: list");
1278
+ process.exitCode = 1;
1279
+ }
1280
+ }
1281
+
1282
+ // src/commands/staff-tasks.ts
1283
+ import { readFile } from "fs/promises";
1284
+ function makeErrorResponse(code, message) {
1285
+ return {
1286
+ ok: false,
1287
+ error: { code, message },
1288
+ meta: { correlationId: "", timestamp: (/* @__PURE__ */ new Date()).toISOString(), version: "v1" }
1289
+ };
1290
+ }
1291
+ async function runStaffTasks(subcommand, options) {
1292
+ switch (subcommand) {
1293
+ case "create-dri-reviews":
1294
+ await createDriReviews(options);
1295
+ break;
1296
+ case "create-supplier-reviews":
1297
+ await createSupplierReviews(options);
1298
+ break;
1299
+ case "create-rfd-reviews":
1300
+ await createRfdReviews(options);
1301
+ break;
1302
+ case "create-address-reviews":
1303
+ await createAddressReviews(options);
1304
+ break;
1305
+ default:
1306
+ console.error(`Unknown "staff tasks" subcommand: ${subcommand}`);
1307
+ console.error("Available: create-dri-reviews, create-supplier-reviews, create-rfd-reviews, create-address-reviews");
1308
+ process.exitCode = 1;
1309
+ }
1310
+ }
1311
+ async function createDriReviews(options) {
1312
+ let ctx;
1313
+ try {
1314
+ ctx = resolveContext(options);
1315
+ } catch (e) {
1316
+ const message = e instanceof Error ? e.message : String(e);
1317
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), "text");
1318
+ return;
1319
+ }
1320
+ let results = [];
1321
+ if (options.file) {
1322
+ let raw;
1323
+ try {
1324
+ raw = await readFile(options.file, "utf-8");
1325
+ } catch (e) {
1326
+ const message = e instanceof Error ? e.message : `Cannot read file: ${options.file}`;
1327
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), ctx.format);
1328
+ return;
1329
+ }
1330
+ try {
1331
+ const parsed = JSON.parse(raw);
1332
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1333
+ } catch {
1334
+ printResponse(makeErrorResponse("VALIDATION_ERROR", `Invalid JSON in file: ${options.file}`), ctx.format);
1335
+ return;
1336
+ }
1337
+ } else if (options.data) {
1338
+ try {
1339
+ const parsed = JSON.parse(options.data);
1340
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1341
+ } catch {
1342
+ printResponse(makeErrorResponse("VALIDATION_ERROR", "Invalid JSON in --data option"), ctx.format);
1343
+ return;
1344
+ }
1345
+ } else {
1346
+ printResponse(
1347
+ makeErrorResponse("VALIDATION_ERROR", "Provide results via --file <path> or --data <json>"),
1348
+ ctx.format
1349
+ );
1350
+ return;
1351
+ }
1352
+ const client = createClient(ctx);
1353
+ const resp = await client.post("/staff-tasks/dri-reviews", {
1354
+ results,
1355
+ dryRun: options.dryRun ?? false
1356
+ });
1357
+ printResponse(resp, ctx.format);
1358
+ }
1359
+ async function createSupplierReviews(options) {
1360
+ let ctx;
1361
+ try {
1362
+ ctx = resolveContext(options);
1363
+ } catch (e) {
1364
+ const message = e instanceof Error ? e.message : String(e);
1365
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), "text");
1366
+ return;
1367
+ }
1368
+ let results = [];
1369
+ if (options.file) {
1370
+ let raw;
1371
+ try {
1372
+ raw = await readFile(options.file, "utf-8");
1373
+ } catch (e) {
1374
+ const message = e instanceof Error ? e.message : `Cannot read file: ${options.file}`;
1375
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), ctx.format);
1376
+ return;
1377
+ }
1378
+ try {
1379
+ const parsed = JSON.parse(raw);
1380
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1381
+ } catch {
1382
+ printResponse(makeErrorResponse("VALIDATION_ERROR", `Invalid JSON in file: ${options.file}`), ctx.format);
1383
+ return;
1384
+ }
1385
+ } else if (options.data) {
1386
+ try {
1387
+ const parsed = JSON.parse(options.data);
1388
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1389
+ } catch {
1390
+ printResponse(makeErrorResponse("VALIDATION_ERROR", "Invalid JSON in --data option"), ctx.format);
1391
+ return;
1392
+ }
1393
+ } else {
1394
+ printResponse(
1395
+ makeErrorResponse("VALIDATION_ERROR", "Provide results via --file <path> or --data <json>"),
1396
+ ctx.format
1397
+ );
1398
+ return;
1399
+ }
1400
+ const client = createClient(ctx);
1401
+ const resp = await client.post("/staff-tasks/supplier-reviews", {
1402
+ results,
1403
+ dryRun: options.dryRun ?? false
1404
+ });
1405
+ printResponse(resp, ctx.format);
1406
+ }
1407
+ async function createRfdReviews(options) {
1408
+ let ctx;
1409
+ try {
1410
+ ctx = resolveContext(options);
1411
+ } catch (e) {
1412
+ const message = e instanceof Error ? e.message : String(e);
1413
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), "text");
1414
+ return;
1415
+ }
1416
+ let results = [];
1417
+ if (options.file) {
1418
+ let raw;
1419
+ try {
1420
+ raw = await readFile(options.file, "utf-8");
1421
+ } catch (e) {
1422
+ const message = e instanceof Error ? e.message : `Cannot read file: ${options.file}`;
1423
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), ctx.format);
1424
+ return;
1425
+ }
1426
+ try {
1427
+ const parsed = JSON.parse(raw);
1428
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1429
+ } catch {
1430
+ printResponse(makeErrorResponse("VALIDATION_ERROR", `Invalid JSON in file: ${options.file}`), ctx.format);
1431
+ return;
1432
+ }
1433
+ } else if (options.data) {
1434
+ try {
1435
+ const parsed = JSON.parse(options.data);
1436
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1437
+ } catch {
1438
+ printResponse(makeErrorResponse("VALIDATION_ERROR", "Invalid JSON in --data option"), ctx.format);
1439
+ return;
1440
+ }
1441
+ } else {
1442
+ printResponse(
1443
+ makeErrorResponse("VALIDATION_ERROR", "Provide results via --file <path> or --data <json>"),
1444
+ ctx.format
1445
+ );
1446
+ return;
1447
+ }
1448
+ const client = createClient(ctx);
1449
+ const resp = await client.post("/staff-tasks/rfd-reviews", {
1450
+ results,
1451
+ dryRun: options.dryRun ?? false
1452
+ });
1453
+ printResponse(resp, ctx.format);
1454
+ }
1455
+ async function createAddressReviews(options) {
1456
+ let ctx;
1457
+ try {
1458
+ ctx = resolveContext(options);
1459
+ } catch (e) {
1460
+ const message = e instanceof Error ? e.message : String(e);
1461
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), "text");
1462
+ return;
1463
+ }
1464
+ let results = [];
1465
+ if (options.file) {
1466
+ let raw;
1467
+ try {
1468
+ raw = await readFile(options.file, "utf-8");
1469
+ } catch (e) {
1470
+ const message = e instanceof Error ? e.message : `Cannot read file: ${options.file}`;
1471
+ printResponse(makeErrorResponse("CONFIG_ERROR", message), ctx.format);
1472
+ return;
1473
+ }
1474
+ try {
1475
+ const parsed = JSON.parse(raw);
1476
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1477
+ } catch {
1478
+ printResponse(makeErrorResponse("VALIDATION_ERROR", `Invalid JSON in file: ${options.file}`), ctx.format);
1479
+ return;
1480
+ }
1481
+ } else if (options.data) {
1482
+ try {
1483
+ const parsed = JSON.parse(options.data);
1484
+ results = Array.isArray(parsed) ? parsed : parsed?.results ?? [];
1485
+ } catch {
1486
+ printResponse(makeErrorResponse("VALIDATION_ERROR", "Invalid JSON in --data option"), ctx.format);
1487
+ return;
1488
+ }
1489
+ } else {
1490
+ printResponse(
1491
+ makeErrorResponse("VALIDATION_ERROR", "Provide results via --file <path> or --data <json>"),
1492
+ ctx.format
1493
+ );
1494
+ return;
1495
+ }
1496
+ const client = createClient(ctx);
1497
+ const resp = await client.post("/staff-tasks/address-reviews", {
1498
+ results,
1499
+ dryRun: options.dryRun ?? false
1500
+ });
1501
+ printResponse(resp, ctx.format);
1502
+ }
1503
+
1504
+ // src/commands/staff.ts
1505
+ function registerStaffCommands(cli2) {
1506
+ cli2.command("staff <group> <subcommand> [...args]", "[internal] Staff monitoring tools").allowUnknownOptions().option("--status <status>", "Filter by status").option("--type <type>", 'Order type (orders filter; required for "sla global")').option("--region <region>", "Filter by region").option("--search <query>", "Full-text search").option("--sort <field>", "Sort by field (default: createdAt)").option("--sort-direction <direction>", "Sort direction: asc or desc (default: desc)").option("--page <page>", "Page number").option("--page-size <size>", "Results per page").option("--name <name>", "Employee name").option("--email <email>", "Employee email").option("--dri-email <email>", 'DRI email (for "orders dri-edit")').option("--include <fields>", "Include additional data (comma-separated): addresses, deviceStatuses, logs, ctEvidence").option("--log-status <status>", "Filter logs by status (comma-separated)").option("--log-limit <limit>", "Maximum logs per order (default: 10, max: 50)").option("--log-comments-only", "Only include comment-type logs").option("--log-author-role <role>", "Filter logs by author role: staff, client, supplier").option("--company-id <id>", "Scope to a specific company (internal keys only; omit to search all)").option("--file <path>", 'Path to JSON file with task results (for "tasks")').option("--data <json>", 'Inline JSON string with task results (for "tasks")').option("--dry-run", 'Preview without creating tasks (for "tasks")').option("--format <format>", "Output format (json | table | text)").option("--api-key <key>", "API key (overrides config)").option("--api-url <url>", "API URL (overrides config)").option("--environment <env>", "Target environment (prod, next, danielgdev, local)").option("--allow-localhost", "Allow local environment (http://localhost)").action(async (group, subcommand, args, options) => {
1507
+ switch (group) {
1508
+ case "orders":
1509
+ return runStaffOrders(subcommand, args, options);
1510
+ case "tracking":
1511
+ return runStaffTracking(subcommand, args, options);
1512
+ case "sla":
1513
+ return runStaffSla(subcommand, args, options);
1514
+ case "dri":
1515
+ return runStaffDri(subcommand, args, options);
1516
+ case "tasks":
1517
+ return runStaffTasks(subcommand, options);
1518
+ default:
1519
+ console.error(`Unknown staff group: ${group}. Available: orders, tracking, sla, dri, tasks`);
1520
+ process.exitCode = 1;
1521
+ }
1522
+ });
1523
+ }
1524
+
1525
+ // src/cli.ts
1526
+ config({ quiet: true });
1527
+ var version = true ? "4.9.0" : "0.0.0-dev";
1528
+ var cli = cac("growrk");
1529
+ registerAuthCommands(cli);
1530
+ registerResourceCommands(cli);
1531
+ registerStaffCommands(cli);
1532
+ cli.command("help [command]", "Display help for a command").action((command) => {
1533
+ if (command) {
1534
+ cli.parse(["", "", command, "--help"], { run: false });
1535
+ } else {
1536
+ cli.outputHelp();
1537
+ }
1538
+ });
1539
+ cli.help((sections) => {
1540
+ for (const section of sections) {
1541
+ if (typeof section.body === "string") {
1542
+ section.body = section.body.split("\n").filter((line) => !/^\s+staff\b/.test(line) && !/\bgrowrk staff\b/.test(line)).join("\n");
1543
+ }
1544
+ }
1545
+ return sections;
1546
+ });
1547
+ cli.version(version);
1548
+ async function main() {
1549
+ const parsed = cli.parse(process.argv, { run: false });
1550
+ if (parsed.options.help || parsed.options.version) {
1551
+ return;
1552
+ }
1553
+ if (!cli.matchedCommand) {
1554
+ if (parsed.args.length > 0) {
1555
+ console.error(`Unknown command: ${parsed.args.join(" ")}`);
1556
+ console.error('Run "growrk help" for available commands.');
1557
+ } else {
1558
+ cli.outputHelp();
1559
+ }
1560
+ process.exitCode = 1;
1561
+ return;
1562
+ }
1563
+ await cli.runMatchedCommand();
1564
+ }
1565
+ main().catch((err) => {
1566
+ console.error(err.message || err);
1567
+ process.exitCode = 1;
1568
+ });