@getxflow/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,6 @@ else's code belongs next to it.
31
31
  | `logs` | browser errors from the released application |
32
32
  | `projects list` / `projects get` | projects of the organization and the application addresses |
33
33
  | `skills` | platform instructions for an AI agent ([Agent Skills](https://agentskills.io) format) |
34
- | `mcp install` | connect the agent to the platform directly, without a terminal |
35
34
  | `update` | update the CLI itself, and the skill that ships inside it |
36
35
 
37
36
  Help for one command: `xflow help <command>`.
@@ -42,9 +41,5 @@ OpenClaw pick up on their own when a task touches XFlow. Your `AGENTS.md` and `C
42
41
  left alone. The description ships inside the package and shares its version, so `xflow update`
43
42
  brings both.
44
43
 
45
- `xflow mcp install` connects the agent to the platform directly: the database, migrations,
46
- functions, schedules, variables and versions become its tools, with no command output to parse.
47
- Code, builds and releases stay in the CLI.
48
-
49
44
  The key is stored in `~/.xflow/credentials.json`, the project configuration in `xflow.json`.
50
45
  For CI, pass the key in the `XFLOW_TOKEN` variable.
package/dist/args.js CHANGED
@@ -17,12 +17,10 @@ exports.BOOLEAN_FLAGS = new Set([
17
17
  'version',
18
18
  'global',
19
19
  'refresh',
20
- 'no-push',
21
20
  'all',
22
21
  'dry-run',
23
22
  'allow-destructive',
24
23
  'allow-removals',
25
- 'show-token',
26
24
  ]);
27
25
  function parseArgs(argv) {
28
26
  const words = [];
package/dist/bin.js CHANGED
@@ -20,7 +20,6 @@ const db_1 = require("./commands/db");
20
20
  const env_1 = require("./commands/env");
21
21
  const functions_1 = require("./commands/functions");
22
22
  const logs_1 = require("./commands/logs");
23
- const mcp_1 = require("./commands/mcp");
24
23
  const org_1 = require("./commands/org");
25
24
  const schedules_1 = require("./commands/schedules");
26
25
  const skills_1 = require("./commands/skills");
@@ -93,12 +92,6 @@ async function run(args) {
93
92
  case 'skills':
94
93
  await (0, skills_1.skills)(rest);
95
94
  return;
96
- case 'mcp':
97
- if (second === undefined || second === 'install') {
98
- await (0, mcp_1.mcpInstall)(rest);
99
- return;
100
- }
101
- throw new errors_1.CliError(`Unknown command: mcp ${second}`, 'Available: install');
102
95
  case 'functions':
103
96
  if (second === 'invoke') {
104
97
  await (0, functions_1.functionsInvoke)(rest);
@@ -143,10 +143,10 @@ async function connectionsUnlink(args) {
143
143
  const inUse = row.env
144
144
  .map((name) => ({ name, users: needed.get(name) ?? [] }))
145
145
  .filter((entry) => entry.users.length > 0);
146
- if (inUse.length > 0 && !(0, args_1.flagBool)(args, 'force')) {
146
+ if (inUse.length > 0 && !(0, args_1.flagBool)(args, 'yes')) {
147
147
  (0, ui_1.fail)(`The functions of this project read the variables of ${row.alias}:`);
148
148
  (0, ui_1.table)(inUse.map((entry) => [entry.name, entry.users.join(', ')]));
149
- throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --force if that is intended');
149
+ throw new errors_1.CliError('They would lose these variables on the next deploy', 'Repeat with --yes if that is intended');
150
150
  }
151
151
  }
152
152
  const result = await (0, api_1.apiJson)(client, `${endpoint(projectId)}?connection_id=${encodeURIComponent(row.id)}`, { method: 'DELETE' });
@@ -296,19 +296,21 @@ async function dbMove(args) {
296
296
  const { projectId, client } = await (0, session_1.projectTarget)(args);
297
297
  const path = `/api/v1/projects/${projectId}/db/move`;
298
298
  let place;
299
+ // The platform takes the project name as the yes; --yes looks it up.
300
+ const confirm = (0, args_1.flagBool)(args, 'yes')
301
+ ? (await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}`)).name
302
+ : undefined;
299
303
  try {
300
304
  place = await (0, api_1.apiJson)(client, path, {
301
305
  method: 'POST',
302
- body: { to, confirm: (0, args_1.flagString)(args, 'confirm') },
306
+ body: { to, confirm },
303
307
  timeoutMs: 60_000,
304
308
  });
305
309
  }
