@oalo-ai/cli 0.0.0-stage → 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -2
  3. package/man/oalo.1 +1530 -0
  4. package/package.json +44 -4
  5. package/plugin/.claude-plugin/plugin.json +12 -0
  6. package/plugin/skills/oalo/SKILL.md +414 -0
  7. package/src/api.js +132 -0
  8. package/src/app-ref.js +11 -0
  9. package/src/args.js +67 -0
  10. package/src/bot-settings.json +468 -0
  11. package/src/channel.js +162 -0
  12. package/src/commands/apps.js +64 -0
  13. package/src/commands/bots.js +278 -0
  14. package/src/commands/call.js +179 -0
  15. package/src/commands/channels.js +110 -0
  16. package/src/commands/data.js +446 -0
  17. package/src/commands/db.js +58 -0
  18. package/src/commands/deploy.js +431 -0
  19. package/src/commands/dockerfile.js +81 -0
  20. package/src/commands/docs.js +70 -0
  21. package/src/commands/emit.js +124 -0
  22. package/src/commands/env.js +223 -0
  23. package/src/commands/graphs.js +570 -0
  24. package/src/commands/incidents.js +44 -0
  25. package/src/commands/link.js +150 -0
  26. package/src/commands/login.js +180 -0
  27. package/src/commands/logs.js +69 -0
  28. package/src/commands/mcp.js +125 -0
  29. package/src/commands/metrics.js +168 -0
  30. package/src/commands/products.js +169 -0
  31. package/src/commands/qa.js +98 -0
  32. package/src/commands/rehearse.js +100 -0
  33. package/src/commands/run.js +152 -0
  34. package/src/commands/secrets.js +68 -0
  35. package/src/commands/setup.js +96 -0
  36. package/src/commands/skill.js +142 -0
  37. package/src/commands/sources.js +377 -0
  38. package/src/commands/switches.js +156 -0
  39. package/src/commands/tail.js +84 -0
  40. package/src/commands/tokens.js +85 -0
  41. package/src/commands/typegen.js +505 -0
  42. package/src/commands/whoami.js +112 -0
  43. package/src/config.js +157 -0
  44. package/src/data.js +392 -0
  45. package/src/envfile.js +71 -0
  46. package/src/events.js +63 -0
  47. package/src/graphfiles.js +273 -0
  48. package/src/help.js +920 -0
  49. package/src/index.js +216 -0
  50. package/src/mcp/tools.js +773 -0
  51. package/src/server.js +73 -0
  52. package/src/workspace.js +44 -0
