@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 +5 -2
- package/dist/cli.js +414 -30
- package/docs/llms-full.txt +340 -38
- package/docs/openapi.json +1 -0
- package/package.json +2 -2
- package/runtime/frame.js +2 -1
- package/runtime/frame.js.map +1 -1
- package/runtime/worker.js +219 -18
- package/runtime/worker.js.map +3 -3
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
|
-
|
|
8
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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((
|
|
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
|
-
|
|
846
|
+
resolve4(null);
|
|
847
847
|
req.destroy();
|
|
848
848
|
} else chunks.push(chunk);
|
|
849
849
|
});
|
|
850
|
-
req.on("end", () =>
|
|
851
|
-
req.on("error", () =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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.
|
|
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((
|
|
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/
|
|
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
|
|
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", "
|
|
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
|
|
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
|
-
|
|
1444
|
-
return
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 =
|
|
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-
|
|
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:
|
|
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:
|
|
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");
|