@odla-ai/cli 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/bin.cjs +566 -332
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -2
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-DC4MIB4X.js → chunk-Y4HWUEFF.js} +488 -256
- package/dist/chunk-Y4HWUEFF.js.map +1 -0
- package/dist/index.cjs +569 -335
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -51
- package/dist/index.d.ts +50 -51
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/skills/odla/SKILL.md +18 -10
- package/skills/odla/references/build.md +17 -14
- package/skills/odla/references/sdks.md +29 -17
- package/skills/odla-migrate/SKILL.md +8 -6
- package/skills/odla-migrate/references/phase-2b-calendar.md +130 -42
- package/skills/odla-migrate/references/secrets-map.md +3 -0
- package/skills/odla-migrate/references/troubleshooting.md +16 -5
- package/dist/chunk-DC4MIB4X.js.map +0 -1
|
@@ -32,34 +32,46 @@ Worker/admin side: `init({ appId: tenantId, adminToken: env.ODLA_API_KEY, endpoi
|
|
|
32
32
|
The publishable key is public; `provision` stores it (`setAuth`). Rules evaluate
|
|
33
33
|
the signed-in user's JWT as `auth` — e.g. `auth.signedIn`, `auth.email`.
|
|
34
34
|
|
|
35
|
-
## @odla-ai/calendar —
|
|
35
|
+
## @odla-ai/calendar — live Google Calendar booking
|
|
36
36
|
|
|
37
37
|
Calendar requires `services: ["db", "calendar"]` plus a `calendar.google`
|
|
38
|
-
block
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
block (`availabilityCalendars` per env, optional `bookingCalendar`). Normal
|
|
39
|
+
provision opens a second, state-bound Google checkpoint issued by the
|
|
40
|
+
platform; the human grants the booking scopes in a browser. Google tokens
|
|
41
|
+
stay platform-side and never become app/CLI secrets. Google Calendar is the
|
|
42
|
+
single source of truth — odla stores no calendar or attendee data, and
|
|
43
|
+
nothing syncs.
|
|
41
44
|
|
|
42
45
|
```ts
|
|
43
46
|
// Trusted Worker only — the admin key must never enter browser code.
|
|
44
|
-
const
|
|
45
|
-
appId: env.
|
|
47
|
+
const cal = initCalendar({
|
|
48
|
+
appId: env.ODLA_APP_ID, // registry app id
|
|
49
|
+
env: env.ODLA_ENV,
|
|
46
50
|
adminToken: env.ODLA_API_KEY,
|
|
47
|
-
endpoint: env.
|
|
51
|
+
endpoint: env.ODLA_PLATFORM ?? "https://odla.ai",
|
|
48
52
|
});
|
|
49
|
-
const
|
|
53
|
+
const fb = await cal.availability.freeBusy({ timeMin, timeMax });
|
|
54
|
+
const slots = computeBookableSlots(fb.busy, { from: timeMin, to: timeMax,
|
|
55
|
+
timezone, slotMinutes: 30 });
|
|
56
|
+
const { booking } = await cal.actions.create(
|
|
57
|
+
{ summary, startAt, endAt, attendees: [email], timezone, meet: true },
|
|
58
|
+
{ idempotencyKey: `booking:${recordId}` }, // retry-safe; Google emails the invite
|
|
59
|
+
);
|
|
50
60
|
```
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
Store the returned `eventId`/`meetUrl` on the app's own record;
|
|
63
|
+
`actions.reschedule(eventId, …)` and `actions.cancel(eventId)` use it. A
|
|
64
|
+
taken slot rejects with non-retryable `calendar_slot_unavailable` — refetch
|
|
65
|
+
slots, don't retry. Browser code never calls `initCalendar` or the platform
|
|
66
|
+
calendar routes; it hits the app's own endpoints and may import pure helpers
|
|
67
|
+
(`computeBookableSlots`, formatters) from `@odla-ai/calendar/client`.
|
|
68
|
+
`SlotPicker` from `@odla-ai/ui` renders the slot output directly.
|
|
58
69
|
|
|
59
70
|
After consent, use `odla-ai calendar calendars --env dev --json` to discover
|
|
60
|
-
selectable ids
|
|
61
|
-
`
|
|
62
|
-
|
|
71
|
+
selectable ids and edit the checked-in list. Re-provision, then verify
|
|
72
|
+
`calendar status --env dev` reports `bookable: yes` and `smoke` passes. A
|
|
73
|
+
pre-pivot read-only grant reports `degraded`/`calendar_reconsent_required`;
|
|
74
|
+
re-run `calendar connect`.
|
|
63
75
|
|
|
64
76
|
## @odla-ai/ai — inference (Claude / GPT / Gemini)
|
|
65
77
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: odla-migrate
|
|
3
3
|
description: >
|
|
4
4
|
Migrate a static site (e.g. GitHub Pages) to odla on Cloudflare in safe
|
|
5
|
-
phases, then add a database, optional
|
|
5
|
+
phases, then add a database, optional Google Calendar booking, Clerk login, and AI. Use when the user wants to
|
|
6
6
|
move a static or GitHub Pages site to Cloudflare/odla, or to add a backend,
|
|
7
7
|
database, login/auth, or AI features to a static site via odla.
|
|
8
8
|
runbookOrder:
|
|
@@ -49,10 +49,12 @@ installed skill at `../odla/SKILL.md` instead.
|
|
|
49
49
|
approved o11y rotation with `--push-secrets` so the Worker is updated in the
|
|
50
50
|
same run. If the final Wrangler transfer fails, use the CLI's printed
|
|
51
51
|
non-rotating `secrets push` retry; never rotate again just to retry delivery.
|
|
52
|
-
7.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
7. Calendar booking is server-side only. `initCalendar` and the platform's
|
|
53
|
+
calendar routes authenticate with the app's full `ODLA_API_KEY` — never
|
|
54
|
+
call them from a browser; the browser talks to the app's own Worker
|
|
55
|
+
endpoints. An old public embed link is not a booking flow; preserve it via
|
|
56
|
+
`bookingPageUrl` only until the native flow is verified. Google OAuth
|
|
57
|
+
tokens never enter the repo, CLI, or chat.
|
|
56
58
|
|
|
57
59
|
## Phase state machine
|
|
58
60
|
|
|
@@ -108,7 +110,7 @@ Read the current phase's file when you enter it — not before:
|
|
|
108
110
|
- references/phase-0-preflight.md
|
|
109
111
|
- references/phase-1-static.md
|
|
110
112
|
- references/phase-2-db.md
|
|
111
|
-
- references/phase-2b-calendar.md (optional:
|
|
113
|
+
- references/phase-2b-calendar.md (optional: live Google Calendar booking — slots, create/reschedule/cancel through the app's own Worker)
|
|
112
114
|
- references/phase-3-auth.md
|
|
113
115
|
- references/phase-3b-user-sync.md (optional: mirror Clerk users into $users)
|
|
114
116
|
- references/phase-4-ai.md
|
|
@@ -1,42 +1,130 @@
|
|
|
1
|
-
# Phase 2b — Google Calendar (optional
|
|
2
|
-
|
|
3
|
-
Use this phase
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
and
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
# Phase 2b — Google Calendar booking (optional)
|
|
2
|
+
|
|
3
|
+
Use this phase when the site should offer real scheduling: visitors pick a
|
|
4
|
+
bookable slot and the app creates a live Google Calendar event — with a Meet
|
|
5
|
+
link and an invitation email that Google itself sends. Google Calendar is the
|
|
6
|
+
single source of truth; odla stores no calendar or attendee data. There is no
|
|
7
|
+
sync, no mirror, and no `$bookings` namespace.
|
|
8
|
+
|
|
9
|
+
Human obligations: complete the server-issued Google consent page in a
|
|
10
|
+
browser (they never paste an OAuth code or token), and receive the test
|
|
11
|
+
invitation email during verification.
|
|
12
|
+
|
|
13
|
+
Boundary that shapes every step: `initCalendar` and the platform's calendar
|
|
14
|
+
routes are **server-side only** — they authenticate with the app's full
|
|
15
|
+
`ODLA_API_KEY`. Never call them from a browser; the browser talks only to
|
|
16
|
+
this app's own Worker endpoints and may import pure helpers from
|
|
17
|
+
`@odla-ai/calendar/client`.
|
|
18
|
+
|
|
19
|
+
1. Require `npm view @odla-ai/calendar@0.2.0 version` to succeed, then
|
|
20
|
+
install `npm i --save-exact @odla-ai/calendar@0.2.0` as a runtime
|
|
21
|
+
dependency. An exact-version `E404` blocks this phase; do not substitute a
|
|
22
|
+
git checkout or another version.
|
|
23
|
+
2. Add `"calendar"` beside `"db"` in services and author the config block —
|
|
24
|
+
booking rides the existing db key, so calendar adds no new secret:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
calendar: {
|
|
28
|
+
google: {
|
|
29
|
+
availabilityCalendars: { dev: ["primary"] }, // 1–10 per env
|
|
30
|
+
bookingCalendar: { dev: "primary" }, // optional; defaults to first availability calendar
|
|
31
|
+
bookingPageUrl: { dev: null }, // optional legacy fallback link
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. Run `doctor`, then `provision --dry-run`; show the human the calendars and
|
|
37
|
+
the booking scopes. Run normal dev provision. Consent runs last: the CLI
|
|
38
|
+
prints/opens a state-bound Google URL and the human grants access in a
|
|
39
|
+
browser. If the platform connector reports a readiness 503
|
|
40
|
+
(`calendar_*_not_configured`), provision still completes and prints a
|
|
41
|
+
`calendar connect --env dev` resume hint — that gap is platform-operator
|
|
42
|
+
work, not yours. Then `calendar calendars --env dev --json` to refine
|
|
43
|
+
checked-in ids, and verify `calendar status --env dev` shows
|
|
44
|
+
`bookable: yes` (a pre-pivot read-only grant shows `degraded` /
|
|
45
|
+
`calendar_reconsent_required`; re-run `calendar connect`).
|
|
46
|
+
4. Build the **slots endpoint** in the app's Worker — capability-gated like
|
|
47
|
+
every other route (a booking endpoint that ignores Phase 3 auth decisions
|
|
48
|
+
is a regression):
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { initCalendar, computeBookableSlots } from "@odla-ai/calendar";
|
|
52
|
+
|
|
53
|
+
// GET /api/booking/slots
|
|
54
|
+
const cal = initCalendar({
|
|
55
|
+
appId: env.ODLA_APP_ID,
|
|
56
|
+
env: env.ODLA_ENV,
|
|
57
|
+
adminToken: env.ODLA_API_KEY,
|
|
58
|
+
endpoint: env.ODLA_PLATFORM ?? "https://odla.ai",
|
|
59
|
+
});
|
|
60
|
+
const fb = await cal.availability.freeBusy({
|
|
61
|
+
timeMin: Date.now(),
|
|
62
|
+
timeMax: Date.now() + 14 * 86_400_000,
|
|
63
|
+
});
|
|
64
|
+
const slots = computeBookableSlots(fb.busy, {
|
|
65
|
+
from: fb.timeMin,
|
|
66
|
+
to: fb.timeMax,
|
|
67
|
+
timezone: BOOKING_TIMEZONE, // app-owned config, not user input
|
|
68
|
+
slotMinutes: 30,
|
|
69
|
+
businessHours: { days: [1, 2, 3, 4, 5], startHour: 9, endHour: 17 },
|
|
70
|
+
minNoticeMs: 24 * 3_600_000,
|
|
71
|
+
});
|
|
72
|
+
return Response.json({ slots, timezone: BOOKING_TIMEZONE });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Business hours, slot length, notice, and buffers are this app's product
|
|
76
|
+
decisions — keep them in app config, and reject slot requests wider than
|
|
77
|
+
the window you offer.
|
|
78
|
+
5. Build the **create endpoint** (`POST /api/booking`). Validate that the
|
|
79
|
+
requested slot is one this app offers (recompute from the same config),
|
|
80
|
+
then create — the platform re-validates availability under its booking
|
|
81
|
+
lease, so a stale slot cannot double-book:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { booking, duplicate } = await cal.actions.create(
|
|
85
|
+
{
|
|
86
|
+
summary: `Intro call — ${name}`,
|
|
87
|
+
startAt, endAt,
|
|
88
|
+
attendees: [email],
|
|
89
|
+
timezone: BOOKING_TIMEZONE,
|
|
90
|
+
meet: true, // Meet link on the event and in the invite
|
|
91
|
+
// notify defaults true → Google emails the invitation
|
|
92
|
+
},
|
|
93
|
+
{ idempotencyKey: `booking:${applicationId}` }, // your own stable record id
|
|
94
|
+
);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Store the returned `booking.eventId` and `booking.meetUrl` on the app's
|
|
98
|
+
own record (its odla-db row) — that record is the app's only persistence;
|
|
99
|
+
attendee data lives in Google. On `calendar_slot_unavailable` (409,
|
|
100
|
+
non-retryable) return a "slot taken" response; the client refetches
|
|
101
|
+
`/api/booking/slots` and re-picks. On retryable
|
|
102
|
+
`calendar_booking_in_progress`, retry once after a short delay.
|
|
103
|
+
6. Reschedule/cancel go through the stored id:
|
|
104
|
+
`cal.actions.reschedule(eventId, { startAt, endAt })` and
|
|
105
|
+
`cal.actions.cancel(eventId)` (idempotent) — expose them as app endpoints
|
|
106
|
+
gated to the record's owner. Attendees can also act through Google's own
|
|
107
|
+
invitation flows; treat Google as authoritative.
|
|
108
|
+
7. Frontend. Two supported shapes:
|
|
109
|
+
- **`@odla-ai/ui` SlotPicker** — renders the slots response directly
|
|
110
|
+
(`{ slots, timezone, onSelect }`). On a non-framework static site use a
|
|
111
|
+
Preact island: a small entry that mounts `<SlotPicker>` into a div,
|
|
112
|
+
bundled with esbuild as an IIFE with `react`/`react-dom` aliased to
|
|
113
|
+
`preact/compat`, committed as a static asset next to the site's other
|
|
114
|
+
assets.
|
|
115
|
+
- **Plain fetch + buttons** — `fetch("/api/booking/slots")`, render times
|
|
116
|
+
with `formatBookingRange` from `@odla-ai/calendar/client`, POST the
|
|
117
|
+
chosen slot.
|
|
118
|
+
Either way the browser only ever talks to this app's Worker.
|
|
119
|
+
8. If the old site had a Google Appointment Schedule embed/link, keep it
|
|
120
|
+
working via `bookingPageUrl` until the native flow is verified, then
|
|
121
|
+
retire it deliberately.
|
|
122
|
+
|
|
123
|
+
Gate (live dev round-trip, all of it): a booking made through the site's own
|
|
124
|
+
UI creates the event on the booking calendar with a Meet link; the Google
|
|
125
|
+
invitation email arrives in the test attendee's inbox; a repeat POST with the
|
|
126
|
+
same idempotency key returns `duplicate: true` (no second event); a taken
|
|
127
|
+
slot returns 409 and the UI refetches slots; reschedule and cancel work via
|
|
128
|
+
the stored `eventId`; zero attendee data is persisted anywhere except the
|
|
129
|
+
app's own tenant record; no Google credential exists in local files; and
|
|
130
|
+
`MIGRATION.md` records the booking-flow decision.
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
| `o11y_…` (o11y ingest token) | wrangler secret `ODLA_O11Y_TOKEN` + `.odla/credentials.local.json` (0600) + `.dev.vars` | only if the app enables o11y; provision issues/reuses it and moves it alongside the db key; never a var, never chat |
|
|
16
16
|
| `ODLA_ENDPOINT` / `ODLA_TENANT` / `ODLA_PLATFORM` / `ODLA_APP_ID` / `ODLA_ENV` | wrangler `vars` | not secrets; keep them set in every env block |
|
|
17
17
|
| `ODLA_O11Y_ENDPOINT` / `ODLA_O11Y_SERVICE` | `.dev.vars` from provision or optional wrangler `vars` overrides | not secrets; public ingest defaults to `https://o11y.odla.ai` and `ODLA_APP_ID` when the token is present |
|
|
18
|
+
| `GOOGLE_CALENDAR_CLIENT_ID` / `GOOGLE_CALENDAR_CLIENT_SECRET` / `CALENDAR_TOKEN_MASTER_KEY` | platform-operator Worker secrets on `odla-apps` | never part of a customer migration; never in chat, the repo, or the app's wrangler config — a readiness 503 (`calendar_*_not_configured`) means the operator sets these, not you |
|
|
19
|
+
| Google calendar grant (OAuth tokens) | platform-side, sealed after human browser consent | the human clicks the server-issued consent URL; no code or token is ever pasted, and the CLI/app/repo never hold one |
|
|
20
|
+
| calendar booking auth (app side) | none — rides the existing `ODLA_API_KEY` | enabling calendar adds NO new app secret; `initCalendar` uses the app's server db key, server-side only, never in a browser |
|
|
18
21
|
|
|
19
22
|
System AI provider credentials are platform-owned and never enter a customer
|
|
20
23
|
migration. `odla-ai security run` receives only app/run-bound role grants; do
|
|
@@ -29,11 +29,22 @@ prints what it verified against.
|
|
|
29
29
|
|
|
30
30
|
Cause: the `odla_dev_…` token expired (~24h) or the handshake was never
|
|
31
31
|
approved.
|
|
32
|
-
Fix: re-run `npx @odla-ai/cli provision` — it
|
|
33
|
-
|
|
34
|
-
`ODLA_USER_EMAIL`, then signs in,
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
Fix: re-run `npx @odla-ai/cli provision` — it resumes a still-pending
|
|
33
|
+
handshake if one exists, else starts a fresh one; the matching existing
|
|
34
|
+
account must be supplied with `--email` or `ODLA_USER_EMAIL`, then signs in,
|
|
35
|
+
reviews, and approves the exact code at the printed URL. Never ask for a
|
|
36
|
+
password or session token.
|
|
37
|
+
|
|
38
|
+
## Handshake exits with code 75 / "handshake still pending"
|
|
39
|
+
|
|
40
|
+
Cause: nobody approved within the wait cap (90s outside an interactive
|
|
41
|
+
terminal; `--wait <seconds>` overrides). This is normal in agent shells and
|
|
42
|
+
loses nothing — the handshake is persisted in `.odla/handshake.local.json`.
|
|
43
|
+
Fix: relay the printed approval URL to the human **verbatim** (browser
|
|
44
|
+
launch is best-effort — a sandboxed shell can report success with no tab
|
|
45
|
+
appearing), wait for them to approve, then re-run the same command; it
|
|
46
|
+
resumes the same code and collects the token. Do not loop unattended
|
|
47
|
+
retries; each handshake lives ~10 minutes.
|
|
37
48
|
|
|
38
49
|
## `smoke` fails: missing credentials
|
|
39
50
|
|