@pancake-pos/app-cli 2026.1.5 → 2026.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,13 @@ The `pos-app` command for Pancake POS apps: `login`, `dev`, `check`, `build`, `d
4
4
 
5
5
  ```bash
6
6
  npx @pancake-pos/app-cli login --shop 123 --token <token> # from POS › Settings › Advanced › Custom apps › Connect CLI
7
- npx pos-app dev
8
- npx pos-app deploy
7
+ npm run dev # in an app created by create-pos-app (scripts call pos-app)
8
+ npm run deploy
9
+ npx @pancake-pos/app-cli versions # any command, from anywhere
9
10
  ```
10
11
 
12
+ Do not run `npx pos-app`: the `pos-app` package on npm is not published by Pancake, and npx downloads it outside an installed project.
13
+
11
14
  Docs: https://pos.pages.fm/developers/apps/en (English), https://pos.pages.fm/developers/apps (Tiếng Việt). For AI tools: https://pos.pages.fm/developers/apps/llms-full.txt.
12
15
 
13
16
  Part of Pancake POS Apps. Use is limited to building apps for Pancake POS.
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
 
17
17
  // src/api.ts
18
18
  async function apiRequest(credentials, method, path, body) {
19
- if (!credentials.token) throw new ApiError(401, "Ch\u01B0a \u0111\u0103ng nh\u1EADp: ch\u1EA1y `pos-app login` tr\u01B0\u1EDBc");
19
+ if (!credentials.token) throw new ApiError(401, 'Ch\u01B0a \u0111\u0103ng nh\u1EADp: ch\u1EA1y l\u1EC7nh \u1EDF n\xFAt "K\u1EBFt n\u1ED1i CLI" tr\xEAn trang \u1EE8ng d\u1EE5ng tu\u1EF3 bi\u1EBFn c\u1EE7a POS (npx @pancake-pos/app-cli login \u2026)');
20
20
  const headers = { Authorization: `Bearer ${credentials.token}`, Accept: "application/json" };
21
21
  let payload;
22
22
  if (body instanceof FormData) payload = body;
@@ -635,10 +635,10 @@ function createSiteHandler(options) {
635
635
  res.writeHead(status, all);
636
636
  res.end(req.method === "HEAD" ? void 0 : buffer);
637
637
  };
638
- const serve = async (req, res, kind, path, body, site, cache2) => {
638
+ const serve = async (req, res, kind, path, body, site, cache3) => {
639
639
  if (!body) return false;
640
640
  const headers = headersFor(kind, path, site.policy);
641
- send(req, res, 200, cache2 ? { ...headers, "Cache-Control": cache2 } : headers, body);
641
+ send(req, res, 200, cache3 ? { ...headers, "Cache-Control": cache3 } : headers, body);
642
642
  return true;
643
643
  };
644
644
  return async (req, res) => {
@@ -837,18 +837,18 @@ var init_server = __esm({
837
837
  init_site();
838
838
  MAX_DEV_ERRORS = 500;
839
839
  clip = (value, max) => typeof value === "string" ? value.slice(0, max) : void 0;
840
- readBody = (req, max) => new Promise((resolve3) => {
840
+ readBody = (req, max) => new Promise((resolve4) => {
841
841
  let size = 0;
842
842
  const chunks = [];
843
843
  req.on("data", (chunk) => {
844
844
  size += chunk.length;
845
845
  if (size > max) {
846
- resolve3(null);
846
+ resolve4(null);
847
847
  req.destroy();
848
848
  } else chunks.push(chunk);
849
849
  });
850
- req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
851
- req.on("error", () => resolve3(null));
850
+ req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
851
+ req.on("error", () => resolve4(null));
852
852
  });
853
853
  LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
854
854
  }
@@ -1017,7 +1017,7 @@ async function startDev(options) {
1017
1017
  versions: { dev: outDir },
1018
1018
  activeVersion: "dev",
1019
1019
  posOrigins: options.posOrigins,
1020
- apiOrigins: [new URL(options.api).origin],
1020
+ apiOrigins: [.../* @__PURE__ */ new Set([new URL(options.api).origin, ...POS_API_ORIGINS])],
1021
1021
  allowedOrigins: readManifest(root).network?.allowed_origins ?? [],
1022
1022
  dev: true,
1023
1023
  devEvents: events,
@@ -1050,11 +1050,11 @@ async function startDev(options) {
1050
1050
  await modules.dispose();
1051
1051
  };
1052
1052
  try {
1053
- await new Promise((resolve3, reject) => {
1053
+ await new Promise((resolve4, reject) => {
1054
1054
  server.once("error", reject);
1055
1055
  server.listen(options.port, "127.0.0.1", () => {
1056
1056
  server.off("error", reject);
1057
- resolve3();
1057
+ resolve4();
1058
1058
  });
1059
1059
  });
1060
1060
  } catch (error2) {
@@ -1069,11 +1069,11 @@ async function startDev(options) {
1069
1069
  buildErrors: () => buildErrors,
1070
1070
  async close() {
1071
1071
  await stopWatching();
1072
- await new Promise((resolve3) => server.close(() => resolve3()));
1072
+ await new Promise((resolve4) => server.close(() => resolve4()));
1073
1073
  }
1074
1074
  };
1075
1075
  }
1076
- var DEFAULT_POS_ORIGINS;
1076
+ var DEFAULT_POS_ORIGINS, POS_API_ORIGINS;
1077
1077
  var init_dev = __esm({
1078
1078
  "src/dev.ts"() {
1079
1079
  "use strict";
@@ -1092,6 +1092,19 @@ var init_dev = __esm({
1092
1092
  "https://crm.pancake.in",
1093
1093
  "https://crm.pancake.biz"
1094
1094
  ];
1095
+ POS_API_ORIGINS = [
1096
+ "https://pos.pancake.vn",
1097
+ "https://pos.pancake.ph",
1098
+ "https://pos.pancake.id",
1099
+ "https://pos.pancake.in",
1100
+ "https://pos.pancake.biz",
1101
+ "https://pos.pages.fm",
1102
+ "https://crm.pancake.ph",
1103
+ "https://crm.pancake.id",
1104
+ "https://crm.pancake.in",
1105
+ "https://crm.pancake.biz",
1106
+ "https://dev.pos.pancake.vn"
1107
+ ];
1095
1108
  }
1096
1109
  });
1097
1110
 
@@ -1147,7 +1160,7 @@ var CLI_VERSION;
1147
1160
  var init_version = __esm({
1148
1161
  "src/version.ts"() {
1149
1162
  "use strict";
1150
- CLI_VERSION = true ? "2026.1.5" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
1163
+ CLI_VERSION = true ? "2026.1.7" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
1151
1164
  }
1152
1165
  });
1153
1166
 
@@ -1227,7 +1240,7 @@ function createMcpServer(options) {
1227
1240
  const task = run2().finally(() => pending.delete(task));
1228
1241
  pending.add(task);
1229
1242
  });
1230
- return new Promise((resolve3) => lines.on("close", () => void Promise.all(pending).then(() => resolve3())));
1243
+ return new Promise((resolve4) => lines.on("close", () => void Promise.all(pending).then(() => resolve4())));
1231
1244
  }
1232
1245
  };
1233
1246
  }
@@ -1398,20 +1411,343 @@ var init_records = __esm({
1398
1411
  }
1399
1412
  });
1400
1413
 
1401
- // src/mcp/docs.ts
1414
+ // src/mcp/api.ts
1402
1415
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
1403
1416
  import { dirname as dirname4, join as join7 } from "node:path";
1404
1417
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1405
- function docsFile() {
1418
+ function apiFile() {
1406
1419
  let dir = dirname4(fileURLToPath2(import.meta.url));
1407
1420
  for (let i = 0; i < 4; i += 1) {
1408
- const file = join7(dir, "docs", "llms-full.txt");
1421
+ const file = join7(dir, "docs", "openapi.json");
1409
1422
  if (existsSync5(file)) return file;
1410
1423
  dir = dirname4(dir);
1411
1424
  }
1412
1425
  return null;
1413
1426
  }
1414
- function loadSections(text = readFileSync4(docsFile() ?? "", "utf8")) {
1427
+ function loadApi(spec) {
1428
+ if (!spec && cache) return cache;
1429
+ const source = spec ?? JSON.parse(readFileSync4(apiFile() ?? "", "utf8"));
1430
+ const operations = [];
1431
+ for (const [path, item] of Object.entries(source.paths ?? {})) {
1432
+ for (const method of METHODS2) {
1433
+ const op = item[method];
1434
+ if (op) operations.push({ method: method.toUpperCase(), path, tag: op.tags?.[0] ?? "", summary: op.summary ?? "", op });
1435
+ }
1436
+ }
1437
+ const api = { spec: source, operations };
1438
+ if (!spec) cache = api;
1439
+ return api;
1440
+ }
1441
+ function routeKey(path) {
1442
+ let value = path.trim().replace(/^https?:\/\/[^/]+/, "").replace(/[?#].*$/, "");
1443
+ value = value.replace(/\$\{[^}]*\}/g, "{}").replace(/\{[^}]*\}/g, "{}").replace(/(^|\/):[A-Za-z_]\w*/g, "$1{}");
1444
+ value = value.replace(/^\/?api\/v1(?=\/)/, "");
1445
+ if (!value.startsWith("/")) value = `/${value}`;
1446
+ return value.replace(/\/+$/, "") || "/";
1447
+ }
1448
+ function findOperations(path, method, api = loadApi()) {
1449
+ const input = routeKey(path).split("/");
1450
+ let best = -1;
1451
+ let found = [];
1452
+ for (const operation of api.operations) {
1453
+ if (method && operation.method !== method.toUpperCase()) continue;
1454
+ const segments = routeKey(operation.path).split("/");
1455
+ if (segments.length !== input.length) continue;
1456
+ let score = 0;
1457
+ const ok2 = segments.every((segment, i) => {
1458
+ if (segment === input[i]) return score += segment === "{}" ? 0 : 1, true;
1459
+ return segment === "{}" && input[i] !== "";
1460
+ });
1461
+ if (!ok2) continue;
1462
+ if (score > best) [best, found] = [score, [operation]];
1463
+ else if (score === best) found.push(operation);
1464
+ }
1465
+ return found;
1466
+ }
1467
+ function searchApi(query, limit = 15, api = loadApi()) {
1468
+ const terms = [...new Set(words(query))];
1469
+ const method = terms.find((term) => METHODS2.includes(term))?.toUpperCase();
1470
+ const keywords = terms.filter((term) => !METHODS2.includes(term));
1471
+ if (!keywords.length) return [];
1472
+ return api.operations.filter((operation) => !method || operation.method === method).map((operation) => {
1473
+ const { op } = operation;
1474
+ const fields = [
1475
+ [`${operation.summary} ${Object.values(op["x-summary-i18n"] ?? {}).join(" ")}`, 4],
1476
+ [`${operation.path.replace(/\{[^}]+\}/g, " ")} ${operation.path.replace(/[{}/_]/g, " ")}`, 3],
1477
+ [`${(op.tags ?? []).join(" ")} ${JSON.stringify(op["x-tags-i18n"] ?? "")}`, 3],
1478
+ [(op.parameters ?? []).map((param) => param.name ?? "").join(" "), 1],
1479
+ [op.description ?? "", 1]
1480
+ ];
1481
+ const folded = fields.map(([text, weight]) => [new Set(words(text)), weight]);
1482
+ let score = 0;
1483
+ let matched = 0;
1484
+ for (const term of keywords) {
1485
+ let hit = 0;
1486
+ for (const [set, weight] of folded) if ([...set].some((word) => word === term || term.length >= 4 && word.startsWith(term))) hit += weight;
1487
+ if (hit) matched += 1;
1488
+ score += hit;
1489
+ }
1490
+ return { operation, score: score * (matched / keywords.length) ** 2 };
1491
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((item) => item.operation);
1492
+ }
1493
+ function formatSearch(query, limit = 15, api = loadApi()) {
1494
+ const hits = searchApi(query, limit, api);
1495
+ if (hits.length) return hits.map((hit) => `${hit.method} ${hit.path}: ${hit.summary} [${hit.tag}]${needs(hit.method)}`).join("\n");
1496
+ const groups = api.spec["x-tagGroups"] ?? [];
1497
+ return [
1498
+ `No endpoint matches "${query}". Try other English keywords (orders, customers, inventory, warehouses\u2026) or a path such as /shops/{SHOP_ID}/orders.`,
1499
+ "Groups:",
1500
+ ...groups.map((group) => `- ${group.name}: ${group.tags.join(", ")}`)
1501
+ ].join("\n");
1502
+ }
1503
+ function resolve2(value, spec) {
1504
+ let current = value;
1505
+ for (let i = 0; i < 5 && current?.$ref; i += 1) {
1506
+ const path = String(current.$ref).replace(/^#\//, "").split("/");
1507
+ current = path.reduce((node, key) => node?.[key], spec);
1508
+ }
1509
+ return current;
1510
+ }
1511
+ function enumText(schema) {
1512
+ const values = schema.enum.slice(0, MAX_ENUM).map((value) => JSON.stringify(value));
1513
+ return `${values.join(" | ")}${schema.enum.length > MAX_ENUM ? ` | \u2026 (${schema.enum.length} values)` : ""}`;
1514
+ }
1515
+ function typeText(input, ctx, indent, depth, chain = []) {
1516
+ if (!input) return "unknown";
1517
+ if (input.$ref) {
1518
+ const name = refName(input.$ref);
1519
+ if (depth >= ctx.maxDepth - 1 || chain.includes(name)) {
1520
+ ctx.refs.add(name);
1521
+ return name;
1522
+ }
1523
+ return typeText(resolve2(input, ctx.spec), ctx, indent, depth, [...chain, name]);
1524
+ }
1525
+ const schema = input;
1526
+ const union = schema.oneOf ?? schema.anyOf;
1527
+ if (Array.isArray(union)) return union.map((item) => typeText(item, ctx, indent, depth, chain)).join(" | ");
1528
+ if (Array.isArray(schema.allOf)) {
1529
+ const merged = { type: "object", properties: {}, required: [] };
1530
+ for (const part of schema.allOf.map((item) => resolve2(item, ctx.spec) ?? {})) {
1531
+ Object.assign(merged.properties, part.properties ?? {});
1532
+ merged.required.push(...part.required ?? []);
1533
+ }
1534
+ return typeText(merged, ctx, indent, depth, chain);
1535
+ }
1536
+ if (Array.isArray(schema.enum)) return enumText(schema);
1537
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type ?? (schema.properties ? "object" : schema.items ? "array" : "unknown")];
1538
+ const nullable = types.includes("null") || schema.nullable ? " | null" : "";
1539
+ const type = types.find((item) => item !== "null") ?? "unknown";
1540
+ if (type === "array") {
1541
+ const item = typeText(schema.items, ctx, indent, depth, chain);
1542
+ return `${item.includes("\n") || item.includes("|") ? `Array<${item}>` : `${item}[]`}${nullable}`;
1543
+ }
1544
+ if (type === "object") {
1545
+ const properties = Object.entries(schema.properties ?? {});
1546
+ if (!properties.length) {
1547
+ const extra = schema.additionalProperties && typeof schema.additionalProperties === "object" ? typeText(schema.additionalProperties, ctx, indent, depth + 1, chain) : "unknown";
1548
+ return `Record<string, ${extra}>${nullable}`;
1549
+ }
1550
+ if (depth >= ctx.maxDepth) return `{ \u2026 }${nullable}`;
1551
+ const required = new Set(schema.required ?? []);
1552
+ const inner = `${indent} `;
1553
+ const lines = properties.map(([name, property]) => {
1554
+ const key = /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
1555
+ const resolved = resolve2(property, ctx.spec) ?? {};
1556
+ return `${inner}${key}${required.has(name) ? "" : "?"}: ${typeText(property, ctx, inner, depth + 1, chain)};${comment(property.description ?? resolved.description)}`;
1557
+ });
1558
+ return `{
1559
+ ${lines.join("\n")}
1560
+ ${indent}}${nullable}`;
1561
+ }
1562
+ const format = schema.format ? ` /* ${schema.format} */` : "";
1563
+ return `${type === "integer" ? "number" : type}${format}${nullable}`;
1564
+ }
1565
+ function renderSchema(schema, spec) {
1566
+ for (const maxDepth of [5, 4, 3, 2, 1]) {
1567
+ const ctx = { spec, maxDepth, refs: /* @__PURE__ */ new Set() };
1568
+ const text = typeText(schema, ctx, "", 0);
1569
+ if (text.split("\n").length <= MAX_LINES || maxDepth === 1) return { text, refs: [...ctx.refs] };
1570
+ }
1571
+ return { text: "unknown", refs: [] };
1572
+ }
1573
+ function paramLine(param, spec) {
1574
+ const schema = resolve2(param.schema, spec) ?? {};
1575
+ const descriptions = schema["x-enum-descriptions"];
1576
+ const { text } = renderSchema(Array.isArray(schema.enum) && descriptions?.length ? { ...schema, enum: void 0 } : schema, spec);
1577
+ const parts = [text.includes("\n") ? "object" : text];
1578
+ if (param.required) parts.push("required");
1579
+ if (schema.default !== void 0) parts.push(`default ${JSON.stringify(schema.default)}`);
1580
+ const values = Array.isArray(schema.enum) && descriptions?.length ? `; values: ${schema.enum.slice(0, MAX_ENUM).map((value, i) => `${JSON.stringify(value)} = ${descriptions[i] ?? ""}`).join(", ")}` : "";
1581
+ const hint = param.in === "path" && ID_HINTS[param.name] ? `; use ${ID_HINTS[param.name]}` : "";
1582
+ const description = String(param.description ?? schema.description ?? "").trim();
1583
+ return `- ${param.name} (${parts.join(", ")})${description ? `: ${description}` : ""}${values}${hint}`;
1584
+ }
1585
+ function callSnippet(operation, query) {
1586
+ const path = operation.path.replace(/\{([^}]+)\}/g, (_, name) => name === "SHOP_ID" ? "${pos.shop.id}" : `\${${camel(name)}}`).replace(/\/$/, "");
1587
+ const required = query.filter((param) => param.required);
1588
+ const options = required.length ? `, { query: { ${required.map((param) => `${JSON.stringify(param.name)}: \u2026`).join(", ")} } }` : "";
1589
+ const method = operation.method.toLowerCase();
1590
+ if (method === "get" || method === "delete") return `const response = await pos.api.${method}(\`${path}\`${options});`;
1591
+ return `const response = await pos.api.${method}(\`${path}\`, body${options});`;
1592
+ }
1593
+ function example(content, spec) {
1594
+ if (!content) return null;
1595
+ let value = content.example;
1596
+ if (value === void 0 && content.examples) {
1597
+ const first = Object.values(content.examples)[0];
1598
+ value = (resolve2(first, spec) ?? first)?.value;
1599
+ }
1600
+ if (value === void 0) return null;
1601
+ const text = JSON.stringify(value, null, 2);
1602
+ return text.length > MAX_EXAMPLE ? `${text.slice(0, MAX_EXAMPLE)}
1603
+ \u2026 (truncated)` : text;
1604
+ }
1605
+ function describeField(operation, field, api = loadApi()) {
1606
+ const { spec } = api;
1607
+ const parts = field.split(".").filter(Boolean);
1608
+ const inBody = parts[0] === "body" || parts[0] === "request";
1609
+ const root = inBody ? bodyContent(operation.op, spec)?.[1] : responseContent(operation.op, spec)?.[1];
1610
+ let node = resolve2(root?.schema, spec);
1611
+ const walked = [];
1612
+ for (const part of inBody ? parts.slice(1) : parts) {
1613
+ while (node && (node.type === "array" || node.items)) node = resolve2(node.items, spec);
1614
+ const next = node?.properties?.[part];
1615
+ if (!next) {
1616
+ const fields = Object.keys(node?.properties ?? {});
1617
+ return `No field "${part}" under ${inBody ? "body" : "response"}${walked.length ? `.${walked.join(".")}` : ""} of ${operation.method} ${operation.path}.${fields.length ? ` Fields: ${fields.join(", ")}.` : ""}`;
1618
+ }
1619
+ walked.push(part);
1620
+ node = resolve2(next, spec);
1621
+ }
1622
+ const rendered = renderSchema(node, spec);
1623
+ return [
1624
+ `# ${operation.method} ${operation.path} \u203A ${inBody ? "body" : "response"}${walked.length ? `.${walked.join(".")}` : ""}`,
1625
+ ...node?.description ? [String(node.description)] : [],
1626
+ "```ts",
1627
+ rendered.text,
1628
+ "```",
1629
+ ...rendered.refs.length ? [`Shared schemas used above: ${rendered.refs.join(", ")}. Call get_api with schema to expand one.`] : []
1630
+ ].join("\n");
1631
+ }
1632
+ function describeOperation(operation, api = loadApi()) {
1633
+ const { spec } = api;
1634
+ const { op } = operation;
1635
+ const params = (op.parameters ?? []).map((param) => resolve2(param, spec) ?? param);
1636
+ const byPlace = (place) => params.filter((param) => param.in === place);
1637
+ const refs = /* @__PURE__ */ new Set();
1638
+ const lines = [
1639
+ `# ${operation.method} ${operation.path}`,
1640
+ operation.summary,
1641
+ ...op.description && op.description !== operation.summary ? [String(op.description).trim()] : [],
1642
+ `Tag: ${operation.tag}`,
1643
+ "",
1644
+ "## Call from an app",
1645
+ "```ts",
1646
+ callSnippet(operation, byPlace("query")),
1647
+ "```",
1648
+ operation.method === "GET" ? "- App tokens may call it; GET needs nothing in access.methods." : `- App tokens may call it after the admin approves "${operation.method}" in access.methods of pos-app.json.`,
1649
+ "- The signed-in user's POS permissions and the app's access limits still apply: handle 403 (and 429).",
1650
+ "- pos.api.get/delete take { query }; post/put take the body, then { query }. pos.api adds the base URL (\u2026/api/v1) and the app token."
1651
+ ];
1652
+ for (const [title, place] of [
1653
+ ["Path parameters", "path"],
1654
+ ["Query parameters", "query"]
1655
+ ]) {
1656
+ const list2 = byPlace(place);
1657
+ if (list2.length) lines.push("", `## ${title}`, ...list2.map((param) => paramLine(param, spec)));
1658
+ }
1659
+ let collapsed = false;
1660
+ const body = bodyContent(op, spec);
1661
+ if (body) {
1662
+ const [type, content] = body;
1663
+ const rendered = renderSchema(content.schema, spec);
1664
+ rendered.refs.forEach((ref) => refs.add(ref));
1665
+ collapsed ||= rendered.text.includes("{ \u2026 }");
1666
+ lines.push("", `## Request body (${type}${content.required ? ", required" : ""})`, "```ts", rendered.text, "```");
1667
+ const sample = example(content, spec);
1668
+ if (sample) lines.push("Example:", "```json", sample, "```");
1669
+ }
1670
+ const response = responseContent(op, spec);
1671
+ if (response) {
1672
+ const [status, content] = response;
1673
+ const rendered = renderSchema(content.schema, spec);
1674
+ rendered.refs.forEach((ref) => refs.add(ref));
1675
+ collapsed ||= rendered.text.includes("{ \u2026 }");
1676
+ lines.push("", `## Response ${status}`, "```ts", rendered.text, "```");
1677
+ const sample = example(content, spec);
1678
+ if (sample) lines.push("Example:", "```json", sample, "```");
1679
+ }
1680
+ if (collapsed) lines.push("", 'Nested objects shown as { \u2026 }: call get_api again with field to expand one, for example "data.items" (response) or "body.customer" (request body).');
1681
+ if (refs.size) lines.push("", `Shared schemas used above: ${[...refs].join(", ")}. Call get_api with schema to expand one.`);
1682
+ return lines.join("\n");
1683
+ }
1684
+ function describeSchema(name, api = loadApi()) {
1685
+ const schemas = api.spec.components?.schemas ?? {};
1686
+ const key = Object.keys(schemas).find((item) => item.toLowerCase() === name.trim().toLowerCase());
1687
+ if (!key) return null;
1688
+ const rendered = renderSchema(schemas[key], api.spec);
1689
+ const description = schemas[key].description ? `
1690
+ ${schemas[key].description}` : "";
1691
+ const refs = rendered.refs.filter((ref) => ref !== key);
1692
+ return [`# ${key}${description}`, "```ts", rendered.text, "```", ...refs.length ? [`Shared schemas used above: ${refs.join(", ")}.`] : []].join("\n");
1693
+ }
1694
+ var METHODS2, MAX_LINES, MAX_ENUM, MAX_EXAMPLE, ID_HINTS, cache, fold, words, needs, refName, comment, camel, bodyContent, responseContent, schemaNames;
1695
+ var init_api2 = __esm({
1696
+ "src/mcp/api.ts"() {
1697
+ "use strict";
1698
+ METHODS2 = ["get", "post", "put", "delete", "patch"];
1699
+ MAX_LINES = 160;
1700
+ MAX_ENUM = 30;
1701
+ MAX_EXAMPLE = 2500;
1702
+ ID_HINTS = {
1703
+ SHOP_ID: "pos.shop.id",
1704
+ ORDER_ID: "the order system_id (record.system_id in pos.data)",
1705
+ CUSTOMER_ID: "the customer id (record.id in customer.* targets, draft.customer_id in order targets)",
1706
+ PRODUCT_ID: "the product id (record.id in product targets)",
1707
+ PURCHASE_ID: "the purchase id (record.id in purchase targets)",
1708
+ EXPORT_ID: "the export id (record.id in export targets)",
1709
+ TRANSFER_ID: "the transfer id (record.id in warehouse_transfer targets)",
1710
+ VARIATION_ID: "a variation id (variation_id of draft items)",
1711
+ WAREHOUSE_ID: "a warehouse id (warehouse_id in pos.data, or list_shop_meta)"
1712
+ };
1713
+ cache = null;
1714
+ fold = (text) => text.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d");
1715
+ words = (text) => fold(text).split(/[^a-z0-9_]+/).filter((word) => word.length >= 2);
1716
+ needs = (method) => method === "GET" ? "" : ` (needs "${method}" in access.methods)`;
1717
+ refName = (ref) => ref.split("/").pop() ?? ref;
1718
+ comment = (text) => {
1719
+ const value = String(text ?? "").replace(/\s+/g, " ").trim();
1720
+ return value ? ` // ${value.length > 120 ? `${value.slice(0, 117)}\u2026` : value}` : "";
1721
+ };
1722
+ camel = (name) => name.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1723
+ bodyContent = (op, spec) => {
1724
+ const body = op.requestBody && (resolve2(op.requestBody, spec) ?? op.requestBody);
1725
+ const entry = body?.content && Object.entries(body.content)[0];
1726
+ return entry ? [entry[0], { ...entry[1], required: body.required }] : null;
1727
+ };
1728
+ responseContent = (op, spec) => {
1729
+ const [status, response] = Object.entries(op.responses ?? {}).find(([code]) => code.startsWith("2")) ?? [];
1730
+ const content = response && (resolve2(response, spec) ?? response).content?.["application/json"];
1731
+ return status && content ? [status, content] : null;
1732
+ };
1733
+ schemaNames = (api = loadApi()) => Object.keys(api.spec.components?.schemas ?? {});
1734
+ }
1735
+ });
1736
+
1737
+ // src/mcp/docs.ts
1738
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
1739
+ import { dirname as dirname5, join as join8 } from "node:path";
1740
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1741
+ function docsFile() {
1742
+ let dir = dirname5(fileURLToPath3(import.meta.url));
1743
+ for (let i = 0; i < 4; i += 1) {
1744
+ const file = join8(dir, "docs", "llms-full.txt");
1745
+ if (existsSync6(file)) return file;
1746
+ dir = dirname5(dir);
1747
+ }
1748
+ return null;
1749
+ }
1750
+ function loadSections(text = readFileSync5(docsFile() ?? "", "utf8")) {
1415
1751
  const sections2 = [];
1416
1752
  for (const block of text.split(/^Source: /m).slice(1)) {
1417
1753
  const newline = block.indexOf("\n");
@@ -1440,8 +1776,8 @@ function loadSections(text = readFileSync4(docsFile() ?? "", "utf8")) {
1440
1776
  return sections2;
1441
1777
  }
1442
1778
  function sections() {
1443
- cache ??= docsFile() ? loadSections() : [];
1444
- return cache;
1779
+ cache2 ??= docsFile() ? loadSections() : [];
1780
+ return cache2;
1445
1781
  }
1446
1782
  function searchDocs(query, limit = 5, all = sections()) {
1447
1783
  const terms = [...new Set(tokenize(query))];
@@ -1466,7 +1802,7 @@ function typeBlock(name, all) {
1466
1802
  }
1467
1803
  return null;
1468
1804
  }
1469
- function example(target, location, resource) {
1805
+ function example2(target, location, resource) {
1470
1806
  const id = target.replace(/[^a-z0-9]+/g, "-").replace(/-+$/, "").slice(0, 40);
1471
1807
  if (target === "nav.item") return '```json\n{ "target": "nav.item", "id": "nav", "label": "Reconciliation", "page": "home" }\n```';
1472
1808
  if (target === "app.page") return '```json\n{ "target": "app.page", "id": "home", "title": "Reconciliation", "render": "iframe", "entry": "page/index.html" }\n```';
@@ -1534,19 +1870,19 @@ function describeTarget(target, all = sections()) {
1534
1870
  ...types.length ? ["", "```ts", ...types, "```"] : [],
1535
1871
  "",
1536
1872
  "## Example",
1537
- example(target, location, resource),
1873
+ example2(target, location, resource),
1538
1874
  ...docs.map((section) => `
1539
1875
  ## Docs: ${section.heading}
1540
1876
  ${section.text.replace(/^#+ .+\n/, "")}`)
1541
1877
  ].join("\n");
1542
1878
  }
1543
- var API_VERSION, cache, tokenize, DATA, MANIFEST, pascal;
1879
+ var API_VERSION, cache2, tokenize, DATA, MANIFEST, pascal;
1544
1880
  var init_docs = __esm({
1545
1881
  "src/mcp/docs.ts"() {
1546
1882
  "use strict";
1547
1883
  init_manifest();
1548
1884
  API_VERSION = "2026-01";
1549
- cache = null;
1885
+ cache2 = null;
1550
1886
  tokenize = (text) => text.toLowerCase().match(/[a-z0-9_.\-*]{2,}/g) ?? [];
1551
1887
  DATA = {
1552
1888
  "details.action": "DetailsData",
@@ -1589,7 +1925,7 @@ function createTools(ctx) {
1589
1925
  let dev = null;
1590
1926
  const devLog = [];
1591
1927
  const shop = () => {
1592
- if (!ctx.shop) throw new Error("Missing shop: start the server with --shop <shop_id> or run pos-app login --shop <shop_id>");
1928
+ if (!ctx.shop) throw new Error("Missing shop: start the server with --shop <shop_id> or run npx @pancake-pos/app-cli login --shop <shop_id>");
1593
1929
  return ctx.shop;
1594
1930
  };
1595
1931
  const get = (path) => apiRequest(ctx.credentials, "GET", `/shops/${shop()}/${path}`);
@@ -1645,6 +1981,52 @@ ${hit.text.slice(0, 4e3)}`).join("\n\n---\n\n") };
1645
1981
  return text ? { text } : fail(`Unknown target. Valid targets: ${targets(API_VERSION2).join(", ")}`);
1646
1982
  }
1647
1983
  },
1984
+ {
1985
+ name: "search_api",
1986
+ title: "Search the Open API",
1987
+ description: 'Find Open API endpoints to call with pos.api. Lists only the operations published in the Open API, the only ones app tokens may call. Keywords in English or Vietnamese, optionally with a method ("post order"). Returns method, path, summary, tag and the access.methods it needs; then call get_api for the details.',
1988
+ inputSchema: { type: "object", properties: { query: { type: "string", description: 'For example "order list", "update customer", "inventory by warehouse"' }, limit: { type: "integer", minimum: 1, maximum: 30 } }, required: ["query"] },
1989
+ annotations: { readOnlyHint: true, openWorldHint: false },
1990
+ async run(args) {
1991
+ return { text: formatSearch(String(args.query ?? ""), Math.min(Number(args.limit) || 15, 30)) };
1992
+ }
1993
+ },
1994
+ {
1995
+ name: "get_api",
1996
+ title: "Describe an Open API endpoint",
1997
+ description: 'How to call one Open API endpoint from an app: the pos.api call, the access.methods it needs, path and query parameters, request body and response shapes (TypeScript-like) and an example. path as in the docs (/shops/{SHOP_ID}/orders/{ORDER_ID}) or as called (/shops/123/orders/45). field expands one nested object ("data.items" in the response, "body.customer" in the request body). schema describes a shared schema such as Order or Customer instead.',
1998
+ inputSchema: {
1999
+ type: "object",
2000
+ properties: {
2001
+ method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] },
2002
+ path: { type: "string", description: "For example /shops/{SHOP_ID}/orders/{ORDER_ID}" },
2003
+ field: { type: "string", description: "Dotted field path to expand, for example data.items or body.customer" },
2004
+ schema: { type: "string", description: "Shared schema name, for example Order" }
2005
+ }
2006
+ },
2007
+ annotations: { readOnlyHint: true, openWorldHint: false },
2008
+ async run(args) {
2009
+ if (args.schema !== void 0) {
2010
+ const text = describeSchema(String(args.schema));
2011
+ return text ? { text } : fail(`Unknown schema. Shared schemas: ${schemaNames().join(", ")}`);
2012
+ }
2013
+ const path = String(args.path ?? "").trim();
2014
+ if (!path) return fail("Pass path (for example /shops/{SHOP_ID}/orders) or schema. Use search_api to find endpoints.");
2015
+ const method = args.method === void 0 ? void 0 : String(args.method).toUpperCase();
2016
+ const found = findOperations(path, method);
2017
+ if (!found.length) {
2018
+ const other = method ? findOperations(path) : [];
2019
+ if (other.length) return fail(`${method} ${path} is not in the Open API. Methods for this path: ${other.map((item) => item.method).join(", ")}.`);
2020
+ return fail(`${path} is not in the Open API, so app tokens cannot call it. Use search_api to find a published endpoint.`);
2021
+ }
2022
+ if (found.length > 1 && new Set(found.map((item) => item.method)).size > 1) {
2023
+ return { text: `Several operations match; pass method:
2024
+ ${found.map((item) => `- ${item.method} ${item.path}: ${item.summary}`).join("\n")}` };
2025
+ }
2026
+ const operation = found[0];
2027
+ return { text: args.field ? describeField(operation, String(args.field)) : describeOperation(operation) };
2028
+ }
2029
+ },
1648
2030
  {
1649
2031
  name: "list_shop_meta",
1650
2032
  title: "List shop metadata",
@@ -1854,6 +2236,7 @@ var init_tools = __esm({
1854
2236
  init_dev();
1855
2237
  init_manifest();
1856
2238
  init_version();
2239
+ init_api2();
1857
2240
  init_docs();
1858
2241
  API_VERSION2 = "2026-01";
1859
2242
  ORDER_STATUSES = [
@@ -1921,13 +2304,14 @@ var init_mcp = __esm({
1921
2304
  init_tools();
1922
2305
  INSTRUCTIONS = `Tools for building a Pancake POS app in this folder.
1923
2306
  Typical loop: get_target and search_docs to pick targets and types; list_shop_meta and sample_context to use real ids and fields;
2307
+ search_api and get_api before every pos.api call (only Open API endpoints work, with their exact parameters and response shapes);
1924
2308
  write code, then check_app until there are no errors; dev_start and try it in POS, then get_errors; deploy (uploads an inactive version for an admin to activate).
1925
2309
  When the user wants to report a POS Apps bug or ask for a feature, draft it, show it, and call send_feedback only after they confirm.`;
1926
2310
  }
1927
2311
  });
1928
2312
 
1929
2313
  // src/cli.ts
1930
- import { resolve as resolve2 } from "node:path";
2314
+ import { resolve as resolve3 } from "node:path";
1931
2315
  function parseArgs(argv) {
1932
2316
  const [command = "help", ...rest] = argv;
1933
2317
  const flags = {};
@@ -1945,7 +2329,7 @@ function parseArgs(argv) {
1945
2329
  async function run(argv, io = defaultIo) {
1946
2330
  const { command, flags } = parseArgs(argv);
1947
2331
  const json2 = flags.json === true;
1948
- const root = resolve2(str2(flags.cwd) ?? process.cwd());
2332
+ const root = resolve3(str2(flags.cwd) ?? process.cwd());
1949
2333
  const emit = (result, text) => io.out(json2 ? JSON.stringify(result) : text);
1950
2334
  const line = (mark, e) => `${mark} ${e.path ? `${e.path}: ` : ""}${e.message}${e.code && e.code !== "error" ? ` [${e.code}]` : ""}`;
1951
2335
  const fail2 = (errors, warnings = []) => {
@@ -1967,7 +2351,7 @@ async function run(argv, io = defaultIo) {
1967
2351
  const credentials = readCredentials();
1968
2352
  const shopOf = () => {
1969
2353
  const shop = str2(flags.shop) ?? credentials.shop;
1970
- if (!shop) throw new Error("Thi\u1EBFu --shop <shop_id> (ho\u1EB7c l\u01B0u b\u1EB1ng `pos-app login --shop`)");
2354
+ if (!shop) throw new Error("Thi\u1EBFu --shop <shop_id> (ho\u1EB7c l\u01B0u b\u1EB1ng `npx @pancake-pos/app-cli login --shop`)");
1971
2355
  return shop;
1972
2356
  };
1973
2357
  switch (command) {
@@ -1996,7 +2380,7 @@ async function run(argv, io = defaultIo) {
1996
2380
  return 0;
1997
2381
  }
1998
2382
  case "build": {
1999
- const result = await buildApp(root, { outDir: resolve2(root, str2(flags.out) ?? "dist") });
2383
+ const result = await buildApp(root, { outDir: resolve3(root, str2(flags.out) ?? "dist") });
2000
2384
  emit({ ok: true, outDir: result.outDir, files: result.files, totalBytes: result.totalBytes }, `\u2714 Build xong ${Object.keys(result.files).length} file (${(result.totalBytes / 1024).toFixed(0)} KB) \u2192 ${result.outDir}`);
2001
2385
  return 0;
2002
2386
  }
@@ -2004,7 +2388,7 @@ async function run(argv, io = defaultIo) {
2004
2388
  const shop = shopOf();
2005
2389
  const code = await lint();
2006
2390
  if (code.errors.length) return fail2(code.errors, json2 ? code.warnings : []);
2007
- const result = await buildApp(root, { outDir: resolve2(root, "dist") });
2391
+ const result = await buildApp(root, { outDir: resolve3(root, "dist") });
2008
2392
  const form = new FormData();
2009
2393
  form.append("bundle", new Blob([Buffer.from(zipDir(result.outDir))], { type: "application/zip" }), "bundle.zip");
2010
2394
  form.append("source", str2(flags.source) === "mcp" ? "mcp" : "cli");