306
310
  catch (e) {
307
311
  // Said in the words of this command: the flag, and the name that goes in it.
308
312
  if (e instanceof api_1.ApiError && e.code === 'confirm_required') {
309
- const found = await projectDatabase(client, projectId).catch(() => null);
310
- const name = found?.database?.name ?? '<project name>';
311
- throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --confirm "${name}"`);
313
+ throw new errors_1.CliError(e.message, `To move it: xflow db move --to "${to}" --yes`);
312
314
  }
313
315
  throw e;
314
316
  }
@@ -10,7 +10,6 @@ const api_1 = require("../api");
10
10
  const args_1 = require("../args");
11
11
  const config_1 = require("../config");
12
12
  const errors_1 = require("../errors");
13
- const prompt_1 = require("../prompt");
14
13
  const session_1 = require("../session");
15
14
  const tree_1 = require("../tree");
16
15
  const ui_1 = require("../ui");
@@ -49,9 +48,8 @@ async function waitForBuild(client, base, started) {
49
48
  throw new errors_1.CliError('The build is taking longer than expected', `To check the state: xflow deployments. Build ${started.deploy_id}`);
50
49
  }
51
50
  /**
52
- * Start the build, asking before anything is destroyed. Removing a cloud function
53
- * cannot be undone, so the platform refuses such a deploy until the caller says yes,
54
- * and the question is asked here, before it starts.
51
+ * Start the build. Removing a cloud function cannot be undone, so the platform
52
+ * refuses such a deploy until --allow-removals says yes.
55
53
  */
56
54
  async function startBuild(client, base, revision, allowRemovals, comment) {
57
55
  const path = `${base}/builds`;
@@ -63,22 +61,9 @@ async function startBuild(client, base, revision, allowRemovals, comment) {
63
61
  reportIssues(e);
64
62
  if (!(e instanceof api_1.ApiError) || e.code !== 'confirm_required')
65
63
  throw e;
66
- const removing = e.removing ?? [];
67
- if (removing.length === 0)
64
+ if (!e.removing?.length)
68
65
  throw e;
69
- // No terminal (CI, an agent): the flag is the only way to say yes.
70
- if (process.stdin.isTTY !== true || process.stderr.isTTY !== true) {
71
- throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. If it is not, bring the functions/<name>/index.ts directories back`);
72
- }
73
- (0, ui_1.warn)(`Gone from the sources, the deploy would remove them: ${removing.join(', ')}`);
74
- (0, ui_1.note)((0, ui_1.dim)(` ${REMOVAL_IS_FINAL}`));
75
- const answer = await (0, prompt_1.select)('Remove them from the cloud?', [
76
- { label: 'No, stop here', hint: 'bring the functions/<name>/index.ts directories back' },
77
- { label: 'Yes, remove and deploy', hint: removing.join(', ') },
78
- ]);
79
- if (answer !== 1)
80
- throw new errors_1.CliError('The deploy did not start', 'Nothing was removed');
81
- return (0, api_1.apiJson)(client, path, { method: 'POST', body: { revision, allow_removals: true, comment } });
66
+ throw new errors_1.CliError(e.message, `${REMOVAL_IS_FINAL}. If that is intended, retry with --allow-removals. If it is not, bring the functions/<name>/index.ts directories back`);
82
67
  }
83
68
  }
84
69
  /** Keep in sync with lib/version-comment.ts on the platform. */
@@ -187,19 +172,10 @@ async function deployTarget(target, args, comment, revision) {
187
172
  await (0, api_1.apiJson)(client, `${base}/builds?check=plan`);
188
173
  if (target.kind === 'app' && target.projectRoot)
189
174
  await warnUndeployedRoot(client, target.projectRoot, target.projectId);
190
- if ((0, args_1.flagBool)(args, 'no-push')) {
191
- const server = await (0, sources_1.latestRevision)(client, base);
192
- if (!server)
193
- throw new errors_1.CliError('The server holds no sources', 'Repeat without --no-push, so that the sources go up first');
194
- revision = server.revision;
195
- (0, ui_1.warn)(`Sources not sent (--no-push), the build runs from revision ${revision}`);
196
- }
197
- else {
198
- const stream = target.kind === 'app'
199
- ? { kind: 'app', root: target.root, appId: target.appId }
200
- : { kind: 'project', root: target.root, projectId: target.projectId };
201
- revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
202
- }
175
+ const stream = target.kind === 'app'
176
+ ? { kind: 'app', root: target.root, appId: target.appId }
177
+ : { kind: 'project', root: target.root, projectId: target.projectId };
178
+ revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
203
179
  if (target.kind === 'project')
204
180
  await (0, db_1.migrateBeforeDeploy)(client, target.root, target.projectId);
205
181
  }
