@rubytech/create-maxy-code 0.1.440 → 0.1.441

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 (55) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +28 -7
  3. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +15 -5
  4. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/functions/api/calendar/free-busy.ts +152 -0
  5. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/public/index.html +6 -3
  6. package/payload/platform/plugins/cloudflare/skills/calendar-site/template/schema.sql +11 -0
  7. package/payload/platform/plugins/docs/references/calendar-booking.md +1 -1
  8. package/payload/platform/plugins/docs/references/graph.md +19 -3
  9. package/payload/platform/plugins/docs/references/outlook-guide.md +7 -2
  10. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  11. package/payload/platform/plugins/outlook/PLUGIN.md +7 -3
  12. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js +100 -10
  13. package/payload/platform/plugins/outlook/mcp/dist/__tests__/account-register.test.js.map +1 -1
  14. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.d.ts +2 -0
  15. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.d.ts.map +1 -0
  16. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js +64 -0
  17. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js.map +1 -0
  18. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.d.ts +2 -0
  19. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js +193 -0
  21. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js.map +1 -0
  22. package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js +58 -1
  23. package/payload/platform/plugins/outlook/mcp/dist/__tests__/token-store.test.js.map +1 -1
  24. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts +81 -0
  25. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts.map +1 -0
  26. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js +171 -0
  27. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js.map +1 -0
  28. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts +16 -1
  29. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.d.ts.map +1 -1
  30. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js +25 -3
  31. package/payload/platform/plugins/outlook/mcp/dist/auth/token-store.js.map +1 -1
  32. package/payload/platform/plugins/outlook/mcp/dist/index.js +118 -49
  33. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  34. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +1 -1
  35. package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
  36. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts +8 -4
  37. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.d.ts.map +1 -1
  38. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js +22 -3
  39. package/payload/platform/plugins/outlook/mcp/dist/tools/account-register.js.map +1 -1
  40. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts +26 -0
  41. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts.map +1 -0
  42. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js +28 -0
  43. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js.map +1 -0
  44. package/payload/platform/plugins/outlook/references/auth.md +36 -8
  45. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.d.ts +27 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.d.ts.map +1 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +241 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -0
  49. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  50. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  51. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  52. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  53. package/payload/server/public/assets/{graph-NgQDY0Pb.js → graph-CboWM2_-.js} +1 -1
  54. package/payload/server/public/graph.html +1 -1
  55. package/payload/server/server.js +69 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.440",
3
+ "version": "0.1.441",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:81b544b4ce131682cab873b1c561b196496d3f57fade3f76245646e3fc16aacc
4
+ content-hash: sha256:9f7fc73400d2f9cf942e2df6f544bc69b95abe820ac1ff9b529807c3c7fd6407
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2031,6 +2031,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2031
2031
  2. **Per account: register the Outlook account** — in admin chat, ask the agent to "register my Outlook account". The agent runs `outlook-account-register`, which returns a short user code and the URL `microsoft.com/devicelogin` and relays both to you.
2032
2032
  3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds. The agent finishes by calling `outlook-account-register-poll`.
2033
2033
  4. **Done.** Subsequent tool calls (mail, calendar, contacts) use the persisted refresh token transparently.
2034
+ 5. **Several mailboxes on one account.** Register again to add a second mailbox; it is stored alongside the first, not in place of it. Every operational tool then takes an optional `mailbox` argument — an email address or graphUserId. With one mailbox, omit it. With two or more, name it; omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows every connected mailbox. Only one registration runs at a time per account; starting another while one is live returns an in-progress signal.
2034
2035
 
2035
2036
  ## Tools
2036
2037
 
@@ -2048,7 +2049,8 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2048
2049
  | `outlook-calendar-respond` | Accept, decline, or tentatively accept an invite. |
2049
2050
  | `outlook-calendar-freebusy` | Availability for one or more addresses over a window. |
2050
2051
  | `outlook-contacts-list` | Top contacts. Default top=50. |
2051
- | `outlook-mailbox-info` | Health probe — auth state, refresh-window, folder count. |
2052
+ | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
2053
+ | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
2052
2054
  | `outlook-mail-send` | Send a message (`POST /me/sendMail`). to/cc/bcc arrays, subject, body, isHtml. Success asserted on Graph 202. Needs `Mail.Send`. |
2053
2055
  | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
2054
2056
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
@@ -2066,7 +2068,9 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2066
2068
  | Pending-store census | `pending-store-census account=<id> live=<N>` |
2067
2069
  | Pending cleared | `pending-cleared account=<id> verified=<bool>` |
