@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.
- package/LICENSE +21 -0
- package/README.md +392 -2
- package/man/oalo.1 +1530 -0
- package/package.json +44 -4
- package/plugin/.claude-plugin/plugin.json +12 -0
- package/plugin/skills/oalo/SKILL.md +414 -0
- package/src/api.js +132 -0
- package/src/app-ref.js +11 -0
- package/src/args.js +67 -0
- package/src/bot-settings.json +468 -0
- package/src/channel.js +162 -0
- package/src/commands/apps.js +64 -0
- package/src/commands/bots.js +278 -0
- package/src/commands/call.js +179 -0
- package/src/commands/channels.js +110 -0
- package/src/commands/data.js +446 -0
- package/src/commands/db.js +58 -0
- package/src/commands/deploy.js +431 -0
- package/src/commands/dockerfile.js +81 -0
- package/src/commands/docs.js +70 -0
- package/src/commands/emit.js +124 -0
- package/src/commands/env.js +223 -0
- package/src/commands/graphs.js +570 -0
- package/src/commands/incidents.js +44 -0
- package/src/commands/link.js +150 -0
- package/src/commands/login.js +180 -0
- package/src/commands/logs.js +69 -0
- package/src/commands/mcp.js +125 -0
- package/src/commands/metrics.js +168 -0
- package/src/commands/products.js +169 -0
- package/src/commands/qa.js +98 -0
- package/src/commands/rehearse.js +100 -0
- package/src/commands/run.js +152 -0
- package/src/commands/secrets.js +68 -0
- package/src/commands/setup.js +96 -0
- package/src/commands/skill.js +142 -0
- package/src/commands/sources.js +377 -0
- package/src/commands/switches.js +156 -0
- package/src/commands/tail.js +84 -0
- package/src/commands/tokens.js +85 -0
- package/src/commands/typegen.js +505 -0
- package/src/commands/whoami.js +112 -0
- package/src/config.js +157 -0
- package/src/data.js +392 -0
- package/src/envfile.js +71 -0
- package/src/events.js +63 -0
- package/src/graphfiles.js +273 -0
- package/src/help.js +920 -0
- package/src/index.js +216 -0
- package/src/mcp/tools.js +773 -0
- package/src/server.js +73 -0
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|