@commercelayer/agent-cli 0.5.0 → 0.5.1

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
@@ -10,6 +10,7 @@ npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11
10
10
  - **The website comes with the store.** Every store has a hosted theme at its preview URL, set by a few `theme:` settings and the product data: `apply` is all it takes. A custom storefront (your own web app) is opt-in: `storefront init`, `publish` and `check`.
11
11
  - **JSON only.** Every command prints one JSON object. A failure exits 1 with `{ "error": { "code", "message", "hint" } }`; the hint is the fix.
12
12
  - **Safe to re-run.** Every command is idempotent; a failed step can be fixed and run again.
13
+ - **Always current.** Run it as `npx -y @commercelayer/agent-cli@latest`, even where a `c11r` binary is installed; the note `init` writes in `AGENTS.md` and `CLAUDE.md` says so. The CLI sends `User-Agent: c11r/<version> (node/<major>)`, and an outdated one gets a `cli_outdated` entry in `warnings`, with `current`, `latest` and a `hint`.
13
14
  - **No secrets in git.** `init` writes the sandbox token to `.commercelayer/credentials` (`C11R_ORG`, `C11R_TOKEN`) and gitignores it. The keys of a catalog source go in the same file.
14
15
  ## Commands
15
16
  | Command | Does |
@@ -40,9 +41,9 @@ npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11
40
41
  | `ucp checkout show <id>` · `cancel <id>` · `complete <id> [--decline]` | Read, cancel, or pay a checkout with the sandbox handler (after the human said yes): `order` has `label` and `permalink_url`. |
41
42
  | `ucp order <id>` · `ucp ask "<question>"` | Read an order the CLI placed; ask the store a question. Every `ucp` buying command takes `--store <host>` to buy from any store that speaks UCP. |
42
43
  | `ucp check` | Walk every UCP capability the store advertises and report what passes. Places and pays one real test order on the way (the output says which). Exits 1 on a failed check. |
43
- | `status` | The sandbox, the sales channel client id, the published storefront, what is applied, the last import job (`apply`), the pending changes (exactly what `plan` lists; a store that does not load is pending too, with the error), orders, the UCP profile URL. |
44
+ | `status` | The sandbox, the sales channel client id, the published storefront, what is applied, the last import job (`apply`), the pending changes (exactly what `plan` lists; a store that does not load is pending too, with the error), orders, the store's semantic search index (`search`: model, status, vectors, embedded, updated_at, error; null before its first run), the UCP profile URL. |
44
45
  | `claim` | Start the device flow that keeps the sandbox: a URL and a code for the human. |
45
- | `help [<command>]` | Describe the commands. `--version` prints the version. |
46
+ | `help [<command>]` | Describe the commands. `--version` prints the version and `latest_hint`, how to always run the newest. |
46
47
  ## store.yml
47
48
  `init` writes a commented template. A filled-in store looks like this:
48
49
  ```yaml
@@ -1,5 +1,6 @@
1
1
  // The client for the management API at api.commercelayer.ai.
2
- import { CliError, fail } from '../output.js';
2
+ import { CliError, fail, notice } from '../output.js';
3
+ import { NPX } from '../version.js';
3
4
  import { apiUrl, DEFAULT_API_URL, send } from './transport.js';
4
5
  export function createApi(auth) {
5
6
  const base = apiUrl() === 'stub' ? DEFAULT_API_URL : apiUrl().replace(/\/$/, '');
@@ -20,6 +21,19 @@ export function createApi(auth) {
20
21
  catch {
21
22
  fail('api_error', `${base} answered ${res.status} with something that is not JSON.`, 'Try again. If it keeps happening, tell the human, with the output of `c11r status`.');
22
23
  }
24
+ // The API flags an outdated CLI (from the User-Agent) in warnings, on any answer, errors included.
25
+ const warnings = data && typeof data === 'object' && !Array.isArray(data) ? data.warnings : undefined;
26
+ if (Array.isArray(warnings)) {
27
+ const isOutdated = (w) => !!w && typeof w === 'object' && w.code === 'cli_outdated';
28
+ for (const n of warnings.filter(isOutdated))
29
+ notice({ ...n, hint: n.hint ?? `Run the commands with ${NPX}. If c11r is installed globally, tell the human it is on ${n.current} and ${n.latest} exists.` });
30
+ // Printed once, at the top of the output, not inside the order or store it came with.
31
+ const rest = warnings.filter((w) => !isOutdated(w));
32
+ if (rest.length)
33
+ data.warnings = rest;
34
+ else
35
+ delete data.warnings;
36
+ }
23
37
  if (res.status < 400)
24
38
  return data;
25
39
  const err = data?.error;
@@ -16,7 +16,7 @@ export function handle(req) {
16
16
  url = new URL(`https://${prefixed[1]}${prefixed[2] ?? '/'}${url.search}`);