2068
2070
  | /me failed post-token | `me-failed account=<id> status=<N>` |
2069
- | Auth ok | `auth-ok account=<id> graphUserId=<id> scopes=<csv> tokenExpSec=<N>` |
2071
+ | Auth ok | `auth-ok account=<id> graphUserId=<id> mail=<addr> scopes=<csv> tokenExpSec=<N>` |
2072
+ | Registration already in flight | `register-in-progress account=<id>` (start refused; one device-code flow at a time) |
2073
+ | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal |
2070
2074
  | Token refreshed | `token-refreshed account=<id> oldExpSec=<N> newExpSec=<N>` |
2071
2075
  | Refresh failed | `token-refresh-failed account=<id> reason=<err>` (terminal) |
2072
2076
  | Mail list | `mail-list op=resolve account=<id> folderIn=<name> folderResolved=<token> asRawId=<bool> count=<N> elapsedMs=<N>` (on error, `error=<msg>` replaces `count`) |
@@ -2081,6 +2085,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2081
2085
  | Scope insufficient | `scope-insufficient tool=<t> account=<id> missing=Calendars.ReadWrite` (stale read-only consent) |
2082
2086
  | Contacts list | `contacts-list account=<id> count=<N> elapsedMs=<N>` |
2083
2087
  | Mailbox info | `mailbox-info account=<id> tokenWithinRefreshWindow=<bool> folderCount=<N>` |
2088
+ | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N>` (unreadable>0 flags an orphaned or corrupt mailbox store) |
2084
2089
  | Graph error | `graph-error account=<id> status=<N> code=<graphErrorCode> retryAfterMs=<N-or-null>` — writes flow through the same `callGraph` loop, so a failed write logs `graph-error` too |
2085
2090
  | On-prem rejected | `on-prem-rejected account=<id> mailServer=<host>` (terminal) |
2086
2091
  | Scope insufficient | `scope-insufficient account=<id> tool=<t> missing=Mail.Send` (terminal — re-register to grant send) |
@@ -2292,7 +2297,7 @@ A `pendingUnreconciled` number that stays above zero across several runs means t
2292
2297
 
2293
2298
  ## What this does not do
2294
2299
 
2295
- One meeting type only (one length, one weekly window, one buffer). No syncing to Google or iCloud calendars: your meetings in Maxy are the source of truth. No rescheduling or cancellation links, reminders, or visitor time-zone selection. The booking page shows availability live from your device, so while your device is offline the page can still take a booking but may not show fresh open times until it is back.
2300
+ One meeting type only (one length, one weekly window, one buffer). No syncing to Google or iCloud calendars: your meetings in Maxy are the source of truth. No rescheduling or cancellation links, reminders, or visitor time-zone selection. The booking page shows open times from a snapshot your device publishes to the edge on a short timer, so the page keeps showing availability and taking bookings even while your device is offline a slot that gets booked drops off at once, and times that have passed are never shown. If your device stays offline a long time the open times can go stale, but they never vanish; they refresh the moment it is back online.
2296
2301
 
2297
2302
  ---
2298
2303
  # LinkedIn Extension
@@ -3182,8 +3187,10 @@ Source: https://docs.getmaxy.com/graph.md
3182
3187
  # Graph View
3183
3188
 
3184
3189
  The **Graph** admin page (`/graph`) renders a force-directed view of your
3185
- account's Neo4j subgraph. Labels on the canvas follow the zoom level, so you
3186
- see the most useful identity at every scale.
3190
+ account's Neo4j subgraph. Node captions are hidden on first load so the
3191
+ whole-graph overview stays uncluttered, and appear the moment you interact.
3192
+ Once shown, captions follow the zoom level so you see the most useful identity
3193
+ at every scale.
3187
3194
 
3188
3195
  ## Default view
3189
3196
 
@@ -3239,11 +3246,25 @@ This is an in-page overlay, so the browser tab and address bar stay put. It is
3239
3246
  not the operating system's full-screen mode. The maximize button shows only on
3240
3247
  the `/graph` page, not where the graph is embedded in another pane.
3241
3248
 
3249
+ ## Captions hidden on first load
3250
+
3251
+ When you open `/graph` (or refresh the page) the whole graph auto-fits into the
3252
+ viewport, which on a busy graph would render every caption at once and overlap
3253
+ them into unreadable clutter. So captions start hidden. They appear on your
3254
+ first interaction of any kind — zoom, drag, a filter chip, a search, clicking a
3255
+ node — and stay visible for the rest of that visit. To get the clean
3256
+ caption-free overview back, reload the page.
3257
+
3258
+ This is a one-off marker tied to page load, not a zoom threshold: after the
3259
+ first interaction, captions follow the zoom tiers below and do not hide again
3260
+ until the next reload.
3261
+
3242
3262
  ## Conversation label tiers
3243
3263
 
3244
3264
  Conversation nodes carry the most operator-meaningful identity in the
3245
3265
  subgraph (the conversation name or summary, the date it started, the message
3246
- count). They render in one of three tiers, switched by canvas scale:
3266
+ count). Once captions are showing they render in one of three tiers, switched
3267
+ by canvas scale:
3247
3268
 
3248
3269
  | Zoom | Label shape | Example |
3249
3270
  |------|-------------|---------|
@@ -24,7 +24,11 @@ A booking visitor and the admin look at the booking from opposite sides — the
24
24
  | Admin calendar (4 views) | the install, at `/calendar` on the admin host (resolve via `admin-hostname`) | admin sign-in | every time-bound item for the account — booked and imported meetings, agent-scheduled appointments, and tasks with a due date |
25
25
  | Public booking page | Cloudflare Pages, on the booking domain | open | free/busy only, never any meeting detail |
26
26
 
27
- Only the public page is deployed here, because it must stay reachable and keep capturing bookings even when the install device is briefly offline. The admin calendar is dynamic, account-scoped, and authenticated, so it is served by the install over the tunnel and is same-origin with its data. Do not try to put the admin calendar on Pages.
27
+ Only the public page is deployed here, because it must stay reachable and keep capturing bookings even when the install device is offline. The admin calendar is dynamic, account-scoped, and authenticated, so it is served by the install over the tunnel and is same-origin with its data. Do not try to put the admin calendar on Pages.
28
+
29
+ ## Availability survives the device going offline
30
+
31
+ The booking page does not fetch open times from the device. It reads them same-origin from its own edge Function, `functions/api/calendar/free-busy.ts`, which serves a snapshot stored in the D1 `availability_snapshot` table minus any slot already booked (so a taken slot disappears at once, and two visitors never see the same slot) and with past slots filtered out (so a stale snapshot never offers an expired time). The snapshot is kept fresh by a publish job the platform server arms on a short timer: while the device is online it pushes the account's computed free/busy into `availability_snapshot`; while the device is off it simply does not run and the edge keeps serving the last snapshot until the device returns. There is no per-account cron or setup; the server arms it for the account it resolves, which is the one account whose booking site this install serves.
28
32
 
29
33
  ## Set the availability window first
30
34
 
@@ -34,7 +38,7 @@ The public page shows open slots computed as the weekly window minus busy `:Meet
34
38
  <accountDir>/calendar-availability.json
