@openluxeco/cli 0.1.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
@@ -38,6 +38,46 @@ openluxe auth logout # forget the local token
38
38
 
39
39
  Revoke a CLI token anytime from **Settings → Integrations** on the web.
40
40
 
41
+ ## Terms of Service
42
+
43
+ Use of the API and this CLI is governed by a binding agreement. Before any
44
+ request will succeed you must read and accept the **OpenLuxe API & CLI Terms
45
+ of Service** and every referenced policy (Terms of Service, Privacy, Acceptable
46
+ Use, Data, Cookie). This is a one-time, in-browser step — authorizing the
47
+ device during `openluxe auth login` records your acceptance. You are
48
+ re-prompted only if the terms materially change (e.g. pricing, rate limits,
49
+ discontinued endpoints).
50
+
51
+ If a token predates the current terms (or they changed), every call returns
52
+ `403` and the CLI tells you exactly where to accept. Review anytime:
53
+
54
+ ```bash
55
+ openluxe terms
56
+ ```
57
+
58
+ Key points: the API and CLI are provided "as is" with no uptime guarantee;
59
+ endpoints, pricing, credits, and rate limits may change or be discontinued at
60
+ any time without liability; AI-generated output may be inaccurate and must be
61
+ independently verified. Full text: `https://openluxe.co/api-terms`.
62
+
63
+ ## Pro access
64
+
65
+ The OpenLuxe API, this CLI, and the MCP server are part of the **Pro suite** —
66
+ using them requires unlocking Pro access on your account (the same one-time
67
+ platform access that gates the Pro apps in the web app). Until you unlock it,
68
+ commands return:
69
+
70
+ ```
71
+ ✗ Pro access required
72
+ The OpenLuxe API, CLI, and MCP server are part of the Pro suite.
73
+ Unlock Pro access ($99 today) to use them.
74
+
75
+ Unlock here: https://openluxe.co/onboarding/access
76
+ ```
77
+
78
+ Authorizing the CLI (`openluxe auth login`) is also blocked in the browser until
79
+ you unlock access. Admins and grandfathered accounts are exempt.
80
+
41
81
  ## Use it
42
82
 
43
83
  Typed resource commands:
@@ -51,8 +91,20 @@ openluxe webhooks events
51
91
  openluxe me show
52
92
  ```
53
93
 
54
- Run `openluxe <resource>` to see its commands, or `openluxe help` for the
55
- full list.
94
+ AI generation is async start a job, then poll the returned handle:
95
+
96
+ ```bash
97
+ # start (returns { data: { id, status, status_url } } with HTTP 202)
98
+ openluxe generate start image -d '{"prompt":"a marble villa at golden hour"}'
99
+ openluxe generate start agent_odysseus -d '{"message":"brief me before the meeting"}'
100
+
101
+ # poll until status is succeeded | failed
102
+ openluxe generations get <id>
103
+ ```
104
+
105
+ Generations are charged the same credits as the equivalent in-app action; a
106
+ shortfall returns `402` with a `topup_url`. Run `openluxe <resource>` to see
107
+ its commands, or `openluxe help` for the full list.
56
108
 
57
109
  Raw passthrough to any v1 endpoint:
58
110
 
@@ -73,6 +125,41 @@ scripts and tool-use loops.
73
125
  export OPENLUXE_API_URL=https://openluxe.co # default; override for staging/local
74
126
  ```
75
127
 
128
+ ## MCP (Model Context Protocol)
129
+
130
+ `openluxe mcp` runs an MCP server over stdio, so any MCP-aware client
131
+ (Claude Code, Claude Desktop, Cursor, …) can drive OpenLuxe in a chat. It
132
+ authenticates with your stored CLI token and proxies to the v1 API — the agent
133
+ gets the same scoped, ToS-gated, billed surface you do.
134
+
135
+ Sign in once (`openluxe auth login`), then register the server with your client.
136
+ Example MCP client config:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "openluxe": { "command": "openluxe", "args": ["mcp"] }
142
+ }
143
+ }
144
+ ```
145
+
146
+ It exposes a lean tool set — `openluxe_list_endpoints` to discover the full v1
147
+ surface and `openluxe_api_request` to call any of it, plus typed helpers
148
+ (`openluxe_me`, `openluxe_search`, `openluxe_contacts_list`,
149
+ `openluxe_create_contact`, `openluxe_credits_balance`). OpenLuxe also hosts a
150
+ remote MCP server at `https://openluxe.co/api/v1/mcp` (Bearer-authed with a
151
+ `ol_itk_` token) for clients that speak Streamable HTTP directly.
152
+
153
+ ## Credits
154
+
155
+ AI-generating calls (`openluxe generate …`) cost credits at web-app parity. A
156
+ shortfall returns `402` with a top-up link; you can also open it anytime:
157
+
158
+ ```bash
159
+ openluxe credits balance
160
+ openluxe credits buy
161
+ ```
162
+
76
163
  ## Environment
77
164
 
78
165
  | Var | Purpose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openluxeco/cli",
3
- "version": "0.1.0",
3
+ "version": "0.5.1",
4
4
  "description": "Official OpenLuxe command-line client — drive the OpenLuxe v1 API from your terminal or an AI agent.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -8,6 +8,28 @@ export class ApiError extends Error {
8
8
  }
9
9
  }
10
10
 
11
+ /**
12
+ * Node's fetch buries the real failure (TLS, DNS, refused) in `error.cause` and
13
+ * reports only "fetch failed" — surface the cause plus an actionable hint.
14
+ */
15
+ function networkErrorMessage(e, url) {
16
+ const cause = e.cause || {};
17
+ const code = cause.code || '';
18
+ const causeMsg = cause.message || '';
19
+ let origin = '';
20
+ try { origin = ' (' + new URL(url).origin + ')'; } catch { /* keep plain */ }
21
+
22
+ let msg = `Network error: ${causeMsg || e.message}${origin}`;
23
+ if (/CERT|certificate|SSL|TLS/i.test(code + ' ' + causeMsg)) {
24
+ msg += '\n Untrusted/self-signed certificate — for a local dev server, retry with OPENLUXE_INSECURE=1';
25
+ } else if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') {
26
+ msg += '\n Host not found — check the API base (OPENLUXE_API_URL, or `openluxe auth status` for the stored base)';
27
+ } else if (code === 'ECONNREFUSED') {
28
+ msg += '\n Connection refused — is the server running at that base URL?';
29
+ }
30
+ return msg;
31
+ }
32
+
11
33
  /**
12
34
  * Perform an authenticated v1 API request. `path` is everything after
13
35
  * /api/v1 (e.g. "/contacts"). `query` is an object, `body` is JSON-able.
@@ -36,7 +58,7 @@ export async function request(method, path, { query, body, token, base } = {}) {
36
58
  try {
37
59
  res = await fetch(url, init);
38
60
  } catch (e) {
39
- throw new ApiError(0, `Network error: ${e.message}`);
61
+ throw new ApiError(0, networkErrorMessage(e, url));
40
62
  }
41
63
 
42
64
  const text = await res.text();
@@ -55,11 +77,16 @@ export async function request(method, path, { query, body, token, base } = {}) {
55
77
  /** Unauthenticated POST used only by the device-auth handshake. */
56
78
  export async function postPublic(base, path, body) {
57
79
  const url = base.replace(/\/$/, '') + '/api/v1' + path;
58
- const res = await fetch(url, {
59
- method: 'POST',
60
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
61
- body: JSON.stringify(body || {}),
62
- });
80
+ let res;
81
+ try {
82
+ res = await fetch(url, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
85
+ body: JSON.stringify(body || {}),
86
+ });
87
+ } catch (e) {
88
+ throw new ApiError(0, networkErrorMessage(e, url));
89
+ }
63
90
  const text = await res.text();
64
91
  let parsed;
65
92
  try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
package/src/auth.js CHANGED
@@ -54,6 +54,11 @@ export async function login({ base } = {}) {
54
54
  const who = poll.data.user?.email || poll.data.user?.name || 'your account';
55
55
  console.log(`\x1b[32m✓ Signed in as ${who}\x1b[0m`);
56
56
  console.log(` Token stored at ${credentialsPath}`);
57
+ console.log('');
58
+ console.log(' Your API use is governed by the OpenLuxe API & CLI Terms');
59
+ console.log(` and all referenced policies — see \x1b[36m${apiBase}/api-terms\x1b[0m`);
60
+ console.log(" (run `openluxe terms`). Authorizing this device recorded");
61
+ console.log(" your acceptance; you'll be re-prompted only if they change.");
57
62
  return;
58
63
  }