17
17
  const org = orgOfHost(url.hostname);
18
18
  if (org === null)
19
- return handleManagement({ ...req, url });
19
+ return outdated(req, handleManagement({ ...req, url }));
20
20
  const db = readDb();
21
21
  const store = db[org];
22
22
  if (!store)
@@ -25,3 +25,28 @@ export function handle(req) {
25
25
  writeDb(db);
26
26
  return res;
27
27
  }
28
+ /** "0.5.1" → [0, 5, 1], for comparing versions. */
29
+ const parts = (v) => v.split('.').map((n) => Number.parseInt(n, 10) || 0);
30
+ const older = (a, b) => {
31
+ const [x, y] = [parts(a), parts(b)];
32
+ for (let i = 0; i < 3; i++)
33
+ if ((x[i] ?? 0) !== (y[i] ?? 0))
34
+ return (x[i] ?? 0) < (y[i] ?? 0);
35
+ return false;
36
+ };
37
+ /**
38
+ * Like the API: a CLI whose User-Agent (c11r/<version>) is older than the latest gets a
39
+ * cli_outdated warning on every JSON object it is answered. The stub's latest is
40
+ * C11R_STUB_LATEST_CLI; unset, every CLI is current.
41
+ */
42
+ function outdated(req, res) {
43
+ const latest = process.env.C11R_STUB_LATEST_CLI;
44
+ const current = req.headers['user-agent']?.match(/^c11r\/(\d+\.\d+\.\d+)/)?.[1];
45
+ if (!latest || !current || !older(current, latest) || !res.body)
46
+ return res;
47
+ const body = JSON.parse(res.body);
48
+ if (!body || typeof body !== 'object' || Array.isArray(body))
49
+ return res;
50
+ const warning = { code: 'cli_outdated', current, latest, hint: `Run the commands with npx -y @commercelayer/agent-cli@latest. If c11r is installed globally, tell the human it is on ${current} and ${latest} exists.` };
51
+ return { ...res, body: JSON.stringify({ ...body, warnings: [...(Array.isArray(body.warnings) ? body.warnings : []), warning] }) };
52
+ }
@@ -5,6 +5,7 @@
5
5
  // stands in for the whole platform.
6
6
  import { fail } from '../output.js';
7
7
  import { handle } from './stub/index.js';
8
+ import { USER_AGENT } from '../version.js';
8
9
  export const DEFAULT_API_URL = 'https://api.commercelayer.ai';
9
10
  export const apiUrl = () => process.env.C11R_API_URL ?? DEFAULT_API_URL;