35
39
  ```
36
40
 
37
- Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Also collect `daysAhead` (the booking horizon — a positive integer count of days the page lets visitors book into, e.g. `14`) and `allowMultiSlot` (a boolean: may a visitor combine consecutive open slots into one longer back-to-back booking? default `false`). Add the `apiBase` (the brand tunnel origin that serves free/busy, `https://<brand-tunnel-domain>`), the `bookingDomain` (the custom domain the page will answer on), and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability — including the horizon and the multi-slot choice — is changed later; there is no separate editor.
41
+ Collect from the operator, then write the file: the account `timezone` (IANA), the meeting `durationMins`, the `bufferMins` between meetings, and the `weekly` window as `{ mon: [["09:00","17:00"]], ... }` with an empty array for days that are closed. Also collect `daysAhead` (the booking horizon — a positive integer count of days the page lets visitors book into, e.g. `14`) and `allowMultiSlot` (a boolean: may a visitor combine consecutive open slots into one longer back-to-back booking? default `false`). Add the `bookingDomain` (the custom domain the page will answer on) and `bookingDbName` (the D1 database name chosen below). Re-running this skill with new numbers is how availability — including the horizon and the multi-slot choice — is changed later; there is no separate editor. The device-side job that keeps the edge snapshot fresh is armed by the always-running platform server, so there is nothing per-account to schedule.
38
42
 
39
43
  ## Assemble the canonical tree
40
44
 
@@ -44,11 +48,11 @@ Copy the skill template into the one canonical Pages source path `site-deploy` e
44
48
  <accountDir>/pages/<project>/
