@arcforgelabs/flow 2026.11.1 → 2026.11.3
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/SECURITY.md +11 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/server/api/invoicing.js +78 -12
- package/server/auth/routes.js +20 -0
- package/server/auth/session.js +38 -1
- package/server/auth/settings.js +16 -0
- package/server/core/invoice-rules.js +25 -10
- package/server/core/job-board.js +12 -4
- package/server/core/job-match.js +6 -4
- package/server/core/ledger.js +84 -13
- package/server/core/line-mapping.js +7 -3
- package/server/core/pipeline.js +6 -2
- package/server/core/sending.js +31 -9
- package/server/integrations/seazona/index.js +6 -1
- package/server/integrations/seazona/routes.js +65 -2
- package/server/integrations/seazona/settings.js +3 -0
- package/server/integrations/xero/ledger.js +5 -1
- package/server/ui/app-page.js +1 -1
- package/server/ui/app.js +9 -6
- package/server/ui/flow.css +75 -4
- package/server/ui/home.js +120 -0
- package/server/ui/invoice-rules.js +24 -3
- package/server/ui/invoicing.js +50 -7
- package/server/ui/job-board.js +20 -10
- package/server/ui/job-change.js +63 -15
- package/server/ui/jobs.js +4 -4
- package/server/ui/nav-pins.js +5 -3
- package/server/ui/reconcile.js +4 -2
- package/server/ui/routes.js +1 -1
- package/skills/flow/SKILL.md +237 -0
- package/skills/flow/flow.sh +90 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flow
|
|
3
|
+
description: Run the lab's Flow app and read Seazona through it - invoice tabs, send ready invoices, void or delete drafts, invoice rules, jobs, reconcile Seazona jobs with Flow and Xero, compare prices across Flow, Xero and Seazona, link price codes, review items, the Seazona history import, and Seazona orders, invoices and clients.
|
|
4
|
+
metadata: { "openclaw": { "requires": { "bins": ["curl", "openssl", "sha256sum"] } } }
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Flow
|
|
8
|
+
|
|
9
|
+
Flow is the lab's invoicing app. It takes jobs from Seazona (the lab's case
|
|
10
|
+
system) and sends invoices to Xero. Use `{baseDir}/flow.sh` for every call.
|
|
11
|
+
It holds the Flow key; never print the key file or put the key in a message.
|
|
12
|
+
|
|
13
|
+
Flow address: saved by `setup` next to the key (or `FLOW_URL`). There is no
|
|
14
|
+
default: the key goes only to this lab's own Flow.
|
|
15
|
+
Key file: `FLOW_AGENT_KEY_FILE` (default `~/.openclaw/credentials/flow-agent-key`).
|
|
16
|
+
|
|
17
|
+
## First time only
|
|
18
|
+
|
|
19
|
+
Run `{baseDir}/flow.sh setup https://<this lab's Flow>`. It saves the address,
|
|
20
|
+
makes the key once and prints only `FLOW_AGENT_KEY_SHA256=<64 hex>`. That digest is safe to share: give it to the
|
|
21
|
+
person who sets up Flow. Flow stores the digest, never the key. Run `setup`
|
|
22
|
+
again any time to see the same digest. To change the key, delete the key file,
|
|
23
|
+
run `setup`, and give the new digest to Flow.
|
|
24
|
+
|
|
25
|
+
Then check: `{baseDir}/flow.sh get me` must answer `HTTP 200`.
|
|
26
|
+
|
|
27
|
+
## Keep the skill current
|
|
28
|
+
|
|
29
|
+
Run `{baseDir}/flow.sh update` after Flow gets a new release, and when a call
|
|
30
|
+
answers 404 for a route this skill lists. It gets this skill from the Flow it
|
|
31
|
+
talks to, so the skill always matches the release that Flow runs. It changes
|
|
32
|
+
nothing if the answer is not a good skill. Install the skill from GitHub only
|
|
33
|
+
the first time.
|
|
34
|
+
|
|
35
|
+
## Calls
|
|
36
|
+
|
|
37
|
+
Every answer ends with a line `HTTP <code>`. 401 or 403: the key is not set up
|
|
38
|
+
in Flow, or the Flow person is turned off. Stop and tell the person.
|
|
39
|
+
|
|
40
|
+
Read first. Before a change, read the record, say what you will change, and
|
|
41
|
+
for sends, voids and deletes get the person's yes in the chat.
|
|
42
|
+
|
|
43
|
+
| Do this | Command |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| Tabs and counts | `flow.sh get 'invoicing/invoices?status=ready&limit=50'` (status: pending, ready, draft, approved, sent, non_chargeable, void) |
|
|
46
|
+
| Search invoices | `flow.sh get 'invoicing/invoices?q=<text>'` |
|
|
47
|
+
| One invoice | `flow.sh get invoicing/invoices/<id>` |
|
|
48
|
+
| Mark ready | `flow.sh post invoicing/invoices/mark-ready '{"invoiceIds":["<id>"]}'` |
|
|
49
|
+
| Send all ready to Xero | `flow.sh post invoicing/invoices/send-ready '{}'` (or `'{"invoiceIds":["<id>"]}'`) |
|
|
50
|
+
| Void an invoice | `flow.sh post invoicing/invoices/<id>/void` |
|
|
51
|
+
| Delete a draft | `flow.sh delete invoicing/invoices/<id>` |
|
|
52
|
+
| Jobs | `flow.sh get 'invoicing/jobs?limit=50'`, one job `invoicing/jobs/<id>/activity` |
|
|
53
|
+
| Invoice rules | `flow.sh get invoicing/rules`; what a rule can use: `flow.sh get invoicing/rule-fields` |
|
|
54
|
+
| Add a rule | `flow.sh post invoicing/rules '{"name":"...","conditions":[{"field":"job_total","test":"at_most","value":0}],"action":{"kind":"wait"}}'` |
|
|
55
|
+
| Change or turn off a rule | `flow.sh put invoicing/rules/<id> '{"enabled":false}'` |
|
|
56
|
+
| Delete a rule | `flow.sh delete invoicing/rules/<id>` |
|
|
57
|
+
| Automations | `flow.sh get invoicing/automations` |
|
|
58
|
+
| Flow settings | `flow.sh get invoicing/settings` |
|
|
59
|
+
|
|
60
|
+
Rule actions: `wait` (hold the job) or `skip` (never invoice it). Take field,
|
|
61
|
+
test and value only from `rule-fields`.
|
|
62
|
+
|
|
63
|
+
Voids: the Void tab holds invoices voided in Flow **or voided or deleted in
|
|
64
|
+
Xero**. Before a clean-up, list the tab, group the rows (Flow void, Xero void,
|
|
65
|
+
deleted draft), show the person the groups, and act only on the group they
|
|
66
|
+
confirm.
|
|
67
|
+
|
|
68
|
+
Sending: Flow sends each invoice to Xero as a draft unless the "Approve and
|
|
69
|
+
email" setting is on (`invoicing/settings`). Say which one applies before a send.
|
|
70
|
+
|
|
71
|
+
## Prices and price codes
|
|
72
|
+
|
|
73
|
+
Flow's price list is the lab's own list of codes and prices
|
|
74
|
+
(`docs/PRICE_LIST.md`). Flow keeps a copy of the Xero items and the Seazona
|
|
75
|
+
products and links each one to a Flow code. `price-list` reads only Flow's
|
|
76
|
+
copies, so it costs no Xero or Seazona calls.
|
|
77
|
+
|
|
78
|
+
| Do this | Command |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| Compare all prices | `flow.sh get invoicing/price-list` |
|
|
81
|
+
| Flow codes only | `flow.sh get 'invoicing/price-items?q=<text>'` |
|
|
82
|
+
| Change a Flow price | `flow.sh put invoicing/price-items/<id> '{"unitAmountCents":13500}'` (also `name`, `taxCode` GST or GST_FREE) |
|
|
83
|
+
| Add a Flow code | `flow.sh post invoicing/price-items '{"code":"...","name":"...","unitAmountCents":13500,"taxCode":"GST"}'` |
|
|
84
|
+
| Link records to codes | `flow.sh post invoicing/price-links '{"links":[{"priceItemId":"<id>","system":"xero","externalId":"<id>"}]}'` |
|
|
85
|
+
| Remove a link | `flow.sh delete invoicing/price-links/<link id>` |
|
|
86
|
+
| Accept a difference | `flow.sh post invoicing/price-items/<id>/resolve` |
|
|
87
|
+
| Lines that wait for a code | `flow.sh get invoicing/line-labels` (rows with `unmapped` above 0) |
|
|
88
|
+
| Give a line its code | `flow.sh post invoicing/line-mappings '{"kind":"saved","label":"<label>","unitAmountCents":<label price>,"itemCode":"<Flow code>"}'` |
|
|
89
|
+
| Saved code choices | `flow.sh get invoicing/line-mappings`; change `put invoicing/line-mappings/<id>`, remove `delete invoicing/line-mappings/<id>` |
|
|
90
|
+
|
|
91
|
+
How to read `price-list`:
|
|
92
|
+
|
|
93
|
+
- `systems`: each connected list (`xero`, `seazona`), its record count and
|
|
94
|
+
`syncedAt`. An old `syncedAt` means Flow's copy can be behind.
|
|
95
|
+
- `items`: each Flow code with its `links` (the linked Xero item or Seazona
|
|
96
|
+
product, as `record`) and `findings`. A finding `kind` is `price` (Flow and
|
|
97
|
+
the other system differ: `flow` and `other` in cents), `tax`, `missing` (the
|
|
98
|
+
linked record is gone) or `not_linked` (no Xero item for this code).
|
|
99
|
+
- `unlinked`: Xero items and Seazona products that no Flow code has yet.
|
|
100
|
+
|
|
101
|
+
Reconcile steps:
|
|
102
|
+
|
|
103
|
+
1. Read `price-list` and `line-labels`. Show the person one table: each code
|
|
104
|
+
with the Flow, Xero and Seazona price, what differs, and each line label
|
|
105
|
+
that waits.
|
|
106
|
+
2. For each difference, ask which price is right. Do not guess.
|
|
107
|
+
3. When the person says yes, fix it in the right place:
|
|
108
|
+
- Flow price or code: the Flow commands above.
|
|
109
|
+
- Xero item: your own `xero` command (Flow never changes a Xero item).
|
|
110
|
+
Then refresh Flow's copy of Xero items and contacts:
|
|
111
|
+
`flow.sh post xero/sync '{"organisationId":"<org>"}'`.
|
|
112
|
+
- Seazona product: read only. Tell the person to change it in Seazona.
|
|
113
|
+
- A record that is the same thing as a code but has no link: link it.
|
|
114
|
+
- A difference that is correct (for example a Seazona product at $0):
|
|
115
|
+
accept it with `resolve`.
|
|
116
|
+
4. Waiting lines: match each label to a code by name and price, show the
|
|
117
|
+
person the pairs, and save each pair they agree with as a line mapping.
|
|
118
|
+
The `label` and `unitAmountCents` come from that `line-labels` row. Jobs
|
|
119
|
+
with that line move on at the next Flow sync (a few minutes).
|
|
120
|
+
5. Read `price-list` and `line-labels` again and show what is left.
|
|
121
|
+
|
|
122
|
+
Every change on this list (price, code, link, resolve, line mapping) needs
|
|
123
|
+
the person's yes in the chat first, like a send.
|
|
124
|
+
|
|
125
|
+
## Seazona (read only)
|
|
126
|
+
|
|
127
|
+
Flow reads Seazona for you with its own Seazona connection, so you need no
|
|
128
|
+
Seazona password. Calls share Seazona's limit of 30 a minute with Flow's
|
|
129
|
+
sync: make few calls, and never loop over many records.
|
|
130
|
+
|
|
131
|
+
| Do this | Command |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| Orders on one day | `flow.sh get 'seazona/api/orders/?ordered=2026-11-11T00:00:00&page=1&pageSize=50'` |
|
|
134
|
+
| One order | `flow.sh get seazona/api/orders/<order id>` |
|
|
135
|
+
| Invoices changed since | `flow.sh get 'seazona/api/invoices/?lastModified=2026-11-01T00:00:00'` |
|
|
136
|
+
| Clients changed since | `flow.sh get 'seazona/api/clients/?lastModified=2026-11-01T00:00:00'` |
|
|
137
|
+
| Products, users | `flow.sh get seazona/api/products/`, `flow.sh get seazona/api/users/` |
|
|
138
|
+
|
|
139
|
+
Answers are `{ "ok": true, "data": ... }`, as Seazona gives them. If the owner
|
|
140
|
+
turned on "Hide patient details", patient, note, file and clinical fields are
|
|
141
|
+
left out. Do not copy patient names outside this chat. 429: wait a
|
|
142
|
+
minute and try once more.
|
|
143
|
+
|
|
144
|
+
## Reconcile: Seazona jobs, Flow invoices, Xero
|
|
145
|
+
|
|
146
|
+
When the person says "reconcile", "tidy up" or "why is this job not
|
|
147
|
+
invoiced", work in this order. Read first, show the person what you found in
|
|
148
|
+
short groups, then act on the group they confirm.
|
|
149
|
+
|
|
150
|
+
1. **Jobs against Flow invoices.** `flow.sh get invoicing/reconcile` lists
|
|
151
|
+
jobs with no invoice and invoices with no job. For one row:
|
|
152
|
+
`flow.sh get invoicing/jobs/<job id>/reconcile` or
|
|
153
|
+
`flow.sh get invoicing/invoices/<id>/reconcile` (each lists the likely other
|
|
154
|
+
side; `best: true` is the top guess).
|
|
155
|
+
- Link a job to its invoice: `flow.sh post invoicing/jobs/<job id>/link '{"invoiceId":"<id>"}'`
|
|
156
|
+
- Job needs no invoice: `flow.sh post invoicing/jobs/<job id>/no-invoice`
|
|
157
|
+
- Make the invoice now: `flow.sh post invoicing/jobs/<job id>/draft`
|
|
158
|
+
- Read the job again from Seazona: `flow.sh post invoicing/jobs/<job id>/recheck`
|
|
159
|
+
- **Stuck on a void invoice** (the job shows "Invoice voided", or its Flow
|
|
160
|
+
invoice is void, and nothing drafts): `flow.sh post invoicing/jobs/<job id>/free '{"reason":"<why, in the person's words>"}'`.
|
|
161
|
+
The job lets go of the void invoice and goes on hold, so it does not
|
|
162
|
+
draft by itself. It refuses a live Flow invoice: void that first, only
|
|
163
|
+
after the person says yes to that invoice
|
|
164
|
+
(`flow.sh post invoicing/invoices/<id>/void`).
|
|
165
|
+
- **Held or in review:** `flow.sh get invoicing/jobs/<job id>/activity`
|
|
166
|
+
shows why. Release it: `flow.sh post invoicing/jobs/<job id>/hold '{"state":"none","reason":"<why>"}'`;
|
|
167
|
+
hold it: `'{"state":"held","reason":"<why>"}'` (or `"review"`).
|
|
168
|
+
A `history_import` hold is refused here: use the history import decisions.
|
|
169
|
+
2. **Flow against Xero.** First get the lab id:
|
|
170
|
+
`flow.sh get invoicing/context` (`organisations[0].id`, below `<org>`).
|
|
171
|
+
- Refresh Flow's copy of Xero: `flow.sh post xero/mirror '{"organisationId":"<org>"}'`
|
|
172
|
+
- Xero invoices against Flow's: `flow.sh get 'xero/import?organisationId=<org>&show=check'`
|
|
173
|
+
(show: `check`, `linked`, `invoice_fits`, `job_fits`, `none`, `all`)
|
|
174
|
+
- Match them all again: `flow.sh post xero/import/match '{"organisationId":"<org>"}'`
|
|
175
|
+
- Link one Xero invoice to a Flow invoice: `flow.sh post xero/import/<xero invoice id>/link '{"organisationId":"<org>","invoiceId":"<id>"}'`
|
|
176
|
+
- Make the Flow invoice for one Xero invoice: `flow.sh post xero/import/<xero invoice id>/make '{"organisationId":"<org>","jobId":"<job id or null>"}'`
|
|
177
|
+
- Xero's state of some Flow invoices: `flow.sh post xero/invoices/status '{"organisationId":"<org>","invoiceIds":["<id>"]}'`
|
|
178
|
+
- Re-check one invoice after a fix in Xero: `flow.sh post xero/invoices/<id>/recheck '{"organisationId":"<org>"}'`
|
|
179
|
+
- Contacts: `flow.sh get 'xero/contacts/match?organisationId=<org>&contactId=<Flow contact id>'`; link to a Xero contact of another name: `flow.sh post xero/contacts/link '{"organisationId":"<org>","contactId":"<id>","xeroContactId":"<id>"}'`
|
|
180
|
+
3. **Fix the cause with a rule.** If many jobs fail the same way, read
|
|
181
|
+
`invoicing/rule-fields`, propose one rule (`wait` or `skip`), and add it after
|
|
182
|
+
the person says yes. Line names that map to the wrong product:
|
|
183
|
+
`flow.sh get invoicing/line-mappings`, then change one with `put
|
|
184
|
+
invoicing/line-mappings/<id>`. Prices: `flow.sh get invoicing/price-list`.
|
|
185
|
+
4. **Check Seazona itself** for one job with the Seazona read above (one order).
|
|
186
|
+
|
|
187
|
+
Links, "no invoice", rules and mappings are Flow records only: say what you
|
|
188
|
+
will change and do it after the person's yes. Mirror, match and recheck only
|
|
189
|
+
read Xero. Sending to Xero still goes through `invoicing/invoices/send-ready`.
|
|
190
|
+
|
|
191
|
+
## Review queue, health and history import
|
|
192
|
+
|
|
193
|
+
Use these to find gaps in past billing: jobs Flow never saw, jobs held from
|
|
194
|
+
the history import, and days where Seazona, Flow and Xero disagree.
|
|
195
|
+
|
|
196
|
+
| Do this | Command |
|
|
197
|
+
| --- | --- |
|
|
198
|
+
| Flow health (build, Seazona sync, daily reconcile, history counts) | `flow.sh health` |
|
|
199
|
+
| Open review items | `flow.sh get 'invoicing/review?limit=100'` (also `kind=`, `state=resolved`) |
|
|
200
|
+
| History import state, backfill and batches | `flow.sh get seazona/history` |
|
|
201
|
+
| One batch, or its rows that wait | `flow.sh get 'seazona/history/batches/<id>?outcome=needs_person'` |
|
|
202
|
+
| Held history jobs that need a person | `flow.sh get 'seazona/history/ledger?state=needs_person'` (no state: all) |
|
|
203
|
+
| Decide batch jobs | `flow.sh post seazona/history/batches/<id>/decisions '{"decisions":[{"jobKey":"<key>","decision":"import"}]}'` (`import`, `skip`, or `null` to undo) |
|
|
204
|
+
| Apply a batch (once only) | `flow.sh post seazona/history/batches/<id>/apply` |
|
|
205
|
+
| Read past Seazona days into Flow | `flow.sh post seazona/history/backfill '{"from":"2026-07-01"}'`; stop: `post seazona/history/backfill/stop` |
|
|
206
|
+
| Let one held history job draft | `flow.sh post seazona/history/ledger/<job id>/release` |
|
|
207
|
+
|
|
208
|
+
How to read them:
|
|
209
|
+
|
|
210
|
+
- `health`: `integrations[].health.history` counts history jobs and open
|
|
211
|
+
batches; `reconcile` shows the last daily check, `daysDifferent` and
|
|
212
|
+
`openItems`; Seazona `missing` lists Seazona records Flow's copy lacks.
|
|
213
|
+
- Review items: `kind` says what differs (for example `reconcile.day`,
|
|
214
|
+
`job.changed`, `job.source_totals`); `evidence` holds the numbers. An item
|
|
215
|
+
closes by itself when its cause is fixed, so fix the cause (link, mapping,
|
|
216
|
+
rule, recheck) and read the list again.
|
|
217
|
+
- History jobs are held with reason `history_import`: they are past work,
|
|
218
|
+
most already billed. A matched one stays held. Release only a job that the
|
|
219
|
+
person agrees was never invoiced.
|
|
220
|
+
|
|
221
|
+
Decisions, apply, backfill and release change Flow records or spend Seazona
|
|
222
|
+
calls: say what you will do and act only after the person's yes. A backfill
|
|
223
|
+
uses the shared Seazona limit a few calls at a time; one at a time only.
|
|
224
|
+
|
|
225
|
+
## Xero
|
|
226
|
+
|
|
227
|
+
Xero has its own `xero` command on this Gateway, for anything Flow does not
|
|
228
|
+
cover. Flow's send and the reconcile steps above use Flow's own Xero
|
|
229
|
+
connection, not yours.
|
|
230
|
+
|
|
231
|
+
## Never
|
|
232
|
+
|
|
233
|
+
- Never print, paste or send the key file, a Xero token or a callback URL.
|
|
234
|
+
- Never send, void or delete without the person's yes in this chat.
|
|
235
|
+
- Never send a turned-off person's work again without asking: a send stopped as
|
|
236
|
+
`requester_disabled` means a person turned the assistant off.
|
|
237
|
+
- Never change Flow people, Integrations or owner settings: Flow refuses them.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Flow command for an AI assistant (docs/AGENT_ACCESS.md, "AI assistant key").
|
|
3
|
+
# flow.sh setup <https://flow url> save this lab's Flow address, make the key
|
|
4
|
+
# once, print only the key's SHA-256
|
|
5
|
+
# flow.sh get <path> GET <FLOW_URL>/api/<path>
|
|
6
|
+
# flow.sh post <path> [json] POST with an optional JSON body
|
|
7
|
+
# flow.sh put <path> <json> PUT with a JSON body
|
|
8
|
+
# flow.sh delete <path> DELETE
|
|
9
|
+
# flow.sh health GET <FLOW_URL>/health (public, no key sent)
|
|
10
|
+
# flow.sh update replace this skill with the copy the
|
|
11
|
+
# saved Flow serves (its own release)
|
|
12
|
+
# The key stays in FLOW_AGENT_KEY_FILE (mode 600). It never goes on a command
|
|
13
|
+
# line or into the output.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
KEY_FILE="${FLOW_AGENT_KEY_FILE:-$HOME/.openclaw/credentials/flow-agent-key}"
|
|
17
|
+
URL_FILE="${KEY_FILE}.url"
|
|
18
|
+
|
|
19
|
+
die() { echo "flow.sh: $*" >&2; exit 2; }
|
|
20
|
+
|
|
21
|
+
# The key goes only to the Flow address saved at setup (or FLOW_URL). There is
|
|
22
|
+
# no default address, so a key never goes to another lab's Flow.
|
|
23
|
+
flow_url() {
|
|
24
|
+
local url="${FLOW_URL:-}"
|
|
25
|
+
[[ -n "$url" || ! -s "$URL_FILE" ]] || url="$(tr -d '\n' < "$URL_FILE")"
|
|
26
|
+
[[ "$url" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$ ]] || die "no Flow address: run flow.sh setup https://<your Flow>"
|
|
27
|
+
printf '%s' "${url%/}"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setup() {
|
|
31
|
+
local url="${1:-}"
|
|
32
|
+
if [[ -n "$url" ]]; then
|
|
33
|
+
[[ "$url" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$ ]] || die "the Flow address must be https://<host>"
|
|
34
|
+
mkdir -p "$(dirname "$URL_FILE")"
|
|
35
|
+
printf '%s\n' "${url%/}" > "$URL_FILE"
|
|
36
|
+
fi
|
|
37
|
+
echo "Flow address: $(flow_url)"
|
|
38
|
+
if [[ ! -s "$KEY_FILE" ]]; then
|
|
39
|
+
mkdir -p "$(dirname "$KEY_FILE")"
|
|
40
|
+
(umask 077 && openssl rand -hex 32 > "$KEY_FILE")
|
|
41
|
+
fi
|
|
42
|
+
chmod 600 "$KEY_FILE"
|
|
43
|
+
echo "FLOW_AGENT_KEY_SHA256=$(tr -d '\n' < "$KEY_FILE" | sha256sum | cut -d' ' -f1)"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
call() {
|
|
47
|
+
local method="$1" path="${2:-}" body="${3:-}"
|
|
48
|
+
[[ -n "$path" ]] || die "give a path, for example: get invoicing/invoices?status=ready"
|
|
49
|
+
[[ -s "$KEY_FILE" ]] || die "no key yet: run flow.sh setup https://<your Flow> first"
|
|
50
|
+
local args=(-sS -X "$method" -K - -H 'Accept: application/json' -w '\nHTTP %{http_code}\n')
|
|
51
|
+
if [[ -n "$body" ]]; then args+=(-H 'Content-Type: application/json' --data-binary "$body"); fi
|
|
52
|
+
printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '\n' < "$KEY_FILE")" \
|
|
53
|
+
| curl "${args[@]}" "$(flow_url)/api/${path#/}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Fetch SKILL.md and flow.sh from the saved Flow, so the skill always matches
|
|
57
|
+
# the release that Flow runs. Both files arrive first, then both replace the
|
|
58
|
+
# old ones; a bad answer changes nothing.
|
|
59
|
+
update() {
|
|
60
|
+
[[ -s "$KEY_FILE" ]] || die "no key yet: run flow.sh setup https://<your Flow> first"
|
|
61
|
+
local dir tmp file code
|
|
62
|
+
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
63
|
+
tmp="$(mktemp -d "$dir/.update.XXXXXX")"
|
|
64
|
+
trap "rm -rf '$tmp'" EXIT
|
|
65
|
+
for file in SKILL.md flow.sh; do
|
|
66
|
+
code="$(printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '\n' < "$KEY_FILE")" \
|
|
67
|
+
| curl -sS -K - -o "$tmp/$file" -w '%{http_code}' "$(flow_url)/api/agent-skill/flow/$file")" \
|
|
68
|
+
|| die "could not reach $(flow_url)"
|
|
69
|
+
[[ "$code" == 200 && -s "$tmp/$file" ]] || die "$file: HTTP $code from $(flow_url); nothing changed"
|
|
70
|
+
done
|
|
71
|
+
head -n 1 "$tmp/flow.sh" | grep -q '^#!/usr/bin/env bash' && bash -n "$tmp/flow.sh" \
|
|
72
|
+
|| die "flow.sh from $(flow_url) is not a valid script; nothing changed"
|
|
73
|
+
grep -q '^name: flow$' "$tmp/SKILL.md" || die "SKILL.md from $(flow_url) is not the flow skill; nothing changed"
|
|
74
|
+
chmod 755 "$tmp/flow.sh"
|
|
75
|
+
chmod 644 "$tmp/SKILL.md"
|
|
76
|
+
mv -f "$tmp/SKILL.md" "$dir/SKILL.md"
|
|
77
|
+
mv -f "$tmp/flow.sh" "$dir/flow.sh"
|
|
78
|
+
echo "Skill updated from $(flow_url)"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "${1:-}" in
|
|
82
|
+
setup) setup "${2:-}" ;;
|
|
83
|
+
get) call GET "${2:-}" ;;
|
|
84
|
+
post) call POST "${2:-}" "${3:-}" ;;
|
|
85
|
+
put) [[ -n "${3:-}" ]] || die "put needs a JSON body"; call PUT "${2:-}" "$3" ;;
|
|
86
|
+
delete) call DELETE "${2:-}" ;;
|
|
87
|
+
health) curl -sS -H 'Accept: application/json' -w '\nHTTP %{http_code}\n' "$(flow_url)/health" ;;
|
|
88
|
+
update) update ;;
|
|
89
|
+
*) sed -n '2,12p' "$0"; exit 2 ;;
|
|
90
|
+
esac
|