package/src/help.js ADDED
@@ -0,0 +1,920 @@
1
+ /**
2
+ * One registry drives every form of help: the overview, per-command detail, and
3
+ * the JSON form an agent can parse. One source so they can't drift, for the
4
+ * same reason typegen reads the manifest instead of hardcoding a schema.
5
+ */
6
+
7
+ export const TAGLINE = "wire a local app into your OALO workspace";
8
+
9
+ export const TOKENS = [
10
+ {
11
+ kind: "sync",
12
+ where: "`oalo login` (read-only), dashboard -> Account -> CLI & agents, or dashboard -> Sync API for an app's own servers",
13
+ plane: "data (/api/sync/v1)",
14
+ usedBy: ["typegen", "data"],
15
+ warning:
16
+ "Server-side only. Carries no end-user identity, so it reads and writes EVERY member's rows. Never ship one inside a mobile or web app.",
17
+ },
18
+ {
19
+ kind: "cli",
20
+ where: "`oalo login`, or dashboard -> Account -> CLI & agents",
21
+ plane: "control (the build surface)",
22
+ usedBy: ["whoami", "tail", "call", "run", "emit", "data", "tokens", "docs", "apps", "deploy", "env", "logs", "secrets", "loops", "flows", "pull", "status", "lint", "push", "qa", "rehearse", "bots", "sources", "mcp"],
23
+ warning: "Acts as you, with exactly your permissions. No read-only mode. Treat it like a password.",
24
+ },
25
+ ];
26
+
27
+ export const COMMANDS = [
28
+ {
29
+ name: "login",
30
+ summary: "Connect this machine to a workspace by approving it in the browser.",
31
+ usage: ['oalo login <workspace> [--name "<this machine>"] [--no-browser]', "oalo login --server <url>"],
32
+ flags: [
33
+ { flag: "--server <url>", desc: "Workspace URL, e.g. https://acme.oalo.ai. Defaults to OALO_URL or the linked project." },
34
+ { flag: "--name <name>", desc: "What the connection is called in the dashboard. Default: this machine's hostname." },
35
+ { flag: "--no-browser", desc: "Print the approval URL without opening it (for a remote shell)." },
36
+ ],
37
+ notes: [
38
+ "Opens the dashboard on Account -> CLI & agents with a code. Approve it there (check the code matches) and the CLI saves a CLI token plus a read-only data token.",
39
+ "Saved to ~/.oalo/credentials.json (mode 0600), by workspace URL, never in a project. Every command, oalo mcp included, finds them there.",
40
+ "The code lasts 10 minutes. Nothing is created until you approve.",
41
+ ],
42
+ exits: { 0: "connected", 1: "denied, expired or unreachable", 2: "missing --server" },
43
+ },
44
+ {
45
+ name: "logout",
46
+ summary: "Revoke this machine's connection and forget its saved tokens.",
47
+ usage: ["oalo logout [--server <url>]"],
48
+ flags: [{ flag: "--server <url>", desc: "Workspace URL. Defaults to OALO_URL or the linked project." }],
49
+ notes: ["Revokes the connection on the server when it was made by oalo login, then removes every saved token for that workspace."],
50
+ exits: { 0: "done", 2: "missing --server" },
51
+ },
52
+ {
53
+ name: "whoami",
54
+ summary: "Show the workspace, person and connection this machine uses, and check its tokens.",
55
+ usage: ["oalo whoami [--json]"],
56
+ flags: [{ flag: "--json", desc: "Print everything as JSON." }],
57
+ notes: [
58
+ "Checks the CLI token by connecting and the data token by reading the manifest, so it tells you which one is the problem when a command is refused.",
59
+ "Also shows the workspace's API version and features, and says when this CLI is too old for it.",
60
+ ],
61
+ exits: { 0: "connected and both checks passed", 1: "a token failed, or the workspace is unreachable", 2: "not connected" },
62
+ },
63
+ {
64
+ name: "link",
65
+ summary: "Point this project at a workspace and save credentials.",
66
+ usage: [
67
+ "oalo link --server <url> [--token <sync token>] [--cli-token <token>] [--app <slug>] [--env dev|prod] [--no-skill]",
68
+ "oalo link --status",
69
+ ],
70
+ flags: [
71
+ { flag: "--server <url>", desc: "Workspace URL, e.g. https://acme.oalo.ai" },
72
+ { flag: "--token <token>", desc: "Sync token (data plane). Not needed after oalo login, which saved one." },
73
+ { flag: "--cli-token <token>", desc: "CLI token (control plane), for tail and mcp. Optional." },
74
+ { flag: "--app <slug>", desc: "The app that deploy, env, logs, secrets and dockerfile use when you don't pass --app (`oalo apps` lists them). `oalo deploy --new` sets it." },
75
+ { flag: "--env dev|prod", desc: "Which schema later commands touch. Default dev." },
76
+ { flag: "--status", desc: "Show the current link and re-check reachability." },
77
+ { flag: "--no-skill", desc: "Don't install or refresh the OALO skill for coding agents (see `oalo skill`)." },
78
+ ],
79
+ notes: [
80
+ "Every token is verified against the thing it is for BEFORE anything is saved, so a bad or mixed-up token fails here rather than later.",
81
+ "Writes .oalo/config.json (safe to commit) and ~/.oalo/credentials.json (mode 0600, never in the project). OALO_HOME relocates the credential store.",
82
+ "Also installs the OALO skill for coding agents (.claude/skills/oalo/SKILL.md), or refreshes it, from this CLI version; an AGENTS.md section from `oalo skill --agents` is refreshed too.",
83
+ ],
84
+ exits: { 0: "linked", 1: "verification failed", 2: "missing or invalid arguments" },
85
+ },
86
+ {
87
+ name: "typegen",
88
+ summary: "Generate TypeScript bindings from the workspace's own schema: its models, and the workflows apps can call.",
89
+ usage: ["oalo typegen [--out <file>] [--check]", "oalo typegen --dart [--out <project dir>] [--check]"],
90
+ flags: [
91
+ { flag: "--out <file>", desc: "Output path. Default oalo.generated.ts. With --dart, the Flutter project's root (default the current folder)." },
92
+ { flag: "--dart", desc: "For a Flutter app from Oalo's template: write lib/sync/generated and lib/models (the Base's own Dart generator, the one the box uses), and .oalo/dart-defines.json with the Base's address, the linked app's slug and the env, for `flutter run --dart-define-from-file=.oalo/dart-defines.json`." },
93
+ { flag: "--check", desc: "Do not write; exit non-zero if the file is stale or missing. For CI." },
94
+ ],
95
+ notes: [
96
+ "Commit the output. Document interfaces are OPEN: the manifest describes some fields and rows may carry more, so undeclared keys stay reachable.",
97
+ "A table-scoped token generates only the tables it can read.",
98
+ "--dart files are the same ones the box writes after a model changes; run it again after `oalo data models create`. Commit them.",
99
+ "The env is stamped in the header: dev and prod are separate schemas.",
100
+ "Workflows with an api trigger get <Name>Input and <Name>Output types and runWorkflow(name, input, {contact, wait}), which calls POST /api/workflows/v1/<name>/run from the app's server with OALO_URL and OALO_SYNC_TOKEN. A preview's token runs it as a test: dev data, outside actions simulated. --check fails when a workflow's input or output changes too.",
101
+ "Every file also gets createChatSession(bot, user), for the chat widget signed in as your app's user: the endpoint in the widget's data-user-url answers with it (see `oalo docs chat-widget`).",
102
+ "And verifyOaloToolCall(req), which an endpoint declared as a bot tool in .oalo/app.json uses to check a call came from Oalo and for which user (see `oalo docs app-tools`).",
103
+ "And the workspace's products: PRODUCTS, the ProductId type and isProductId() (Dart: the Products class and allProductIds). Use them wherever code names a product. --check fails when one is added or archived.",
104
+ ],
105
+ exits: { 0: "written, or up to date under --check", 1: "stale, missing, or fetch failed", 2: "not linked" },
106
+ },
107
+ {
108
+ name: "tail",
109
+ summary: "Watch loop runs, jobs, builds and activity as they happen.",
110
+ usage: ["oalo tail [--filter <text>] [--json] [--all]"],
111
+ flags: [
112
+ { flag: "--filter <text>", desc: "Only events whose name or payload contains this text." },
113
+ { flag: "--json", desc: "One JSON object per line. Pipes into jq." },
114
+ { flag: "--all", desc: "Include high-frequency streaming chunk events." },
115
+ ],
116
+ notes: [
117
+ "Answers 'I did a thing, did the expected thing happen?'. A flow that never fired shows as silence.",
118
+ "Does NOT show direct Sync REST writes (they do not broadcast); you see their consequences.",
119
+ "Events go to stdout, the banner to stderr, so grep and jq both behave. Runs until interrupted.",
120
+ "Needs a CLI token.",
121
+ ],
122
+ exits: { 0: "stopped cleanly", 1: "could not connect", 2: "no CLI token" },
123
+ },
124
+ {
125
+ name: "call",
126
+ summary: "Call any build-surface event directly. The escape hatch.",
127
+ usage: ["oalo call <event> ['<json params>']", "oalo call --list [text] [--json]"],
128
+ flags: [
129
+ { flag: "--list [text]", desc: "List the events you can call, from the workspace. With text, the matching events and the params each accepts." },
130
+ { flag: "--namespaces", desc: "The events you may call, counted per namespace (what `--list` with no text shows)." },
131
+ { flag: "--compact", desc: "Print the reply as one line of JSON." },
132
+ ],
133
+ notes: [
134
+ "The build surface has ~500 events. First-class commands cover the common ones; this reaches the rest without a command per feature.",
135
+ "Params are a JSON object. Org and user identity come from your token, never the params, so a call cannot exceed what you can do.",
136
+ "--list comes from the server's own handlers and is filtered to what you may call, so it never goes stale. On an older workspace it falls back to a list of namespaces.",
137
+ "An unknown event returns a clean error naming what you sent. An event that exists but gets the wrong params says which params it takes.",
138
+ "Example: oalo call loops:run_now '{\"name\":\"Nurture\"}'",
139
+ "Needs a CLI token.",
140
+ ],
141
+ exits: { 0: "the call succeeded", 1: "the call failed or was rejected", 2: "bad arguments or no token" },
142
+ },
143
+ {
144
+ name: "run",
145
+ summary: "Fire a loop, or send a bot a test message, and watch what happens.",
146
+ usage: ['oalo run loop <name> [--env dev|prod] [--watch]', 'oalo run bot <id> "<message>"'],
147
+ flags: [
148
+ { flag: "--watch", desc: "Stream events after firing. Default on for bots." },
149
+ { flag: "--timeout <s>", desc: "How long to watch. Default 30." },
150
+ { flag: "--env dev|prod", desc: "Which schema the loop runs against." },
151
+ { flag: "--app <slug>", desc: "Only for a loop pinned to one app: that app's slug (`oalo apps`) or id. Loops are org-wide by default." },
152
+ ],
153
+ notes: [
154
+ "IMPORTANT: `run loop` fires the loop DIRECTLY and does NOT exercise its trigger binding. Triggers come from the event bus (a real form submission, a real inbound message). So this proves the loop body works; to test that its trigger is wired, use `oalo emit`.",
155
+ "bot ids accept an integer, a numeric string, or a public bot_ id.",
156
+ "Needs a CLI token.",
157
+ ],
158
+ exits: { 0: "ran, or watch window elapsed", 1: "the run failed to start", 2: "bad arguments or no token" },
159
+ },
160
+ {
161
+ name: "emit",
162
+ summary: "Fire a domain event (form.submitted, order.created, ...) to test a loop's trigger.",
163
+ usage: ["oalo emit <event type> [--payload '<json>'] [--contact <id>] [--dry-run] [--json]", "oalo emit --list [--json]"],
164
+ flags: [
165
+ { flag: "--payload <json>", desc: "The event's payload, a JSON object. `oalo emit --list` shows each event's fields." },
166
+ { flag: "--contact <id>", desc: "An existing contact the event is about. Never creates one." },
167
+ { flag: "--dry-run", desc: "List the loops it would fire, and emit nothing." },
168
+ { flag: "--list", desc: "Every event a trigger can listen for, with its payload fields and the loops listening now." },
169
+ { flag: "--json", desc: "Print the reply as JSON." },
170
+ ],
171
+ notes: [
172
+ "`oalo run loop` fires a loop directly and skips its trigger. emit goes through the real event bus, so it tests the trigger and its payload filter too.",
173
+ "The loops it fires do real things: send email or WhatsApp, mark invoices paid. Run --dry-run first.",
174
+ "Admins and members with Developer access only (an admin turns it on under Team). Events are tagged as coming from the CLI and posted to the team feed.",
175
+ "The type must be one a trigger can listen for; a typo gets a suggestion instead of vanishing.",
176
+ ],
177
+ exits: { 0: "emitted (or dry run done)", 1: "refused or failed", 2: "bad arguments or no token" },
178
+ },
179
+ {
180
+ name: "data",
181
+ summary: "Models and rows in this workspace: list, read, push (bulk upsert), delete, watch, promote, and make tables searchable by bots.",
182
+ usage: [
183
+ "oalo data models [--json]",
184
+ "oalo data models create <name> --field name:type [--field ...] [--owned-by <field>]",
185
+ "oalo data list <table> [--since N] [--limit N] [--include-deleted] [--json]",
186
+ "oalo data get <table> <id> [--json]",
187
+ "oalo data push <table> <file.csv|.json|.jsonl> [--id-field id] [--format csv|json|jsonl] [--types] [--from N] [--new-ids] [--dry-run] [--json]",
188
+ "oalo data delete <table> <id>",
189
+ "oalo data watch <table> [--since N] [--json]",
190
+ "oalo data promote <table>",
191
+ "oalo data routes [--json]",
192
+ "oalo data events <table> created [updated] | --off",
193
+ "oalo data vectorize <table> [--fields a,b] [--where '<field> in a,b'] [--describe '<text>'] [--id <column>] [--json]",
194
+ "oalo data vectorize <table> --off",
195
+ "oalo data keep-from-ai <table> [<field,...>] [--off] [--json]",
196
+ ],
197
+ flags: [
198
+ { flag: "--env dev|prod", desc: "Which schema. Default: the project's link, else dev. Writing to prod needs --env prod on the command line." },
199
+ { flag: "--since N", desc: "list, watch: only rows changed after this sequence number." },
200
+ { flag: "--limit N", desc: "list: how many rows (the server caps it)." },
201
+ { flag: "--order seqnum|-seqnum", desc: "list: oldest change first (the default), or newest first." },
202
+ { flag: "--include-deleted", desc: "list: include deleted rows." },
203
+ { flag: "--format csv|json|jsonl", desc: "push: the file's format, when its extension doesn't say." },
204
+ { flag: "--field name:type", desc: "models create: a field. Repeat it, or comma-separate. Types: string text integer float boolean datetime date json list references:<table>." },
205
+ { flag: "--owned-by <field>", desc: "models create: rows belong to the app user in this field (each member sees only their own). Without it the model is shared." },
206
+ { flag: "--id-field <name>", desc: "push: which field holds each record's id (default id). Re-sending a record updates it." },
207
+ { flag: "--types", desc: "push, CSV only: turn plain numbers, true/false and empty cells into numbers, booleans and null. Off by default so ids like 007 stay text." },
208
+ { flag: "--from N", desc: "push: skip the first N records, to resume a push that stopped." },
209
+ { flag: "--new-ids", desc: "push: let the server make ids for records without one. A re-send then adds copies." },
210
+ { flag: "--dry-run", desc: "push: parse, check the model exists, show the first row, write nothing." },
211
+ { flag: "--fields a,b", desc: "vectorize: the fields (a model's) or columns (a plain table's) whose text bots search. Turns it on, and embeds the existing rows." },
212
+ { flag: "--where <filter>", desc: "vectorize, models only: embed only matching rows. `status in active,draft`, `status not in archived`, `status = active`, `status != archived`, `cover present`, `cover absent`." },
213
+ { flag: "--describe <text>", desc: "vectorize: what the table holds. Bots see its first 120 characters when deciding what to search." },
214
+ { flag: "--id <column>", desc: "vectorize, plain tables: the unique column each row is keyed by (default id)." },
215
+ { flag: "--off", desc: "events, vectorize: turn it off. keep-from-ai: take the named fields off the list, or all of them when none are named." },
216
+ { flag: "--json", desc: "Print the reply as JSON." },
217
+ ],
218
+ notes: [
219
+ "dev is a sandbox; live apps read prod. Push to dev, check it, then `oalo data promote <table>` copies dev rows to prod.",
220
+ "Reads use the saved data token (read-only). Row writes use a write token limited to the tables written, made as you over the CLI token the first time you write a table and saved beside it (`oalo tokens list` shows it; it expires in 30 days).",
221
+ "models create and promote run as you over the CLI token, so they follow your dashboard access.",
222
+ "push sends up to 1,000 rows per request, waits out the rate limit (120 writes a minute), retries network errors, and lists rows the server rejected.",
223
+ "Every write syncs to apps on that schema at once. A delete marks the row deleted; it leaves synced apps.",
224
+ "The exact contract: `oalo data routes`, or the OpenAPI spec at <workspace>/api/docs.",
225
+ "vectorize works on an Oalo model (both schemas) or a table the app made (the --env schema). With no flags it shows the status: fields, filter, rows embedded. New and edited rows are embedded within 15 minutes, whoever writes them. Then attach it to a bot with `oalo bots knowledge <bot> --add <table>`.",
226
+ "vectorize sends the text of the chosen fields to the embedding provider (Voyage); a bot's answer sends the rows its search finds to its AI model. Leave out fields that shouldn't leave Oalo.",
227
+ "keep-from-ai names fields a bot's data tools (data_search, data_related, table_search, search) leave out of what they hand its AI model, and that can't be embedded. They're still stored, synced to your apps, and read by workflows. A field that's embedded now has to be vectorized away first. With no fields, the list.",
228
+ ],
229
+ exits: { 0: "done", 1: "failed, or some rows were rejected", 2: "bad arguments or no token" },
230
+ },
231
+ {
232
+ name: "incidents",
233
+ summary: "A live app's outages and restart bursts, as Oalo's uptime check saw them, newest first.",
234
+ usage: ["oalo incidents [--app <slug>] [--all] [--json]"],
235
+ flags: [
236
+ { flag: "--app <slug>", desc: "The app. Default: the one saved with `oalo link --app`." },
237
+ { flag: "--all", desc: "Show the details of closed incidents too (open ones always show them)." },
238
+ { flag: "--json", desc: "The reply as JSON." },
239
+ ],
240
+ notes: [
241
+ "Oalo checks each live server app every minute: its port answers (and .oalo/app.json's health path, when set), and how often Docker restarted its processes. An incident opens after 3 failed checks in a row, or 3 restarts in 10 minutes; it closes after 2 good checks.",
242
+ "Each one says what was seen: which process, how many restarts, a memory kill with the size and the fix, the exit code, and the last log lines. The workspace is told in the dashboard's bell and by email (each person can turn that off under Notifications).",
243
+ ],
244
+ exits: { 0: "no open incident", 1: "an incident is open, or the request failed", 2: "bad arguments or no CLI token" },
245
+ },
246
+ {
247
+ name: "db",
248
+ summary: "All of an env's data as plain SQL (every app's own tables and the Oalo models), to keep a copy or take it anywhere.",
249
+ usage: ["oalo db dump [--env dev|prod] [--out <file.sql>] [--force]"],
250
+ flags: [
251
+ { flag: "--env dev|prod", desc: "Which env's data. Default prod." },
252
+ { flag: "--out <file.sql>", desc: "Where to write it. Default <workspace>-<env>-<date>.sql here." },
253
+ { flag: "--force", desc: "Replace the file if it exists." },
254
+ ],
255
+ notes: [
256
+ "Workspace admins only: it holds every member's data. Restore it into any Postgres with psql -f.",
257
+ "Your code is in git (oalo deploy --pull); this is the data. Together they are everything you need to run your apps elsewhere.",
258
+ ],
259
+ exits: { 0: "a whole dump was written", 1: "refused, unreachable, or cut off (the partial file is removed)", 2: "bad arguments, the file exists, or no CLI token" },
260
+ },
261
+ {
262
+ name: "metrics",
263
+ summary: "A number from the workspace's data: count, sum or average over a model's rows, events or contacts, grouped or over time.",
264
+ usage: [
265
+ "oalo metrics <model | events | contacts> [--sum|--avg|--min|--max|--distinct <field>] [--by <field>] [--every day|week|month] [--days N] [--where <filter>] [--json]",
266
+ ],
267
+ flags: [
268
+ { flag: "--sum <field>", desc: "The sum of a number field. Also --avg, --min, --max, and --distinct (how many different values). Default: count the rows." },
269
+ { flag: "--avg <field>", desc: "The average of a number field." },
270
+ { flag: "--min <field>", desc: "The lowest value of a number field." },
271
+ { flag: "--max <field>", desc: "The highest value of a number field." },
272
+ { flag: "--distinct <field>", desc: "How many different values a field has." },
273
+ { flag: "--by <field>", desc: "Group by a field, largest first. Contacts group by status, source or custom_fields.<name>." },
274
+ { flag: "--every <day|week|month>", desc: "Over time, one row per day, week or month." },
275
+ { flag: "--days N", desc: "The window, ending now. Default: all time." },
276
+ { flag: "--where <filter>", desc: 'Only matching rows: "status = paid and total > 100". Also !=, >=, <, <=, ~ (contains), in a,b, present, absent.' },
277
+ { flag: "--event <type>", desc: "events: one type, like order.created. Default: every type." },
278
+ { flag: "--time <created|changed|field>", desc: "A model row counts at when it was created (default), last changed, or one of its date fields." },
279
+ { flag: "--compare", desc: "Also the same over the window before (needs --days)." },
280
+ { flag: "--env dev|prod", desc: "A model's live (default) or dev data." },
281
+ { flag: "--json", desc: "The answer as JSON." },
282
+ ],
283
+ notes: [
284
+ "The same query the dashboards draw. Model rows count at when they were created, stamped by the server; rows from before the stamp are counted as untracked.",
285
+ "A model's fields are checked: `oalo data models` lists them. An app's server asks the same with queryMetric from `oalo typegen` (see `oalo docs dashboards`).",
286
+ ],
287
+ exits: { 0: "done", 1: "the query failed", 2: "bad arguments or no CLI token" },
288
+ },
289
+ {
290
+ name: "dashboards",
291
+ summary: "The workspace's dashboards, and a chart put on one (made when no dashboard has that name).",
292
+ usage: [
293
+ "oalo dashboards [--json]",
294
+ 'oalo dashboards add <dashboard> <model | events | contacts> --title "<title>" --kind number|line|bars|table [metrics flags] [--app <slug>] [--width 1|2|3]',
295
+ 'oalo dashboards add <dashboard> --title "<title>" --kind funnel --steps a,b,c [--days N]',
296
+ ],
297
+ flags: [
298
+ { flag: "--title <title>", desc: "The chart's title. A chart with the same title on that dashboard is replaced." },
299
+ { flag: "--kind <kind>", desc: "number (a total; --compare adds the window before), line (needs --every), bars (needs --by), table, funnel." },
300
+ { flag: "--steps a,b", desc: "funnel: event types, in order." },
301
+ { flag: "--width <1|2|3>", desc: "Columns of three." },
302
+ { flag: "--app <slug>", desc: "A dashboard made now shows on this app too (its Dashboard tab)." },
303
+ { flag: "--json", desc: "The answer as JSON." },
304
+ ],
305
+ notes: [
306
+ "add runs the query first and refuses one that can't draw, then prints its numbers. It changes what everyone with access sees.",
307
+ "The flags after the model are the same as oalo metrics.",
308
+ ],
309
+ exits: { 0: "done", 1: "the request failed", 2: "bad arguments or no CLI token" },
310
+ },
311
+ {
312
+ name: "products",
313
+ summary: "What the workspace sells (Oalo commerce on its own Stripe account): list, create, rename, change a price, archive, and see orders.",
314
+ usage: [
315
+ "oalo products [--all] [--json]",
316
+ 'oalo products create <id> --name "<name>" --price <amount> --once|--monthly|--yearly [--trial <days>] [--currency <code>] [--description <text>] [--live] [--yes]',
317
+ 'oalo products edit <id> [--name "<name>"] [--description <text>]',
318
+ "oalo products price <id> <amount> [--yes]",
319
+ "oalo products archive <id> --yes",
320
+ "oalo products delete <id> --yes",
321
+ "oalo products orders [<id>] [--limit N]",
322
+ ],
323
+ flags: [
324
+ { flag: "--name <name>", desc: "create, edit: the name buyers see." },
325
+ { flag: "--price <amount>", desc: "create: the price, like 9.99." },
326
+ { flag: "--once", desc: "create: paid once." },
327
+ { flag: "--monthly", desc: "create: a subscription, billed every month." },
328
+ { flag: "--yearly", desc: "create: a subscription, billed every year." },
329
+ { flag: "--trial <days>", desc: "create: free days before a subscription's first charge." },
330
+ { flag: "--currency <code>", desc: "create: ISO code, like EUR. Default USD." },
331
+ { flag: "--description <text>", desc: "create, edit: shown on the checkout page." },
332
+ { flag: "--live", desc: "create: on your live Stripe key (real charges). Without it, the test key." },
333
+ { flag: "--yes", desc: "Go ahead: needed for archive and delete, for --live, and for a price over 500." },
334
+ { flag: "--all", desc: "list: archived products too." },
335
+ { flag: "--limit N", desc: "orders: how many, newest first. Default 50." },
336
+ { flag: "--json", desc: "The reply as JSON." },
337
+ ],
338
+ notes: [
339
+ "The id (premium, pro_yearly) is what code uses: BuyButton productId, a row's product_id (with availability product), and a member's purchased_products. `oalo typegen` writes the ids as PRODUCTS and the ProductId type (Dart: the Products class), so a typo fails to compile.",
340
+ "Payments run on the workspace's own Stripe key (Payment settings). A product made with the test key is bought with test cards only.",
341
+ "price makes a new Stripe price and archives the old one: new buyers pay the new price, existing subscriptions keep theirs.",
342
+ "archive stops a product being sold; its orders stay and its buyers keep access. delete is for an archived product nobody bought, and frees its id.",
343
+ "PayPal, Apple and Google prices are set up from the dashboard's Storefront or by asking the builder.",
344
+ ],
345
+ exits: { 0: "done", 1: "the request failed", 2: "bad arguments, a confirmation (--yes) needed, or no CLI token" },
346
+ },
347
+ {
348
+ name: "channels",
349
+ summary: "Every channel the workspace has (WhatsApp, Instagram, email, web chat) and which bot answers each; change the bot, check or connect a number.",
350
+ usage: [
351
+ "oalo channels [--json]",
352
+ "oalo channels set whatsapp|instagram <number or @account> --bot <bot>",
353
+ "oalo channels check whatsapp <number>",
354
+ "oalo channels connect whatsapp --bot <bot>",
355
+ "oalo channels connect instagram",
356
+ "oalo channels disconnect whatsapp|instagram <number or @account> --yes",
357
+ ],
358
+ flags: [
359
+ { flag: "--bot <bot>", desc: "The bot's public id (`oalo bots` lists them)." },
360
+ { flag: "--yes", desc: "Go ahead with disconnect. Without it, disconnect only says what it would do." },
361
+ { flag: "--json", desc: "The reply as JSON." },
362
+ ],
363
+ notes: [
364
+ "A WhatsApp number can be given as shown (+54 9 11 5555-0100), as its digits, or by its id; an Instagram account as @username or its id.",
365
+ "WhatsApp numbers and Instagram accounts are kept by the platform, where Meta's messages arrive; your Base asks it. Email is the sending address (no bot answers it); the web chat is each published bot's own widget.",
366
+ "connect prints a link to open in a browser, because Meta's own sign-in has to run there. The WhatsApp link is for you only and works for a few minutes.",
367
+ "Changes (set, connect, disconnect) are for workspace admins. Reading is for admins and members with Developer access. Disconnecting stops the bot answering at once; the conversations stay in the Inbox.",
368
+ ],
369
+ exits: { 0: "done (check: Oalo can send on the number)", 1: "the request failed, or check found it can't send", 2: "bad arguments or no CLI token" },
370
+ },
371
+ {
372
+ name: "setup",
373
+ summary: "The setup agent on your Base makes an app run that Oalo can't run on its own, on a branch you then accept or reject.",
374
+ usage: [
375
+ "oalo setup [--app <slug>]",
376
+ "oalo setup status [--app <slug>] [--json]",
377
+ "oalo setup accept [--app <slug>]",
378
+ "oalo setup reject [--app <slug>]",
379
+ "oalo setup auto on|off [--app <slug>]",
380
+ ],
381
+ flags: [
382
+ { flag: "--app <slug>", desc: "The app. Default: the one saved with `oalo link --app`." },
383
+ { flag: "--json", desc: "The reply as JSON." },
384
+ ],
385
+ notes: [
386
+ "The agent works in the app's folder on a branch, oalo/setup: it reads the repo and adds what's missing (a Procfile, a Dockerfile, .oalo/app.json, a fix for a hard-coded port), the smallest change that makes it run. It never writes a secret; it names the variables to set.",
387
+ "Oalo checks each attempt itself: the app is detected and, for a server app, installed, its `release:` run, and its preview answering. Up to 6 attempts and a dollar budget (shown), then it stops and says why.",
388
+ "When it runs, the preview shows the branch. accept makes its commits the app's code (a fast-forward); reject throws them away. Deploys wait until then.",
389
+ "status has the agent's report: what the app is, what it changed and why, variables to set, whether it can run as several copies, what it couldn't fix.",
390
+ "auto on: a dev deploy that finds nothing runnable, or a preview that doesn't run, starts one by itself. Off by default.",
391
+ "Admins and members with Developer access only.",
392
+ ],
393
+ exits: { 0: "done", 1: "refused, or (status) the last setup failed", 2: "bad arguments or no CLI token" },
394
+ },
395
+ {
396
+ name: "sources",
397
+ summary: "Feeds that send Oalo rows from outside (another system, a webhook, an export), keeping only the fields you choose.",
398
+ usage: [
399
+ "oalo sources [list] [--json]",
400
+ "oalo sources create <name> --from <sample.csv|.json|.jsonl> [--table <new model>] [--id-field <field>] [--mode upsert|snapshot] [--auth bearer|hmac|url] [--env dev|prod]",
401
+ "oalo sources create <name> --table <existing model or table> [--fields a,b] [--id-field <field>] [--auth bearer|hmac|url]",
402
+ " (either form) [--keep-from-ai a,b] [--retention <days>]",
403
+ "oalo sources show <name> [--json]",
404
+ "oalo sources create <name> --bot <bot> [--auth bearer|hmac|url]",
405
+ "oalo sources push <name> <file.csv|.json|.jsonl> [--json]",
406
+ "oalo sources push <name> <doc.pdf|.docx|.md|.html|.txt> [more...] [--json]",
407
+ "oalo sources remove <name> <document>",
408
+ "oalo sources fetch <name> <https url> [--every <minutes>] [--header <\"Name: value\">] [--records-at <path>] [--document <name>] [--now]",
409
+ "oalo sources fetch <name> <postgres url> --relation <schema.view> [--key <column>] [--cursor <column>] [--every <minutes>] [--now]",
410
+ "oalo sources fetch <name> --now | --off",
411
+ "oalo sources fields <name> a,b",
412
+ "oalo sources retention <name> <days|off>",
413
+ "oalo sources rotate <name>",
414
+ "oalo sources delete <name> [--wipe]",
415
+ ],
416
+ flags: [
417
+ { flag: "--from <file>", desc: "create: a sample of what the sender sends. Its fields and their types make the source's fields and a new model (--table, default the source's name)." },
418
+ { flag: "--bot <bot>", desc: "create: send documents into this bot's knowledge (its public id, from oalo bots) instead of rows into a table. Each upload is one document by name; the same name replaces it." },
419
+ { flag: "--table <model>", desc: "create: the model or plain table the rows go to (a table the app made with its own SQL, or an import kept as it is). An existing one keeps all its fields or columns, or the ones in --fields. A plain table's --id-field must be a unique column." },
420
+ { flag: "--keep-from-ai a,b", desc: "create: fields stored and synced but never handed to a bot's AI model (see oalo data keep-from-ai)." },
421
+ { flag: "--retention <days>", desc: "create, a new model only: drop rows the source hasn't sent in this many days (1 to 3650), each night. Rows added by hand stay." },
422
+ { flag: "--fields a,b", desc: "create, existing model: keep only these fields. Anything else a sender sends is dropped, never stored." },
423
+ { flag: "--id-field <field>", desc: "create: the field that identifies a record (default id). A record sent again updates its row." },
424
+ { flag: "--mode snapshot", desc: "create: each upload is the whole set, and rows it leaves out are deleted. Only for a model the source makes. Default upsert." },
425
+ { flag: "--auth bearer|hmac|url", desc: "create: how a sender proves it's allowed. bearer (default): Authorization: Bearer <secret>. hmac: a signature header over the raw body (--hmac-header, default x-signature; --hmac-format sha256=hex, hex or base64), for tools that sign their webhooks. url: the secret is the last part of the address, for tools that can only send to an address." },
426
+ { flag: "--env dev|prod", desc: "create: which copy of the model the rows go to. Default prod." },
427
+ { flag: "--every <minutes>", desc: "fetch: how often Oalo fetches the address, 5 to 10080 minutes. Default 60." },
428
+ { flag: "--header <value>", desc: "fetch: a request header, \"Name: value\" (an API key, say: --header \"Authorization: Bearer $KEY\"). Repeat it for more, up to 10. Sealed on the server; show prints only the names. Left out, the ones set before stay; --header \"\" clears them." },
429
+ { flag: "--records-at <path>", desc: "fetch, a table's source: the records are the list at this key of the JSON answer (data.items)." },
430
+ { flag: "--document <name>", desc: "fetch, a knowledge source: the document's name (default the address's last part)." },
431
+ { flag: "--relation <name>", desc: "fetch, a postgres:// address: the view (or table) to read, like public.orders_for_oalo. Write a view of just the columns to share." },
432
+ { flag: "--key <column>", desc: "fetch, a postgres:// address: the view's column that identifies a row. Default the source's id field." },
433
+ { flag: "--cursor <column>", desc: "fetch, a postgres:// address: a column that grows as rows change (updated_at). Each run then reads only rows past the last one seen, up to 50,000. Without it, each run reads the whole view (up to 200,000 rows), so a snapshot source deletes what the view no longer has." },
434
+ { flag: "--now", desc: "fetch: fetch once now, and say what was stored." },
435
+ { flag: "--off", desc: "fetch: stop fetching." },
436
+ { flag: "--wipe", desc: "delete: also delete the rows, when the source made the model." },
437
+ { flag: "--json", desc: "The reply as JSON." },
438
+ ],
439
+ notes: [
440
+ "A sender posts JSON (an object or an array), NDJSON or CSV to the source's address. Keys and CSV headers are snake-cased as the CSV import does it, so \"Order ID\" and order_id are one field.",
441
+ "The secret is shown when the source is made, and by rotate; it only writes. Each upload answers what it stored, rejected and dropped, is listed in show, and fires source.<name>.received for workflows.",
442
+ "Rows sync to your apps at once and fire the model's row events, as Sync API writes do. Up to 10,000 records a request, 120 requests a minute per source, 200,000 rows a day per workspace.",
443
+ "push sends a file from here, over your CLI token, in batches of 1,000; in snapshot mode the rows missing from the whole file are deleted after its last batch.",
444
+ "A --bot source takes one document an upload: the file as the body named by ?name= (or an X-Document-Name header, or a form upload), or JSON {\"name\", \"text\"}. Its text is read on arrival, so a file without text (a scanned PDF) is refused then. Oalo keeps the text, not the file. DELETE to the address with ?name= removes a document. The bot answers from it once indexed, live at once (bots have no draft knowledge).",
445
+ "fetch from a postgres:// address reads a view in your own database: tested when set (connected, columns read), then on the schedule. Public addresses only, TLS required, a read-only session with a 30 s statement timeout, opened per run. The address, password included, is sealed; show prints it without the password. Pass it from an environment variable (\"$ORDERS_DB_URL\") to keep it out of your shell history.",
446
+ "fetch is for a system that can't post: Oalo GETs the address on a schedule and stores the answer as a post would be (JSON, NDJSON or CSV by its Content-Type; a knowledge source's document). Public https addresses only, up to 3 redirects, 10 MB. An answer that hasn't changed is skipped. show has the last run and its error.",
447
+ "Into a plain table, each record writes only the columns it has, cast by Postgres to the column's type; a value that doesn't fit rejects that record and says why. Snapshot, retention and delete --wipe are for a model the source made.",
448
+ ],
449
+ exits: { 0: "done", 1: "refused, failed, or some records were rejected", 2: "bad arguments or no CLI token" },
450
+ },
451
+ {
452
+ name: "bots",
453
+ summary: "The workspace's bots: the tables each can search, and every setting (knowledge, memory, model, replies, limits).",
454
+ usage: [
455
+ "oalo bots [--json]",
456
+ "oalo bots knowledge <bot> [--json]",
457
+ "oalo bots knowledge <bot> --add <source> [--add ...] [--remove <source>] [--json]",
458
+ "oalo bots tools <bot> [--add <tool>] [--remove <tool>] [--json]",
459
+ "oalo bots settings <bot> [--json]",
460
+ "oalo bots settings <bot> set key=value [key=value ...] [--yes] [--dry-run] [--json]",
461
+ "oalo bots settings --explain [<key> | <group>] [--json]",
462
+ ],
463
+ flags: [
464
+ { flag: "tools --add <tool>", desc: "Let the bot call one of the workspace's tools, by name. Your server apps' own endpoints are listed once a deploy reads their .oalo/app.json (see `oalo docs app-tools`). --remove takes it away." },
465
+ { flag: "--add <source>", desc: "knowledge: attach a searchable table: its handle (model:products, table:prod.faq) or just its name when that's unique. Repeat it, or comma-separate." },
466
+ { flag: "--remove <source>", desc: "Detach one. The table stays searchable for other bots." },
467
+ { flag: "set key=value", desc: "settings: change one or more. A value starting with @ is read from that file (for the frame). none, default or an empty value clears a setting where its help says so." },
468
+ { flag: "--yes", desc: "settings set: apply to a published bot. Without it a published bot's change is shown and not saved." },
469
+ { flag: "--dry-run", desc: "settings set: show what would change; save nothing." },
470
+ { flag: "--explain [key]", desc: "settings: what each setting does, its values and default. A key, a group (knowledge, memory, model, replies, limits, data) or all." },
471
+ { flag: "--json", desc: "The bots, or the bot's sources and what else could be attached, as JSON." },
472
+ ],
473
+ notes: [
474
+ "<bot> is the bot's public id (bot_...), as `oalo bots` prints it.",
475
+ "A source is a table made searchable with `oalo data vectorize <table> --fields ...`. A bot always searches its own knowledge base (documents) too.",
476
+ "The same as ticking a table in the bot's Knowledge pane in the dashboard, with the same access as there.",
477
+ "settings go through the dashboard's own actions, with the same checks and access. A change sets only the settings it names: the model without the provider keeps the provider, one limit keeps the others, one context.* key keeps the rest of the context config.",
478
+ "Every setting is described in `man oalo` (BOT SETTINGS) and by `oalo bots settings --explain`.",
479
+ ],
480
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
481
+ },
482
+ {
483
+ name: "tokens",
484
+ summary: "List, create and revoke the workspace's sync tokens, for an app's own server.",
485
+ usage: [
486
+ "oalo tokens list [--json]",
487
+ "oalo tokens create --name <name> --scope write:<table> [--scope ...] [--days N] [--json]",
488
+ "oalo tokens revoke <id>",
489
+ ],
490
+ flags: [
491
+ { flag: "--name <name>", desc: "What the token is for, shown in the dashboard." },
492
+ { flag: "--scope <scope>", desc: "read, write, read_manifest, read:<table>, write:<table>, events (send events to POST /api/events/v1), workflows (call workflows, POST /api/workflows/v1/<name>/run), chat (sign the chat widget in as your app's user, POST /api/chat/v1/sessions), or env:dev / env:prod (lock it to one env's data: env:dev makes events and workflow calls safe test runs, for running your app on your machine). Repeat it. Prefer the narrowest. An unknown scope is refused." },
493
+ { flag: "--days N", desc: "Expiry in days (default 90; 0 for none)." },
494
+ ],
495
+ notes: [
496
+ "A sync token reads or writes every row of its tables, for every member. It belongs in a server's secrets, never in a mobile or web app.",
497
+ "The token is printed once. Copy it into the app server's secrets right away.",
498
+ ],
499
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
500
+ },
501
+ {
502
+ name: "docs",
503
+ summary: "Read the OALO platform docs from your workspace: list, search, or print a page as markdown.",
504
+ usage: ["oalo docs [--audience developer|trainer] [--json]", "oalo docs <slug> [--json]", "oalo docs --search \"<words>\" [--json]"],
505
+ flags: [
506
+ { flag: "--search <words>", desc: "Pages ranked for the words, each with the line that matched." },
507
+ { flag: "--audience developer|trainer", desc: "Only pages for that reader (pages for both always show)." },
508
+ { flag: "--json", desc: "Structured output: summaries for a list or search, the full page with markdown for a slug." },
509
+ ],
510
+ notes: [
511
+ "Served by your workspace's own server (its Base), so the pages match the version it runs.",
512
+ "Good starting pages for building on OALO: bring-your-repo and connect-your-own-frontend for your own code, app-environment, tokens-and-credentials, data-models, sync-rest-api, triggers-and-chaining.",
513
+ "A page goes to stdout as markdown, so `oalo docs data-models > notes.md` saves it. Related pages are listed on stderr.",
514
+ ],
515
+ exits: { 0: "done", 1: "no such page, or the workspace failed", 2: "no CLI token" },
516
+ },
517
+ {
518
+ name: "apps",
519
+ summary: "List the workspace's apps: type, state, preview and live addresses, and the live release.",
520
+ usage: ["oalo apps [--json]"],
521
+ flags: [{ flag: "--json", desc: "Print the apps as JSON." }],
522
+ notes: ["The first column is the slug, which `oalo deploy --app` and `oalo link --app` take."],
523
+ exits: { 0: "done", 1: "the workspace failed", 2: "no CLI token" },
524
+ },
525
+ {
526
+ name: "deploy",
527
+ summary: "Send this repo's commits to an app and run them: the preview (dev) by default, the live release with --env prod.",
528
+ usage: [
529
+ "oalo deploy [--app <slug>] [--env dev|prod] [--allow-dirty] [--json]",
530
+ "oalo deploy --new [<name>] [--name \"Display name\"]",
531
+ "oalo deploy --new [<name>] --from <https url> [--branch <name>] [--token-env <VAR>]",
532
+ "oalo deploy --app <slug> --pull",
533
+ "oalo deploy --app <slug> --replace",
534
+ "oalo deploy --app <slug> --history",
535
+ "oalo deploy --app <slug> --rollback [--to <sha>]",
536
+ ],
537
+ flags: [
538
+ { flag: "--app <slug>", desc: "The app (`oalo apps` lists them). Default: the app saved with `oalo link --app`." },
539
+ { flag: "--env dev|prod", desc: "dev updates the preview; prod builds and swaps the live release. prod must be on the command line." },
540
+ { flag: "--new [<name>]", desc: "Make a new app from this repo: its type found from the files at the repo's root (no Dockerfile needed), its commits kept as the app's history, its preview set up. The name (default: the repo's folder) becomes its slug, and this project is linked to it. When nothing at the root says what the app is (a monorepo, say), it is made as code only and the deploy says what to add: see `oalo docs bring-your-repo`." },
541
+ { flag: "--name <display name>", desc: "With --new: the app's display name. Default: its slug." },
542
+ { flag: "--from <https url>", desc: "With --new: make the app from a Git repository at this address instead of this folder (no local checkout needed). The Base clones one branch with its whole history (https to a public host only, up to 200 MB, 5 minutes), then makes the app as --new does. The name defaults to the repository's. Nothing is linked here." },
543
+ { flag: "--branch <name>", desc: "With --from: the branch to import. Default: the repository's default branch." },
544
+ { flag: "--token-env <VAR>", desc: "With --from: for a private repository, the environment variable holding an access token that can read it (a GitHub, GitLab, Bitbucket or Forgejo personal access token). Without it, at a terminal, you're asked for the token, hidden, when the host wants one. It is used for that one clone: never stored, logged or kept in the app's repo." },
545
+ { flag: "--pull", desc: "Fetch the app's code as OALO has it into oalo/<slug>, to merge changes made there (by the builder agent or a teammate)." },
546
+ { flag: "--replace", desc: "Make this branch the app's code even though the histories differ. The app's current code is tagged first so it can be recovered." },
547
+ { flag: "--allow-dirty", desc: "Deploy the last commit even though there are uncommitted changes (which are not sent)." },
548
+ { flag: "--history", desc: "The live releases the app has had, newest first." },
549
+ { flag: "--rollback", desc: "Make the release before the current one live again (or --to <sha>). Again goes further back; it never returns to a release you rolled back from. It is built and checked like any live deploy; a failing one leaves the current release live." },
550
+ { flag: "--to <sha>", desc: "With --rollback: the release to make live (`--history` lists them)." },
551
+ ],
552
+ notes: [
553
+ "What is sent is the current branch's commits, as a git bundle, signed in with your CLI token. No git remote or extra credential is needed. Uncommitted changes are never sent.",
554
+ "The app only moves forward. If it has commits your repo lacks, the deploy stops and lists them: run --pull, merge, deploy again.",
555
+ "dev: dependencies are reinstalled when package.json, a lockfile, requirements.txt, mix.exs or pubspec.yaml changed, then the preview restarts.",
556
+ "prod: the app is built (install, then its build script), a Procfile `release:` line runs, and the live release swaps over once the new one answers on its port. A failed build or release command leaves the old release live; a release that crashes or never answers is removed, and the old one keeps serving (the error shows its last lines).",
557
+ "How a server app starts: its Procfile (`web:`, workers, `release:`), else its package.json `start` (`dev` in the preview), else the runtime's default (index.js, main.py, app.py, manage.py, mix).",
558
+ "--from is what the dashboard's Add app, From Git does: the same app, the same type detection, secret scan and preview. A token is never a flag value, since that stays in your shell history.",
559
+ "Admins and members with Developer access only. Progress streams while it runs.",
560
+ ],
561
+ exits: { 0: "deployed", 1: "refused or failed", 2: "bad arguments, no CLI token, or not in a git repo" },
562
+ },
563
+ {
564
+ name: "env",
565
+ summary: "An app's environment variables on OALO (API keys, settings), per env: list, set, unset, rotate-token.",
566
+ usage: [
567
+ "oalo env [--app <slug>] [--env dev|prod] [--json]",
568
+ "oalo env set NAME=value [NAME=value ...] [--secret | --public] [--no-restart]",
569
+ "oalo env set NAME --secret",
570
+ "oalo env set --file .env.production --env prod",
571
+ "oalo env unset NAME [NAME ...]",
572
+ "oalo env rotate-token [--env dev|prod]",
573
+ ],
574
+ flags: [
575
+ { flag: "--app <slug>", desc: "The app (`oalo apps` lists them). Default: the app saved with `oalo link --app`." },
576
+ { flag: "--env dev|prod", desc: "dev is the preview, prod the live app. Each has its own variables. Changing prod needs --env prod on the command line." },
577
+ { flag: "--secret", desc: "Store as secret: never shown again, and only server apps get it. A NAME with no =value asks for the value without echoing it (or reads stdin when piped)." },
578
+ { flag: "--public", desc: "Store as public: shown by `oalo env`, and given to static apps too." },
579
+ { flag: "--file <path>", desc: "Set every NAME=value line of a .env file in one step (all or none)." },
580
+ { flag: "--no-restart", desc: "Don't restart the preview (dev) or relaunch the live app (prod) after this change." },
581
+ ],
582
+ notes: [
583
+ "A variable is secret unless its name starts with VITE_, EXPO_PUBLIC_, NEXT_PUBLIC_, PUBLIC_ or REACT_APP_, or it's set with --public.",
584
+ "Server apps get every variable, in the preview and live. Static apps (React, Expo, Flutter) only get public ones, because whatever their build sees ends up in the browser.",
585
+ "Names Oalo sets itself can't be set: any starting with OALO_, PORT, DATABASE_URL, APP_SCHEMA, APP_SLUG, CONTAINER_HOST (and their VITE_/EXPO_PUBLIC_ forms), and the process basics (PATH, HOME, LANG, TZ, the Mix, Hex and Flutter homes). `oalo env set` refuses one by name.",
586
+ "A server app gets OALO_URL and OALO_SYNC_TOKEN: a Sync API token for its own env's data that can also send events, call workflows and start chat sessions, so it needs no token of its own. It also signs OALO's calls to the app's endpoints (.oalo/app.json tools). The preview's token can't touch live data, and its events and workflow calls are test runs. `rotate-token` replaces it at once and restarts the app on the new one.",
587
+ "A dev change restarts the preview. A prod change relaunches the live app at the commit it already runs (a build; no new code goes out). --no-restart skips that, to batch several changes: the last one without it relaunches.",
588
+ "Admins and members with Developer access only.",
589
+ ],
590
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
591
+ },
592
+ {
593
+ name: "secrets",
594
+ summary: "Keys and tokens found committed to an app's code (on import, and in each push or deploy).",
595
+ usage: ["oalo secrets [--app <slug>] [--all] [--json]", "oalo secrets dismiss <fingerprint> [--app <slug>]"],
596
+ flags: [
597
+ { flag: "--app <slug>", desc: "The app (`oalo apps` lists them). Default: the app saved with `oalo link --app`." },
598
+ { flag: "--all", desc: "Dismissed findings too." },
599
+ { flag: "--json", desc: "The findings as JSON." },
600
+ ],
601
+ notes: [
602
+ "Only where each one is and what it looks like: no value is ever kept.",
603
+ "For each: rotate it with its provider (it stays in the repo's history), set the new one with `oalo env set NAME --secret`, and read it from the environment in the code.",
604
+ "Admins and members with Developer access only.",
605
+ ],
606
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
607
+ },
608
+ {
609
+ name: "dockerfile",
610
+ summary: "How a server app's live image is built, and the Dockerfile it builds from.",
611
+ usage: ["oalo dockerfile [--app <slug>] [--json]", "oalo dockerfile --write [--force] [--app <slug>]"],
612
+ flags: [
613
+ { flag: "--app <slug>", desc: "The app (`oalo apps` lists them). Default: the app saved with `oalo link --app`." },
614
+ { flag: "--write", desc: "Save OALO's Dockerfile as ./Dockerfile, to change. Once committed, the repo's copy is used." },
615
+ { flag: "--force", desc: "With --write, replace an existing ./Dockerfile." },
616
+ { flag: "--json", desc: "method, source (repo or oalo), runtime and the Dockerfile's text." },
617
+ ],
618
+ notes: [
619
+ "The first that fits: the repo's own Dockerfile; OALO's for the runtime, for the languages buildpacks can't build; buildpacks for the rest. `oalo docs live-server-apps` lists which language gets which.",
620
+ "Every image runs as uid 1000 with a read-only filesystem (/tmp is writable) and must serve $PORT. A Procfile's lines still name the processes: web runs the image's own command when the Procfile has no web line.",
621
+ ],
622
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
623
+ },
624
+ {
625
+ name: "logs",
626
+ summary: "What an app's own processes printed (server, workers, release command), per env.",
627
+ usage: ["oalo logs [--app <slug>] [--env dev|prod] [--process web|<worker>] [--limit N] [--follow] [--json]"],
628
+ flags: [
629
+ { flag: "--app <slug>", desc: "The app (`oalo apps` lists them). Default: the app saved with `oalo link --app`." },
630
+ { flag: "--env dev|prod", desc: "dev is the preview, prod the live app. Default dev." },
631
+ { flag: "--process <name>", desc: "Only this process: web, a Procfile worker's name, release, build (a live build's output, with --env prod), or oalo (OALO's own notes)." },
632
+ { flag: "--limit N", desc: "How many recent lines (default 200, at most 2000)." },
633
+ { flag: "--follow", desc: "Keep printing new lines as they come, until Ctrl-C." },
634
+ { flag: "--json", desc: "One JSON object per line: at, process, line." },
635
+ ],
636
+ notes: [
637
+ "The workspace keeps the last 2,000 lines per env in memory; restarting the workspace empties them.",
638
+ "Lines from `oalo` are OALO's own: which command started, and when a process exited and with what status.",
639
+ "Admins and members with Developer access only: an app can print secrets or customers' details.",
640
+ ],
641
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
642
+ },
643
+ {
644
+ name: "loops",
645
+ summary: "Test a loop (sends simulated), see its past runs, turn it on or off, or delete it, without pulling and pushing it.",
646
+ usage: [
647
+ "oalo loops test <name> [--sample latest|example] [--input <json|@file.json>] [--json]",
648
+ "oalo loops on <name>",
649
+ "oalo loops off <name>",
650
+ "oalo loops delete <name>",
651
+ "oalo loops runs [<name>] [--limit N] [--json]",
652
+ "oalo loops runs --id <run> [--json]",
653
+ ],
654
+ flags: [
655
+ { flag: "--sample latest|example", desc: "Test with the loop's most recent real event, or one built from its trigger's fields." },
656
+ { flag: "--input <json|@file>", desc: "Test with this input instead: an event envelope {type, contact_id, payload}, or what a scheduled loop's first step reads." },
657
+ { flag: "--app <slug>", desc: "Only for a loop pinned to one app: that app's slug (`oalo apps`) or id. Works with test, runs, on, off and delete." },
658
+ { flag: "--id <run>", desc: "runs: one run step by step, with each step's output." },
659
+ { flag: "--limit N", desc: "runs: at most this many (default 20)." },
660
+ { flag: "--json", desc: "Print the test result as JSON." },
661
+ ],
662
+ notes: [
663
+ "A test runs every step. Steps that send messages or change records are simulated, and their output says what they would have done. Custom tools and AI steps run for real. It works on a loop that is off.",
664
+ "A loop that is on runs for real whenever its triggers fire. Starting a run now, for real: `oalo run loop <name>`.",
665
+ "Deleting can't be undone; turning a loop off keeps it.",
666
+ ],
667
+ exits: { 0: "done (a test passed)", 1: "refused, failed, or the test run failed", 2: "bad arguments or no CLI token" },
668
+ },
669
+ {
670
+ name: "flows",
671
+ summary: "Turn a chat flow on or off, or delete it, by bot and flow name, without pulling and pushing it.",
672
+ usage: ["oalo flows on <bot> <flow>", "oalo flows off <bot> <flow>", "oalo flows delete <bot> <flow>"],
673
+ flags: [],
674
+ notes: [
675
+ "<bot> is the bot's public id (the folder under .oalo/flows/ after `oalo pull`, or the MCP tool oalo_list_bots), <flow> its name or id.",
676
+ "A flow that is on can start in real conversations. A call_other_flow step only runs a flow that is on.",
677
+ "Deleting can't be undone; turning a flow off keeps it.",
678
+ ],
679
+ exits: { 0: "done", 1: "refused or failed", 2: "bad arguments or no CLI token" },
680
+ },
681
+ {
682
+ name: "pull",
683
+ summary: "Fetch chat flows, loops and code tools into .oalo/ as files you can edit.",
684
+ usage: ["oalo pull [flows|loops|tools] [--bot <id>] [--name <name>] [--force]"],
685
+ flags: [
686
+ { flag: "--bot <id>", desc: "Only this bot's flows (numeric or public bot_ id)." },
687
+ { flag: "--name <name>", desc: "Only the flow or loop with this name." },
688
+ { flag: "--force", desc: "Overwrite files you have edited since the last pull." },
689
+ ],
690
+ notes: [
691
+ "Writes .oalo/flows/<bot>/<flow>.json, .oalo/loops/<loop>.json (loops pinned to an app under loops/apps/<app>/), .oalo/tools/<tool>.js + .json for code tools, and the node contracts .oalo/catalog/flow.json and loop.json.",
692
+ "Only code (JavaScript) tools are pulled; webhooks, built-ins and Elixir actions are configured in the dashboard. A tool's code is compiled when pushed, so code that doesn't build is refused.",
693
+ "A loop file also lists the loop's `triggers` (what starts it). Add or delete entries and push: push adds the new ones and removes the ones you deleted, and keeps any a teammate added since your pull. Set a trigger's `enabled` to turn it on or off.",
694
+ "Each file holds the editable object (`flow` or `loop`) plus the CLI's bookkeeping: id, version (sent back as base_version on push) and base_hash (how the CLI knows you edited it). Edit the object; leave the bookkeeping alone.",
695
+ "A file you have edited is kept, not overwritten, unless you pass --force.",
696
+ "To create a flow or loop, write a file in the same shape without id or version, then push it.",
697
+ "Needs a CLI token.",
698
+ ],
699
+ exits: { 0: "pulled", 1: "pull failed", 2: "bad arguments or no token" },
700
+ },
701
+ {
702
+ name: "status",
703
+ summary: "Show which flow and loop files changed, locally or on the server.",
704
+ usage: ["oalo status [<file>...] [--json]"],
705
+ flags: [{ flag: "--json", desc: "One JSON array of {path, state, note}." }],
706
+ notes: [
707
+ "States: new (never pushed), clean, modified (push it), behind (the server has a newer version: pull it), conflict (both changed: merge by hand), gone (deleted on the server), invalid (not a readable file), error (the server check failed; the note says why).",
708
+ "Needs a CLI token.",
709
+ ],
710
+ exits: { 0: "nothing in conflict", 1: "a conflict, an invalid file, or a failed check", 2: "no token" },
711
+ },
712
+ {
713
+ name: "lint",
714
+ summary: "Check flow and loop files without saving anything.",
715
+ usage: ["oalo lint [<file>...] [--verbose] [--json]"],
716
+ flags: [
717
+ { flag: "--verbose", desc: "Also show info-level findings." },
718
+ { flag: "--json", desc: "One JSON object per file: {path, findings}." },
719
+ ],
720
+ notes: [
721
+ "The same checks the server runs before a strict save: node types and keys against the catalog, required settings, edges and conditions, references between steps, and tools the flow or loop calls.",
722
+ "Each finding names the node, what is wrong and how to fix it.",
723
+ "Needs a CLI token.",
724
+ ],
725
+ exits: { 0: "no errors (warnings allowed)", 1: "errors found", 2: "no token" },
726
+ },
727
+ {
728
+ name: "push",
729
+ summary: "Lint and save edited flow, loop and tool files, guarded against concurrent edits.",
730
+ usage: ["oalo push [<file>...] [--dry-run] [--no-strict] [--force] [--all] [--verbose]"],
731
+ flags: [
732
+ { flag: "--dry-run", desc: "Lint only; save nothing." },
733
+ { flag: "--no-strict", desc: "Save even with error-level findings. Structural damage is still refused." },
734
+ { flag: "--force", desc: "Drop the version guard: overwrite whatever the server has." },
735
+ { flag: "--all", desc: "Push every file, not only the edited ones." },
736
+ { flag: "--verbose", desc: "Also show info-level findings." },
737
+ ],
738
+ notes: [
739
+ "Without files, pushes every file edited since its pull (and new ones).",
740
+ "Each save sends the pulled version: if someone saved in between, the push is refused as a conflict and nothing is overwritten. Pull, merge, push again.",
741
+ "Saves are strict: error-level lint findings block them. Fix the findings (each says how) or pass --no-strict.",
742
+ "After a save the file is rewritten from the server's copy with the new version.",
743
+ "Needs a CLI token.",
744
+ ],
745
+ exits: { 0: "every file saved", 1: "a file was refused, in conflict, or failed", 2: "bad arguments or no token" },
746
+ },
747
+ {
748
+ name: "qa",
749
+ summary: "Run a scripted test conversation with a bot and print the transcript.",
750
+ usage: [
751
+ 'oalo qa <bot> "<message>" ["<message>" ...] [--channel web|whatsapp|instagram|sms] [--flow <name>] [--timeout <s>] [--json]',
752
+ "oalo qa <bot> --script <file.json>",
753
+ ],
754
+ flags: [
755
+ { flag: "--channel <name>", desc: "Act as this channel: web (default), whatsapp, instagram, sms, embed. Changes how the bot behaves (text mode vs pickers)." },
756
+ { flag: "--flow <name>", desc: "Start this flow with the first message, even while it's off, so a new or changed flow is tested before it's turned on." },
757
+ { flag: "--script <file>", desc: 'A JSON file: {"channel": "whatsapp", "flow": "<name>", "messages": ["...", "..."]}.' },
758
+ { flag: "--timeout <s>", desc: "How long one turn may take to settle. Default 90, at most 300." },
759
+ { flag: "--label <text>", desc: "Tag stored on the test conversation." },
760
+ { flag: "--json", desc: "Print the whole run as JSON: turns (replies, flow state, ms, settled), flow_path, cost_usd." },
761
+ ],
762
+ notes: [
763
+ "The conversation is a preview with no phone number or external id: the inbox leaves it out and nothing is sent to WhatsApp, Instagram or SMS.",
764
+ "Everything else is real: the bot's tools and flows run, collected contact details are saved, handoffs happen. Use a test bot, or flows whose side effects you are fine repeating.",
765
+ "Each message waits until the bot settles (no new message and no running flow), then the next is sent. Any flow still active at the end is cancelled and the conversation is closed.",
766
+ "At most 20 messages per run. bot accepts a numeric id or a public bot_ id.",
767
+ "Needs a CLI token.",
768
+ ],
769
+ exits: { 0: "every turn settled", 1: "a turn timed out or the run failed", 2: "bad arguments or no token" },
770
+ },
771
+ {
772
+ name: "rehearse",
773
+ summary: "Rehearse inbound WhatsApp conversations end to end: routing, triage and the bot's reply.",
774
+ usage: ["oalo rehearse [--number <phone_number_id>] [--responder <bot_...>] [--timeout <s>] [--json]", "oalo rehearse --script <file.json>"],
775
+ flags: [
776
+ { flag: "--number <id>", desc: "The WhatsApp number (phone_number_id) the customer writes to. Default: the workspace's default number." },
777
+ { flag: "--responder <bot_...>", desc: "The bot that should end up answering, by its public id (`oalo bots` lists them). Default: the bot an enabled triage loop hands threads to, else the number's bot." },
778
+ { flag: "--script <file>", desc: 'A JSON file: {"scenarios": [{"name", "messages", "known_contact", "opted_out", "open_thread": {"bot_id", "number", "hours_ago", "tags", "assigned", "deal"}, "expect": {"reply", "bot_id", "assigned"}}]}.' },
779
+ { flag: "--timeout <s>", desc: "How long one message may take to settle. Default 120, at most 300." },
780
+ { flag: "--json", desc: "Print every scenario as JSON: turns (what was sent), threads, loop runs and their steps, checks, why_silent, cost_usd." },
781
+ ],
782
+ notes: [
783
+ "Without --script it runs the standard scenarios: a new customer, a known contact whose thread is still open on another number, returning the same day, returning after 24 hours, writing again after their deal was won, writing again after opting out, handed to a teammate earlier today, and with a teammate but writing from another number.",
784
+ "Each message goes through the same code as a real WhatsApp message. The customer is a fictional 555-01XX number: sends to it are recorded, never posted, its threads stay out of the Inbox, and assignment emails, team-chat cards and workflow notices are skipped.",
785
+ "Real: LLM calls and their cost, loop runs in the run history. At the end its threads are closed and its contact removed.",
786
+ "A FAIL prints why the customer got no reply (opted out, a failed triage step, autopilot off, assigned to a teammate).",
787
+ "Needs a CLI token.",
788
+ ],
789
+ exits: { 0: "every scenario passed", 1: "a scenario failed or the run failed", 2: "bad arguments or no token" },
790
+ },
791
+ {
792
+ name: "skill",
793
+ summary: "Teach this project's coding agent how to work on OALO.",
794
+ usage: ["oalo skill [--agents]", "oalo skill --print"],
795
+ flags: [
796
+ { flag: "--agents", desc: "Also write the rules into AGENTS.md (between oalo markers), for agents other than Claude Code." },
797
+ { flag: "--print", desc: "Print the skill instead of installing it." },
798
+ ],
799
+ notes: [
800
+ "Installs .claude/skills/oalo/SKILL.md, which Claude Code loads on its own: the pull, lint, push, qa loop, and the rules (read the node catalog, never force past a conflict, test on the customer's channel, test conversations do real work).",
801
+ "The skill ships with this CLI version, and the installed copy says which. Re-running replaces it, and the AGENTS.md section when there is one, nothing else. Commit both.",
802
+ "`oalo link` installs it too. After a CLI update, the first command in a project with an older copy says so, once; `oalo skill` updates it.",
803
+ ],
804
+ exits: { 0: "installed or printed" },
805
+ },
806
+ {
807
+ name: "mcp",
808
+ summary: "Serve the workspace over MCP on stdio, for Claude Code or Cursor.",
809
+ usage: ["oalo mcp"],
810
+ flags: [],
811
+ notes: [
812
+ "stdout is the MCP protocol stream. Diagnostics go to stderr.",
813
+ "Exposes a curated tool set plus an oalo_call passthrough; the build surface has ~442 events and mapping them all would wreck tool selection.",
814
+ "Reads OALO_URL and OALO_CLI_TOKEN, or falls back to what `oalo link --cli-token` saved.",
815
+ "Needs a CLI token.",
816
+ ],
817
+ exits: { 0: "stopped cleanly", 1: "no workspace or CLI token" },
818
+ },
819
+ {
820
+ name: "coord",
821
+ // Not in the npm package yet: it needs Bun (bun:sqlite, TypeScript), so
822
+ // it's left out of help, the man page and help --json until it ships as a
823
+ // binary. `bun src/index.js coord` still runs it in development.
824
+ hidden: true,
825
+ summary: "Coordinate several agents on one codebase: task board, worktrees, merge queue.",
826
+ usage: [
827
+ "oalo coord init [--main <branch>] [--test-cmd <cmd>] [--merge auto|manual] [--signal-review]",
828
+ "oalo coord mcp",
829
+ "oalo coord board [--json]",
830
+ "oalo coord events [--follow] [--since <id>] [--json]",
831
+ "oalo coord claim <task>",
832
+ "oalo coord merge [--all]",
833
+ "oalo coord approve plan | merge <task> | signal <task> <milestone> | proposal <id>",
834
+ "oalo coord reject proposal <id>",
835
+ "oalo coord hooks install [--claude] [--opencode]",
836
+ "oalo coord hook <pre-tool|stop|subagent-stop|check-write|check-stop>",
837
+ ],
838
+ flags: [
839
+ { flag: "--main <branch>", desc: "init: branch the merge queue integrates into. Default: current branch." },
840
+ { flag: "--test-cmd <cmd>", desc: "init: merge-gate test command. Default: detected (mix test, npm test, ...)." },
841
+ { flag: "--merge auto|manual", desc: "init: manual holds each merge for `approve merge`." },
842
+ { flag: "--signal-review", desc: "init: milestones unblock dependents only after `approve signal`." },
843
+ { flag: "--json", desc: "board/events: machine-readable output." },
844
+ { flag: "--follow", desc: "events: keep streaming." },
845
+ { flag: "--all", desc: "merge: keep merging until the queue is idle or blocked." },
846
+ ],
847
+ notes: [
848
+ "Local only: the board is .oalo/coord.db in the project; no server, no account.",
849
+ "Every agent session attaches `oalo coord mcp`. The manager plans tasks; workers claim them in their own git worktree.",
850
+ "Human gates are CLI-only so no agent can approve its own work: approve plan, merge, signal, scope proposals.",
851
+ "`hooks install` adds the guards: edits only inside your task's worktree and write set; no stopping while holding a claim; the manager waits for its inbox instead of polling.",
852
+ "Requires the oalo binary (Bun runtime); the other commands also run under Node.",
853
+ ],
854
+ exits: { 0: "ok", 1: "operation refused or failed (message on stderr)", 2: "bad arguments", 3: "no board here; run `oalo coord init`" },
855
+ },
856
+ ];
857
+
858
+ export const ENV_VARS = [
859
+ { name: "OALO_URL", desc: "Workspace URL; overrides the linked config." },
860
+ { name: "OALO_SYNC_TOKEN", desc: "Sync token; overrides the stored one." },
861
+ { name: "OALO_CLI_TOKEN", desc: "CLI token; overrides the stored one." },
862
+ { name: "OALO_HOME", desc: "Relocate the credential store (CI, containers, tests)." },
863
+ { name: "OALO_DEBUG", desc: "Log channel transport activity to stderr." },
864
+ { name: "NO_COLOR", desc: "Disable colour in tail output." },
865
+ ];
866
+
867
+ const pad = (s, n) => s + " ".repeat(Math.max(0, n - s.length));
868
+
869
+ /** The commands `help`, `help --json` and the man page show (not the hidden ones). */
870
+ export const PUBLIC = COMMANDS.filter((c) => !c.hidden);
871
+
872
+ export function overview() {
873
+ const lines = [`oalo: ${TAGLINE}`, ``, `Commands:`];
874
+ for (const c of PUBLIC) lines.push(` ${pad(c.name, 10)} ${c.summary}`);
875
+ lines.push(
876
+ ``,
877
+ `Run \`oalo help <command>\` for detail, or \`oalo help --json\` for a`,
878
+ `machine-readable description of every command, flag and exit code.`,
879
+ ``,
880
+ `Two token kinds, NOT interchangeable:`,
881
+ );
882
+ for (const t of TOKENS) {
883
+ lines.push(` ${pad(t.kind, 5)} ${t.plane}`);
884
+ lines.push(` from ${t.where}`);
885
+ lines.push(` used by ${t.usedBy.join(", ")}`);
886
+ }
887
+ return lines.join("\n");
888
+ }
889
+
890
+ export function commandHelp(name) {
891
+ const c = COMMANDS.find((x) => x.name === name);
892
+ if (!c) return null;
893
+
894
+ const lines = [`oalo ${c.name}: ${c.summary}`, ``, `Usage:`];
895
+ for (const u of c.usage) lines.push(` ${u}`);
896
+
897
+ if (c.flags.length) {
898
+ lines.push(``, `Flags:`);
899
+ const w = Math.max(...c.flags.map((f) => f.flag.length));
900
+ for (const f of c.flags) lines.push(` ${pad(f.flag, w)} ${f.desc}`);
901
+ }
902
+
903
+ if (c.notes.length) {
904
+ lines.push(``, `Notes:`);
905
+ for (const n of c.notes) lines.push(` - ${n}`);
906
+ }
907
+
908
+ lines.push(``, `Exit codes:`);
909
+ for (const [code, meaning] of Object.entries(c.exits)) lines.push(` ${code} ${meaning}`);
910
+ return lines.join("\n");
911
+ }
912
+
913
+ /** Machine-readable. Intended for an agent deciding how to drive the CLI. */
914
+ export function helpJson() {
915
+ return JSON.stringify(
916
+ { name: "oalo", tagline: TAGLINE, commands: PUBLIC, tokens: TOKENS, env: ENV_VARS },
917
+ null,
918
+ 2,
919
+ );
920
+ }