45
49
  ```
46
50
 
47
- The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `wrangler.toml`, and `schema.sql`. The substitutions that make it concrete are: in `public/index.html`, set the `<meta name="api-base">` content to the `apiBase` value (the booking page fetches free/busy cross-origin from the brand tunnel), set `<meta name="days-ahead">` content to `daysAhead`, and set `<meta name="allow-multi-slot">` content to `allowMultiSlot` as the literal `true` or `false`; and fill `wrangler.toml`'s `__PROJECT_NAME__`, `__D1_DATABASE_NAME__`, and `__D1_DATABASE_ID__` from the project name and the database created below. Leave no `__...__` placeholder in the assembled tree.
51
+ The template ships `public/index.html`, `public/booking.js`, `public/booking.css`, `functions/api/book.ts`, `functions/api/calendar/free-busy.ts`, `wrangler.toml`, and `schema.sql`. The substitutions that make it concrete are: in `public/index.html`, set `<meta name="days-ahead">` content to `daysAhead` and set `<meta name="allow-multi-slot">` content to `allowMultiSlot` as the literal `true` or `false`; and fill `wrangler.toml`'s `__PROJECT_NAME__`, `__D1_DATABASE_NAME__`, and `__D1_DATABASE_ID__` from the project name and the database created below. Leave `<meta name="api-base">` empty as it ships; the page reads availability same-origin from the site's own `free-busy` edge Function (see the snapshot section below), never the device tunnel. Leave no `__...__` placeholder in the assembled tree.
48
52
 
49
53
  ## Provision the D1 bookings table
50
54
 
51
- This is a `bookings` table, not the `leads` table of a contact form, so the booking-specific schema ships with the template. Load the stable `<brand>-pages-d1` token from the account secrets file, minting it once only if absent and persisting it, exactly as `references/api.md` and `references/d1-data-capture.md` prescribe; the token needs both Pages Edit and D1 Edit. Create the database, record its id into `wrangler.toml`, and apply `schema.sql` to the remote database. The token is never written into the tree, never committed, never echoed.
55
+ The booking-specific schema ships with the template: `schema.sql` creates both the `bookings` table (written by the booking page) and the `availability_snapshot` table (the single-row free/busy cache the publish job upserts and the `free-busy` edge Function reads). Applying `schema.sql` provisions both. Load the stable `<brand>-pages-d1` token from the account secrets file, minting it once only if absent and persisting it, exactly as `references/api.md` and `references/d1-data-capture.md` prescribe; the token needs both Pages Edit and D1 Edit. Create the database, record its id into `wrangler.toml`, and apply `schema.sql` to the remote database. The token is never written into the tree, never committed, never echoed.
52
56
 
53
57
  ## Deploy through site-deploy
54
58
 
@@ -59,7 +63,13 @@ Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a sta
59
63
  Done is a live behavioural signal on the booking domain, surfaced verbatim:
60
64
 
61
65
  - The booking domain returns `HTTP/2 200` on a cache-busted request.
62
- - The page's free/busy call succeeds: `curl` the brand tunnel free/busy endpoint and confirm it returns a `slots` array and carries no meeting title, description, or location field. A `404` here means availability was not written; a blocked or unauthenticated response means the free/busy path is not reachable on the brand host.
66
+ - The page's free/busy call succeeds same-origin: `curl` the booking domain's own `free-busy` edge Function and confirm it returns a `slots` array (with `updatedAt`) and carries no meeting title, description, or location field. Immediately after deploy it may return `{"slots":[],"updatedAt":null,"stale":true}` until the platform server's publish job runs once (within ~5 minutes); re-check until `updatedAt` is fresh and `slots` is populated. A response missing the `slots` field means the edge Function did not deploy; empty-with-`stale` after several minutes means the publish job is not reaching D1 (check the brand journal for `[calendar-availability]`).
67
+
68
+ ```bash
69
+ curl -s "https://<booking-domain>/api/calendar/free-busy?from=$(date -u +%Y-%m-%dT00:00:00Z)&to=$(date -u -v+14d +%Y-%m-%dT00:00:00Z)" | head -c 400
70
+ ```
71
+
72
+ The device's own free/busy (the publish job's source) should also return a `slots` array; a `404` there means the availability window was not written:
63
73
 
64
74
  ```bash
65
75
  curl -s "https://<brand-tunnel-domain>/api/calendar/free-busy?from=$(date -u +%Y-%m-%dT00:00:00Z)&to=$(date -u -v+14d +%Y-%m-%dT00:00:00Z)" | head -c 400