@@ -24,7 +24,7 @@ async function functionsList(args) {
24
24
  fn.last_called_at ? `called ${(0, ui_1.formatAge)(fn.last_called_at)}` : 'never called',
25
25
  fn.last_error_at ? `failed ${(0, ui_1.formatAge)(fn.last_error_at)}` : '',
26
26
  ]));
27
- (0, ui_1.note)((0, ui_1.dim)(' callers: the app folders and page names allowed to call it (functions in the root xflow.json)'));
27
+ (0, ui_1.note)((0, ui_1.dim)(' callers: the app folders allowed to call it (functions in the root xflow.json)'));
28
28
  (0, ui_1.note)((0, ui_1.dim)(' open: a caller is public, so anyone on the internet can call it; no callers: a workflow'));
29
29
  (0, ui_1.note)((0, ui_1.dim)(` Webhooks and other functions call ${functions[0].url.replace(/\/[^/]+$/, '/<name>')} with a key`));
30
30
  }
@@ -9,7 +9,6 @@ const credentials_1 = require("../credentials");
9
9
  const errors_1 = require("../errors");
10
10
  const session_1 = require("../session");
11
11
  const ui_1 = require("../ui");
12
- const mcp_1 = require("./mcp");
13
12
  function label(org) {
14
13
  return org.name ?? org.organizationId;
15
14
  }
@@ -75,16 +74,10 @@ function orgSwitch(args) {
75
74
  (0, ui_1.ok)(`Active organization: "${label(target)}"`);
76
75
  const root = (0, config_1.findProjectRoot)();
77
76
  if (root) {
78
- // The agent entry of a project folder belongs to the folder's organization,
79
- // so the switch leaves it alone.
80
77
  const boundId = (0, config_1.readState)(root).organizationId;
81
78
  if (boundId && boundId !== target.organizationId) {
82
79
  const bound = orgs.find((org) => org.organizationId === boundId);
83
80
  (0, ui_1.note)((0, ui_1.dim)(` This folder is bound to "${bound ? label(bound) : boundId}": commands here stay in it`));
84
81
  }
85
- return;
86
- }
87
- if ((0, mcp_1.refreshAgentKey)({ apiUrl, token: target.token })) {
88
- (0, ui_1.note)((0, ui_1.dim)(' The agent key here is rewritten: restart the agent session to pick it up'));
89
82
  }
90
83
  }
