@getxflow/cli 0.12.0 → 0.13.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/dist/api.js +1 -1
- package/dist/bin.js +5 -1
- package/dist/commands/connections.js +134 -0
- package/dist/commands/skills.js +29 -5
- package/dist/flags.js +1 -0
- package/dist/help.js +37 -1
- package/dist/json-arg.js +23 -13
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +31 -1
package/dist/api.js
CHANGED
|
@@ -94,7 +94,7 @@ async function send(client, path, options = {}) {
|
|
|
94
94
|
}
|
|
95
95
|
catch (e) {
|
|
96
96
|
const reason = e instanceof Error ? e.message : String(e);
|
|
97
|
-
throw new ApiError(`The platform is unreachable: ${reason}`, 'network', 0, `Check the connection and the address ${client.apiUrl}`);
|
|
97
|
+
throw new ApiError(`The platform is unreachable: ${reason}`, 'network', 0, `Check the connection and the address ${client.apiUrl}. Inside an agent sandbox the network is usually switched off: then the command belongs to a person in their own terminal`);
|
|
98
98
|
}
|
|
99
99
|
assertProtocol(response);
|
|
100
100
|
return response;
|
package/dist/bin.js
CHANGED
|
@@ -133,11 +133,15 @@ async function run(args) {
|
|
|
133
133
|
await (0, connections_1.connectionsUnlink)(rest);
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
|
+
if (second === 'call') {
|
|
137
|
+
await (0, connections_1.connectionsCall)(rest);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
136
140
|
if (second === undefined || second === 'list') {
|
|
137
141
|
await (0, connections_1.connectionsList)(rest);
|
|
138
142
|
return;
|
|
139
143
|
}
|
|
140
|
-
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link and
|
|
144
|
+
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link, unlink and call');
|
|
141
145
|
case 'schedules':
|
|
142
146
|
if (second === 'set') {
|
|
143
147
|
await (0, schedules_1.schedulesSet)(rest);
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.connectionsList = connectionsList;
|
|
4
4
|
exports.connectionsLink = connectionsLink;
|
|
5
5
|
exports.connectionsUnlink = connectionsUnlink;
|
|
6
|
+
exports.connectionsCall = connectionsCall;
|
|
6
7
|
const api_1 = require("../api");
|
|
7
8
|
const args_1 = require("../args");
|
|
8
9
|
const config_1 = require("../config");
|
|
9
10
|
const errors_1 = require("../errors");
|
|
11
|
+
const json_arg_1 = require("../json-arg");
|
|
10
12
|
const session_1 = require("../session");
|
|
11
13
|
const ui_1 = require("../ui");
|
|
12
14
|
const env_1 = require("./env");
|
|
@@ -135,3 +137,135 @@ async function connectionsUnlink(args) {
|
|
|
135
137
|
(0, ui_1.ok)(`${(0, ui_1.bold)(row.label)} unlinked, ${result.alias} is gone`);
|
|
136
138
|
(0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the values until their next deploy'));
|
|
137
139
|
}
|
|
140
|
+
/** Budget for the shown body. Enough to read the shape of an answer, cheap in context. */
|
|
141
|
+
const SHOWN_CHARS = 8000;
|
|
142
|
+
/**
|
|
143
|
+
* Head of the body, cut on a line boundary so JSON is never sliced mid-structure. Only the
|
|
144
|
+
* head is kept: a stitched head and tail would read as one answer and invite conclusions the
|
|
145
|
+
* data does not support.
|
|
146
|
+
*/
|
|
147
|
+
function head(text) {
|
|
148
|
+
if (text.length <= SHOWN_CHARS)
|
|
149
|
+
return { text, cut: false };
|
|
150
|
+
const kept = [];
|
|
151
|
+
let used = 0;
|
|
152
|
+
for (const line of text.split('\n')) {
|
|
153
|
+
if (used + line.length + 1 > SHOWN_CHARS)
|
|
154
|
+
break;
|
|
155
|
+
kept.push(line);
|
|
156
|
+
used += line.length + 1;
|
|
157
|
+
}
|
|
158
|
+
// One minified line longer than the whole budget: cut it by characters, there is no
|
|
159
|
+
// boundary to respect.
|
|
160
|
+
if (kept.length === 0)
|
|
161
|
+
return { text: text.slice(0, SHOWN_CHARS), cut: true };
|
|
162
|
+
return { text: kept.join('\n'), cut: true };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Response headers worth a line each: rate limits, the type, and where a redirect points.
|
|
166
|
+
* A CDN adds two dozen more, and on every call that is noise.
|
|
167
|
+
*
|
|
168
|
+
* Request headers are not filtered: there are a few, they are yours, and seeing them is how
|
|
169
|
+
* a `{NAME}` the connection does not have shows itself.
|
|
170
|
+
*/
|
|
171
|
+
function shownHeaders(headers) {
|
|
172
|
+
const wanted = (name) => name === 'content-type' || name === 'retry-after' || name === 'location' || name.startsWith('x-ratelimit');
|
|
173
|
+
return Object.entries(headers)
|
|
174
|
+
.filter(([name]) => wanted(name.toLowerCase()))
|
|
175
|
+
.map(([name, value]) => `${name}: ${value}`);
|
|
176
|
+
}
|
|
177
|
+
function allHeaders(headers) {
|
|
178
|
+
return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
179
|
+
}
|
|
180
|
+
/** Headers typed as JSON. Values may carry {NAME} placeholders, the platform fills those in. */
|
|
181
|
+
function headerArg(args) {
|
|
182
|
+
const raw = (0, args_1.flagString)(args, 'header');
|
|
183
|
+
if (raw === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const problem = (0, json_arg_1.jsonProblem)(raw);
|
|
186
|
+
if (problem)
|
|
187
|
+
throw new errors_1.CliError(`--header is not valid JSON: ${raw}`, (0, json_arg_1.jsonHint)('header', raw));
|
|
188
|
+
const parsed = JSON.parse(raw);
|
|
189
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
190
|
+
throw new errors_1.CliError('--header takes an object', 'For example: --header \'{"Client-Id":"{CLIENT_ID}"}\'');
|
|
191
|
+
}
|
|
192
|
+
const headers = {};
|
|
193
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
194
|
+
if (typeof value !== 'string') {
|
|
195
|
+
throw new errors_1.CliError(`The value of ${name} has to be a string`, 'Header values are text');
|
|
196
|
+
}
|
|
197
|
+
headers[name] = value;
|
|
198
|
+
}
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
|
201
|
+
/** Is the body meant to be JSON? A form content-type says it is not, and then it goes as typed. */
|
|
202
|
+
function looksJson(headers) {
|
|
203
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
204
|
+
if (name.toLowerCase() === 'content-type')
|
|
205
|
+
return /json/i.test(value);
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* One-off call to the API behind a connected account, with the platform attaching the
|
|
211
|
+
* credentials. For looking at what an API returns without deploying a function for it.
|
|
212
|
+
*/
|
|
213
|
+
async function connectionsCall(args) {
|
|
214
|
+
const alias = args.words[1];
|
|
215
|
+
const path = args.words[2];
|
|
216
|
+
if (!alias) {
|
|
217
|
+
throw new errors_1.CliError('A connection is required', 'Its alias in this project: xflow connections');
|
|
218
|
+
}
|
|
219
|
+
if (!path) {
|
|
220
|
+
throw new errors_1.CliError('A path is required', 'For example: xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01');
|
|
221
|
+
}
|
|
222
|
+
const headers = headerArg(args);
|
|
223
|
+
const body = (0, json_arg_1.bodyArg)(args, 'data');
|
|
224
|
+
// Judged before anything goes over the network, the same as in functions invoke: a broken
|
|
225
|
+
// body is the caller's own line. A form body is not judged at all, it is text by design.
|
|
226
|
+
if (body && looksJson(headers)) {
|
|
227
|
+
const problem = (0, json_arg_1.jsonProblem)(body.text);
|
|
228
|
+
if (problem) {
|
|
229
|
+
throw body.fromFile
|
|
230
|
+
? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
|
|
231
|
+
: new errors_1.CliError(`--data is not valid JSON: ${body.text}`, (0, json_arg_1.jsonHint)('data', body.text));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
235
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/connections/call`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: {
|
|
238
|
+
connection: alias,
|
|
239
|
+
path,
|
|
240
|
+
method: (0, args_1.flagString)(args, 'method') ?? (body ? 'POST' : 'GET'),
|
|
241
|
+
...(body ? { body: body.text } : {}),
|
|
242
|
+
...(headers ? { headers } : {}),
|
|
243
|
+
},
|
|
244
|
+
// Past the platform's own cap on the call, so a slow API reads as a slow API and not as
|
|
245
|
+
// an unreachable platform.
|
|
246
|
+
timeoutMs: 60_000,
|
|
247
|
+
});
|
|
248
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
249
|
+
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const { request, response } = data;
|
|
253
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
|
|
254
|
+
for (const line of allHeaders(request.headers))
|
|
255
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${line}`));
|
|
256
|
+
const size = response.truncated ? `at least ${(0, ui_1.formatBytes)(response.bytes)}` : (0, ui_1.formatBytes)(response.bytes);
|
|
257
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${response.status} in ${data.elapsed_ms} ms, ${size}`));
|
|
258
|
+
for (const line of shownHeaders(response.headers))
|
|
259
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${line}`));
|
|
260
|
+
const text = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
|
|
261
|
+
const shown = head(text);
|
|
262
|
+
if (shown.text)
|
|
263
|
+
(0, ui_1.out)(shown.text);
|
|
264
|
+
if (shown.cut || response.truncated) {
|
|
265
|
+
(0, ui_1.note)((0, ui_1.dim)(' This is the head of the answer, not all of it. Narrow the request with the API own ' +
|
|
266
|
+
'parameters (a limit, a shorter period), or process the whole set in a cloud function'));
|
|
267
|
+
}
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
throw new errors_1.CliError(`The API answered ${response.status}`, 'The answer above is theirs, not the platform own');
|
|
270
|
+
}
|
|
271
|
+
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -73,13 +73,29 @@ function skillPath(base, agent, global) {
|
|
|
73
73
|
function installed(base, agent, global) {
|
|
74
74
|
return (0, node_fs_1.existsSync)(skillPath(base, agent, global));
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* A refused write turned into an answer. Agent sandboxes keep `.agents`, `.codex` and
|
|
78
|
+
* `.git` read-only precisely so that an agent cannot rewrite its own instructions, so
|
|
79
|
+
* this is the ordinary outcome of an agent running the command, not a broken machine.
|
|
80
|
+
*/
|
|
81
|
+
function writeRefused(path, e) {
|
|
82
|
+
const code = e instanceof Error ? e.code : undefined;
|
|
83
|
+
if (code !== 'EACCES' && code !== 'EPERM' && code !== 'EROFS')
|
|
84
|
+
return e;
|
|
85
|
+
return new errors_1.CliError(`No permission to write ${path}`, 'An agent sandbox usually keeps .agents, .codex and .git read-only. Ask the person to run this command in their own terminal');
|
|
86
|
+
}
|
|
76
87
|
/** Writes only when the content differs. */
|
|
77
88
|
function writeIfChanged(path, content) {
|
|
78
89
|
const before = (0, node_fs_1.existsSync)(path) ? (0, node_fs_1.readFileSync)(path, 'utf-8') : null;
|
|
79
90
|
if (before === content)
|
|
80
91
|
return null;
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
try {
|
|
93
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
94
|
+
(0, node_fs_1.writeFileSync)(path, content, 'utf-8');
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
throw writeRefused(path, e);
|
|
98
|
+
}
|
|
83
99
|
return before === null ? 'new' : 'updated';
|
|
84
100
|
}
|
|
85
101
|
/**
|
|
@@ -118,7 +134,12 @@ function writePointer(base, create) {
|
|
|
118
134
|
}
|
|
119
135
|
if (before === next)
|
|
120
136
|
return null;
|
|
121
|
-
|
|
137
|
+
try {
|
|
138
|
+
(0, node_fs_1.writeFileSync)(path, next, 'utf-8');
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
throw writeRefused(path, e);
|
|
142
|
+
}
|
|
122
143
|
return { path, state: before === null ? 'new' : 'updated' };
|
|
123
144
|
}
|
|
124
145
|
function install(base, ids, global) {
|
|
@@ -268,8 +289,11 @@ function installSkillQuietly(base) {
|
|
|
268
289
|
}
|
|
269
290
|
return true;
|
|
270
291
|
}
|
|
271
|
-
catch {
|
|
272
|
-
|
|
292
|
+
catch (e) {
|
|
293
|
+
// The reason travels with the failure: a refused write is fixed by a person running
|
|
294
|
+
// the command, and "try xflow skills" alone sends the agent round the same wall.
|
|
295
|
+
(0, ui_1.note)((0, ui_1.dim)(` Could not lay out the AI agent instructions: ${e instanceof Error ? e.message : e}`));
|
|
296
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${e instanceof errors_1.CliError && e.hint ? e.hint : 'Try again: xflow skills'}`));
|
|
273
297
|
return false;
|
|
274
298
|
}
|
|
275
299
|
}
|
package/dist/flags.js
CHANGED
|
@@ -61,6 +61,7 @@ exports.KNOWN = {
|
|
|
61
61
|
'connections list': ['project'],
|
|
62
62
|
'connections link': ['as', 'project'],
|
|
63
63
|
'connections unlink': ['force'],
|
|
64
|
+
'connections call': ['method', 'data', 'data-file', 'header', 'json', 'project'],
|
|
64
65
|
schedules: ['project'],
|
|
65
66
|
'schedules list': ['project'],
|
|
66
67
|
'schedules set': ['payload', 'payload-file', 'project'],
|
package/dist/help.js
CHANGED
|
@@ -367,6 +367,8 @@ the platform into the function.
|
|
|
367
367
|
xflow connections link <name> --as ALIAS
|
|
368
368
|
hand its credentials to the functions
|
|
369
369
|
xflow connections unlink <ALIAS> take them away again
|
|
370
|
+
xflow connections call <ALIAS> <path>
|
|
371
|
+
ask that API something, once, without deploying
|
|
370
372
|
|
|
371
373
|
Every row says whether the connection is linked to this project (its alias) and what
|
|
372
374
|
state the access is in. ${(0, ui_1.bold)('available, not linked')} is the useful one: the account
|
|
@@ -394,7 +396,41 @@ not to look for another route. Either way only the accounts granted to you perso
|
|
|
394
396
|
be linked at all: that rule holds whatever the key is allowed to do.
|
|
395
397
|
|
|
396
398
|
Connecting a new account and switching one off stay with a person, in the platform
|
|
397
|
-
interface. There is no command for either
|
|
399
|
+
interface. There is no command for either.
|
|
400
|
+
|
|
401
|
+
${(0, ui_1.bold)('call')} answers the question an API only answers by being asked: what does this
|
|
402
|
+
endpoint actually return, and is the field you need in it. The platform attaches the
|
|
403
|
+
credentials of the connected account and makes the request itself, so nothing has to be
|
|
404
|
+
deployed to find out, and the key never reaches you.
|
|
405
|
+
|
|
406
|
+
xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
|
|
407
|
+
xflow connections call AMO /api/v4/leads --method POST --data '{"name":"Test"}'
|
|
408
|
+
xflow connections call OZON /v1/product/list --method POST --data '{}' \\
|
|
409
|
+
--header '{"Client-Id":"{CLIENT_ID}"}'
|
|
410
|
+
|
|
411
|
+
The path is written after the address of the connector, query string and all, or given in
|
|
412
|
+
full as ${(0, ui_1.bold)('https://...')}. Only the hosts of that connector are reachable, and a
|
|
413
|
+
refusal lists them. ${(0, ui_1.bold)('--method')} defaults to GET, or to POST when there is a body.
|
|
414
|
+
|
|
415
|
+
${(0, ui_1.bold)('{NAME}')} in the path or in a header value is filled in by the platform from the
|
|
416
|
+
fields of the connection. That is how the two-header APIs are called: Ozon wants
|
|
417
|
+
${(0, ui_1.bold)('Client-Id')} beside its key, Yandex Direct wants ${(0, ui_1.bold)('Client-Login')} for an
|
|
418
|
+
agency account, and you never see those values. The names are the ones ${(0, ui_1.bold)('xflow env')}
|
|
419
|
+
shows without the alias in front: ${(0, ui_1.bold)('OZON_CLIENT_ID')} is written ${(0, ui_1.bold)('{CLIENT_ID}')}.
|
|
420
|
+
A name the connection does not have is left in the text as you typed it, so a typo shows up
|
|
421
|
+
in the printed request instead of failing somewhere far away.
|
|
422
|
+
|
|
423
|
+
The answer is printed head first, at most a few kilobytes, and it says so when there is
|
|
424
|
+
more. Ask for the shape, not the volume: one day and ${(0, ui_1.bold)('limit=1')} tells you what the
|
|
425
|
+
fields are called, while the whole dataset belongs in a cloud function. ${(0, ui_1.bold)('--json')}
|
|
426
|
+
prints the envelope for a pipe: ${(0, ui_1.bold)("--json | jq '.response.body | keys'")} gives the
|
|
427
|
+
field names without the body ever entering your context.
|
|
428
|
+
|
|
429
|
+
Only an account already linked to this project can be called, because its credentials
|
|
430
|
+
already reach the functions of that project: the command saves a deploy, it does not widen
|
|
431
|
+
what you may touch. An account that is merely available has to be linked first, and that is
|
|
432
|
+
a separate right and a personal grant. The call also goes out for real: ${(0, ui_1.bold)('POST')} and
|
|
433
|
+
${(0, ui_1.bold)('DELETE')} change things in somebody's live account, and no flag will undo that.`,
|
|
398
434
|
schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
|
|
399
435
|
|
|
400
436
|
A schedule is a Yandex timer trigger: it calls the function itself, with no
|
package/dist/json-arg.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonProblem = jsonProblem;
|
|
4
4
|
exports.jsonHint = jsonHint;
|
|
5
|
+
exports.bodyArg = bodyArg;
|
|
5
6
|
exports.jsonArg = jsonArg;
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
7
8
|
const args_1 = require("./args");
|
|
@@ -39,8 +40,13 @@ function jsonHint(flag, raw) {
|
|
|
39
40
|
}
|
|
40
41
|
return `A single JSON value is expected, or keep the body in a file: ${file}`;
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The body from --<flag> or --<flag>-file, whatever it is.
|
|
45
|
+
*
|
|
46
|
+
* Not every body is JSON: a connector call carrying a form body sends the text as typed, and
|
|
47
|
+
* judging it as JSON there would refuse a correct request.
|
|
48
|
+
*/
|
|
49
|
+
function bodyArg(args, flag) {
|
|
44
50
|
const fileFlag = `${flag}-file`;
|
|
45
51
|
const inline = args.flags[flag];
|
|
46
52
|
const path = (0, args_1.flagString)(args, fileFlag);
|
|
@@ -49,21 +55,25 @@ function jsonArg(args, flag) {
|
|
|
49
55
|
}
|
|
50
56
|
if (path !== undefined) {
|
|
51
57
|
if (!(0, node_fs_1.existsSync)(path)) {
|
|
52
|
-
throw new errors_1.CliError(`No file ${path}`, `--${fileFlag} takes the path to a file with
|
|
58
|
+
throw new errors_1.CliError(`No file ${path}`, `--${fileFlag} takes the path to a file with the body`);
|
|
53
59
|
}
|
|
54
|
-
|
|
55
|
-
const problem = jsonProblem(text);
|
|
56
|
-
if (problem)
|
|
57
|
-
throw new errors_1.CliError(`${path} is not valid JSON: ${problem}`, 'The file has to hold one JSON value');
|
|
58
|
-
return text;
|
|
60
|
+
return { text: (0, node_fs_1.readFileSync)(path, 'utf-8').trim(), fromFile: path };
|
|
59
61
|
}
|
|
60
62
|
if (inline === undefined)
|
|
61
63
|
return undefined;
|
|
62
64
|
if (inline === true)
|
|
63
65
|
throw new errors_1.CliError(`--${flag} needs a value`, `For example: --${flag} '{"mode":"full"}'`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
return { text: String(inline).trim(), fromFile: null };
|
|
67
|
+
}
|
|
68
|
+
/** The body from --<flag> or --<flag>-file, already known to be JSON. */
|
|
69
|
+
function jsonArg(args, flag) {
|
|
70
|
+
const body = bodyArg(args, flag);
|
|
71
|
+
if (body === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const problem = jsonProblem(body.text);
|
|
74
|
+
if (!problem)
|
|
75
|
+
return body.text;
|
|
76
|
+
throw body.fromFile
|
|
77
|
+
? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
|
|
78
|
+
: new errors_1.CliError(`--${flag} is not valid JSON: ${body.text}`, jsonHint(flag, body.text));
|
|
69
79
|
}
|
package/dist/version.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
|
|
4
4
|
/** Keep in sync with cli/package.json. */
|
|
5
|
-
exports.CLI_VERSION = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.13.0';
|
|
6
6
|
/** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
|
|
7
7
|
exports.DEFAULT_API_URL = 'https://app.getxflow.com';
|
package/package.json
CHANGED
package/skills/xflow/SKILL.md
CHANGED
|
@@ -24,7 +24,7 @@ contains the fix.
|
|
|
24
24
|
|
|
25
25
|
## Keeping these instructions current
|
|
26
26
|
|
|
27
|
-
These instructions ship with xflow CLI 0.
|
|
27
|
+
These instructions ship with xflow CLI 0.13.0. They travel inside the package, so the copy
|
|
28
28
|
you are reading can be older than the CLI answering your commands, and nothing about that
|
|
29
29
|
is visible in the text itself.
|
|
30
30
|
|
|
@@ -368,6 +368,36 @@ owner of the key personally can be linked at all. If the right was taken away, s
|
|
|
368
368
|
ask the person to turn it back on in the platform settings under Developers: a key cannot
|
|
369
369
|
grant it to itself. Connecting a new account and switching one off stay with a person too.
|
|
370
370
|
|
|
371
|
+
### Asking an API what it returns
|
|
372
|
+
|
|
373
|
+
Before writing a function against an unfamiliar API, find out what it actually answers:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
|
|
377
|
+
xflow connections call OZON /v1/product/list --method POST --data '{}' \
|
|
378
|
+
--header '{"Client-Id":"{CLIENT_ID}"}'
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The platform attaches the credentials and makes the request itself, so no deploy is needed
|
|
382
|
+
and the key never reaches you. Only an account already linked to this project can be called:
|
|
383
|
+
its credentials already reach that project's functions, so this saves a deploy rather than
|
|
384
|
+
widening what you may touch. An account that is only `available, not linked` has to be linked
|
|
385
|
+
first, and that stays a decision for a person.
|
|
386
|
+
|
|
387
|
+
`{NAME}` in the path or in a header value is filled in from the fields of the connection: the
|
|
388
|
+
names are what `xflow env` shows without the alias in front, so `OZON_CLIENT_ID` is written
|
|
389
|
+
`{CLIENT_ID}`. That is how APIs wanting a second header are called, and you never see the
|
|
390
|
+
value. Only the hosts of that connector are reachable and a refusal lists them.
|
|
391
|
+
|
|
392
|
+
**Ask for the shape, not the volume.** One day and `limit=1` is enough to learn what the
|
|
393
|
+
fields are called; the answer is printed head first and says when there is more. The whole
|
|
394
|
+
dataset belongs in a cloud function, which is where you were going anyway. `--json | jq
|
|
395
|
+
'.response.body | keys'` gives the field names without the body entering your context at all.
|
|
396
|
+
|
|
397
|
+
The call goes out for real, against a live account: `POST` and `DELETE` change things there,
|
|
398
|
+
and nothing undoes that. Read the method of the endpoint before calling it, and ask the
|
|
399
|
+
person first when the endpoint is not clearly a read.
|
|
400
|
+
|
|
371
401
|
## Schedules
|
|
372
402
|
|
|
373
403
|
`xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
|