@@ -0,0 +1,152 @@
1
+ // Edge free/busy Function (calendar-site skill) — offline-resilient availability.
2
+ //
3
+ // Served at GET /api/calendar/free-busy on the booking domain, same-origin with
4
+ // the page. It serves open times from a snapshot the device publishes into D1
5
+ // on a short timer (platform/ui server → publish-availability), MINUS any slot
6
+ // already booked in the D1 `bookings` table:
7
+ // - Serving the snapshot means the public page keeps showing availability
8
+ // while the install device is offline (the device's own tunnel free/busy is
9
+ // then unreachable, but the snapshot survives on the edge).
10
+ // - Subtracting accepted bookings here — at the edge — means a booked slot
11
+ // disappears the instant it is taken, with no wait for the device to
12
+ // reconcile and republish, so two visitors are never shown the same slot.
13
+ // - Past slots are always filtered out, so a stale snapshot never offers a
14
+ // time that has already passed.
15
+ //
16
+ // Split like book.ts so the logic is verifiable without the Pages runtime:
17
+ // computeFreeBusy takes an injected DB + logger and returns the HTTP shape.
18
+
19
+ interface D1PreparedStatement {
20
+ bind: (...values: unknown[]) => D1PreparedStatement
21
+ first: <T = Record<string, unknown>>() => Promise<T | null>
22
+ all: <T = Record<string, unknown>>() => Promise<{ results?: T[] }>
23
+ }
24
+ interface D1Database {
25
+ prepare: (query: string) => D1PreparedStatement
26
+ }
27
+ interface FreeBusyEnv {
28
+ DB: D1Database
29
+ }
30
+
31
+ interface Slot {
32
+ start: string
33
+ end: string
34
+ }
35
+ interface Snapshot {
36
+ durationMins?: number
37
+ timezone?: string
38
+ slots?: Slot[]
39
+ }
40
+
41
+ type Logger = (line: string) => void
42
+
43
+ interface FreeBusyQuery {
44
+ from?: string | null
45
+ to?: string | null
46
+ nowMs: number
47
+ }
48
+
49
+ // Default horizon when `to` is absent — a generous ceiling so a snapshot always
50
+ // covers the page's own (operator-set) horizon.
51
+ const DEFAULT_HORIZON_MS = 31 * 24 * 3600 * 1000
52
+
53
+ export interface FreeBusyResult {
54
+ durationMins: number | null
55
+ timezone: string | null
56
+ slots: Slot[]
57
+ updatedAt: string | null
58
+ stale?: boolean
59
+ }
60
+
61
+ export async function computeFreeBusy(
62
+ env: FreeBusyEnv,
63
+ query: FreeBusyQuery,
64
+ log: Logger,
65
+ ): Promise<FreeBusyResult> {
66
+ const { nowMs } = query
67
+ const from = query.from ? Date.parse(query.from) : nowMs
68
+ const to = query.to ? Date.parse(query.to) : nowMs + DEFAULT_HORIZON_MS
69
+
70
+ let row: { payload?: string; updatedAt?: string } | null = null
71
+ try {
72
+ row = await env.DB
73
+ .prepare('SELECT payload, updatedAt FROM availability_snapshot WHERE id = 1')
74
+ .first<{ payload?: string; updatedAt?: string }>()
75
+ } catch {
76
+ row = null
77
+ }
78
+
79
+ // No snapshot published yet (first deploy) is distinct from a corrupt payload
80
+ // below — both otherwise collapse to empty slots, so name them apart in the log.
81
+ if (!row || !row.payload) {
82
+ log('[calendar-availability] op=serve result=no-snapshot slots=0')
83
+ return { durationMins: null, timezone: null, slots: [], updatedAt: null, stale: true }
84
+ }
85
+
86
+ let snap: Snapshot
87
+ try {
88
+ snap = JSON.parse(row.payload) as Snapshot
89
+ } catch {
90
+ log('[calendar-availability] op=serve result=corrupt slots=0')
91
+ return { durationMins: null, timezone: null, slots: [], updatedAt: row.updatedAt ?? null, stale: true }
92
+ }
93
+
94
+ const lower = Math.max(Number.isNaN(from) ? nowMs : from, nowMs)
95
+ const upper = Number.isNaN(to) ? nowMs + DEFAULT_HORIZON_MS : to
96
+
97
+ // Slots already taken (optimistic subtraction). Bound the scan to bookings at
98
+ // or after `lower`: reconcile only marks rows swept, never de-accepts them, so
99
+ // without this the accepted set grows without bound over the calendar's life —
100
+ // and a past booking can never match a snapshot slot that survives the
101
+ // start >= lower filter below, so the bound drops nothing. A failed read
102
+ // serves the snapshot as-is rather than 500 — the booking page still works.
103
+ const booked = new Set<string>()
104
+ try {
105
+ const res = await env.DB
106
+ .prepare("SELECT slotStart FROM bookings WHERE status = 'accepted' AND slotStart >= ?1")
107
+ .bind(new Date(lower).toISOString())
108
+ .all<{ slotStart?: string }>()
109
+ for (const b of res.results || []) {
110
+ if (b && typeof b.slotStart === 'string') booked.add(b.slotStart)
111
+ }
112
+ } catch {
113
+ /* keep the snapshot as-is */
114
+ }
115
+ const slots = (snap.slots || []).filter((s) => {
116
+ const t = Date.parse(s.start)
117
+ return !Number.isNaN(t) && t >= lower && t < upper && !booked.has(s.start)
118
+ })
119
+
120
+ const ageMs = row.updatedAt ? nowMs - Date.parse(row.updatedAt) : null
121
+ log(`[calendar-availability] op=serve result=ok slots=${slots.length} ageMs=${ageMs ?? 'na'}`)
122
+
123
+ return {
124
+ durationMins: snap.durationMins ?? null,
125
+ timezone: snap.timezone ?? null,
126
+ slots,
127
+ updatedAt: row.updatedAt ?? null,
128
+ }
129
+ }
130
+
131
+ interface PagesContext {
132
+ request: Request
133
+ env: FreeBusyEnv
134
+ }
135
+
136
+ export async function onRequestGet(context: PagesContext): Promise<Response> {
137
+ const url = new URL(context.request.url)
138
+ const result = await computeFreeBusy(
139
+ context.env,
140
+ {
141
+ from: url.searchParams.get('from'),
142
+ to: url.searchParams.get('to'),
143
+ nowMs: Date.now(),
144
+ },
145
+ (line) => console.log(line),
146
+ )
147
+ // Free/busy carries no credentials; a wildcard origin keeps it readable if a
148
+ // page ever fetches it cross-origin, matching the device endpoint's CORS.
149
+ return Response.json(result, {
150
+ headers: { 'Access-Control-Allow-Origin': '*' },
151
+ })
152
+ }
@@ -3,9 +3,12 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <!-- The calendar-site skill rewrites the content of this tag to the brand
7
- tunnel origin that serves /api/calendar/free-busy. -->
8
- <meta name="api-base" content="__API_BASE__" />
6
+ <!-- Availability is read same-origin from this site's own edge Function
7
+ (functions/api/calendar/free-busy.ts), which serves the snapshot the
8
+ device publishes to D1. Empty = same-origin, so the page keeps working
9
+ when the install device is offline. Left blank deliberately; do not point
10
+ it at the device tunnel. -->
11
+ <meta name="api-base" content="" />
9
12
  <!-- Operator-set booking horizon (positive integer days). The skill rewrites