59
64
  if (poll.ok && poll.data?.status === 'pending') {
package/src/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { request, ApiError } from './api.js';
2
2
  import { RESOURCES } from './resources.js';
3
3
  import * as auth from './auth.js';
4
- import { load } from './config.js';
4
+ import { load, VERSION } from './config.js';
5
+ import { serve as mcpServe } from './mcp.js';
5
6
 
6
7
  const C = {
7
8
  dim: (s) => `\x1b[2m${s}\x1b[0m`,
@@ -53,6 +54,100 @@ function out(data) {
53
54
  process.stdout.write(typeof data === 'string' ? data + '\n' : JSON.stringify(data, null, 2) + '\n');
54
55
  }
55
56
 
57
+ /** The server hard-blocks every call until the API & CLI Terms are accepted. */
58
+ function isTermsBlock(e) {
59
+ return e?.status === 403
60
+ && typeof e?.body?.type === 'string'
61
+ && e.body.type.endsWith('/api_terms_required');
62
+ }
63
+
64
+ /** The API/CLI/MCP are Pro surfaces — blocked until the user unlocks Pro access. */
65
+ function isPlatformAccessBlock(e) {
66
+ return e?.status === 402
67
+ && ((typeof e?.body?.type === 'string' && e.body.type.endsWith('/platform_access_required'))
68
+ || e?.body?.error === 'platform_access_required');
69
+ }
70
+
71
+ function printPlatformAccessBlock(body = {}) {
72
+ const url = body.upgrade_url || body.landing_url || `${load().base}/pro`;
73
+ const price = typeof body.price_cents === 'number' ? `$${(body.price_cents / 100).toLocaleString()}` : null;
74
+ console.error(C.red('✗ Pro access required'));
75
+ console.error('');
76
+ console.error(' The OpenLuxe API, CLI, and MCP server are part of the Pro suite.');
77
+ console.error(` Unlock Pro access${price ? ` (${price} today)` : ''} to use them.`);
78
+ console.error('');
79
+ console.error(` Unlock here: ${C.cyan(url)}`);
80
+ console.error(C.dim(' Then re-run your command.'));
81
+ }
82
+
83
+ function printTermsBlock(body = {}) {
84
+ const acceptUrl = body.accept_url || `${load().base}/developers`;
85
+ console.error(C.red('✗ API & CLI Terms acceptance required'));
86
+ console.error('');
87
+ console.error(' Before using the API you must read and accept the current');
88
+ console.error(' OpenLuxe API & CLI Terms of Service and every referenced policy.');
89
+ console.error(' This is a one-time, in-browser step (it re-prompts only when');
90
+ console.error(' the terms change).');
91
+ console.error('');
92
+ console.error(` Sign in and accept here: ${C.cyan(acceptUrl)}`);
93
+ if (Array.isArray(body.required_policies) && body.required_policies.length) {
94
+ console.error('');
95
+ console.error(' You will be asked to accept:');
96
+ for (const p of body.required_policies) {
97
+ console.error(` • ${p.title}${p.url ? C.dim(' ' + p.url) : ''}`);
98
+ }
99
+ }
100
+ console.error('');
101
+ console.error(C.dim(' Then re-run your command.'));
102
+ }
103
+
104
+ /** A billable call hit a credit shortfall — surface the top-up path (no in-app modal here). */
105
+ function printInsufficientCredits(body = {}) {
106
+ const { base } = load();
107
+ const topup = body.topup_url || `${base}/credits/buy`;
108
+ console.error(C.red('✗ 402 Insufficient credits'));
109
+ if (body.message) console.error(` ${body.message}`);
110
+ if (body.required_credits !== undefined) {
111
+ console.error(C.dim(` required: ${body.required_credits} available: ${body.available_credits ?? 0} type: ${body.credit_type ?? 'credits'}`));
112
+ }
113
+ console.error('');
114
+ console.error(` Top up here: ${C.cyan(topup)}`);
115
+ console.error(C.dim(' Then re-run your command.'));
116
+ }
117
+
118
+ /** `openluxe credits buy` — open / print the credit top-up page. */
119
+ function creditsBuy(flags = {}) {
120
+ const { base } = load();
121
+ const url = new URL(`${base.replace(/\/$/, '')}/credits/buy`);
122
+ if (flags.topup) url.searchParams.set('topup', flags.topup);
123
+ if (flags.required) url.searchParams.set('required', flags.required);
124
+ url.searchParams.set('label', flags.label || 'API credit top-up');
125
+ console.log(`Open this page to buy OpenLuxe credits:\n\n ${C.cyan(url.toString())}\n`);
126
+ console.log(C.dim('Credits fund AI-generating API calls (openluxe generate ...) at the same cost as the web app.'));
127
+ }
128
+
129
+ function termsHelp() {
130
+ const { base } = load();
131
+ console.log(`
132
+ ${C.bold('openluxe terms')} — API & CLI Terms of Service
133
+
134
+ Using the OpenLuxe API or CLI is governed by a binding agreement.
135
+ You must read and accept it (and every referenced policy) once, in
136
+ your browser while signed in, before any request will work. You'll
137
+ be re-prompted only if the terms materially change.
138
+
139
+ Accept / review:
140
+ API & CLI Terms ${C.cyan(base + '/api-terms')}
141
+ Terms of Service ${C.cyan(base + '/terms-of-service')}
142
+ Privacy Policy ${C.cyan(base + '/privacy-policy')}
143
+ Acceptable Use ${C.cyan(base + '/acceptable-use')}
144
+ Data Policy ${C.cyan(base + '/data-policy')}
145
+ Cookie Policy ${C.cyan(base + '/cookie-policy')}
146
+
147
+ Accept from: ${C.cyan(base + '/developers')}
148
+ `);
149
+ }
150
+
56
151
  async function callApi(method, path, { positionals = [], flags = {}, body }) {
57
152
  // Fill :placeholders from --flag of the same name, else positionals in order.
58
153
  const used = new Set();
@@ -80,7 +175,19 @@ async function callApi(method, path, { positionals = [], flags = {}, body }) {
80
175
  out(res);
81
176
  } catch (e) {
82
177
  if (e instanceof ApiError) {
83
- console.error(C.red(`✗ ${e.status} ${e.message}`));
178
+ if (isTermsBlock(e)) {
179
+ printTermsBlock(e.body);
180
+ process.exit(1);
181
+ }
182
+ if (isPlatformAccessBlock(e)) {
183
+ printPlatformAccessBlock(e.body || {});
184
+ process.exit(1);
185
+ }
186
+ if (e.status === 402) {
187
+ printInsufficientCredits(e.body || {});
188
+ process.exit(1);
189
+ }
190
+ console.error(C.red('✗ ' + (e.status ? e.status + ' ' : '') + e.message));
84
191
  if (e.body && typeof e.body === 'object') console.error(C.dim(JSON.stringify(e.body, null, 2)));
85
192
  if (e.status === 401) console.error(C.dim(' Run: openluxe auth login'));
86
193
  process.exit(1);
@@ -101,12 +208,19 @@ ${C.bold('AUTH')}
101
208
  auth login Sign in via your browser (device flow)
102
209
  auth logout Forget the local token
103
210
  auth status Show who you're signed in as
211
+ terms Review / accept the API & CLI Terms
104
212
 
105
213
  ${C.bold('RAW')}
106
214
  api <METHOD> <path> Call any v1 endpoint directly
107
215
  e.g. openluxe api GET /contacts --per_page 5
108
216
  openluxe api POST /notes -d '{"contact_id":1,"body":"hi"}'
109
217
 
218
+ ${C.bold('AI / MCP')}
219
+ mcp Run an MCP server over stdio (point Claude Code,
220
+ Cursor, etc. at this to drive OpenLuxe in chat)
221
+ credits buy Open the credit top-up page (funds AI calls)
222
+ manifest Print the typed-command surface as JSON
223
+
110
224
  ${C.bold('RESOURCES')}
111
225
  ${Object.entries(RESOURCES).map(([g, r]) => ` ${g.padEnd(16)} ${C.dim(r.summary)}`).join('\n')}
112
226
 
@@ -131,7 +245,22 @@ export async function run(argv) {
131
245
  const [cmd, ...rest] = argv;
132
246
 
133
247
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return topHelp();
134
- if (cmd === '--version' || cmd === '-v') return out('openluxe 0.1.0');
248
+ if (cmd === '--version' || cmd === '-v') return out('openluxe ' + VERSION);
249
+ if (cmd === 'terms') return termsHelp();
250
+
251
+ // MCP server over stdio — point Claude Code / Cursor at `openluxe mcp`.
252
+ if (cmd === 'mcp') return mcpServe();
253
+
254
+ // Emit the typed-command surface as JSON (feeds the coverage tooling).
255
+ if (cmd === 'manifest') {
256
+ const commands = [];
257
+ for (const [resource, def] of Object.entries(RESOURCES)) {
258
+ for (const [command, spec] of Object.entries(def.commands)) {
259
+ commands.push({ resource, command, method: spec.method, path: spec.path, summary: spec.summary || null });
260
+ }
261
+ }
262
+ return out({ version: VERSION, count: commands.length, commands });
263
+ }
135
264
 
136
265
  if (cmd === 'auth') {
137
266
  const sub = rest[0];
@@ -150,6 +279,12 @@ export async function run(argv) {
150
279
  return callApi(method, path, { flags, body });
151
280
  }
152
281
 
282
+ // `credits buy` opens the top-up page (not an API call).
283
+ if (cmd === 'credits' && rest[0] === 'buy') {
284
+ const { flags } = parseArgs(rest.slice(1));
285
+ return creditsBuy(flags);
286
+ }
287
+
153
288
  const group = RESOURCES[cmd];
154
289
  if (!group) return die(`Unknown command: ${cmd}. Run 'openluxe help'.`);
155
290
 
package/src/config.js CHANGED
@@ -5,6 +5,11 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, chmodSync, rmSync }
5
5
  const DIR = join(homedir(), '.openluxe');
6
6
  const FILE = join(DIR, 'credentials.json');
7
7
 
8
+ /** Single source of truth for the CLI version — read from package.json. */
9
+ export const VERSION = JSON.parse(
10
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
11
+ ).version;
12
+
8
13
  const DEFAULT_BASE = process.env.OPENLUXE_API_URL || 'https://openluxe.co';
9
14
 
10
15
  export function load() {
package/src/mcp.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * `openluxe mcp` — a zero-dependency MCP (Model Context Protocol) server over
3
+ * stdio that exposes the OpenLuxe v1 API to local AI clients (Claude Code,
4
+ * Cursor, etc.). It authenticates with the token stored by `openluxe auth login`
5
+ * and proxies every call to https://<base>/api/v1, so an agent gets the same
6
+ * scoped, ToS-gated, billed surface a human gets from the CLI.
7
+ *
8
+ * Transport: newline-delimited JSON-RPC 2.0 on stdin/stdout (the MCP stdio
9
+ * transport). We implement just enough of the protocol — initialize, tools/list,
10
+ * tools/call, ping — to be a useful client target.
11
+ *
12
+ * Tools are intentionally lean (a generic passthrough + endpoint discovery +
13
+ * a few high-value typed tools) rather than one-per-endpoint, so the agent's
14
+ * tool list stays small; `openluxe_api_request` covers the full ~300-endpoint
15
+ * surface. This mirrors the hosted /api/v1/mcp server.
16
+ */
17
+ import { createInterface } from 'node:readline';
18
+ import { request, ApiError } from './api.js';
19
+ import { RESOURCES } from './resources.js';
20
+ import { load, VERSION } from './config.js';
21
+
22
+ const PROTOCOL_VERSION = '2025-06-18';
23
+
24
+ function send(msg) {
25
+ process.stdout.write(JSON.stringify(msg) + '\n');
26
+ }
27
+
28
+ function result(id, result) {
29
+ send({ jsonrpc: '2.0', id, result });
30
+ }
31
+
32
+ function error(id, code, message) {
33
+ send({ jsonrpc: '2.0', id, error: { code, message } });
34
+ }
35
+
36
+ /** A flat catalog of every typed CLI command, for the discovery tool. */
37
+ function endpointCatalog() {
38
+ const rows = [];
39
+ for (const [resource, def] of Object.entries(RESOURCES)) {
40
+ for (const [command, spec] of Object.entries(def.commands)) {
41
+ rows.push({ resource, command, method: spec.method, path: spec.path, summary: spec.summary || null });
42
+ }
43
+ }
44
+ return rows;
45
+ }
46
+
47
+ const TOOLS = [
48
+ {
49
+ name: 'openluxe_api_request',
50
+ description:
51
+ 'Call any OpenLuxe v1 API endpoint. Use openluxe_list_endpoints first to discover paths. '
52
+ + 'GET/DELETE use `query`; POST/PATCH/PUT use `body`. Paths are relative to /api/v1 (e.g. "/contacts").',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ method: { type: 'string', enum: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], description: 'HTTP method' },
57
+ path: { type: 'string', description: 'Path after /api/v1, e.g. /contacts or /contacts/42' },
58
+ query: { type: 'object', description: 'Query parameters (GET/DELETE)', additionalProperties: true },
59
+ body: { type: 'object', description: 'JSON body (POST/PATCH/PUT)', additionalProperties: true },
60
+ },
61
+ required: ['method', 'path'],
62
+ },
63
+ run: (args) => request(args.method, args.path, { query: args.query, body: args.body }),
64
+ },
65
+ {
66
+ name: 'openluxe_list_endpoints',
67
+ description: 'List every available OpenLuxe v1 endpoint (method, path, summary) grouped by resource, so you know what to pass to openluxe_api_request.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: { resource: { type: 'string', description: 'Optional: filter to one resource (e.g. "contacts")' } },
71
+ },
72
+ run: async (args) => {
73
+ const rows = endpointCatalog();
74
+ return args?.resource ? rows.filter((r) => r.resource === args.resource) : rows;
75
+ },
76
+ },
77
+ {
78
+ name: 'openluxe_me',
79
+ description: 'Show the authenticated user and the abilities (scopes) of the presenting token.',
80
+ inputSchema: { type: 'object', properties: {} },
81
+ run: () => request('GET', '/me'),
82
+ },
83
+ {
84
+ name: 'openluxe_search',
85
+ description: 'Universal search across the user\'s OpenLuxe records (contacts, listings, deals, etc.).',
86
+ inputSchema: { type: 'object', properties: { q: { type: 'string', description: 'Search query' } }, required: ['q'] },
87
+ run: (args) => request('GET', '/search', { query: { q: args.q } }),
88
+ },
89
+ {
90
+ name: 'openluxe_contacts_list',
91
+ description: 'List the user\'s CRM contacts. Optional filters: industry, email, phone, search, updated_since, created_via, per_page.',
92
+ inputSchema: { type: 'object', properties: { search: { type: 'string' }, industry: { type: 'string' }, per_page: { type: 'number' } } },
93
+ run: (args) => request('GET', '/contacts', { query: args || {} }),
94
+ },
95
+ {
96
+ name: 'openluxe_create_contact',
97
+ description: 'Create a CRM contact. Fields: email, first_name, last_name, phone, company, position, industry, etc.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ first_name: { type: 'string' }, last_name: { type: 'string' }, email: { type: 'string' },
102
+ phone: { type: 'string' }, company: { type: 'string' }, industry: { type: 'string' },
103
+ },
104
+ },
105
+ run: (args) => request('POST', '/contacts', { body: args || {} }),
106
+ },
107
+ {
108
+ name: 'openluxe_credits_balance',
109
+ description: 'Show the user\'s credit balance (what API generation calls are billed against).',
110
+ inputSchema: { type: 'object', properties: {} },
111
+ run: () => request('GET', '/credits/balance'),
112
+ },
113
+ ];
114
+
115
+ const TOOL_INDEX = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
116
+
117
+ async function handle(msg) {
118
+ const { id, method, params } = msg;
119
+ const isRequest = id !== undefined && id !== null;
120
+
121
+ switch (method) {
122
+ case 'initialize':
123
+ return result(id, {
124
+ protocolVersion: PROTOCOL_VERSION,
125
+ capabilities: { tools: {} },
126
+ serverInfo: { name: 'openluxe', version: VERSION },
127
+ instructions: 'Drive the OpenLuxe v1 API (CRM, listings, business, generation, …). '
128
+ + 'Start with openluxe_list_endpoints, then openluxe_api_request for anything not covered by a typed tool.',
129
+ });
130
+
131
+ case 'notifications/initialized':
132
+ case 'notifications/cancelled':
133
+ return; // notifications get no response
134
+
135
+ case 'ping':
136
+ return isRequest && result(id, {});
137
+
138
+ case 'tools/list':
139
+ return result(id, {
140
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
141
+ });
142
+
143
+ case 'tools/call': {
144
+ const tool = TOOL_INDEX[params?.name];
145
+ if (!tool) return error(id, -32602, `Unknown tool: ${params?.name}`);
146
+ try {
147
+ const data = await tool.run(params.arguments || {});
148
+ return result(id, { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
149
+ } catch (e) {
150
+ // Surface API errors to the model as tool errors (isError), not protocol errors.
151
+ const text = e instanceof ApiError
152
+ ? `API error ${e.status}: ${e.message}\n${JSON.stringify(e.body ?? {}, null, 2)}`
153
+ : `Error: ${e.message}`;
154
+ return result(id, { content: [{ type: 'text', text }], isError: true });
155
+ }
156
+ }
157
+
158
+ default:
159
+ return isRequest && error(id, -32601, `Method not found: ${method}`);
160
+ }
161
+ }
162
+
163
+ export async function serve() {
164
+ const { token, base } = load();
165
+ if (!token) {
166
+ process.stderr.write('openluxe mcp: not signed in — run `openluxe auth login` first.\n');
167
+ process.exit(1);
168
+ }
169
+ process.stderr.write(`openluxe mcp server ready (${base}) — ${TOOLS.length} tools.\n`);
170
+
171
+ const rl = createInterface({ input: process.stdin });
172
+ for await (const line of rl) {
173
+ const trimmed = line.trim();
174
+ if (!trimmed) continue;
175
+ let msg;
176
+ try {
177
+ msg = JSON.parse(trimmed);
178
+ } catch {
179
+ error(null, -32700, 'Parse error');
180
+ continue;
181
+ }
182
+ try {
183
+ await handle(msg);
184
+ } catch (e) {
185
+ if (msg?.id !== undefined && msg?.id !== null) error(msg.id, -32603, `Internal error: ${e.message}`);
186
+ }
187
+ }
188
+ }
package/src/resources.js CHANGED
@@ -8,7 +8,9 @@
8
8
  * - For POST/PATCH: --flags + `-d '<json>'` merge into the JSON body.
9
9
  *
10
10
  * This is intentionally data-driven so the CLI mirrors the API surface
11
- * without 100+ hand-written command files.
11
+ * without 100+ hand-written command files. Curated resources are kept
12
+ * verbatim; the rest are generated for full parity with the v1 route table
13
+ * (regenerate via the main repo's scripts + `openluxe manifest`).
12
14
  */
13
15
  export const RESOURCES = {
14
16
  contacts: {
@@ -19,6 +21,46 @@ export const RESOURCES = {
19
21
  create: { method: 'POST', path: '/contacts', summary: 'Create a contact' },
20
22
  update: { method: 'PATCH', path: '/contacts/:contact', summary: 'Update a contact' },
21
23
  delete: { method: 'DELETE', path: '/contacts/:contact', summary: 'Delete a contact' },
24
+ problems: { method: 'GET', path: '/contacts/:contact/problems', summary: 'GET /contacts/{contact}/problems [scope: crm:contacts:read]' },
25
+ 'cultural-briefing': { method: 'GET', path: '/contacts/:contact/cultural-briefing', summary: 'GET /contacts/{contact}/cultural-briefing [scope: crm:cultural:read]' },
26
+ personality: { method: 'GET', path: '/contacts/:contact/personality', summary: 'GET /contacts/{contact}/personality [scope: crm:contacts:read]' },
27
+ favors: { method: 'GET', path: '/contacts/:contact/favors', summary: 'GET /contacts/{contact}/favors [scope: crm:contacts:read]' },
28
+ 'compliance-limits': { method: 'GET', path: '/contacts/:contact/compliance-limits', summary: 'GET /contacts/{contact}/compliance-limits [scope: crm:contacts:read]' },
29
+ status: { method: 'GET', path: '/contacts/:contact/compliance-limits/status', summary: 'GET /contacts/{contact}/compliance-limits/status [scope: crm:contacts:read]' },
30
+ value: { method: 'GET', path: '/contacts/:contact/value', summary: 'GET /contacts/{contact}/value [scope: crm:contacts:read]' },
31
+ economics: { method: 'GET', path: '/contacts/:contact/economics', summary: 'GET /contacts/{contact}/economics [scope: crm:contacts:read]' },
32
+ 'buyer-profile': { method: 'GET', path: '/contacts/:contact/buyer-profile', summary: 'GET /contacts/{contact}/buyer-profile [scope: crm:contacts:read]' },
33
+ 'user-links': { method: 'GET', path: '/contacts/:contact/user-links', summary: 'GET /contacts/{contact}/user-links [scope: crm:contacts:read]' },
34
+ 'update-personality': { method: 'PUT', path: '/contacts/:contact/personality', summary: 'PUT /contacts/{contact}/personality [scope: crm:contacts:write]' },
35
+ 'create-favors': { method: 'POST', path: '/contacts/:contact/favors', summary: 'POST /contacts/{contact}/favors [scope: crm:contacts:write]' },
36
+ 'update-favors': { method: 'PUT', path: '/contacts/:contact/favors/:favor', summary: 'PUT /contacts/{contact}/favors/{favor} [scope: crm:contacts:write]' },
37
+ 'delete-favors': { method: 'DELETE', path: '/contacts/:contact/favors/:favor', summary: 'DELETE /contacts/{contact}/favors/{favor} [scope: crm:contacts:write]' },
38
+ reciprocate: { method: 'POST', path: '/contacts/:contact/favors/:favor/reciprocate', summary: 'POST /contacts/{contact}/favors/{favor}/reciprocate [scope: crm:contacts:write]' },
39
+ 'create-compliance-limits': { method: 'POST', path: '/contacts/:contact/compliance-limits', summary: 'POST /contacts/{contact}/compliance-limits [scope: crm:contacts:write]' },
40
+ 'update-compliance-limits': { method: 'PUT', path: '/contacts/:contact/compliance-limits/:limit', summary: 'PUT /contacts/{contact}/compliance-limits/{limit} [scope: crm:contacts:write]' },
41
+ 'delete-compliance-limits': { method: 'DELETE', path: '/contacts/:contact/compliance-limits/:limit', summary: 'DELETE /contacts/{contact}/compliance-limits/{limit} [scope: crm:contacts:write]' },
42
+ issue: { method: 'POST', path: '/contacts/:contact/compliance-override/issue', summary: 'POST /contacts/{contact}/compliance-override/issue [scope: crm:contacts:write]' },
43
+ 'create-value-manual-entry': { method: 'POST', path: '/contacts/:contact/value/manual-entry', summary: 'POST /contacts/{contact}/value/manual-entry [scope: crm:contacts:write]' },
44
+ 'update-value-expected-ltv': { method: 'PUT', path: '/contacts/:contact/value/expected-ltv', summary: 'PUT /contacts/{contact}/value/expected-ltv [scope: crm:contacts:write]' },
45
+ 'create-user-links': { method: 'POST', path: '/contacts/:contact/user-links', summary: 'POST /contacts/{contact}/user-links [scope: crm:contacts:write]' },
46
+ discover: { method: 'POST', path: '/contacts/:contact/user-links/discover', summary: 'POST /contacts/{contact}/user-links/discover [scope: crm:contacts:write]' },
47
+ confirm: { method: 'POST', path: '/contacts/:contact/user-links/:link/confirm', summary: 'POST /contacts/{contact}/user-links/{link}/confirm [scope: crm:contacts:write]' },
48
+ reject: { method: 'POST', path: '/contacts/:contact/user-links/:link/reject', summary: 'POST /contacts/{contact}/user-links/{link}/reject [scope: crm:contacts:write]' },
49
+ overview: { method: 'GET', path: '/contacts/:contact/owned-assets/:asset/overview', summary: 'GET /contacts/{contact}/owned-assets/{asset}/overview [scope: crm:assets:read]' },
50
+ 'owned-assets-service-events': { method: 'GET', path: '/contacts/:contact/owned-assets/:asset/service-events', summary: 'GET /contacts/{contact}/owned-assets/{asset}/service-events [scope: crm:assets:read]' },
51
+ 'owned-assets-locations': { method: 'GET', path: '/contacts/:contact/owned-assets/:asset/locations', summary: 'GET /contacts/{contact}/owned-assets/{asset}/locations [scope: crm:assets:read]' },
52
+ 'owned-assets-upgrades': { method: 'GET', path: '/contacts/:contact/owned-assets/:asset/upgrades', summary: 'GET /contacts/{contact}/owned-assets/{asset}/upgrades [scope: crm:assets:read]' },
53
+ 'create-owned-assets-service-events': { method: 'POST', path: '/contacts/:contact/owned-assets/:asset/service-events', summary: 'POST /contacts/{contact}/owned-assets/{asset}/service-events [scope: crm:assets:write]' },
54
+ 'update-owned-assets-service-events': { method: 'PUT', path: '/contacts/:contact/owned-assets/:asset/service-events/:event', summary: 'PUT /contacts/{contact}/owned-assets/{asset}/service-events/{event} [scope: crm:assets:write]' },
55
+ 'delete-owned-assets-service-events': { method: 'DELETE', path: '/contacts/:contact/owned-assets/:asset/service-events/:event', summary: 'DELETE /contacts/{contact}/owned-assets/{asset}/service-events/{event} [scope: crm:assets:write]' },
56
+ 'create-owned-assets-locations': { method: 'POST', path: '/contacts/:contact/owned-assets/:asset/locations', summary: 'POST /contacts/{contact}/owned-assets/{asset}/locations [scope: crm:assets:write]' },
57
+ 'create-owned-assets-upgrades': { method: 'POST', path: '/contacts/:contact/owned-assets/:asset/upgrades', summary: 'POST /contacts/{contact}/owned-assets/{asset}/upgrades [scope: crm:assets:write]' },
58
+ 'commission-partnerships': { method: 'GET', path: '/contacts/:contact/commission-partnerships', summary: 'GET /contacts/{contact}/commission-partnerships [scope: crm:commission:read]' },
59
+ earnings: { method: 'GET', path: '/contacts/:contact/commission-partnerships/:partnership/earnings', summary: 'GET /contacts/{contact}/commission-partnerships/{partnership}/earnings [scope: crm:commission:read]' },
60
+ 'create-commission-partnerships': { method: 'POST', path: '/contacts/:contact/commission-partnerships', summary: 'POST /contacts/{contact}/commission-partnerships [scope: crm:commission:write]' },
61
+ 'create-earnings': { method: 'POST', path: '/contacts/:contact/commission-partnerships/:partnership/earnings', summary: 'POST /contacts/{contact}/commission-partnerships/{partnership}/earnings [scope: crm:commission:write]' },
62
+ 'update-commission-partnerships': { method: 'PUT', path: '/contacts/:contact/commission-partnerships/:partnership', summary: 'PUT /contacts/{contact}/commission-partnerships/{partnership} [scope: crm:commission:write]' },
63
+ 'delete-commission-partnerships': { method: 'DELETE', path: '/contacts/:contact/commission-partnerships/:partnership', summary: 'DELETE /contacts/{contact}/commission-partnerships/{partnership} [scope: crm:commission:write]' },
22
64
  },
23
65
  },
24
66
  notes: {
@@ -66,6 +108,8 @@ export const RESOURCES = {
66
108
  create: { method: 'POST', path: '/contact-lists' },
67
109
  update: { method: 'PATCH', path: '/contact-lists/:list' },
68
110
  delete: { method: 'DELETE', path: '/contact-lists/:list' },
111
+ 'create-contacts': { method: 'POST', path: '/contact-lists/:list/contacts', summary: 'POST /contact-lists/{list}/contacts [scope: crm:lists:write]' },
112
+ 'delete-contacts': { method: 'DELETE', path: '/contact-lists/:list/contacts', summary: 'DELETE /contact-lists/{list}/contacts [scope: crm:lists:write]' },
69
113
  },
70
114
  },
71
115
  listings: {
@@ -134,11 +178,20 @@ export const RESOURCES = {
134
178
  enrollments: { method: 'GET', path: '/course-enrollments' },
135
179
  },
136
180
  },
181
+ smartboards: {
182
+ summary: 'Smartboards / collaborative diagrams (read-only in v1)',
183
+ commands: {
184
+ list: { method: 'GET', path: '/smartboards' },
185
+ get: { method: 'GET', path: '/smartboards/:smartboard', summary: 'One board with its elements' },
186
+ },
187
+ },
137
188
  credits: {
138
189
  summary: 'Credit balance & ledger',
139
190
  commands: {
140
191
  balance: { method: 'GET', path: '/credits/balance' },
141
192
  ledger: { method: 'GET', path: '/credits/ledger' },
193
+ pricing: { method: 'GET', path: '/credits/pricing', summary: 'GET /credits/pricing [scope: credits:balance:read]' },
194
+ usage: { method: 'GET', path: '/credits/usage', summary: 'GET /credits/usage [scope: credits:balance:read]' },
142
195
  },
143
196
  },
144
197
  webhooks: {
@@ -178,4 +231,544 @@ export const RESOURCES = {
178
231
  show: { method: 'GET', path: '/me', summary: 'Who am I + token scopes' },
179
232
  },
180
233
  },
234
+ generate: {
235
+ summary: 'AI generation (async): start a job, then poll with `generations get <id>`',
236
+ commands: {
237
+ start: { method: 'POST', path: '/generate/:feature', summary: 'Start a generation. feature ∈ image, video, sound_effect, email_template, sales_presentation, blog_article, podcast, sticker, brand_colors, dossier, agent_odysseus, agent_apollo, agent_atticus, agent_eva, agent_alfred. Inputs via -d \'<json>\'. Returns a 202 handle.' },
238
+ },
239
+ },
240
+ generations: {
241
+ summary: 'Poll an async AI generation handle',
242
+ commands: {
243
+ get: { method: 'GET', path: '/generations/:id', summary: 'Poll a generation by id — status (queued|processing|succeeded|failed) + result' },
244
+ },
245
+ },
246
+ cli: {
247
+ summary: 'cli (v1)',
248
+ commands: {
249
+ 'create-auth-start': { method: 'POST', path: '/cli/auth/start', summary: 'POST /cli/auth/start' },
250
+ 'create-auth-poll': { method: 'POST', path: '/cli/auth/poll', summary: 'POST /cli/auth/poll' },
251
+ },
252
+ },
253
+ industries: {
254
+ summary: 'industries (v1)',
255
+ commands: {
256
+ list: { method: 'GET', path: '/industries', summary: 'GET /industries' },
257
+ schema: { method: 'GET', path: '/industries/:industry/schema', summary: 'GET /industries/{industry}/schema' },
258
+ },
259
+ },
260
+ 'live-shop': {
261
+ summary: 'live shop (v1)',
262
+ commands: {
263
+ list: { method: 'GET', path: '/live-shop', summary: 'GET /live-shop [scope: livestreams:read]' },
264
+ },
265
+ },
266
+ kanbans: {
267
+ summary: 'kanbans (v1)',
268
+ commands: {
269
+ get: { method: 'GET', path: '/kanbans/:kanban', summary: 'GET /kanbans/{kanban} [scope: crm:kanban:read]' },
270
+ list: { method: 'GET', path: '/kanbans', summary: 'GET /kanbans [scope: crm:kanban:read]' },
271
+ lists: { method: 'GET', path: '/kanbans/:kanban/lists', summary: 'GET /kanbans/{kanban}/lists [scope: crm:kanban:read]' },
272
+ },
273
+ },
274
+ 'business-org-chart': {
275
+ summary: 'business org chart (v1)',
276
+ commands: {
277
+ list: { method: 'GET', path: '/business/org-chart', summary: 'GET /business/org-chart [scope: business:org:read]' },
278
+ },
279
+ },
280
+ 'business-departments': {
281
+ summary: 'business departments (v1)',
282
+ commands: {
283
+ list: { method: 'GET', path: '/business/departments', summary: 'GET /business/departments [scope: business:org:read]' },
284
+ },
285
+ },
286
+ 'crm-cultural-library': {
287
+ summary: 'crm cultural library (v1)',
288
+ commands: {
289
+ list: { method: 'GET', path: '/crm/cultural-library', summary: 'GET /crm/cultural-library [scope: crm:cultural:read]' },
290
+ get: { method: 'GET', path: '/crm/cultural-library/:profile', summary: 'GET /crm/cultural-library/{profile} [scope: crm:cultural:read]' },
291
+ },
292
+ },
293
+ 'crm-portfolio': {
294
+ summary: 'crm portfolio (v1)',
295
+ commands: {
296
+ get: { method: 'GET', path: '/crm/portfolio/:section', summary: 'GET /crm/portfolio/{section} [scope: crm:portfolio:read]' },
297
+ },
298
+ },
299
+ 'client-deals': {
300
+ summary: 'client deals (v1)',
301
+ commands: {
302
+ list: { method: 'GET', path: '/client-deals', summary: 'GET /client-deals [scope: crm:client_deals:read]' },
303
+ get: { method: 'GET', path: '/client-deals/:uuid', summary: 'GET /client-deals/{uuid} [scope: crm:client_deals:read]' },
304
+ },
305
+ },
306
+ invoices: {
307
+ summary: 'invoices (v1)',
308
+ commands: {
309
+ list: { method: 'GET', path: '/invoices', summary: 'GET /invoices [scope: crm:invoices:read]' },
310
+ get: { method: 'GET', path: '/invoices/:invoice', summary: 'GET /invoices/{invoice} [scope: crm:invoices:read]' },
311
+ },
312
+ },
313
+ reviews: {
314
+ summary: 'reviews (v1)',
315
+ commands: {
316
+ list: { method: 'GET', path: '/reviews', summary: 'GET /reviews [scope: crm:reviews:read]' },
317
+ get: { method: 'GET', path: '/reviews/:review', summary: 'GET /reviews/{review} [scope: crm:reviews:read]' },
318
+ },
319
+ },
320
+ 'message-campaigns': {
321
+ summary: 'message campaigns (v1)',
322
+ commands: {
323
+ list: { method: 'GET', path: '/message-campaigns', summary: 'GET /message-campaigns [scope: crm:campaigns:read]' },
324
+ get: { method: 'GET', path: '/message-campaigns/:uuid', summary: 'GET /message-campaigns/{uuid} [scope: crm:campaigns:read]' },
325
+ create: { method: 'POST', path: '/message-campaigns', summary: 'POST /message-campaigns [scope: crm:campaigns:write]' },
326
+ enroll: { method: 'POST', path: '/message-campaigns/:uuid/enroll', summary: 'POST /message-campaigns/{uuid}/enroll [scope: crm:campaigns:write]' },
327
+ },
328
+ },
329
+ 'phone-numbers': {
330
+ summary: 'phone numbers (v1)',
331
+ commands: {
332
+ list: { method: 'GET', path: '/phone-numbers', summary: 'GET /phone-numbers [scope: comms:numbers:read]' },
333
+ 'list-2': { method: 'GET', path: '/phone-numbers/:uuid', summary: 'GET /phone-numbers/{uuid} [scope: comms:numbers:read]' },
334
+ create: { method: 'POST', path: '/phone-numbers', summary: 'POST /phone-numbers [scope: comms:numbers:write]' },
335
+ },
336
+ },
337
+ 'command-centers': {
338
+ summary: 'command centers (v1)',
339
+ commands: {
340
+ overview: { method: 'GET', path: '/command-centers/:slug/overview', summary: 'GET /command-centers/{slug}/overview [scope: business:command-center:read]' },
341
+ missions: { method: 'GET', path: '/command-centers/:slug/missions', summary: 'GET /command-centers/{slug}/missions [scope: business:command-center:read]' },
342
+ 'missions-2': { method: 'GET', path: '/command-centers/:slug/missions/:mission', summary: 'GET /command-centers/{slug}/missions/{mission} [scope: business:command-center:read]' },
343
+ 'work-feed': { method: 'GET', path: '/command-centers/:slug/work-feed', summary: 'GET /command-centers/{slug}/work-feed [scope: business:command-center:read]' },
344
+ 'performance-competencies': { method: 'GET', path: '/command-centers/:slug/performance/competencies', summary: 'GET /command-centers/{slug}/performance/competencies [scope: business:performance:read]' },
345
+ 'performance-templates': { method: 'GET', path: '/command-centers/:slug/performance/templates', summary: 'GET /command-centers/{slug}/performance/templates [scope: business:performance:read]' },
346
+ reviews: { method: 'GET', path: '/command-centers/:slug/performance/reviews', summary: 'GET /command-centers/{slug}/performance/reviews [scope: business:performance:read]' },
347
+ 'reviews-2': { method: 'GET', path: '/command-centers/:slug/performance/reviews/:review', summary: 'GET /command-centers/{slug}/performance/reviews/{review} [scope: business:performance:read]' },
348
+ 'performance-subjects-rollup': { method: 'GET', path: '/command-centers/:slug/performance/subjects/:subject/rollup', summary: 'GET /command-centers/{slug}/performance/subjects/{subject}/rollup [scope: business:performance:read]' },
349
+ 'performance-plans': { method: 'GET', path: '/command-centers/:slug/performance/plans', summary: 'GET /command-centers/{slug}/performance/plans [scope: business:performance:read]' },
350
+ },
351
+ },
352
+ 'email-messages': {
353
+ summary: 'email messages (v1)',
354
+ commands: {
355
+ list: { method: 'GET', path: '/email-messages', summary: 'GET /email-messages [scope: comms:email:read]' },
356
+ get: { method: 'GET', path: '/email-messages/:emailMessage', summary: 'GET /email-messages/{emailMessage} [scope: comms:email:read]' },
357
+ send: { method: 'POST', path: '/email-messages/send', summary: 'POST /email-messages/send [scope: comms:email:send]' },
358
+ },
359
+ },
360
+ 'email-deliverability-contact-lists': {
361
+ summary: 'email deliverability contact lists (v1)',
362
+ commands: {
363
+ status: { method: 'GET', path: '/email-deliverability/contact-lists/:contactList/status', summary: 'GET /email-deliverability/contact-lists/{contactList}/status [scope: comms:email:deliverability:read]' },
364
+ validate: { method: 'POST', path: '/email-deliverability/contact-lists/:contactList/validate', summary: 'POST /email-deliverability/contact-lists/{contactList}/validate [scope: comms:email:deliverability:write]' },
365
+ },
366
+ },
367
+ 'email-suppression': {
368
+ summary: 'email suppression (v1)',
369
+ commands: {
370
+ list: { method: 'GET', path: '/email-suppression', summary: 'GET /email-suppression [scope: comms:email:suppression:read]' },
371
+ },
372
+ },
373
+ 'sms-messages': {
374
+ summary: 'sms messages (v1)',
375
+ commands: {
376
+ list: { method: 'GET', path: '/sms-messages', summary: 'GET /sms-messages [scope: comms:sms:read]' },
377
+ get: { method: 'GET', path: '/sms-messages/:smsMessage', summary: 'GET /sms-messages/{smsMessage} [scope: comms:sms:read]' },
378
+ send: { method: 'POST', path: '/sms-messages/send', summary: 'POST /sms-messages/send [scope: comms:sms:write]' },
379
+ },
380
+ },
381
+ calls: {
382
+ summary: 'calls (v1)',
383
+ commands: {
384
+ list: { method: 'GET', path: '/calls', summary: 'GET /calls [scope: comms:phone:read]' },
385
+ get: { method: 'GET', path: '/calls/:call', summary: 'GET /calls/{call} [scope: comms:phone:read]' },
386
+ },
387
+ },
388
+ 'kanban-cards': {
389
+ summary: 'kanban cards (v1)',
390
+ commands: {
391
+ list: { method: 'GET', path: '/kanban-cards', summary: 'GET /kanban-cards [scope: crm:kanban:read]' },
392
+ },
393
+ },
394
+ 'email-templates': {
395
+ summary: 'email templates (v1)',
396
+ commands: {
397
+ list: { method: 'GET', path: '/email-templates', summary: 'GET /email-templates [scope: comms:email-templates:read]' },
398
+ get: { method: 'GET', path: '/email-templates/:slug', summary: 'GET /email-templates/{slug} [scope: comms:email-templates:read]' },
399
+ },
400
+ },
401
+ 'mini-games': {
402
+ summary: 'mini games (v1)',
403
+ commands: {
404
+ plays: { method: 'GET', path: '/mini-games/plays', summary: 'GET /mini-games/plays [scope: games:mini-games:read]' },
405
+ },
406
+ },
407
+ arena: {
408
+ summary: 'arena (v1)',
409
+ commands: {
410
+ matches: { method: 'GET', path: '/arena/matches', summary: 'GET /arena/matches [scope: games:arena:read]' },
411
+ },
412
+ },
413
+ nft: {
414
+ summary: 'nft (v1)',
415
+ commands: {
416
+ collections: { method: 'GET', path: '/nft/collections', summary: 'GET /nft/collections [scope: nft:collections:read]' },
417
+ assets: { method: 'GET', path: '/nft/assets', summary: 'GET /nft/assets [scope: nft:assets:read]' },
418
+ 'collections-2': { method: 'GET', path: '/nft/collections/:collection', summary: 'GET /nft/collections/{collection} [scope: nft:collections:read]' },
419
+ 'assets-2': { method: 'GET', path: '/nft/assets/:asset', summary: 'GET /nft/assets/{asset} [scope: nft:assets:read]' },
420
+ },
421
+ },
422
+ openflix: {
423
+ summary: 'openflix (v1)',
424
+ commands: {
425
+ movies: { method: 'GET', path: '/openflix/movies', summary: 'GET /openflix/movies [scope: media:openflix:read]' },
426
+ series: { method: 'GET', path: '/openflix/series', summary: 'GET /openflix/series [scope: media:openflix:read]' },
427
+ 'movies-2': { method: 'GET', path: '/openflix/movies/:movie', summary: 'GET /openflix/movies/{movie} [scope: media:openflix:read]' },
428
+ 'series-2': { method: 'GET', path: '/openflix/series/:series', summary: 'GET /openflix/series/{series} [scope: media:openflix:read]' },
429
+ },
430
+ },
431
+ livestreams: {
432
+ summary: 'livestreams (v1)',
433
+ commands: {
434
+ list: { method: 'GET', path: '/livestreams', summary: 'GET /livestreams [scope: media:livestreams:read]' },
435
+ get: { method: 'GET', path: '/livestreams/:room', summary: 'GET /livestreams/{room} [scope: media:livestreams:read]' },
436
+ },
437
+ },
438
+ profile: {
439
+ summary: 'profile (v1)',
440
+ commands: {
441
+ list: { method: 'GET', path: '/profile', summary: 'GET /profile [scope: profile:read]' },
442
+ },
443
+ },
444
+ affiliate: {
445
+ summary: 'affiliate (v1)',
446
+ commands: {
447
+ program: { method: 'GET', path: '/affiliate/program', summary: 'GET /affiliate/program [scope: affiliate:program:read]' },
448
+ commissions: { method: 'GET', path: '/affiliate/commissions', summary: 'GET /affiliate/commissions [scope: affiliate:program:read]' },
449
+ payouts: { method: 'GET', path: '/affiliate/payouts', summary: 'GET /affiliate/payouts [scope: affiliate:payouts:read]' },
450
+ },
451
+ },
452
+ print: {
453
+ summary: 'print (v1)',
454
+ commands: {
455
+ 'minimum-quantities': { method: 'GET', path: '/print/minimum-quantities', summary: 'GET /print/minimum-quantities [scope: print:fulfillment:read]' },
456
+ 'minimum-quantities-2': { method: 'GET', path: '/print/minimum-quantities/:productTypeId', summary: 'GET /print/minimum-quantities/{productTypeId} [scope: print:fulfillment:read]' },
457
+ },
458
+ },
459
+ fulfillment: {
460
+ summary: 'fulfillment (v1)',
461
+ commands: {
462
+ queue: { method: 'GET', path: '/fulfillment/queue', summary: 'GET /fulfillment/queue [scope: fulfillment:read]' },
463
+ analytics: { method: 'GET', path: '/fulfillment/analytics', summary: 'GET /fulfillment/analytics [scope: fulfillment:read]' },
464
+ },
465
+ },
466
+ 'branding-projects': {
467
+ summary: 'branding projects (v1)',
468
+ commands: {
469
+ list: { method: 'GET', path: '/branding-projects', summary: 'GET /branding-projects [scope: brand:projects:read]' },
470
+ get: { method: 'GET', path: '/branding-projects/:project', summary: 'GET /branding-projects/{project} [scope: brand:projects:read]' },
471
+ },
472
+ },
473
+ brands: {
474
+ summary: 'brands (v1)',
475
+ commands: {
476
+ list: { method: 'GET', path: '/brands', summary: 'GET /brands [scope: brand:assets:read]' },
477
+ get: { method: 'GET', path: '/brands/:brand', summary: 'GET /brands/{brand} [scope: brand:assets:read]' },
478
+ },
479
+ },
480
+ associations: {
481
+ summary: 'associations (v1)',
482
+ commands: {
483
+ list: { method: 'GET', path: '/associations', summary: 'GET /associations [scope: community:associations:read]' },
484
+ get: { method: 'GET', path: '/associations/:association', summary: 'GET /associations/{association} [scope: community:associations:read]' },
485
+ posts: { method: 'GET', path: '/associations/:association/posts', summary: 'GET /associations/{association}/posts [scope: community:associations:read]' },
486
+ },
487
+ },
488
+ policies: {
489
+ summary: 'policies (v1)',
490
+ commands: {
491
+ list: { method: 'GET', path: '/policies', summary: 'GET /policies [scope: business:policies:read]' },
492
+ },
493
+ },
494
+ meetups: {
495
+ summary: 'meetups (v1)',
496
+ commands: {
497
+ list: { method: 'GET', path: '/meetups', summary: 'GET /meetups [scope: community:meetups:read]' },
498
+ get: { method: 'GET', path: '/meetups/:meetup', summary: 'GET /meetups/{meetup} [scope: community:meetups:read]' },
499
+ },
500
+ },
501
+ clones: {
502
+ summary: 'clones (v1)',
503
+ commands: {
504
+ list: { method: 'GET', path: '/clones', summary: 'GET /clones [scope: profile:clones:read]' },
505
+ get: { method: 'GET', path: '/clones/:clone', summary: 'GET /clones/{clone} [scope: profile:clones:read]' },
506
+ create: { method: 'POST', path: '/clones', summary: 'POST /clones [scope: profile:clones:write]' },
507
+ samples: { method: 'POST', path: '/clones/:clone/samples', summary: 'POST /clones/{clone}/samples [scope: profile:clones:write]' },
508
+ iterate: { method: 'POST', path: '/clones/:clone/iterate', summary: 'POST /clones/{clone}/iterate [scope: profile:clones:write]' },
509
+ tts: { method: 'POST', path: '/clones/:clone/tts', summary: 'POST /clones/{clone}/tts [scope: profile:clones:tts]' },
510
+ },
511
+ },
512
+ 'vms-communities': {
513
+ summary: 'vms communities (v1)',
514
+ commands: {
515
+ passes: { method: 'GET', path: '/vms/communities/:communitySlug/passes', summary: 'GET /vms/communities/{communitySlug}/passes [scope: vms:passes:read]' },
516
+ 'passes-2': { method: 'GET', path: '/vms/communities/:communitySlug/passes/:pass', summary: 'GET /vms/communities/{communitySlug}/passes/{pass} [scope: vms:passes:read]' },
517
+ 'create-passes': { method: 'POST', path: '/vms/communities/:communitySlug/passes', summary: 'POST /vms/communities/{communitySlug}/passes [scope: vms:passes:write]' },
518
+ 'delete-passes': { method: 'DELETE', path: '/vms/communities/:communitySlug/passes/:pass', summary: 'DELETE /vms/communities/{communitySlug}/passes/{pass} [scope: vms:passes:write]' },
519
+ 'gate-events': { method: 'GET', path: '/vms/communities/:communitySlug/gate-events', summary: 'GET /vms/communities/{communitySlug}/gate-events [scope: vms:events:read]' },
520
+ 'create-gate-events': { method: 'POST', path: '/vms/communities/:communitySlug/gate-events', summary: 'POST /vms/communities/{communitySlug}/gate-events [scope: vms:events:write]' },
521
+ residents: { method: 'GET', path: '/vms/communities/:communitySlug/residents', summary: 'GET /vms/communities/{communitySlug}/residents [scope: vms:residents:read]' },
522
+ 'create-broadcasts': { method: 'POST', path: '/vms/communities/:communitySlug/broadcasts', summary: 'POST /vms/communities/{communitySlug}/broadcasts [scope: vms:broadcasts:write]' },
523
+ gates: { method: 'GET', path: '/vms/communities/:communitySlug/gates', summary: 'GET /vms/communities/{communitySlug}/gates [scope: vms:gates:read]' },
524
+ 'gates-2': { method: 'GET', path: '/vms/communities/:communitySlug/gates/:gate', summary: 'GET /vms/communities/{communitySlug}/gates/{gate} [scope: vms:gates:read]' },
525
+ relays: { method: 'GET', path: '/vms/communities/:communitySlug/relays', summary: 'GET /vms/communities/{communitySlug}/relays [scope: vms:relays:read]' },
526
+ 'relays-2': { method: 'GET', path: '/vms/communities/:communitySlug/relays/:relay', summary: 'GET /vms/communities/{communitySlug}/relays/{relay} [scope: vms:relays:read]' },
527
+ fire: { method: 'POST', path: '/vms/communities/:communitySlug/relays/:relay/fire', summary: 'POST /vms/communities/{communitySlug}/relays/{relay}/fire [scope: vms:relays:fire]' },
528
+ 'parking-permits': { method: 'GET', path: '/vms/communities/:communitySlug/parking/permits', summary: 'GET /vms/communities/{communitySlug}/parking/permits [scope: vms:permits:read]' },
529
+ 'parking-allocations': { method: 'GET', path: '/vms/communities/:communitySlug/parking/allocations', summary: 'GET /vms/communities/{communitySlug}/parking/allocations [scope: vms:permits:read]' },
530
+ 'create-trafficlogix-event': { method: 'POST', path: '/vms/communities/:communitySlug/trafficlogix/event', summary: 'POST /vms/communities/{communitySlug}/trafficlogix/event [scope: vms:events:write]' },
531
+ 'create-telephone-entry-lookup': { method: 'POST', path: '/vms/communities/:communitySlug/telephone-entry/lookup', summary: 'POST /vms/communities/{communitySlug}/telephone-entry/lookup [scope: vms:events:write]' },
532
+ 'create-telephone-entry-pin': { method: 'POST', path: '/vms/communities/:communitySlug/telephone-entry/pin', summary: 'POST /vms/communities/{communitySlug}/telephone-entry/pin [scope: vms:events:write]' },
533
+ 'create-camera-ai-event': { method: 'POST', path: '/vms/communities/:communitySlug/camera-ai/:vendor/event', summary: 'POST /vms/communities/{communitySlug}/camera-ai/{vendor}/event [scope: vms:events:write]' },
534
+ 'predictive-pulse': { method: 'POST', path: '/vms/communities/:communitySlug/gates/:gate/predictive-pulse', summary: 'POST /vms/communities/{communitySlug}/gates/{gate}/predictive-pulse [scope: vms:gates:predictive_pulse]' },
535
+ 'create-backup-heartbeat': { method: 'POST', path: '/vms/communities/:communitySlug/backup/heartbeat', summary: 'POST /vms/communities/{communitySlug}/backup/heartbeat [scope: vms:failsafe:sync]' },
536
+ 'create-backup-mode': { method: 'POST', path: '/vms/communities/:communitySlug/backup/mode', summary: 'POST /vms/communities/{communitySlug}/backup/mode [scope: vms:failsafe:sync]' },
537
+ 'backup-snapshot': { method: 'GET', path: '/vms/communities/:communitySlug/backup/snapshot', summary: 'GET /vms/communities/{communitySlug}/backup/snapshot [scope: vms:failsafe:sync]' },
538
+ 'create-backup-relay-fire': { method: 'POST', path: '/vms/communities/:communitySlug/backup/relay-fire', summary: 'POST /vms/communities/{communitySlug}/backup/relay-fire [scope: vms:failsafe:sync]' },
539
+ 'create-backup-visitor-verification': { method: 'POST', path: '/vms/communities/:communitySlug/backup/visitor-verification', summary: 'POST /vms/communities/{communitySlug}/backup/visitor-verification [scope: vms:failsafe:sync]' },
540
+ 'residents-2': { method: 'GET', path: '/vms/communities/:communitySlug/residents/:resident', summary: 'GET /vms/communities/{communitySlug}/residents/{resident} [scope: vms:residents:read]' },
541
+ },
542
+ },
543
+ goals: {
544
+ summary: 'goals (v1)',
545
+ commands: {
546
+ list: { method: 'GET', path: '/goals', summary: 'GET /goals [scope: goals:read]' },
547
+ },
548
+ },
549
+ epics: {
550
+ summary: 'epics (v1)',
551
+ commands: {
552
+ list: { method: 'GET', path: '/epics', summary: 'GET /epics [scope: goals:read]' },
553
+ },
554
+ },
555
+ 'professional-profile': {
556
+ summary: 'professional profile (v1)',
557
+ commands: {
558
+ list: { method: 'GET', path: '/professional-profile', summary: 'GET /professional-profile [scope: professional_profile:read]' },
559
+ portfolio: { method: 'GET', path: '/professional-profile/portfolio', summary: 'GET /professional-profile/portfolio [scope: professional_profile:read]' },
560
+ },
561
+ },
562
+ 'professional-profiles': {
563
+ summary: 'professional profiles (v1)',
564
+ commands: {
565
+ get: { method: 'GET', path: '/professional-profiles/:handle', summary: 'GET /professional-profiles/{handle} [scope: professional_profile:read]' },
566
+ },
567
+ },
568
+ accounting: {
569
+ summary: 'accounting (v1)',
570
+ commands: {
571
+ 'chart-of-accounts': { method: 'GET', path: '/accounting/chart-of-accounts', summary: 'GET /accounting/chart-of-accounts [scope: accounting:read]' },
572
+ 'journal-entries': { method: 'GET', path: '/accounting/journal-entries', summary: 'GET /accounting/journal-entries [scope: accounting:read]' },
573
+ 'trial-balance': { method: 'GET', path: '/accounting/trial-balance', summary: 'GET /accounting/trial-balance [scope: accounting:read]' },
574
+ 'statements-income': { method: 'GET', path: '/accounting/statements/income', summary: 'GET /accounting/statements/income [scope: accounting:read]' },
575
+ 'statements-balance-sheet': { method: 'GET', path: '/accounting/statements/balance-sheet', summary: 'GET /accounting/statements/balance-sheet [scope: accounting:read]' },
576
+ 'consolidated-trial-balance': { method: 'GET', path: '/accounting/consolidated/trial-balance', summary: 'GET /accounting/consolidated/trial-balance [scope: accounting:read]' },
577
+ 'consolidated-income': { method: 'GET', path: '/accounting/consolidated/income', summary: 'GET /accounting/consolidated/income [scope: accounting:read]' },
578
+ 'consolidated-balance-sheet': { method: 'GET', path: '/accounting/consolidated/balance-sheet', summary: 'GET /accounting/consolidated/balance-sheet [scope: accounting:read]' },
579
+ 'tax-liability': { method: 'GET', path: '/accounting/tax-liability', summary: 'GET /accounting/tax-liability [scope: accounting:read]' },
580
+ 'segregation-of-duties': { method: 'GET', path: '/accounting/segregation-of-duties', summary: 'GET /accounting/segregation-of-duties [scope: accounting:read]' },
581
+ 'audit-trail': { method: 'GET', path: '/accounting/audit-trail', summary: 'GET /accounting/audit-trail [scope: accounting:read]' },
582
+ 'e-invoices': { method: 'GET', path: '/accounting/e-invoices', summary: 'GET /accounting/e-invoices [scope: accounting:read]' },
583
+ 'bank-reconciliations': { method: 'GET', path: '/accounting/bank-reconciliations', summary: 'GET /accounting/bank-reconciliations [scope: accounting:read]' },
584
+ },
585
+ },
586
+ receivables: {
587
+ summary: 'receivables (v1)',
588
+ commands: {
589
+ dunning: { method: 'GET', path: '/receivables/dunning', summary: 'GET /receivables/dunning [scope: receivables:read]' },
590
+ subscriptions: { method: 'GET', path: '/receivables/subscriptions', summary: 'GET /receivables/subscriptions [scope: receivables:read]' },
591
+ },
592
+ },
593
+ payables: {
594
+ summary: 'payables (v1)',
595
+ commands: {
596
+ 'discount-opportunities': { method: 'GET', path: '/payables/discount-opportunities', summary: 'GET /payables/discount-opportunities [scope: payables:read]' },
597
+ },
598
+ },
599
+ procurement: {
600
+ summary: 'procurement (v1)',
601
+ commands: {
602
+ suppliers: { method: 'GET', path: '/procurement/suppliers', summary: 'GET /procurement/suppliers [scope: procurement:read]' },
603
+ 'purchase-orders': { method: 'GET', path: '/procurement/purchase-orders', summary: 'GET /procurement/purchase-orders [scope: procurement:read]' },
604
+ requisitions: { method: 'GET', path: '/procurement/requisitions', summary: 'GET /procurement/requisitions [scope: procurement:read]' },
605
+ },
606
+ },
607
+ inventory: {
608
+ summary: 'inventory (v1)',
609
+ commands: {
610
+ 'stock-levels': { method: 'GET', path: '/inventory/stock-levels', summary: 'GET /inventory/stock-levels [scope: inventory:read]' },
611
+ valuation: { method: 'GET', path: '/inventory/valuation', summary: 'GET /inventory/valuation [scope: inventory:read]' },
612
+ warehouses: { method: 'GET', path: '/inventory/warehouses', summary: 'GET /inventory/warehouses [scope: inventory:read]' },
613
+ },
614
+ },
615
+ hr: {
616
+ summary: 'hr (v1)',
617
+ commands: {
618
+ employees: { method: 'GET', path: '/hr/employees', summary: 'GET /hr/employees [scope: hr:read]' },
619
+ headcount: { method: 'GET', path: '/hr/headcount', summary: 'GET /hr/headcount [scope: hr:read]' },
620
+ },
621
+ },
622
+ 'fixed-assets': {
623
+ summary: 'fixed assets (v1)',
624
+ commands: {
625
+ list: { method: 'GET', path: '/fixed-assets', summary: 'GET /fixed-assets [scope: fixed_assets:read]' },
626
+ overview: { method: 'GET', path: '/fixed-assets/overview', summary: 'GET /fixed-assets/overview [scope: fixed_assets:read]' },
627
+ },
628
+ },
629
+ manufacturing: {
630
+ summary: 'manufacturing (v1)',
631
+ commands: {
632
+ 'work-orders': { method: 'GET', path: '/manufacturing/work-orders', summary: 'GET /manufacturing/work-orders [scope: manufacturing:read]' },
633
+ 'bills-of-materials': { method: 'GET', path: '/manufacturing/bills-of-materials', summary: 'GET /manufacturing/bills-of-materials [scope: manufacturing:read]' },
634
+ },
635
+ },
636
+ 'tech-tree': {
637
+ summary: 'tech tree (v1)',
638
+ commands: {
639
+ nodes: { method: 'GET', path: '/tech-tree/nodes', summary: 'GET /tech-tree/nodes [scope: tech_tree:read]' },
640
+ graph: { method: 'GET', path: '/tech-tree/graph', summary: 'GET /tech-tree/graph [scope: tech_tree:read]' },
641
+ },
642
+ },
643
+ influence: {
644
+ summary: 'influence (v1)',
645
+ commands: {
646
+ layers: { method: 'GET', path: '/influence/layers', summary: 'GET /influence/layers [scope: business:influence:read]' },
647
+ 'missions-context': { method: 'GET', path: '/influence/missions/:mission/context', summary: 'GET /influence/missions/{mission}/context [scope: business:influence:read]' },
648
+ },
649
+ },
650
+ qms: {
651
+ summary: 'qms (v1)',
652
+ commands: {
653
+ readiness: { method: 'GET', path: '/qms/readiness', summary: 'GET /qms/readiness [scope: business:qms:read]' },
654
+ objectives: { method: 'GET', path: '/qms/objectives', summary: 'GET /qms/objectives [scope: business:qms:read]' },
655
+ risks: { method: 'GET', path: '/qms/risks', summary: 'GET /qms/risks [scope: business:qms:read]' },
656
+ capas: { method: 'GET', path: '/qms/capas', summary: 'GET /qms/capas [scope: business:qms:read]' },
657
+ audits: { method: 'GET', path: '/qms/audits', summary: 'GET /qms/audits [scope: business:qms:read]' },
658
+ competence: { method: 'GET', path: '/qms/competence', summary: 'GET /qms/competence [scope: business:qms:read]' },
659
+ documents: { method: 'GET', path: '/qms/documents', summary: 'GET /qms/documents [scope: business:qms:read]' },
660
+ certification: { method: 'GET', path: '/qms/certification', summary: 'GET /qms/certification [scope: business:qms:read]' },
661
+ obligations: { method: 'GET', path: '/qms/obligations', summary: 'GET /qms/obligations [scope: business:qms:read]' },
662
+ },
663
+ },
664
+ 'data-objects': {
665
+ summary: 'data objects (v1)',
666
+ commands: {
667
+ list: { method: 'GET', path: '/data/objects', summary: 'GET /data/objects [scope: data:objects:read]' },
668
+ schema: { method: 'GET', path: '/data/objects/:object/schema', summary: 'GET /data/objects/{object}/schema [scope: data:objects:read]' },
669
+ create: { method: 'POST', path: '/data/objects', summary: 'POST /data/objects [scope: data:objects:write]' },
670
+ records: { method: 'GET', path: '/data/objects/:object/records', summary: 'GET /data/objects/{object}/records [scope: data:records:read]' },
671
+ 'create-records': { method: 'POST', path: '/data/objects/:object/records', summary: 'POST /data/objects/{object}/records [scope: data:records:write]' },
672
+ upsert: { method: 'POST', path: '/data/objects/:object/records/upsert', summary: 'POST /data/objects/{object}/records/upsert [scope: data:records:write]' },
673
+ },
674
+ },
675
+ 'data-records': {
676
+ summary: 'data records (v1)',
677
+ commands: {
678
+ get: { method: 'GET', path: '/data/records/:uuid', summary: 'GET /data/records/{uuid} [scope: data:records:read]' },
679
+ update: { method: 'PATCH', path: '/data/records/:uuid', summary: 'PATCH /data/records/{uuid} [scope: data:records:write]' },
680
+ delete: { method: 'DELETE', path: '/data/records/:uuid', summary: 'DELETE /data/records/{uuid} [scope: data:records:write]' },
681
+ },
682
+ },
683
+ kits: {
684
+ summary: 'kits (v1)',
685
+ commands: {
686
+ list: { method: 'GET', path: '/kits', summary: 'GET /kits [scope: kits:read]' },
687
+ },
688
+ },
689
+ 'business-dna': {
690
+ summary: 'business dna (v1)',
691
+ commands: {
692
+ list: { method: 'GET', path: '/business/dna', summary: 'GET /business/dna [scope: business:dna:read]' },
693
+ },
694
+ },
695
+ 'business-plan': {
696
+ summary: 'business plan (v1)',
697
+ commands: {
698
+ list: { method: 'GET', path: '/business/plan', summary: 'GET /business/plan [scope: business:plan:read]' },
699
+ },
700
+ },
701
+ iot: {
702
+ summary: 'iot (v1)',
703
+ commands: {
704
+ devices: { method: 'GET', path: '/iot/devices', summary: 'GET /iot/devices [scope: iot:read]' },
705
+ 'create-devices': { method: 'POST', path: '/iot/devices', summary: 'POST /iot/devices [scope: iot:write]' },
706
+ 'create-readings': { method: 'POST', path: '/iot/readings', summary: 'POST /iot/readings [scope: iot:write]' },
707
+ 'create-alarms': { method: 'POST', path: '/iot/alarms', summary: 'POST /iot/alarms [scope: iot:write]' },
708
+ },
709
+ },
710
+ property: {
711
+ summary: 'property (v1)',
712
+ commands: {
713
+ locations: { method: 'GET', path: '/property/locations', summary: 'GET /property/locations [scope: property:read]' },
714
+ 'work-orders': { method: 'GET', path: '/property/work-orders', summary: 'GET /property/work-orders [scope: property:read]' },
715
+ assets: { method: 'GET', path: '/property/assets', summary: 'GET /property/assets [scope: property:read]' },
716
+ },
717
+ },
718
+ funnels: {
719
+ summary: 'funnels (v1)',
720
+ commands: {
721
+ list: { method: 'GET', path: '/funnels', summary: 'GET /funnels [scope: funnels:read]' },
722
+ },
723
+ },
724
+ 'ad-simulations': {
725
+ summary: 'ad simulations (v1)',
726
+ commands: {
727
+ list: { method: 'GET', path: '/ad-simulations', summary: 'GET /ad-simulations [scope: business:ad-simulations:read]' },
728
+ },
729
+ },
730
+ sites: {
731
+ summary: 'sites (v1)',
732
+ commands: {
733
+ 'store-locations': { method: 'GET', path: '/sites/:token/store-locations', summary: 'GET /sites/{token}/store-locations' },
734
+ products: { method: 'GET', path: '/sites/:token/products', summary: 'GET /sites/{token}/products' },
735
+ search: { method: 'GET', path: '/sites/:token/products/search', summary: 'GET /sites/{token}/products/search' },
736
+ 'products-2': { method: 'GET', path: '/sites/:token/products/:slug', summary: 'GET /sites/{token}/products/{slug}' },
737
+ related: { method: 'GET', path: '/sites/:token/products/:slug/related', summary: 'GET /sites/{token}/products/{slug}/related' },
738
+ reviews: { method: 'GET', path: '/sites/:token/products/:slug/reviews', summary: 'GET /sites/{token}/products/{slug}/reviews' },
739
+ collections: { method: 'GET', path: '/sites/:token/collections', summary: 'GET /sites/{token}/collections' },
740
+ 'collections-products': { method: 'GET', path: '/sites/:token/collections/:slug/products', summary: 'GET /sites/{token}/collections/{slug}/products' },
741
+ cart: { method: 'GET', path: '/sites/:token/cart', summary: 'GET /sites/{token}/cart' },
742
+ 'cart-count': { method: 'GET', path: '/sites/:token/cart/count', summary: 'GET /sites/{token}/cart/count' },
743
+ 'create-cart-items': { method: 'POST', path: '/sites/:token/cart/items', summary: 'POST /sites/{token}/cart/items' },
744
+ 'update-cart-items': { method: 'PUT', path: '/sites/:token/cart/items/:itemId', summary: 'PUT /sites/{token}/cart/items/{itemId}' },
745
+ 'delete-cart-items': { method: 'DELETE', path: '/sites/:token/cart/items/:itemId', summary: 'DELETE /sites/{token}/cart/items/{itemId}' },
746
+ 'delete-cart': { method: 'DELETE', path: '/sites/:token/cart', summary: 'DELETE /sites/{token}/cart' },
747
+ 'create-cart-apply-code': { method: 'POST', path: '/sites/:token/cart/apply-code', summary: 'POST /sites/{token}/cart/apply-code' },
748
+ 'delete-cart-apply-code': { method: 'DELETE', path: '/sites/:token/cart/apply-code/:applicationId', summary: 'DELETE /sites/{token}/cart/apply-code/{applicationId}' },
749
+ 'create-checkout-payment-intent': { method: 'POST', path: '/sites/:token/checkout/payment-intent', summary: 'POST /sites/{token}/checkout/payment-intent' },
750
+ 'create-checkout-process': { method: 'POST', path: '/sites/:token/checkout/process', summary: 'POST /sites/{token}/checkout/process' },
751
+ orders: { method: 'GET', path: '/sites/:token/orders/:number', summary: 'GET /sites/{token}/orders/{number}' },
752
+ 'blog-articles': { method: 'GET', path: '/sites/:token/blog-articles', summary: 'GET /sites/{token}/blog-articles' },
753
+ 'create-contact-submissions': { method: 'POST', path: '/sites/:token/contact-submissions', summary: 'POST /sites/{token}/contact-submissions' },
754
+ },
755
+ },
756
+ 'travel-flights': {
757
+ summary: 'travel flights (v1)',
758
+ commands: {
759
+ get: { method: 'GET', path: '/travel/flights/:flight', summary: 'GET /travel/flights/{flight} [scope: travel:flights:read]' },
760
+ },
761
+ },
762
+ 'travel-stays': {
763
+ summary: 'travel stays (v1)',
764
+ commands: {
765
+ get: { method: 'GET', path: '/travel/stays/:stay', summary: 'GET /travel/stays/{stay} [scope: travel:stays:read]' },
766
+ },
767
+ },
768
+ 'travel-cars': {
769
+ summary: 'travel cars (v1)',
770
+ commands: {
771
+ get: { method: 'GET', path: '/travel/cars/:car', summary: 'GET /travel/cars/{car} [scope: travel:cars:read]' },
772
+ },
773
+ },
181
774
  };