@@ -99,7 +99,6 @@ async function pagesCreate(args) {
99
99
  (0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
100
100
  (0, ui_1.out)(result.page.url);
101
101
  (0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The next version: xflow pages deploy ${result.page.slug} <path>`));
102
- (0, ui_1.note)((0, ui_1.dim)(` A backend for it: name "${result.page.slug}" in functions.<name>.callers of a project, xflow deploy functions, then fetch /_fn/<name>`));
103
102
  }
104
103
  function contentTypeOf(upload) {
105
104
  return upload.kind === 'zip' ? 'application/zip' : 'text/html';
@@ -13,7 +13,6 @@ const node_os_1 = require("node:os");
13
13
  const node_path_1 = require("node:path");
14
14
  const args_1 = require("../args");
15
15
  const errors_1 = require("../errors");
16
- const prompt_1 = require("../prompt");
17
16
  const update_1 = require("./update");
18
17
  const ui_1 = require("../ui");
19
18
  const AGENTS = [
@@ -322,30 +321,6 @@ function listAgents(base) {
322
321
  (0, ui_1.note)((0, ui_1.dim)(' * installed. Columns: id, agent, in the project, in the home folder'));
323
322
  (0, ui_1.note)((0, ui_1.dim)(' Install without questions: xflow skills --agent <id,id> [--global]'));
324
323
  }
325
- async function pick(globalFlag) {
326
- let global = globalFlag;
327
- if (!globalFlag) {
328
- const scope = await (0, prompt_1.select)('Where to install the xflow skill', [
329
- { label: 'This project', hint: process.cwd() },
330
- { label: 'Globally', hint: 'home folder, visible to every project' },
331
- ]);
332
- if (scope === null)
333
- return null;
334
- global = scope === 1;
335
- }
336
- const base = process.cwd();
337
- const present = AGENTS.map((agent) => installed(base, agent, global));
338
- const anyPresent = present.includes(true);
339
- const defaults = global ? DEFAULT_GLOBAL : DEFAULT_PROJECT;
340
- const picked = await (0, prompt_1.multiSelect)('Which agents get the skill', AGENTS.map((agent, i) => ({
341
- label: agent.label,
342
- hint: pathLabel(agent, global) + (present[i] ? ', installed' : ''),
343
- checked: anyPresent ? present[i] : defaults.includes(agent.id),
344
- })));
345
- if (picked === null || picked.length === 0)
346
- return null;
347
- return { ids: picked.map((i) => AGENTS[i].id), global };
348
- }
349
324
  async function skills(args) {
350
325
  const sub = args.words[0];
351
326
  if (sub === 'list') {
@@ -365,7 +340,9 @@ async function skills(args) {
365
340
  if (sub !== undefined) {
366
341
  throw new errors_1.CliError(`Unknown command: skills ${sub}`, 'Available: xflow skills [list]');
367
342
  }
368
- let global = (0, args_1.flagBool)(args, 'global');
343
+ // --yes stays in older install lines; there is nothing left to say yes to.
344
+ (0, args_1.flagBool)(args, 'yes');
345
+ const global = (0, args_1.flagBool)(args, 'global');
369
346
  const agentFlag = (0, args_1.flagString)(args, 'agent');
370
347
  if (args.flags.agent !== undefined && agentFlag === undefined) {
371
348
  throw new errors_1.CliError('The --agent flag needs a value', 'For example: --agent claude,cursor. The ids: xflow skills list');
@@ -382,15 +359,6 @@ async function skills(args) {
382
359
  throw new errors_1.CliError(unknown.length > 0 ? `Unknown agent: ${unknown.join(', ')}` : 'No agents named', 'The ids: xflow skills list');
383
360
  }
384
361
  }
385
- else if (process.stdin.isTTY === true && process.stderr.isTTY === true && !(0, args_1.flagBool)(args, 'yes')) {
386
- const picked = await pick(global);
387
- if (picked === null) {
388
- (0, ui_1.note)((0, ui_1.dim)(' nothing chosen, nothing changed'));
389
- return;
390
- }
391
- ids = picked.ids;
392
- global = picked.global;
393
- }
394
362
  else {
395
363
  ids = defaultIds(process.cwd(), global);
396
364
  }
@@ -22,6 +22,8 @@ const zip_1 = require("../zip");
22
22
  const ui_1 = require("../ui");
23
23
  /** Server cap, checked locally to fail before uploading. Keep in sync with the server. */
24
24
  const MAX_ARCHIVE_BYTES = 10 * 1024 * 1024;
25
+ /** Keep in sync with KEEP_REVISIONS in lib/project-sources/revisions.ts. */
26
+ const KEPT_REVISIONS = 10;
25
27
  /** Where a stream lives on the platform: the same routes under two prefixes. */
26
28
  function streamPath(target) {
27
29
  return target.kind === 'app' ? `/api/v1/apps/${target.appId}` : `/api/v1/projects/${target.projectId}`;
@@ -68,34 +70,9 @@ async function latestRevision(client, base) {
68
70
  const { revisions } = await (0, api_1.apiJson)(client, `${base}/revisions`);
69
71
  return revisions[0] ?? null;
70
72
  }
71
- /** Show what --force would overwrite and ask for confirmation by the name of the project or app. */
72
- async function confirmForce(client, base, server, local) {
73
- const { name } = await (0, api_1.apiJson)(client, base);
74
- (0, ui_1.note)('');
75
- (0, ui_1.warn)(`The server holds revision ${server.revision} from ${(0, ui_1.formatAge)(server.created_at)}, ${server.file_count} files`);
76
- try {
77
- const info = await (0, api_1.apiJson)(client, `${base}/pull`);
78
- const serverPaths = new Set((0, zip_1.dropDirectoryEntries)((0, zip_1.zipRead)(await (0, api_1.downloadUrl)(info.download_url))).map((e) => e.path));
79
- const localPaths = new Set(local.map((f) => f.path));
80
- const disappearing = [...serverPaths].filter((p) => !localPaths.has(p));
81
- if (disappearing.length > 0) {
82
- (0, ui_1.note)(` Files that will disappear (${disappearing.length}):`);
83
- for (const path of disappearing.slice(0, 20))
84
- (0, ui_1.note)((0, ui_1.dim)(` ${path}`));
85
- if (disappearing.length > 20)
86
- (0, ui_1.note)((0, ui_1.dim)(` …and ${disappearing.length - 20} more`));
87
- }
88
- else {
89
- (0, ui_1.note)((0, ui_1.dim)(' No files disappear, but the contents of the server revision are replaced by yours'));
90
- }
91
- }
92
- catch {
93
- (0, ui_1.warn)('Could not read the server copy: the list of disappearing files is unavailable');
94
- }
95
- (0, ui_1.note)('');
96
- const confirmed = await (0, ui_1.confirmWord)('The server revision will be replaced by the contents of this folder. Anything newer on the platform goes away, and it cannot be recovered from here.', name);
97
- if (!confirmed)
98
- throw new errors_1.CliError('Cancelled');
73
+ /** How the stream is named on the command line: `xflow pull functions`, `xflow deploy apps/shop`. */
74
+ function streamWord(stream) {
75
+ return stream.kind === 'app' ? `${config_1.APPS_DIR}/${(0, node_path_1.basename)(stream.root)}` : 'functions';
99
76
  }
100
77
  /** The first step of deploy: sources of one stream go up as a revision before the build. */
101
78
  async function pushSources(stream, client, options) {
@@ -113,12 +90,10 @@ async function pushSources(stream, client, options) {
113
90
  // Asked before the upload, so a doomed archive never leaves the machine.
114
91
  if (relation === 'diverged' && !options.force) {
115
92
  throw new errors_1.CliError(`The server holds revision ${server.revision}, and this folder does not stand on it`, 'Someone deployed before you, or this folder worked on a different project. ' +
116
- 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy');
93
+ 'Fetch the server copy alongside and compare: xflow pull --into ./server-copy. ' +
94
+ `To build what the server holds as it is: xflow deploy ${streamWord(stream)} --revision ${server.revision} -m "..."`);
117
95
  }
118
- if (options.force) {
119
- await confirmForce(client, base, server, tree.files);
120
- }
121
- else if (relation === 'unrecorded') {
96
+ if (!options.force && relation === 'unrecorded') {
122
97
  (0, ui_1.warn)(`This folder has no record of the server revision: the code goes up on top of revision ${server.revision}`);
123
98
  }
124
99
  }
@@ -134,6 +109,10 @@ async function pushSources(stream, client, options) {
134
109
  (0, ui_1.ok)(`No changes, revision ${result.revision}`);
135
110
  else
136
111
  (0, ui_1.ok)(`Revision ${result.revision}: ${result.file_count} files, ${(0, ui_1.formatBytes)(result.size_bytes)}`);
112
+ if (options.force && server && result.status === 'created') {
113
+ (0, ui_1.warn)(`Revision ${server.revision} is no longer the latest; it is kept while it is among the last ${KEPT_REVISIONS}`);
114
+ (0, ui_1.note)((0, ui_1.dim)(` Get it back: xflow pull ${streamWord(stream)} --revision ${server.revision} --into ./server-copy`));
115
+ }
137
116
  return { revision: result.revision, status: result.status };
138
117
  }
139
118
  /**
package/dist/flags.js CHANGED
@@ -53,8 +53,6 @@ exports.KNOWN = {
53
53
  'apps list': ['json', 'project'],
54
54
  'apps add': ['name', 'color'],
55
55
  skills: ['refresh', 'global', 'agent', 'yes'],
56
- mcp: ['show-token'],
57
- 'mcp install': ['show-token'],
58
56
  functions: ['project'],
59
57
  'functions list': ['project'],
60
58
  'functions invoke': ['data', 'data-file', 'method', 'project'],
@@ -69,7 +67,7 @@ exports.KNOWN = {
69
67
  connections: ['project'],
70
68
  'connections list': ['project'],
71
69
  'connections link': ['as', 'project'],
72
- 'connections unlink': ['force', 'project'],
70
+ 'connections unlink': ['yes', 'project'],
73
71
  'connections call': ['method', 'data', 'data-file', 'header', 'json', 'project'],
74
72
  schedules: ['project'],
75
73
  'schedules list': ['project'],
@@ -82,7 +80,7 @@ exports.KNOWN = {
82
80
  'db schema': ['project'],
83
81
  'db query': ['json', 'project'],
84
82
  'db migrate': ['dry-run', 'allow-destructive'],
85
- 'db move': ['to', 'confirm', 'project'],
83
+ 'db move': ['to', 'yes', 'project'],
86
84
  storage: ['json', 'project'],
87
85
  'storage ls': ['json', 'project'],
88
86
  'storage list': ['json', 'project'],
@@ -100,7 +98,7 @@ exports.KNOWN = {
100
98
  status: [],
101
99
  dev: [],
102
100
  pull: ['into', 'force', 'revision'],
103
- deploy: ['no-push', 'force', 'allow-removals', 'message', 'revision', 'project', 'app'],
101
+ deploy: ['force', 'allow-removals', 'message', 'revision', 'project', 'app'],
104
102
  publish: ['app'],
105
103
  rollback: ['app'],
106
104
  deployments: ['project', 'app'],
@@ -147,7 +145,6 @@ const NAMESPACES = new Set([
147
145
  'db',
148
146
  'storage',
149
147
  'pages',
150
- 'mcp',
151
148
  ]);
152
149
  /** The command these words name, as far as the table knows it. */
153
150
  function commandOf(words) {
package/dist/help.js CHANGED
@@ -48,7 +48,6 @@ ${(0, ui_1.bold)('Getting started')}
48
48
  xflow link <id> link this folder to a project, an empty one gets the whole project
49
49
  xflow dev in an app folder: run it on 127.0.0.1:3000, functions included
50
50
  xflow skills pick the agents that get the platform instructions
51
- xflow mcp install give the agent platform access without a terminal
52
51
 
53
52
  ${(0, ui_1.bold)('Sources')}
54
53
  xflow status what is on the server and how the local copies differ
@@ -87,7 +86,7 @@ ${(0, ui_1.bold)('Functions')}
87
86
  xflow connections connected accounts the project can use, and their variables
88
87
  xflow connections link <name> --as ALIAS
89
88
  give the functions the credentials of an account
90
- xflow connections unlink <ALIAS> [--force]
89
+ xflow connections unlink <ALIAS> [--yes]
91
90
  take them away again
92
91
 
93
92
  ${(0, ui_1.bold)('Database')}
@@ -97,7 +96,7 @@ ${(0, ui_1.bold)('Database')}
97
96
  xflow db query 'select ...' read data, in a read-only transaction
98
97
  xflow db migrate [--dry-run] [--allow-destructive]
99
98
  apply the migrations from migrations/*.sql
100
- xflow db move --to <server> --confirm <database>
99
+ xflow db move --to <server> --yes
101
100
  move the project database to another server
102
101
 
103
102
  ${(0, ui_1.bold)('Pages')}
@@ -160,10 +159,6 @@ the platform. A folder bound to an organization is not affected, its commands
160
159
  stay in its own organization: two projects of two organizations in two terminals
161
160
  work without switching anything.
162
161
 
163
- Outside a project folder the switch also rewrites the key in the agent config
164
- (see ${(0, ui_1.bold)('xflow mcp')}) when an xflow entry already exists there; a running agent
165
- session picks the new key up only after a restart.
166
-
167
162
  A project id is unique across the whole platform, so "project not found" under
168
163
  the wrong organization can never touch somebody else's project. When that error
169
164
  names a project you know exists, the key is simply from another organization:
@@ -301,11 +296,10 @@ build it and upload what the build writes. Dot entries are skipped. At most 5 MB
301
296
  unpacked and 1000 files.
302
297
 
303
298
  The page is public, so a file holding a secret key is refused whole: every visitor would read
304
- it. A page that needs a backend (a form, a lookup) calls ${(0, ui_1.bold)('/_fn/<name>')} on its own address,
305
- and only a function whose callers in the root xflow.json of its project name the page answers:
306
- add the page name there and run ${(0, ui_1.bold)('xflow deploy functions')}. The page has to exist first.
307
- Such a function is open to the whole internet. Search engines are asked not to index pages,
308
- so do not promise SEO.
299
+ it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
300
+ key a page may hold is a function key, and it opens that function to every visitor: an HTML
301
+ form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
302
+ (no CORS). Search engines are asked not to index pages, so do not promise SEO.
309
303
 
310
304
  ${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
311
305
  is no undo and no copy on the platform side. The name stays reserved for the organization, so
@@ -347,7 +341,7 @@ will work.`,
347
341
  one, 300 at most; --json prints the values whole)
348
342
  xflow db query <file>.sql the same, the query read from a file
349
343
  xflow db migrate store the project root, then apply migrations/*.sql
350
- xflow db move --to <server> --confirm <project name>
344
+ xflow db move --to <server> --yes
351
345
  move the project database to another server
352
346
 
353
347
  Every project has exactly one database of its own, shared by all of its apps and functions,
@@ -362,7 +356,7 @@ ${(0, ui_1.bold)('move')} takes the database of this project to another server o
362
356
  ${(0, ui_1.bold)('platform')} or an own server by name (or id, when two share a name). The platform copies
363
357
  the tables, switches the functions of the project and removes the old copy; nothing needs a
364
358
  deploy. While it copies, the apps read data but cannot save anything new, usually for a
365
- couple of minutes, which is why ${(0, ui_1.bold)('--confirm')} takes the project name. It also needs the
359
+ couple of minutes, which is why it asks for ${(0, ui_1.bold)('--yes')}. It also needs the
366
360
  right to move databases on the key: off by default, only its owner turns it on, in the
367
361
  platform settings where the key permissions live. The command follows the move to the end;
368
362
  stopping it does not stop the move, and ${(0, ui_1.bold)('xflow db status')} shows where it stands. A move
@@ -519,7 +513,7 @@ of the same service under one name; then the command prints their identifiers an
519
513
  for one of those instead of guessing.
520
514
 
521
515
  ${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
522
- those functions; ${(0, ui_1.bold)('--force')} goes through anyway. Seeing them needs the sources,
516
+ those functions; ${(0, ui_1.bold)('--yes')} goes through anyway. Seeing them needs the sources,
523
517
  so under ${(0, ui_1.bold)('--project')} there is no such check: the link goes, and the variables
524
518
  that go with it are printed instead. Both commands take the flag, because an account
525
519
  linked to a service project without a folder has to be removable the same way.
@@ -618,14 +612,14 @@ of the project runs the new code the moment it finishes; there is no publish ste
618
612
 
619
613
  Who may call a function is its callers, in the root xflow.json:
620
614
 
621
- { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis", "clinic-landing"] } } }
615
+ { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
622
616
 
623
- A caller is an app folder of this project (apps/zapis is "zapis") or the name of a page of
624
- the organization. A function nobody lists has no callers: a workflow, woken by a schedule,
625
- by a service holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a
626
- caller is public (a page, or an app anyone with the link opens), so anyone on the internet
627
- can call it. The address in the note under the table is for webhooks and other functions,
628
- with a key; apps and pages call ${(0, ui_1.bold)('/_fn/<name>')} on their own address.
617
+ A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
618
+ nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
619
+ holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
620
+ anyone with the link opens, so anyone on the internet can call it. The address in the note
621
+ under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
622
+ their own address.
629
623
 
630
624
  ${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
631
625
  the function a one-minute ticket and calls it itself, so the function sees
@@ -643,24 +637,6 @@ Prints the body of the answer and nothing else, so it goes straight into a pipe
643
637
  (${(0, ui_1.bold)('| jq')}, ${(0, ui_1.bold)('| python')}). A non-zero exit code on 4xx and 5xx: in CI such a call has to fail the step. A
644
638
  refusal of the guard names its reason in the body (${(0, ui_1.bold)('not_bound')}, ${(0, ui_1.bold)('no_access')},
645
639
  ${(0, ui_1.bold)('functions_stopped')}); for a crash the hint points at ${(0, ui_1.bold)('xflow functions logs <name>')}.`,
646
- mcp: `${(0, ui_1.bold)('xflow mcp install')}: connect the agent to the platform directly
647
-
648
- Writes the MCP server into the configuration of a client that has its own connect
649
- command: today that is Claude Code, and for the rest the command prints what to enter
650
- by hand. After that the agent runs the platform without a terminal: it inspects the
651
- database, applies migrations, calls functions, sets schedules, publishes app builds, reads
652
- logs. The commands stay: code, builds and functions deploys are still done by the CLI,
653
- because pushing sources through MCP means filling the agent context and spending your money.
654
-
655
- The command takes the access key from your own login and does not print it: usually
656
- the agent runs this command itself, and everything printed lands in its context and in
657
- the chat history.
658
-
659
- --show-token print the key anyway (not while under an agent)
660
-
661
- There is one key per person per organization and its scope is not narrowed: the agent
662
- sees every project of the organization and names the one it needs. It takes the
663
- project from the ${(0, ui_1.bold)('project_id')} argument, not from your folder, which it cannot see.`,
664
640
  skills: `${(0, ui_1.bold)('xflow skills')}: platform instructions for an AI agent
665
641
 
666
642
  An agent has no way to know about XFlow: the platform is not in its training. The
@@ -670,16 +646,12 @@ down on their own for the common agents, so this command exists to pick tools
670
646
  precisely. Refreshing after a CLI update is not your job any more: the skill and the
671
647
  CLI ship as one version, and ${(0, ui_1.bold)('xflow update')} rewrites the copies you already have.
672
648
 
673
- Run in a terminal, it asks two questions: where (this project, or the home folder,
674
- which makes the skill visible in every project) and for which agents. Copies that are
675
- already installed come pre-selected and are updated in place. Without a terminal, in
676
- CI or when an agent runs the command, there are no questions: the defaults plus every
677
- already installed copy are refreshed.
649
+ Without flags it installs for Claude Code, Cursor and Codex in this project (globally:
650
+ Claude Code, Codex and the shared folder) and refreshes every copy already installed.
678
651
 
679
652
  xflow skills list the agents, their folders and what is installed
680
- --agent claude,cursor exact agents, no questions asked
653
+ --agent claude,cursor exact agents instead of the defaults
681
654
  --global the home folder instead of the project
682
- --yes no questions: defaults plus what is installed
683
655
  --refresh rewrite the copies that exist, create none
684
656
 
685
657
  The format is shared (agentskills.io) and inside a project most tools read the shared
@@ -731,7 +703,6 @@ apps never go in one run, because a change of functions reaches every app at onc
731
703
 
732
704
  -m <text> the version comment, required: one short line in the language the
733
705
  user speaks, saying what changed (--message is the long form)
734
- --no-push do not send sources, build from the latest server revision
735
706
  --revision <N> run a revision the platform already holds: that is how the functions
736
707
  come back (xflow deployments functions lists them)
737
708
  --project <id> with --revision only: the functions of that project, no folder needed
@@ -747,12 +718,12 @@ ignore field of xflow.json.
747
718
  If the server holds a revision newer than the one you worked from, the sources are
748
719
  rejected and the build does not start. That means somebody deployed before you: fetch
749
720
  their changes alongside (${(0, ui_1.bold)('xflow pull --into ./server-copy')}), merge them in git on
750
- your side, and retry. A folder with no record of the server revision (a fresh clone, a CI
721
+ your side, and retry; or build what the server holds as it is with ${(0, ui_1.bold)('--revision <N>')}, the
722
+ refusal names the number. A folder with no record of the server revision (a fresh clone, a CI
751
723
  checkout) is not rejected: its first deploy goes on top of the current revision and says
752
- so, the check applies from the next one. ${(0, ui_1.bold)('--force')} overwrites the server revision, and before that the
753
- CLI shows whose work you are about to destroy and asks for confirmation by typing the
754
- project name. In a non-interactive run (CI, an agent) there is no way to confirm: a
755
- version conflict has to fail the build rather than silently destroy somebody else's work.
724
+ so, the check applies from the next one. ${(0, ui_1.bold)('--force')} sends this folder over the server revision
725
+ anyway. The overwritten revision is not lost at once: it stays in the history while it is among
726
+ the last 10, and the deploy prints the ${(0, ui_1.bold)('xflow pull --revision <N>')} line that brings it back.
756
727
 
757
728
  The platform builds, in a clean sandbox on one Node version for everybody. Before that the
758
729
  sources are checked: what breaks silently (the platform files, the calls in the entry point,
@@ -770,9 +741,8 @@ internet now. A change that touches data and screens goes in this order: the fun
770
741
  changes nobody deployed.
771
742
 
772
743
  A function gone from the sources would be deleted from the cloud along with its keys and
773
- schedules, and that is final. So the deploy does not start at all until you say yes. In a terminal the CLI names the functions
774
- and asks; without one (CI, an agent) it stops and ${(0, ui_1.bold)('--allow-removals')} is the only way
775
- to agree. If the sources hold no functions at all while the cloud holds several, nothing
744
+ schedules, and that is final. So the deploy does not start at all: it names the functions and stops, and ${(0, ui_1.bold)('--allow-removals')}
745
+ is the way to agree. If the sources hold no functions at all while the cloud holds several, nothing
776
746
  is deleted and nothing is asked: that looks like a directory which never made it (a
777
747
  ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
778
748
 
package/dist/limits.js CHANGED
@@ -15,6 +15,7 @@ const DENIAL_LABELS = {
15
15
  limit_builds_concurrent: 'builds running at once',
16
16
  limit_seats: 'people in the organization',
17
17
  limit_schedule_interval: 'schedule frequency',
18
+ limit_schedules: 'schedules in the organization',
18
19
  db_write_locked: 'database volume, writes are off',
19
20
  plan_no_schedules: 'schedules',
20
21
  subscription_blocked: 'the subscription is unpaid',
@@ -38,6 +39,7 @@ const QUOTA_ORDER = [
38
39
  'connections',
39
40
  'pages',
40
41
  'functions',
42
+ 'schedules',
41
43
  'people',
42
44
  'database_mb',
43
45
  'files_mb',
@@ -49,6 +51,7 @@ const QUOTA_LABELS = {
49
51
  connections: 'Active connections',
50
52
  pages: 'Public pages',
51
53
  functions: 'Cloud functions',
54
+ schedules: 'Schedules',
52
55
  people: 'People',
53
56
  database_mb: 'Database',
54
57
  files_mb: 'Files',
package/dist/template.js CHANGED
@@ -13,7 +13,7 @@ const README = `# %NAME%
13
13
  A project on XFlow: one database, cloud functions and the apps that use them.
14
14
 
15
15
  \`\`\`
16
- xflow.json projectId, and which apps and pages may call each function
16
+ xflow.json projectId, and which apps may call each function
17
17
  package.json dependencies of the functions
18
18
  functions/<name>/ cloud functions, shared by every app
19
19
  migrations/ the database of the project