10
13
  the content; booking.js falls back to 14 when it is left unsubstituted. -->
11
14
  <meta name="days-ahead" content="__DAYS_AHEAD__" />
@@ -16,3 +16,14 @@ CREATE TABLE IF NOT EXISTS bookings (
16
16
  );
17
17
 
18
18
  CREATE INDEX IF NOT EXISTS bookings_unswept ON bookings (status, swept);
19
+
20
+ -- Single-row cache of the account's computed free/busy, published by the device
21
+ -- on a short timer (platform/ui server → publish-availability). The booking
22
+ -- page's edge free/busy Function serves open times from this snapshot minus any
23
+ -- accepted booking, so the public page keeps showing availability while the
24
+ -- device is offline. id is pinned to 1: one account per booking site, one row.
25
+ CREATE TABLE IF NOT EXISTS availability_snapshot (
26
+ id INTEGER PRIMARY KEY CHECK (id = 1),
27
+ payload TEXT,
28
+ updatedAt TEXT
29
+ );
@@ -55,4 +55,4 @@ A `pendingUnreconciled` number that stays above zero across several runs means t
55
55
 
56
56
  ## What this does not do
57
57
 
58
- One meeting type only (one length, one weekly window, one buffer). No syncing to Google or iCloud calendars: your meetings in Maxy are the source of truth. No rescheduling or cancellation links, reminders, or visitor time-zone selection. The booking page shows availability live from your device, so while your device is offline the page can still take a booking but may not show fresh open times until it is back.
58
+ One meeting type only (one length, one weekly window, one buffer). No syncing to Google or iCloud calendars: your meetings in Maxy are the source of truth. No rescheduling or cancellation links, reminders, or visitor time-zone selection. The booking page shows open times from a snapshot your device publishes to the edge on a short timer, so the page keeps showing availability and taking bookings even while your device is offline a slot that gets booked drops off at once, and times that have passed are never shown. If your device stays offline a long time the open times can go stale, but they never vanish; they refresh the moment it is back online.
@@ -1,8 +1,10 @@
1
1
  # Graph View
2
2
 