10
11
  export async function send(method, url, headers = {}, body) {
@@ -12,6 +13,8 @@ export async function send(method, url, headers = {}, body) {
12
13
  const lower = {};
13
14
  for (const [k, v] of Object.entries(headers))
14
15
  lower[k.toLowerCase()] = v;
16
+ // On every request, to the API and to stores: the API tells an outdated CLI so (cli_outdated).
17
+ lower['user-agent'] ??= USER_AGENT;
15
18
  if (body !== undefined && !lower['content-type'])
16
19
  lower['content-type'] = 'application/json';
17
20
  if (base === 'stub')
package/dist/bin.js CHANGED
@@ -4,6 +4,7 @@
4
4
  // Output is always JSON; exit code 1 on an error or a failed check.
5
5
  import * as cmd from './commands.js';
6
6
  import { CliError, print } from './output.js';
7
+ import { LATEST_HINT, NPX } from './version.js';
7
8
  const VALUE_OPTIONS = new Set(['--name', '--sku', '--quantity', '--country', '--code', '--file', '--store', '--item', '--email', '--first-name', '--last-name', '--phone', '--street', '--city', '--postal-code', '--region', '--shipping', '--category', '--min-price', '--max-price', '--status', '--payment', '--fulfillment', '--limit', '--archived', '--amount', '--carrier', '--tracking', '--tracking-url']);
8
9
  const argv = process.argv.slice(2);
9
10
  const flags = new Set();
@@ -36,7 +37,7 @@ for (let i = 0; i < argv.length; i++) {
36
37
  const [name, sub] = args;
37
38
  async function run() {
38
39
  if (flags.has('--version') || name === 'version')
39
- return { version: cmd.VERSION };
40
+ return { version: cmd.VERSION, latest_hint: LATEST_HINT };
40
41
  switch (name) {
41
42
  case undefined:
42
43
  case 'help':
@@ -151,7 +152,7 @@ run()
151
152
  }
152
153
  else {
153
154
  const message = e instanceof Error ? e.message : String(e);
154
- print({ error: { code: 'internal', message, hint: 'This is a bug in the CLI. Tell the human, with this output, and try again with npx -y @commercelayer/agent-cli@latest in case it is already fixed.' } });
155
+ print({ error: { code: 'internal', message, hint: `This is a bug in the CLI. Tell the human, with this output, and try again with ${NPX} in case it is already fixed.` } });
155
156
  }
156
157
  process.exit(1);
157
158
  });
package/dist/commands.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // One function per command. Each returns the object the CLI prints, or throws a
2
2
  // CliError. Every command is safe to re-run.
3
3
  import fs from 'node:fs';
4
- import { createRequire } from 'node:module';
5
4
  import path from 'node:path';
6
5
  import { createApi } from './api/client.js';
7
6
  import { ignoreCredentials, paths, readCredentials, requireCredentials, writeAgentNotes, writeCredentials } from './files.js';
@@ -19,8 +18,8 @@ import { check } from './ucp/check.js';
19
18
  import { discover } from './ucp/client.js';
20
19
  import * as shop from './ucp/shop.js';
21
20
  import { agentProfile, AGENT_PROFILE_URL } from './ucp/platform.js';
22
- /** The package version, read from package.json so a release bumps it in one place. */
23
- export const VERSION = createRequire(import.meta.url)('../package.json').version;
21
+ import { LATEST_HINT, VERSION } from './version.js';
22
+ export { VERSION } from './version.js';
24
23
  export const HELP = {
25
24
  init: 'Create a sandbox store for this repo, with no login: c11r init --name "<business name>" (the folder name when --name is absent). The name becomes the organization\'s name, and the org slug and store hostname derive from it. Writes .commercelayer/store.yml and .commercelayer/credentials, and a section in AGENTS.md and CLAUDE.md (created, or appended once) that points later agents in this repo to the skills: commit them. Reuses the store if store.yml already exists.',
26
25
  plan: 'Show what apply would create, update or delete: markets, products, images, and the store\'s name, language, policies and theme; missing_images lists references with no file in .commercelayer/img/ (apply refuses them), unused_images files nothing references, from .commercelayer/store.yml and the product files under .commercelayer/products/ (*.yml lists, *.csv rows, merged by sku). Products may carry other languages: name, description and category as { <ll>: text } maps (the base language, store.yml language:, default en, among them), or name_<ll>, description_<ll>, category_<ll> CSV columns; at most 6 languages. warnings lists a language no country of the store speaks.',
@@ -70,7 +69,7 @@ export const HELP = {
70
69
  'ucp checkout complete': "Buy for the human, step 4, only after the human said yes to the total: pay the checkout. c11r ucp checkout complete <id> [--decline] [--store <host>]. Pays with the ai.commercelayer.sandbox test handler (no money moves): only stores that advertise it can be paid; --decline uses the declined test card. Returns the checkout with status completed and order: { id, label, permalink_url }, plus payment_handler. Tell the human the order label and give them permalink_url. A store without that handler (any store taking real payments) answers payment_handler_unsupported: give the human continue_url to pay there. A declined payment leaves status ready_for_complete with a payment_failed message.",
71
70
  'ucp order': 'Read an order this CLI placed: c11r ucp order <id> [--store <host>]. Returns { id, label, permalink_url, store, checkout_id, currency, line_items: [{ item_id, title, quantity, status, total }], fulfillment: [{ method, destination, description }], events, totals, signature }.',
72
71
  'ucp ask': 'Ask a store a question in plain language over UCP (shipping, returns, what it sells): c11r ucp ask "<question>" [--store <host>]. Returns { store, question, answer, links, messages, signature }. The answer is the store\'s: quote it, do not embellish it. Ask only answers; buy with ucp checkout.',
73
- status: 'Show the sandbox, what is applied, and whether the store files have changes not yet applied (the same changes plan lists).',
72
+ status: 'Show the sandbox, what is applied, and whether the store files have changes not yet applied (the same changes plan lists). search is the store\'s semantic search index: { model, status: indexing|ready|failed|too_large, vectors, embedded, updated_at, error }, or null before it first runs; until it is ready the store\'s search matches words only.',
74
73
  claim: 'Start keeping this sandbox store: returns a URL and a code for the human to approve in the browser.',
75
74
  help: 'Describe the commands. `c11r help <command>` for one command, `c11r help <group>` (catalog, fulfillment, links, orders, promotions, storefront, ucp) for a group.',
76
75
  };
@@ -87,7 +86,7 @@ export function help(name) {
87
86
  const showsFormat = name === 'apply' || name === 'plan';
88
87
  return { command: name, description: HELP[name], store_format: showsFormat ? template('<name>') : undefined };
89
88
  }
90
- return { version: VERSION, commands: HELP, note: 'Every command prints JSON. The store format is documented in the comments of .commercelayer/store.yml; large catalogs go in .commercelayer/products/ as .yml or .csv files.' };
89
+ return { version: VERSION, latest_hint: LATEST_HINT, commands: HELP, note: 'Every command prints JSON. The store format is documented in the comments of .commercelayer/store.yml; large catalogs go in .commercelayer/products/ as .yml or .csv files.' };
91
90
  }
92
91
  export async function init(values) {
93
92
  const existing = readCredentials();
@@ -462,6 +461,8 @@ export async function status() {
462
461
  markets: s.applied ? Object.keys(s.applied.markets) : [],
463
462
  products: s.applied ? Object.keys(s.applied.products).length : 0,
464
463
  orders: s.orders.map((o) => ({ id: o.id, label: o.label })),
464
+ // Semantic search: until it is ready, the store's search matches words only.
465
+ search: s.search ? { model: s.search.model, status: s.search.status, vectors: s.search.vectors, embedded: s.search.embedded, updated_at: s.search.updated_at, error: s.search.error } : null,
465
466
  ucp_profile: `${s.preview_url}/.well-known/ucp`,
466
467
  next: s.apply?.status === 'in_progress' ? 'An apply is running: run `c11r status` again to follow it, or `c11r apply` to wait for it.' : storeError ? 'Fix the store files (see store_error), then run `c11r plan`.' : pending.length ? 'Run `c11r plan` to see the changes, then `c11r apply`.' : undefined,
467
468
  };
package/dist/files.js CHANGED
@@ -63,18 +63,30 @@ export function ignoreCredentials() {
63
63
  }
64
64
  /** The note that sends a future agent in this repo to the skills. No skill names: it never goes stale. */
65
65
  const AGENT_NOTE_HEADING = '# Store on Commerce Layer';
66
+ const SKILLS = 'Before any task about the store (products, prices, markets, storefront, orders, promotions, agents), fetch `https://commercelayer.ai/skills.md` with `curl -fsSL` and load the skill for the task from `skills/<name>.md`; follow it rather than calling the Commerce Layer API directly. If `.commercelayer/store.yml` is missing, start from `https://commercelayer.ai/start.md`.';
66
67
  export const AGENT_NOTE = `${AGENT_NOTE_HEADING}
67
- This store is managed with \`c11r\` (\`npx @commercelayer/agent-cli\`), configured in \`.commercelayer/store.yml\`. Before any task about the store (products, prices, markets, storefront, orders, promotions, agents), fetch \`https://commercelayer.ai/skills.md\` with \`curl -fsSL\` and load the skill for the task from \`skills/<name>.md\`; follow it rather than calling the Commerce Layer API directly. If \`.commercelayer/store.yml\` is missing, start from \`https://commercelayer.ai/start.md\`.
68
+ This store is managed with \`c11r\`, run as \`npx -y @commercelayer/agent-cli@latest\`, configured in \`.commercelayer/store.yml\`. Always use that form, even if a \`c11r\` binary is installed: it stays on the current version. ${SKILLS}
69
+ `;
70
+ /** The note CLIs before 0.5.1 wrote: replaced by the current one when a file has it word for word. */
71
+ const OLD_AGENT_NOTE = `${AGENT_NOTE_HEADING}
72
+ This store is managed with \`c11r\` (\`npx @commercelayer/agent-cli\`), configured in \`.commercelayer/store.yml\`. ${SKILLS}
68
73
  `;
69
74
  /**
70
75
  * Write the note to AGENTS.md and CLAUDE.md (Claude Code reads that name) in the repo root:
71
76
  * created when absent, appended as a section when the file has other content, left alone
72
- * when the section is already there. Returns the files written.
77
+ * when the section is already there (replaced when it is an older CLI's, word for word).
78
+ * Returns the files written.
73
79
  */
74
80
  export function writeAgentNotes() {
75
81
  const written = [];
76
82
  for (const file of ['AGENTS.md', 'CLAUDE.md']) {
77
83
  const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
84
+ // A note an older CLI wrote, untouched since: bring it up to date. An edited one stays as it is.
85
+ if (before.includes(OLD_AGENT_NOTE)) {
86
+ fs.writeFileSync(file, before.replace(OLD_AGENT_NOTE, AGENT_NOTE));
87
+ written.push(file);
88
+ continue;
89
+ }
78
90
  if (before.split('\n').some((line) => line.trim() === AGENT_NOTE_HEADING))
79
91
  continue;
80
92
  fs.writeFileSync(file, before.trim() ? `${before.replace(/\s*$/, '')}\n\n${AGENT_NOTE}` : AGENT_NOTE);
package/dist/output.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // A failure is { error: { code, message, hint } } with exit code 1. The hint is
3
3
  // the fix: an agent reads it and acts on it before trying anything else.
4
4
  // A command that waits (apply) writes progress to stderr, one JSON object per line.
5
+ // Warnings the API sends about the CLI itself (cli_outdated) go into whatever is printed.
5
6
  export class CliError extends Error {
6
7
  code;
7
8
  hint;
@@ -17,6 +18,18 @@ export function fail(code, message, hint) {
17
18
  export function progress(data) {
18
19
  process.stderr.write(JSON.stringify(data) + '\n');
19
20
  }
21
+ const notices = [];
22
+ /** Keep a warning to print with the result, once per code. */
23
+ export function notice(n) {
24
+ if (!notices.some((x) => x.code === n.code))
25
+ notices.push(n);
26
+ }
20
27
  export function print(data) {
21
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
28
+ let out = data;
29
+ if (notices.length && data && typeof data === 'object' && !Array.isArray(data)) {
30
+ const own = data.warnings;
31
+ // A command's own warnings are strings or objects; the API's are added after them.
32
+ out = { ...data, warnings: [...(Array.isArray(own) ? own : own === undefined ? [] : [own]), ...notices] };
33
+ }
34
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
22
35
  }
@@ -0,0 +1,9 @@
1
+ // The CLI's version and how it names itself on the wire.
2
+ import { createRequire } from 'node:module';
3
+ /** The package version, read from package.json so a release bumps it in one place. */
4
+ export const VERSION = createRequire(import.meta.url)('../package.json').version;
5
+ /** The User-Agent of every request: the API answers cli_outdated when a newer CLI exists. */
6
+ export const USER_AGENT = `c11r/${VERSION} (node/${process.versions.node.split('.')[0]})`;
7
+ /** The one way to run the CLI that is always current, whatever is installed. */
8
+ export const NPX = 'npx -y @commercelayer/agent-cli@latest';
9
+ export const LATEST_HINT = `${NPX} always runs the newest version, even where a c11r binary is installed.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercelayer/agent-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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": [