@commercelayer/agent-cli 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -3
- package/dist/api/client.js +39 -1
- package/dist/api/stub/http.js +2 -2
- package/dist/api/stub/management.js +19 -2
- package/dist/api/stub/messages.js +126 -0
- package/dist/api/stub/orders.js +286 -24
- package/dist/bin.js +26 -4
- package/dist/commands.js +462 -4
- package/dist/cursor.js +31 -0
- package/dist/files.js +5 -3
- package/dist/inbox/cursor.js +11 -0
- package/dist/inbox/fake.js +95 -0
- package/dist/inbox/imap.js +139 -0
- package/dist/inbox/mailbox.js +25 -0
- package/dist/inbox/message.js +88 -0
- package/dist/ops.js +190 -0
- package/dist/orderRequest.js +141 -0
- package/dist/output.js +7 -3
- package/package.json +3 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// An order request from an email or a message (orders create), checked before it is sent: the
|
|
2
|
+
// same rules the API applies, so a request missing what only the human knows fails here, naming
|
|
3
|
+
// the path. Nothing is ever made up: no placeholder names, phones or addresses.
|
|
4
|
+
import { fail } from './output.js';
|
|
5
|
+
export const MAX_ITEMS = 25;
|
|
6
|
+
const KEYS = ['source', 'customer', 'ship_to', 'bill_to', 'items', 'notes', 'reference', 'language', 'place', 'force'];
|
|
7
|
+
const ASK = 'Ask the human for it; never invent it.';
|
|
8
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
9
|
+
const bad = (path, what, hint = ASK) => fail('invalid_argument', `${path}: ${what}`, hint, path);
|
|
10
|
+
/** A text field: absent is undefined; present, it is a non-empty string of at most max characters. */
|
|
11
|
+
function text(o, key, path, opts = {}) {
|
|
12
|
+
const v = o[key];
|
|
13
|
+
if (v === undefined || v === null || (typeof v === 'string' && !v.trim())) {
|
|
14
|
+
if (opts.required)
|
|
15
|
+
bad(`${path}.${key}`, 'is missing.');
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (typeof v !== 'string' && typeof v !== 'number')
|
|
19
|
+
bad(`${path}.${key}`, `is ${JSON.stringify(v)}, not text.`, 'Give it as text.');
|
|
20
|
+
const s = String(v).trim();
|
|
21
|
+
// A dash or a question mark stands for "unknown": the human has to say it.
|
|
22
|
+
if (/^[-?.]+$/.test(s))
|
|
23
|
+
bad(`${path}.${key}`, `is "${s}", a placeholder.`);
|
|
24
|
+
if (s.length > (opts.max ?? 255))
|
|
25
|
+
bad(`${path}.${key}`, `is longer than ${opts.max ?? 255} characters.`, 'Shorten it.');
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
function unknownKeys(o, allowed, path) {
|
|
29
|
+
const extra = Object.keys(o).filter((k) => !allowed.includes(k));
|
|
30
|
+
if (extra.length)
|
|
31
|
+
bad(`${path ? `${path}.` : ''}${extra[0]}`, `is not a field${path ? ` of ${path}` : ''}.`, `The fields are ${allowed.join(', ')}.`);
|
|
32
|
+
}
|
|
33
|
+
function address(o, path, withNames) {
|
|
34
|
+
if (!isObj(o))
|
|
35
|
+
return bad(path, 'is missing: the address the order ships to.');
|
|
36
|
+
const fields = ['line_1', 'line_2', 'city', 'postal_code', 'region', 'country', 'phone', ...(withNames ? ['first_name', 'last_name', 'company'] : [])];
|
|
37
|
+
unknownKeys(o, fields, path);
|
|
38
|
+
const country = text(o, 'country', path, { required: true }).toUpperCase();
|
|
39
|
+
if (!/^[A-Z]{2}$/.test(country))
|
|
40
|
+
bad(`${path}.country`, `is "${country}", not an ISO 3166 country code.`, 'Give the two-letter code: IT, FR, US.');
|
|
41
|
+
const out = {
|
|
42
|
+
line_1: text(o, 'line_1', path, { required: true }),
|
|
43
|
+
line_2: text(o, 'line_2', path),
|
|
44
|
+
city: text(o, 'city', path, { required: true }),
|
|
45
|
+
postal_code: text(o, 'postal_code', path, { required: true, max: 20 }),
|
|
46
|
+
region: text(o, 'region', path, { max: 50 }),
|
|
47
|
+
country,
|
|
48
|
+
phone: text(o, 'phone', path, { max: 40 }),
|
|
49
|
+
...(withNames ? { first_name: text(o, 'first_name', path), last_name: text(o, 'last_name', path), company: text(o, 'company', path) } : {}),
|
|
50
|
+
};
|
|
51
|
+
return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined));
|
|
52
|
+
}
|
|
53
|
+
/** The request, checked and trimmed; invalid_argument naming the path, or too_many_items. */
|
|
54
|
+
export function checkOrderRequest(raw) {
|
|
55
|
+
if (!isObj(raw))
|
|
56
|
+
return bad('request', 'is not a set of fields.', `Give it as YAML or JSON with ${KEYS.join(', ')}.`);
|
|
57
|
+
unknownKeys(raw, KEYS, '');
|
|
58
|
+
if (!isObj(raw.customer))
|
|
59
|
+
bad('customer', 'is missing: who the order is for (email, first_name, last_name or company, phone).');
|
|
60
|
+
const c = raw.customer;
|
|
61
|
+
unknownKeys(c, ['email', 'first_name', 'last_name', 'company', 'phone'], 'customer');
|
|
62
|
+
const email = text(c, 'email', 'customer', { required: true });
|
|
63
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
|
|
64
|
+
bad('customer.email', `is "${email}", not an email address.`);
|
|
65
|
+
const customer = { email, first_name: text(c, 'first_name', 'customer'), last_name: text(c, 'last_name', 'customer'), company: text(c, 'company', 'customer'), phone: text(c, 'phone', 'customer', { max: 40 }) };
|
|
66
|
+
if (!customer.company && !(customer.first_name && customer.last_name))
|
|
67
|
+
bad(customer.first_name ? 'customer.last_name' : 'customer.first_name', 'is missing: an order needs the first and last name, or a company.');
|
|
68
|
+
const ship_to = address(raw.ship_to, 'ship_to', false);
|
|
69
|
+
if (!customer.phone && !ship_to.phone)
|
|
70
|
+
bad('customer.phone', 'is missing: Commerce Layer needs a phone number for the shipping address.');
|
|
71
|
+
const bill_to = raw.bill_to === undefined || raw.bill_to === null ? undefined : address(raw.bill_to, 'bill_to', true);
|
|
72
|
+
if (!Array.isArray(raw.items) || raw.items.length === 0)
|
|
73
|
+
bad('items', 'is missing: at least one line, { sku, quantity } or { query, quantity }.', 'Give each line a sku, or the words that name the product as query.');
|
|
74
|
+
const list = raw.items;
|
|
75
|
+
if (list.length > MAX_ITEMS)
|
|
76
|
+
fail('too_many_items', `items: ${list.length} lines, more than the ${MAX_ITEMS} one order takes.`, `Split it into orders of at most ${MAX_ITEMS} lines, with the human.`, 'items');
|
|
77
|
+
const items = list.map((it, i) => {
|
|
78
|
+
const path = `items[${i}]`;
|
|
79
|
+
if (!isObj(it))
|
|
80
|
+
return bad(path, `is ${JSON.stringify(it)}, not { sku, quantity } or { query, quantity }.`, 'Give each line a sku or a query, and a quantity.');
|
|
81
|
+
unknownKeys(it, ['sku', 'query', 'quantity'], path);
|
|
82
|
+
const sku = text(it, 'sku', path, { max: 100 });
|
|
83
|
+
const query = text(it, 'query', path, { max: 200 });
|
|
84
|
+
if (!sku && !query)
|
|
85
|
+
bad(path, 'has neither sku nor query.', 'Give the sku, or the words that name the product as query.');
|
|
86
|
+
const q = it.quantity ?? 1;
|
|
87
|
+
if (typeof q !== 'number' || !Number.isInteger(q) || q < 1 || q > 10_000)
|
|
88
|
+
bad(`${path}.quantity`, `is ${JSON.stringify(q)}, not a whole number from 1 to 10,000.`, 'Give how many, as the message says; ask the human if it looks wrong.');
|
|
89
|
+
return { ...(sku ? { sku } : {}), ...(query ? { query } : {}), quantity: q };
|
|
90
|
+
});
|
|
91
|
+
// Required: it makes the request idempotent, so a message never makes two orders.
|
|
92
|
+
let source;
|
|
93
|
+
{
|
|
94
|
+
const how = 'Give where the order came from: --source email:<Message-ID> for an email, --source message:<id> for anything else (the chat message or form id), or source: { kind, id } in the request.';
|
|
95
|
+
if (raw.source === undefined || raw.source === null)
|
|
96
|
+
bad('source', 'is missing: where the order came from.', how);
|
|
97
|
+
if (!isObj(raw.source))
|
|
98
|
+
return bad('source', 'is not { kind, id }.', how);
|
|
99
|
+
const s = raw.source;
|
|
100
|
+
unknownKeys(s, ['kind', 'id', 'from', 'subject', 'received_at'], 'source');
|
|
101
|
+
const kind = text(s, 'kind', 'source', { required: true });
|
|
102
|
+
if (kind !== 'email' && kind !== 'message')
|
|
103
|
+
bad('source.kind', `is "${kind}", not email or message.`, 'Use email for an email, message for anything else (a chat, a form, a call).');
|
|
104
|
+
source = { kind: kind, id: text(s, 'id', 'source', { required: true }), from: text(s, 'from', 'source'), subject: text(s, 'subject', 'source', { max: 500 }), received_at: text(s, 'received_at', 'source', { max: 40 }) };
|
|
105
|
+
source = Object.fromEntries(Object.entries(source).filter(([, v]) => v !== undefined));
|
|
106
|
+
}
|
|
107
|
+
const language = text(raw, 'language', 'request', { max: 5 })?.toLowerCase();
|
|
108
|
+
if (language && !/^[a-z]{2}$/.test(language))
|
|
109
|
+
bad('language', `is "${language}", not an ISO 639-1 code.`, 'Give the two-letter code: en, it, fr.');
|
|
110
|
+
for (const k of ['place', 'force'])
|
|
111
|
+
if (raw[k] !== undefined && typeof raw[k] !== 'boolean')
|
|
112
|
+
bad(k, `is ${JSON.stringify(raw[k])}, not true or false.`, `Write ${k}: true or leave it out.`);
|
|
113
|
+
const out = {
|
|
114
|
+
source,
|
|
115
|
+
customer: Object.fromEntries(Object.entries(customer).filter(([, v]) => v !== undefined)),
|
|
116
|
+
ship_to,
|
|
117
|
+
...(bill_to ? { bill_to } : {}),
|
|
118
|
+
items,
|
|
119
|
+
...(text(raw, 'notes', 'request', { max: 1000 }) ? { notes: text(raw, 'notes', 'request', { max: 1000 }) } : {}),
|
|
120
|
+
...(text(raw, 'reference', 'request') ? { reference: text(raw, 'reference', 'request') } : {}),
|
|
121
|
+
...(language ? { language } : {}),
|
|
122
|
+
...(raw.place === true ? { place: true } : {}),
|
|
123
|
+
...(raw.force === true ? { force: true } : {}),
|
|
124
|
+
};
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** "SKU:3" → { sku, quantity }, "dark roast:2" → { query, quantity }; no ":<n>" is 1. */
|
|
128
|
+
export function lineFlag(raw, kind) {
|
|
129
|
+
const m = raw.match(/^(.*?)(?::\s*(\d+))?$/s);
|
|
130
|
+
const value = m[1].trim();
|
|
131
|
+
if (!value)
|
|
132
|
+
fail('invalid_argument', `--${kind === 'sku' ? 'item' : 'query'} ${raw} names nothing.`, kind === 'sku' ? 'Use --item <sku>:<quantity>, for example --item ETH:3.' : 'Use --query "<words>:<quantity>", for example --query "dark roast:2".');
|
|
133
|
+
return { [kind]: value, quantity: m[2] === undefined ? 1 : Number(m[2]) };
|
|
134
|
+
}
|
|
135
|
+
/** "email:<id>" or "message:<id>": the kind is everything before the first colon. */
|
|
136
|
+
export function sourceFlag(raw) {
|
|
137
|
+
const i = raw.indexOf(':');
|
|
138
|
+
if (i < 1 || !raw.slice(i + 1).trim())
|
|
139
|
+
fail('invalid_argument', `--source ${raw} is not kind:id.`, 'Use --source email:<Message-ID> for an email, --source message:<id> for anything else.');
|
|
140
|
+
return { kind: raw.slice(0, i), id: raw.slice(i + 1).trim() };
|
|
141
|
+
}
|
package/dist/output.js
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
export class CliError extends Error {
|
|
7
7
|
code;
|
|
8
8
|
hint;
|
|
9
|
-
|
|
9
|
+
path;
|
|
10
|
+
constructor(code, message, hint,
|
|
11
|
+
/** The field of a request the error is about: customer.phone, items[0].quantity. */
|
|
12
|
+
path) {
|
|
10
13
|
super(message);
|
|
11
14
|
this.code = code;
|
|
12
15
|
this.hint = hint;
|
|
16
|
+
this.path = path;
|
|
13
17
|
}
|
|
14
18
|
}
|
|
15
|
-
export function fail(code, message, hint) {
|
|
16
|
-
throw new CliError(code, message, hint);
|
|
19
|
+
export function fail(code, message, hint, path) {
|
|
20
|
+
throw new CliError(code, message, hint, path);
|
|
17
21
|
}
|
|
18
22
|
export function progress(data) {
|
|
19
23
|
process.stderr.write(JSON.stringify(data) + '\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "c11r: the Commerce Layer CLI for coding agents. Builds a sandbox store that people and shopping agents can buy from.",
|
|
5
5
|
"homepage": "https://commercelayer.ai",
|
|
6
6
|
"keywords": [
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"prepublishOnly": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"imapflow": "^2.10.2",
|
|
35
|
+
"postal-mime": "^4.2.0",
|
|
34
36
|
"yaml": "^2.6.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|