3
3
  The **Graph** admin page (`/graph`) renders a force-directed view of your
4
- account's Neo4j subgraph. Labels on the canvas follow the zoom level, so you
5
- see the most useful identity at every scale.
4
+ account's Neo4j subgraph. Node captions are hidden on first load so the
5
+ whole-graph overview stays uncluttered, and appear the moment you interact.
6
+ Once shown, captions follow the zoom level so you see the most useful identity
7
+ at every scale.
6
8
 
7
9
  ## Default view
8
10
 
@@ -58,11 +60,25 @@ This is an in-page overlay, so the browser tab and address bar stay put. It is
58
60
  not the operating system's full-screen mode. The maximize button shows only on
59
61
  the `/graph` page, not where the graph is embedded in another pane.
60
62
 
63
+ ## Captions hidden on first load
64
+
65
+ When you open `/graph` (or refresh the page) the whole graph auto-fits into the
66
+ viewport, which on a busy graph would render every caption at once and overlap
67
+ them into unreadable clutter. So captions start hidden. They appear on your
68
+ first interaction of any kind — zoom, drag, a filter chip, a search, clicking a
69
+ node — and stay visible for the rest of that visit. To get the clean
70
+ caption-free overview back, reload the page.
71
+
72
+ This is a one-off marker tied to page load, not a zoom threshold: after the
73
+ first interaction, captions follow the zoom tiers below and do not hide again
74
+ until the next reload.
75
+
61
76
  ## Conversation label tiers
62
77
 
63
78
  Conversation nodes carry the most operator-meaningful identity in the
64
79
  subgraph (the conversation name or summary, the date it started, the message
65
- count). They render in one of three tiers, switched by canvas scale:
80
+ count). Once captions are showing they render in one of three tiers, switched
81
+ by canvas scale:
66
82
 
67
83
  | Zoom | Label shape | Example |
68
84
  |------|-------------|---------|
@@ -8,6 +8,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
8
8
  2. **Per account: register the Outlook account** — in admin chat, ask the agent to "register my Outlook account". The agent runs `outlook-account-register`, which returns a short user code and the URL `microsoft.com/devicelogin` and relays both to you.
