@oalo-ai/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@oalo-ai/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Wire a local app into your OALO workspace: link a token, generate typed bindings, and serve the build surface over MCP.",
5
+ "type": "module",
6
+ "bin": {
7
+ "oalo": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "plugin",
12
+ "man",
13
+ "README.md",
14
+ "LICENSE",
15
+ "!src/coord",
16
+ "!src/commands/coord.ts"
17
+ ],
18
+ "man": [
19
+ "./man/oalo.1"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "smoke": "node test/smoke.mjs",
26
+ "smoke:mcp": "node test/mcp-smoke.mjs",
27
+ "test:coord": "bun test test/coord.test.ts",
28
+ "test:unit": "node --test test/*.test.mjs",
29
+ "man": "node scripts/man.mjs"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.0.0",
36
+ "phoenix": "^1.7.0",
37
+ "ws": "^8.16.0"
38
+ },
39
+ "keywords": [
40
+ "oalo",
41
+ "mcp",
42
+ "sync",
43
+ "codegen"
44
+ ],
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "oalo",
3
+ "description": "Work on an Oalo workspace from Claude Code: edit chat flows, loops and code tools, lint and push them safely, and run test conversations with a bot.",
4
+ "version": "0.1.0",
5
+ "author": { "name": "Oalo" },
6
+ "mcpServers": {
7
+ "oalo": {
8
+ "command": "npx",
9
+ "args": ["-y", "@oalo-ai/cli", "mcp"]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,414 @@
1
+ ---
2
+ name: oalo
3
+ description: Build and change an Oalo workspace (the person's Oalo Base) from the terminal with the oalo CLI or its MCP server: chat flows, loops (automations), code tools, bots and what they know, data models and rows, hosting a repo and its environment variables, and what an app gets from Oalo (its own token, events into workflows, workflows as functions, its endpoints as bot tools, a chat widget, member sign-in and sync). Use when editing a flow, loop or tool, testing how a bot answers, moving data in or out of Oalo, bringing or deploying an app, or wiring an app's own code to Oalo.
4
+ ---
5
+
6
+ # Working on an Oalo workspace
7
+
8
+ Oalo runs chatbots, chat flows, loops (background automations) and code tools in
9
+ the cloud. You edit their definitions locally and push them; they run on the
10
+ server. A push is a deploy: a saved flow or loop is live for real customers the
11
+ moment it saves, with no staging copy. (Data and apps do have two: dev and prod.)
12
+
13
+ Don't guess a command, flag or endpoint: `oalo help <command>` has every flag,
14
+ `oalo docs --search "<words>"` finds Oalo's own pages, and `oalo call --list
15
+ <word>` finds anything the commands don't cover.
16
+
17
+ ## The loop
18
+
19
+ ```sh
20
+ oalo pull # flows, loops and code tools into .oalo/, plus the node catalogs
21
+ # edit .oalo/flows/<bot>/<flow>.json, .oalo/loops/<loop>.json, .oalo/tools/<tool>.js
22
+ oalo lint # check every changed file; nothing is saved
23
+ oalo push # lint, then save with the version you pulled (strict)
24
+ oalo qa <bot> "hola" "..." --channel whatsapp # talk to the bot, read the transcript
25
+ oalo status # what changed locally and on the server
26
+ ```
27
+
28
+ Over MCP the same steps are `oalo_node_catalog`, `oalo_get_flow` / `oalo_get_loop` /
29
+ `oalo_get_tool_source`, `oalo_lint_flow` / `oalo_lint_loop`, `oalo_save_flow` /
30
+ `oalo_save_loop` / `oalo_save_tool_source`, `oalo_test_conversation` and
31
+ `oalo_test_loop`.
32
+
33
+ ## Rules
34
+
35
+ 1. **Never guess a node's keys.** The contract is `.oalo/catalog/flow.json` and
36
+ `.oalo/catalog/loop.json` (or `oalo_node_catalog`): every node type, its keys,
37
+ which are required, allowed values, what it outputs, and the edge and
38
+ reference rules. The runner ignores keys it doesn't know, so a misspelled
39
+ key silently does nothing.
40
+ 2. **Edit the content, not the bookkeeping.** In a pulled file, change `flow`,
41
+ `loop` or `tool` (and a tool's `.js`), and a loop's `triggers`. Leave `id`,
42
+ `version`, `base_hash` and `trigger_base` alone: they are how push detects a
43
+ concurrent edit and what you changed.
44
+ 3. **Keep what you don't change.** A save replaces the whole graph. Keep every
45
+ node and edge, and the editor's keys (`x`, `y`, `position`, `label`) so the
46
+ visual editor still lays it out.
47
+ 4. **Fix every lint error before pushing.** Each finding names the node, what
48
+ is wrong and how to fix it. Read the warnings too: most are real (a condition
49
+ that can never match, a reference to a field a step doesn't produce).
50
+ 5. **Never force your way past a conflict.** A conflict means someone saved
51
+ after you pulled. Push saves their copy next to yours (`*.server.json`, or
52
+ `*.server.js` for a tool): merge your change into it and push again. Use
53
+ `--force` only when you have checked their change can be dropped. A code
54
+ tool is the exception: after merging their `.server.js` into your `.js`,
55
+ `oalo push <file> --force` is the right step (a tool has no version to
56
+ re-base on).
57
+ 6. **Test after every change** with `oalo qa`, using the channel customers use.
58
+ WhatsApp, Instagram and SMS run in text mode (no pickers or forms), so a flow
59
+ that works on web can still fail there. `--flow <name>` starts that flow with
60
+ the first message even while it's off, so test a new flow before anyone
61
+ turns it on.
62
+ 7. **Test conversations do real work.** They never reach a customer's phone,
63
+ but the bot's tools and flows run: contact details are saved, handoffs
64
+ happen. Use a test bot, or flows whose side effects are fine to repeat.
65
+ Test a loop with `oalo loops test <name>`: sends are simulated.
66
+ To test a loop's trigger, `oalo emit <event> --dry-run` (MCP: `oalo_emit`
67
+ with `dry_run`) lists the loops an event would fire. Without `--dry-run` it
68
+ fires them for real, and they may email or message real contacts, so ask
69
+ the person before a real emit.
70
+ 8. **Customer-facing words are product copy.** Keep the bot's language and
71
+ tone, and never show customers internal names: SKUs, codes, step ids, tool
72
+ names.
73
+
74
+ ## Flows and loops differ
75
+
76
+ | | Chat flow | Loop |
77
+ |---|---|---|
78
+ | First step | `graph.entry` (required) | `entry` if set, else the first node no edge points to |
79
+ | Reading an earlier step | `{{state.<id>.<field>}}` | `{{<id>.<field>}}`; the previous step is `{{input}}` |
80
+ | Several conditions match | the first match wins | every match is followed |
81
+ | No condition matches, no default edge | the flow ends quietly | the run fails |
82
+ | Action settings | keys on the node | inside `params` |
83
+
84
+ Edge conditions read the source step's own output, with no prefix:
85
+ `_ok == false`, `count > 0`, `decision == "refund"`.
86
+
87
+ ## Starting, chaining, on and off
88
+
89
+ ```sh
90
+ oalo loops test welcome --sample example # run every step, sends simulated
91
+ oalo loops on welcome # or off; MCP: oalo_set_loop_enabled
92
+ oalo flows on <bot> quote_helper # or off; MCP: oalo_set_flow_enabled
93
+ ```
94
+
95
+ 1. **What starts a flow** is its `trigger`: `llm_tool` (the bot decides, from
96
+ `trigger_instructions`), `manual` (only another flow or the API),
97
+ `on_media_upload`, plus `auto_start_on_open` for a web chat.
98
+ 2. **What starts a loop** is its triggers, kept apart from its definition:
99
+ `triggers` in a pulled loop file (push adds and removes them), or
100
+ `oalo_loop_triggers` (catalog, list, add, remove). Types: `event` (with an
101
+ optional payload `filter`), `scheduled` (cron, UTC), `date_field` (a
102
+ contact's date), `widget_submit`, `exit`, and `api` (an app calls the loop:
103
+ `oalo docs workflow-calls`). An event is one from the catalog, an `app.*`
104
+ name an app sends (fine before it first sends it), `data.<table>.created` or
105
+ `.updated` (fires once `oalo data events <table> created` is on), or
106
+ `source.<name>.received`. Don't invent other names.
107
+ **To stop a drip once the contact does what it's for** (books, pays,
108
+ replies), add an `exit` trigger on that event (`appointment.booked`,
109
+ `order.created`, ...). It never starts a run: it cancels the contact's
110
+ pending waits in that loop, so the later steps never happen.
111
+ 3. **Chaining.** A flow runs another flow of the same bot with a
112
+ `call_other_flow` step. Give the helper `trigger: "manual"` and turn it on,
113
+ because a call can't run a flow that is off. A loop starts another loop with
114
+ a `function_call` to `enroll_in_loop` (now, for the contact), and the other
115
+ loop must be on: enrolling into a loop that is off is refused. A
116
+ `schedule_event` step fires a catalog event later, which starts every loop
117
+ with that trigger.
118
+ 4. **New and changed work stays off until the person says to turn it on.**
119
+ Turning something off when asked is safe. Delete only when they ask: it
120
+ can't be undone, and off keeps it.
121
+ 5. **Test a loop with `oalo loops test`** (MCP: `oalo_test_loop`), not by
122
+ running it: `oalo run loop` and a real `oalo emit` send real messages. In a
123
+ loop, `{{trigger.payload.x}}` is the event in any step; `{{input}}` is only
124
+ the previous step's output.
125
+ 6. `oalo docs triggers-and-chaining` has the whole picture: every trigger, what
126
+ on and off mean for each, and the common mistakes.
127
+
128
+ ## When something went wrong, and what flows and loops use
129
+
130
+ ```sh
131
+ oalo loops runs welcome_series # its recent runs; MCP: oalo_loop_runs
132
+ oalo loops runs --id 4812 # one run, step by step
133
+ ```
134
+
135
+ 1. **Look at what ran before changing anything.** A loop's runs and each
136
+ step's output: `oalo_loop_runs` (`name`, then `run_id`). A flow's runs:
137
+ `oalo_flow_runs` (then `run_id`). One real conversation with its flow
138
+ steps: `oalo_get_conversation`. Find the step that failed or the condition
139
+ that didn't match, then fix that.
140
+ 2. **Conversations are real customers'.** Read only what the task needs, and
141
+ never copy their details into files, code or your reply.
142
+ 3. **The pieces a flow or loop names must exist:**
143
+ - a widget (form or picker) a flow shows: `oalo_widgets`. `fields`
144
+ replaces the whole list on update, so `get` it first. `assign` it to the
145
+ bot that uses it.
146
+ - a WhatsApp message outside the 24-hour reply window needs an approved
147
+ template: `oalo_wa_templates` (`list`, or `create` a draft). **Submitting
148
+ a template sends it to Meta for review: only when the person says so.**
149
+ - stage ids for steps that move deals: `oalo_pipelines` (`list`).
150
+ - code tools: `oalo_tool_templates` (ready-made, or `copy` a built-in) before
151
+ `oalo_generate_tool` writes a new one.
152
+ - notes loops keep between runs: `oalo_loop_docs`.
153
+ 4. **No MCP?** Every `oalo_*` tool above has an event behind it: `oalo call
154
+ --list <word>` finds it and the params it takes, and `oalo call <event>
155
+ '<json>'` runs it.
156
+
157
+ ## Data: models and rows
158
+
159
+ Every workspace has data models (tables) in two schemas: `dev`, a sandbox, and
160
+ `prod`, which live apps read. A row is `{id, document, owner_id, availability,
161
+ product_id, ...}`; the fields are in `document`.
162
+
163
+ ```sh
164
+ oalo data models # what exists: fields, shared or owned
165
+ oalo data models create orders_mirror --field total:float --field email # dev and prod
166
+ oalo data push orders_mirror orders.csv --id-field order_no --dry-run # check first
167
+ oalo data push orders_mirror orders.csv --id-field order_no # upsert into dev
168
+ oalo data list orders_mirror --limit 5 # look at what landed
169
+ oalo data promote orders_mirror # copy dev rows to prod, when the person wants it live
170
+ oalo data routes # the Sync API's endpoints, scopes and limits
171
+ ```
172
+
173
+ Over MCP: `oalo_list_models`, `oalo_create_model`, `oalo_read_rows`,
174
+ `oalo_write_rows` (env required; prod needs `confirm`), `oalo_promote_model`,
175
+ `oalo_sync_routes`.
176
+
177
+ 1. **Look before you create.** Reuse a model that exists (`oalo data models`).
178
+ Use `--owned-by <field>` only when each app user owns their rows.
179
+ 2. **Always give rows an id** (`--id-field`), usually the source system's own
180
+ key. Then sending a row again updates it instead of adding a copy.
181
+ 3. **Write to dev, check, then promote.** Rows written to prod sync to live apps
182
+ at once, so writing there needs `--env prod` and the person's go-ahead.
183
+ 4. **For an app's own server** (mirroring its database into Oalo, reading data
184
+ out): a server app hosted on Oalo already has `OALO_URL` and
185
+ `OALO_SYNC_TOKEN` in its environment, locked to its own env's data. Use
186
+ them: there is no token to make. For a server hosted elsewhere, the person
187
+ runs `oalo tokens create --name <what> --scope write:<table>` themselves (it
188
+ prints the token once) and stores it in that host's secrets. Don't run it
189
+ yourself: the token would land in your context. The app calls
190
+ `/api/sync/v1`; `oalo docs sync-rest-api` explains it and `oalo data routes`
191
+ lists the endpoints. Never put a sync token in a mobile or web app: it reads
192
+ and writes every member's rows.
193
+ 5. **For an app's members** (a web or mobile app showing each member their own
194
+ data): the app signs the member in with an email code and syncs as that member
195
+ with the sync SDK (`@synclib-io/client`, or the Flutter scaffold), so Oalo
196
+ returns only their rows of an owned model. **Before writing any of it, read
197
+ `oalo docs connect-your-own-frontend`**: the sign-in endpoints, the socket
198
+ URL, the one channel topic every app uses, and how to write rows. Don't guess
199
+ these; a wrong topic or write call fails silently. To start from working code
200
+ instead, `oalo deploy --app <slug> --pull` fetches an Oalo app that already
201
+ has sign-in and sync wired.
202
+
203
+ ## Two tokens
204
+
205
+ - **CLI token**: everything above. It acts as you, with your permissions.
206
+ - **Sync token**: data only, for `oalo typegen` and `oalo data` reads.
207
+ Server-side only; never ship it in an app. `oalo data` writes make
208
+ their own token limited to the tables written, the first time.
209
+
210
+ `oalo login <workspace>` (their Base, `<workspace>.oalo.ai`; `--server <url>`
211
+ for a custom domain) gets both: the person approves it in the dashboard
212
+ (Account, CLI & agents), so ask them to run it rather than asking for tokens.
213
+ If a command says it needs a CLI token, that is the fix. `oalo whoami` shows
214
+ which workspace and person the CLI acts as and checks both tokens.
215
+ `oalo help --json` describes every command, flag and exit code.
216
+
217
+ ## Deploying an app
218
+
219
+ ```sh
220
+ oalo apps # the apps: slug, preview and live address, live release
221
+ oalo deploy --new [<name>] # this repo becomes a new app, its history kept
222
+ oalo deploy --new --from <https url> # a repo on a Git host becomes an app, no checkout
223
+ oalo deploy --app shop # this branch's commits to the preview (dev)
224
+ oalo deploy --app shop --env prod # build and swap the live release
225
+ ```
226
+
227
+ 0. **A repo that isn't on Oalo yet** (the person wants to host their code):
228
+ check `oalo apps` first, then run `oalo deploy --new` from the repo's root.
229
+ It needs no Dockerfile, no git remote and no credential besides the login:
230
+ Oalo works out the kind of app from the files at the root and starts a
231
+ preview. Don't write a Dockerfile or create the app in the dashboard first.
232
+ If the deploy says it couldn't tell what the app is (a monorepo, an unusual
233
+ framework), `oalo docs bring-your-repo` says what to add. When the code is
234
+ on a Git host and not checked out here, `oalo deploy --new --from <https
235
+ url>`; for a private repo the person puts a read token in an environment
236
+ variable and you pass `--token-env VAR`. Never put a token in the URL or a
237
+ flag.
238
+ 1. **Commit first.** A deploy sends commits; uncommitted changes are not sent.
239
+ 2. **Preview first, live when the person says so.** `--env prod` changes what
240
+ live visitors see as soon as the build passes. A failed build leaves the old
241
+ release live.
242
+ 3. **If a deploy is refused because the app has commits you don't** (the builder
243
+ agent or a teammate changed it on Oalo): `oalo deploy --app shop --pull`, then
244
+ `git merge oalo/shop`, then deploy again. Don't use `--replace` unless the
245
+ person wants their branch to overwrite the app's code: it discards what was
246
+ made on Oalo (it is tagged, so it can be recovered).
247
+ 4. **How Oalo starts a server app:** a `Procfile` (`web:` serves HTTP, other
248
+ lines are workers, `release:` runs once per live deploy, e.g. migrations),
249
+ else package.json `start` (`dev` in the preview), else the runtime's default
250
+ (index.js, main.py, app.py, manage.py). The app must listen on `$PORT`.
251
+ When a repo starts some other way, add a Procfile rather than renaming files.
252
+ 5. **When an app doesn't start or misbehaves, read its logs first:**
253
+ a dev deploy whose preview doesn't run says so and exits 1 (the code is
254
+ saved).
255
+ `oalo logs --app <slug> [--env prod]` (MCP: `oalo_app_logs`). Fix the repo
256
+ yourself; `oalo setup` (an agent on the Base does it on a branch for the
257
+ person to accept) is for when they ask for it. A live deploy
258
+ only swaps once the new release answers on its port; one that crashes
259
+ brings the previous release back, and the error shows its last lines.
260
+ When the live app went down or keeps restarting, `oalo incidents --app
261
+ <slug>` says when and why: a memory kill names the size to raise in
262
+ `.oalo/app.json`.
263
+ 6. **Rolling back** (`oalo deploy --rollback`, MCP: `oalo_releases`) changes what
264
+ live visitors get: only when the person asks. `--history` lists the
265
+ releases.
266
+
267
+ ## An app's environment variables
268
+
269
+ ```sh
270
+ oalo env --app shop # dev's variables; secrets show only as "secret"
271
+ oalo env set API_BASE_URL=https://api.example.com --app shop --public
272
+ oalo env set --file .env.production --app shop --env prod
273
+ oalo env unset OLD_FLAG --app shop
274
+ ```
275
+
276
+ 1. **Never handle a secret value.** When the app needs a key the person holds
277
+ (a payment or model provider key, a password), don't ask them to paste it to
278
+ you, and don't put it in a command, a file or your reply. Tell them to run
279
+ `oalo env set NAME --secret --app <slug>` themselves: it asks for the value
280
+ without showing it. You may set public values (URLs, switches) yourself.
281
+ 2. **Keys go to server apps only.** A static app (React, Expo, Flutter) never
282
+ gets secret variables, because its build ships to the visitor's browser.
283
+ Don't get a key into one with `--public` or a `VITE_`/`EXPO_PUBLIC_` name.
284
+ Have the static app call a server app that holds the key.
285
+ 3. **dev and prod are separate sets.** A dev change restarts the preview. A
286
+ prod change relaunches the live app at the commit it already runs (a build;
287
+ no new code goes out), so change prod only when the person says so. Don't
288
+ run `oalo deploy --env prod` to apply a variable: that also ships whatever
289
+ is committed. `--no-restart` batches several changes; the last one without
290
+ it relaunches.
291
+ 4. **Read them from the environment in code** (`process.env.NAME`,
292
+ `import.meta.env.VITE_NAME`), and don't commit `.env` files with values.
293
+ `oalo docs app-environment` has the details.
294
+
295
+ ## What your app gets from Oalo
296
+
297
+ Each of these is a few lines of the app's own code. Read its page before
298
+ writing it: the endpoints, headers and shapes are there.
299
+
300
+ | To | Do | `oalo docs` |
301
+ |---|---|---|
302
+ | Call Oalo from a server app | Use `OALO_URL` and `OALO_SYNC_TOKEN`, already in its environment (data, events, workflows, chat; locked to its env). `DATABASE_URL` is its PostgreSQL. | `app-environment` |
303
+ | Start workflows when something happens in the app | `POST /api/events/v1` with an `app.*` name, or `oalo data events <table> created updated` for row changes, then a loop trigger on that event. Check with `oalo emit <event> --dry-run`. | `app-events` |
304
+ | Run a workflow like a function | Give the loop an `api` trigger; the app calls `POST /api/workflows/v1/<name>/run`. | `workflow-calls` |
305
+ | Let a bot call the app's endpoints | Declare them in `.oalo/app.json`, deploy, then `oalo bots tools <bot> --add <tool>`. The calls are signed. | `app-tools` |
306
+ | Put a bot on the app's pages | One script tag (`chat-widget.js`), signed in as the app's user through `POST /api/chat/v1/sessions`. | `chat-widget` |
307
+ | Sign members in and sync their data | An email code, then `@synclib-io/client`. | `connect-your-own-frontend` |
308
+ | Take rows from another system | `oalo sources create <name> ...` gives it an address to post to. A system that can't post: `oalo sources fetch <name> <https url>`, or a view in its own database: `oalo sources fetch <name> "$DB_URL" --relation <schema.view> --cursor updated_at`. Documents for a bot: `--bot <bot>`. | `data-sources` |
309
+ | Run the app on this machine | The person makes a dev-only token (`oalo tokens create --name local --scope read,write,events,workflows,chat,env:dev`) and puts it in an uncommitted `.env`. Its events and workflow calls are test runs. | `tokens-and-credentials` |
310
+ | Give the live app more CPU or memory, a health check, or required variables | All in one committed file, `.oalo/app.json`: `{"sizes": {"web": "medium"}, "health": "/healthz", "env": {"required": ["STRIPE_KEY"]}}`. A live deploy is refused while a required variable isn't set for prod. There is no `infra.json`. | `live-server-apps` |
311
+ | Show the app's own numbers to its users (a chart in the app) | `queryMetric({ table: "sessions", every: "week", days: 90, by: "coach_id" })` from `oalo typegen`, called on the app's server. | `dashboards` |
312
+ | Work on an app made from Oalo's Flutter template on this machine | `oalo typegen --dart` (the models, and `.oalo/dart-defines.json`), then `flutter run -d chrome --dart-define-from-file=.oalo/dart-defines.json`. | `flutter-on-your-machine` |
313
+
314
+ ## Selling: products and paid content
315
+
316
+ ```sh
317
+ oalo products # what the workspace sells, by id
318
+ oalo products create premium --name "Premium" --price 9.99 --monthly # test key; --once, --yearly
319
+ oalo typegen # PRODUCTS and the ProductId type
320
+ oalo products orders premium
321
+ ```
322
+
323
+ 1. **The id is what code names.** Run `oalo products` and use an id it lists;
324
+ if what the app sells isn't there, create it. After a create, run `oalo
325
+ typegen` and type product ids as `ProductId` (Dart: the `Products`
326
+ class), so a typo doesn't compile. The id goes in `BuyButton
327
+ productId`, in a row's `product_id` (with `availability` `product`, or
328
+ `premium` for any subscriber), and is what a member's
329
+ `purchased_products` holds.
330
+ 2. **The server does the locking.** A locked row reaches the app with
331
+ `_stripped: true`: show the paywall on that (`oalo docs commerce` has the
332
+ checkout and paywall code). Never decide access in the
333
+ app from `availability` or by hiding rows.
334
+ 3. **Payments run on the workspace's own Stripe key,** which the person
335
+ pastes in the dashboard's Payment settings. Never ask for it. Create
336
+ products with the test key; `--live` is for when the person says to sell
337
+ for real, and needs `--yes` (so does a price over 500).
338
+ 4. **Buyers are affected at once.** `price` (new buyers pay it, subscribers
339
+ keep theirs) and `archive` (no longer sold; buyers keep access) only when
340
+ the person asks. Archive, don't delete: `delete` is for an archived
341
+ product nobody bought.
342
+
343
+ ## Numbers and dashboards
344
+
345
+ ```sh
346
+ oalo metrics orders --sum total --by status --days 30 --where "status != test" # a number, now
347
+ oalo metrics contacts --every week --days 90 # also: events --event order.created
348
+ oalo dashboards # the dashboards
349
+ oalo dashboards add Sales orders --title "Revenue by plan" --kind bars --sum total --by plan --days 30
350
+ ```
351
+
352
+ 1. **A question about numbers** (how many, how much, per week): `oalo metrics`
353
+ (MCP `oalo_query_metrics`). Don't read rows and add them up.
354
+ 2. **A chart the person wants to keep** goes on a dashboard with `oalo
355
+ dashboards add` (MCP `oalo_save_dashboard_widget`); the dashboard is made
356
+ when none has that name. Check `oalo dashboards` first: the same title
357
+ replaces a chart. Everyone with access sees it, so say which dashboard.
358
+ 3. **The workspace's own numbers are a dashboard, not an app.** Don't build or
359
+ deploy a page for them. An app's charts for its own users call `queryMetric`
360
+ (from `oalo typegen`) on the app's server.
361
+
362
+ ## Bots
363
+
364
+ ```sh
365
+ oalo bots # the bots, by public id (bot_...)
366
+ oalo data vectorize products --fields title,description # make a table searchable
367
+ oalo bots knowledge bot_ABC --add products # let the bot search it
368
+ oalo bots tools bot_ABC --add order_status # let it call a tool
369
+ oalo bots settings --explain model # what each setting does
370
+ oalo bots settings bot_ABC set <key>=<value> --dry-run
371
+ oalo channels # WhatsApp, Instagram, email, web chat: which bot answers
372
+ ```
373
+
374
+ 1. **To make a bot answer from a table,** vectorize the table, then attach it
375
+ with `oalo bots knowledge`. Attaching alone does nothing for a table that
376
+ isn't searchable.
377
+ 2. **Read a setting with `--explain` before changing it.** Don't guess keys or
378
+ values.
379
+ 3. **A published bot is live for customers.** `set` on one shows the change and
380
+ saves nothing unless you pass `--yes`: show the person the `--dry-run` and
381
+ use `--yes` only when they say so.
382
+ 4. **Test with `oalo qa`** after any change to what a bot knows or how it
383
+ answers.
384
+ 5. **Which bot answers a WhatsApp number or an Instagram account:**
385
+ `oalo channels` lists them, `oalo channels set whatsapp "<number>" --bot
386
+ <bot>` changes it at once for every customer writing in, so only when the
387
+ person asks. Connecting a number needs Meta's sign-in in a browser: `oalo
388
+ channels connect whatsapp --bot <bot>` prints the link for the person to
389
+ open. Never ask for their Meta login or token.
390
+ 6. **A field the bot must never see** (a cost price, an internal note):
391
+ `oalo data keep-from-ai <table> cost_price`. It stays stored and synced,
392
+ but the bot's data tools leave it out, and it can't be embedded. Don't
393
+ vectorize it.
394
+
395
+ ## Reading the docs
396
+
397
+ Before building on a feature you don't know, read Oalo's own docs. They come
398
+ from the workspace's own server (its Base), so they match what it runs:
399
+
400
+ ```sh
401
+ oalo docs --search "data models owner" # find the page
402
+ oalo docs data-models # read it (markdown)
403
+ oalo docs # every page, with a one-line summary
404
+ ```
405
+
406
+ Over MCP: `oalo_docs` with `q` or `slug`. Good first pages:
407
+ `bring-your-repo` and `connect-your-own-frontend` for your own code,
408
+ `app-environment`, `tokens-and-credentials`, `data-models`, `sync-rest-api`
409
+ and `triggers-and-chaining`.
410
+
411
+ For anything the commands don't cover, `oalo call --list <text>` lists the
412
+ events you may call and the params each accepts (MCP: `oalo_call` with event
413
+ `cli_api:events` and `{"q": "<text>"}`). Don't guess param names: a wrong
414
+ shape is refused with the shapes the event takes.
package/src/api.js ADDED
@@ -0,0 +1,132 @@
1
+ /** Minimal Sync API client. No dependencies — fetch is built in on Node 18+. */
2
+
3
+ export class ApiError extends Error {
4
+ constructor(message, status, body) {
5
+ super(message);
6
+ this.name = "ApiError";
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+
12
+ export async function syncGet(path, { serverUrl, token, env }) {
13
+ const url = new URL(`${serverUrl.replace(/\/$/, "")}/api/sync/v1${path}`);
14
+ if (env) url.searchParams.set("env", env);
15
+
16
+ let res;
17
+ try {
18
+ res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
19
+ } catch (e) {
20
+ // A bad host is by far the most common setup mistake, so name it.
21
+ throw new ApiError(`could not reach ${serverUrl} (${e.message})`, 0, null);
22
+ }
23
+
24
+ const text = await res.text();
25
+ let body = null;
26
+ try {
27
+ body = JSON.parse(text);
28
+ } catch {
29
+ /* non-JSON (a proxy error page, say) */
30
+ }
31
+
32
+ if (!res.ok) {
33
+ // 403 here almost always means the token lacks the scope, which is the
34
+ // failure worth spelling out rather than echoing a bare status.
35
+ //
36
+ // `error` is not always a string: some replies nest an object, and
37
+ // interpolating one yields the useless "[object Object]". Flatten to
38
+ // something a human can act on, and never lose the status.
39
+ throw new ApiError(errorDetail(body, text) || `HTTP ${res.status}`, res.status, body);
40
+ }
41
+
42
+ return body;
43
+ }
44
+
45
+ export const fetchManifest = (cfg) => syncGet("/manifest", cfg);
46
+
47
+ /**
48
+ * The workflows apps can call, with their input and output. `[]` from a
49
+ * workspace too old to have the route (404), so typegen still works there.
50
+ */
51
+ export async function fetchWorkflows({ serverUrl, token }, fetchImpl = fetch) {
52
+ let res;
53
+ try {
54
+ res = await fetchImpl(new URL(`${serverUrl.replace(/\/$/, "")}/api/workflows/v1`), { headers: { Authorization: `Bearer ${token}` } });
55
+ } catch (e) {
56
+ throw new ApiError(`could not reach ${serverUrl} (${e.message})`, 0, null);
57
+ }
58
+ if (res.status === 404) return [];
59
+ const text = await res.text();
60
+ let body = null;
61
+ try {
62
+ body = JSON.parse(text);
63
+ } catch {
64
+ /* non-JSON */
65
+ }
66
+ if (!res.ok) throw new ApiError(errorDetail(body, text) || `HTTP ${res.status}`, res.status, body);
67
+ return body?.workflows || [];
68
+ }
69
+
70
+ /**
71
+ * Any Sync API call, for the data commands. Unlike `syncGet` it hands back the
72
+ * status and headers on success too, because a bulk writer needs the rate-limit
73
+ * headers to pace itself.
74
+ *
75
+ * `query` values that are null or undefined are left out. `fetchImpl` is for
76
+ * tests.
77
+ */
78
+ export async function syncRequest(method, path, { serverUrl, token, env }, { query = {}, body, fetchImpl = fetch } = {}) {
79
+ const url = new URL(`${serverUrl.replace(/\/$/, "")}/api/sync/v1${path}`);
80
+ if (env) url.searchParams.set("env", env);
81
+ for (const [k, v] of Object.entries(query)) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
82
+
83
+ const headers = { Authorization: `Bearer ${token}` };
84
+ if (body !== undefined) headers["Content-Type"] = "application/json";
85
+
86
+ let res;
87
+ try {
88
+ res = await fetchImpl(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
89
+ } catch (e) {
90
+ throw new ApiError(`could not reach ${serverUrl} (${e.message})`, 0, null);
91
+ }
92
+
93
+ const text = await res.text();
94
+ let parsed = null;
95
+ try {
96
+ parsed = JSON.parse(text);
97
+ } catch {
98
+ /* non-JSON */
99
+ }
100
+
101
+ if (!res.ok) {
102
+ const err = new ApiError(errorDetail(parsed, text) || `HTTP ${res.status}`, res.status, parsed);
103
+ err.headers = res.headers;
104
+ throw err;
105
+ }
106
+
107
+ return { status: res.status, body: parsed, headers: res.headers };
108
+ }
109
+
110
+ /**
111
+ * Pull a human-readable reason out of an error body.
112
+ *
113
+ * The API is not uniform here: a 401 replies
114
+ * {"error":{"code":"invalid_token","message":"Missing or invalid ..."}}
115
+ * while a scope failure replies {"error":"token lacks read scope"}. Handle both,
116
+ * prefer the nested `message` over dumping JSON, and keep the `code` since it is
117
+ * the part worth searching for.
118
+ */
119
+ function errorDetail(body, text) {
120
+ const e = body?.error ?? body?.reason ?? body?.message;
121
+
122
+ if (typeof e === "string" && e) return e;
123
+
124
+ if (e && typeof e === "object") {
125
+ const msg = typeof e.message === "string" ? e.message : null;
126
+ if (msg) return e.code ? `${msg} (${e.code})` : msg;
127
+ return JSON.stringify(e);
128
+ }
129
+
130
+ if (body && typeof body === "object" && Object.keys(body).length) return JSON.stringify(body);
131
+ return (text || "").trim().slice(0, 200);
132
+ }
package/src/app-ref.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * An app named on the command line, by slug (what `oalo apps` shows and
3
+ * `oalo deploy --app` takes) or by id (what the pulled loop files use under
4
+ * `.oalo/loops/apps/<id>/`). Returns the app's id, or throws with what to do.
5
+ */
6
+ export async function appIdFor(ch, ref) {
7
+ const { apps = [] } = await ch.call("list_apps", {});
8
+ const app = apps.find((a) => a.slug === ref || a.id === ref);
9
+ if (!app) throw new Error(`no app ${ref} in this workspace. \`oalo apps\` lists them.`);
10
+ return app.id;
11
+ }