9
9
  3. **Consent on any device** — open `microsoft.com/devicelogin` on your phone or laptop, enter the code, sign in to your Microsoft account, and consent to the requested scopes (`offline_access`, `User.Read`, `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `Contacts.Read`). `Mail.Send` enables sending and `Calendars.ReadWrite` enables calendar control; a mailbox last consented on an older read-only scope set must re-register before send or any calendar write succeeds. The agent finishes by calling `outlook-account-register-poll`.
10
10
  4. **Done.** Subsequent tool calls (mail, calendar, contacts) use the persisted refresh token transparently.
11
+ 5. **Several mailboxes on one account.** Register again to add a second mailbox; it is stored alongside the first, not in place of it. Every operational tool then takes an optional `mailbox` argument — an email address or graphUserId. With one mailbox, omit it. With two or more, name it; omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows every connected mailbox. Only one registration runs at a time per account; starting another while one is live returns an in-progress signal.
11
12
 
12
13
  ## Tools
13
14
 
@@ -25,7 +26,8 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
25
26
  | `outlook-calendar-respond` | Accept, decline, or tentatively accept an invite. |
26
27
  | `outlook-calendar-freebusy` | Availability for one or more addresses over a window. |
27
28
  | `outlook-contacts-list` | Top contacts. Default top=50. |
28
- | `outlook-mailbox-info` | Health probe — auth state, refresh-window, folder count. |
29
+ | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
30
+ | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
29
31
  | `outlook-mail-send` | Send a message (`POST /me/sendMail`). to/cc/bcc arrays, subject, body, isHtml. Success asserted on Graph 202. Needs `Mail.Send`. |
30
32
  | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
31
33
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
@@ -43,7 +45,9 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
43
45
  | Pending-store census | `pending-store-census account=<id> live=<N>` |
44
46
  | Pending cleared | `pending-cleared account=<id> verified=<bool>` |
45
47
  | /me failed post-token | `me-failed account=<id> status=<N>` |
46
- | Auth ok | `auth-ok account=<id> graphUserId=<id> scopes=<csv> tokenExpSec=<N>` |
48
+ | Auth ok | `auth-ok account=<id> graphUserId=<id> mail=<addr> scopes=<csv> tokenExpSec=<N>` |
49
+ | Registration already in flight | `register-in-progress account=<id>` (start refused; one device-code flow at a time) |
50
+ | Credential resolve | `cred-resolve account=<id> mailbox=<id-or-empty> source=arg|sole|none|refused` — names the resolved mailbox and how it was chosen; `source=refused` is the ambiguity/not-found refusal |
47
51
  | Token refreshed | `token-refreshed account=<id> oldExpSec=<N> newExpSec=<N>` |
48
52
  | Refresh failed | `token-refresh-failed account=<id> reason=<err>` (terminal) |
49
53
  | Mail list | `mail-list op=resolve account=<id> folderIn=<name> folderResolved=<token> asRawId=<bool> count=<N> elapsedMs=<N>` (on error, `error=<msg>` replaces `count`) |
@@ -58,6 +62,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
58
62
  | Scope insufficient | `scope-insufficient tool=<t> account=<id> missing=Calendars.ReadWrite` (stale read-only consent) |
59
63
  | Contacts list | `contacts-list account=<id> count=<N> elapsedMs=<N>` |
60
64
  | Mailbox info | `mailbox-info account=<id> tokenWithinRefreshWindow=<bool> folderCount=<N>` |
65
+ | Mailbox list | `mailbox-list account=<id> count=<N> unreadable=<N>` (unreadable>0 flags an orphaned or corrupt mailbox store) |
61
66
  | Graph error | `graph-error account=<id> status=<N> code=<graphErrorCode> retryAfterMs=<N-or-null>` — writes flow through the same `callGraph` loop, so a failed write logs `graph-error` too |
62
67
  | On-prem rejected | `on-prem-rejected account=<id> mailServer=<host>` (terminal) |
63
68
  | Scope insufficient | `scope-insufficient account=<id> tool=<t> missing=Mail.Send` (terminal — re-register to grant send) |
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "outlook",
3
- "description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read + draft + send; contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox; outlook-mail-send / outlook-draft / outlook-draft-send: compose and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count.",
3
+ "description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read + draft + send; contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox; outlook-mail-send / outlook-draft / outlook-draft-send: compose and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: outlook
3
- description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read + draft + send; contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox; outlook-mail-send / outlook-draft / outlook-draft-send: compose and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count.
3
+ description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read + draft + send; contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox; outlook-mail-send / outlook-draft / outlook-draft-send: compose and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
4
4
  tools:
5
5
  - name: outlook-account-register
6
6
  publicAllowlist: false
@@ -50,6 +50,9 @@ tools:
50
50
  - name: outlook-mailbox-info
51
51
  publicAllowlist: false
52
52
  adminAllowlist: false
53
+ - name: outlook-mailbox-list
54
+ publicAllowlist: false
55
+ adminAllowlist: false
53
56
  always: false
54
57
  embed: ["admin"]
55
58
  metadata: {"platform":{"optional":true}}
@@ -75,12 +78,13 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read,
75
78
 
76
79
  ## Capabilities
77
80
 
78
- - **Register:** `outlook-account-register` — starts the OAuth device-code flow against the brand's Entra app and returns a user code + verification URL immediately. The operator opens `microsoft.com/devicelogin` on any device, enters the code, and consents. `outlook-account-register-poll` completes registration — one check per call. Tokens persist per-account, encrypted on disk (AES-256-CBC).
81
+ - **Register:** `outlook-account-register` — starts the OAuth device-code flow against the brand's Entra app and returns a user code + verification URL immediately. The operator opens `microsoft.com/devicelogin` on any device, enters the code, and consents. `outlook-account-register-poll` completes registration — one check per call. Registering a second mailbox on the same account adds it alongside the first rather than replacing it; each mailbox holds its own encrypted tokens (AES-256-CBC) under a shared account key. One device-code flow runs at a time per account; starting another while one is live returns an `in-progress` signal.
82
+ - **Multiple mailboxes:** one account can hold several mailboxes. Every operational tool takes an optional `mailbox` argument (email or graphUserId). With one mailbox, omit it. With two or more, name it — omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows them all.
79
83
  - **Mail (read):** `outlook-mail-list` returns recent messages; `outlook-mail-search` runs a Graph `$search` query.
80
84
  - **Mail (send / draft):** `outlook-mail-send` sends via `POST /me/sendMail` (success asserted on Graph 202); `outlook-draft` creates a draft and returns its id; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
81
85
  - **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
82
86
  - **Contacts (read-only):** `outlook-contacts-list` returns contacts.
83
- - **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count. Use this to answer "did account X auth?" without grepping logs.
87
+ - **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every connected mailbox with its email, scopes, and token health, reading local stores only — no network — so it also flags a mailbox whose stored credentials cannot be read. Use these to answer "did mailbox X auth?" without grepping logs.
84
88
 
85
89
  ## Out of scope
86
90