@rubytech/create-maxy-code 0.1.473 → 0.1.475

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 (119) hide show
  1. package/dist/__tests__/google-client-id-env.test.js +3 -2
  2. package/package.json +1 -1
  3. package/payload/platform/neo4j/schema.cypher +4 -1
  4. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +29 -16
  5. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  6. package/payload/platform/plugins/docs/references/google-guide.md +25 -12
  7. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +2 -2
  8. package/payload/platform/plugins/google/.claude-plugin/plugin.json +1 -1
  9. package/payload/platform/plugins/google/PLUGIN.md +73 -14
  10. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.d.ts +2 -0
  11. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.d.ts.map +1 -0
  12. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.js +229 -0
  13. package/payload/platform/plugins/google/mcp/dist/__tests__/account-audit.test.js.map +1 -0
  14. package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js +9 -9
  15. package/payload/platform/plugins/google/mcp/dist/__tests__/account-register.test.js.map +1 -1
  16. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.d.ts +2 -0
  17. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.d.ts.map +1 -0
  18. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.js +17 -0
  19. package/payload/platform/plugins/google/mcp/dist/__tests__/consent-url-redirect.test.js.map +1 -0
  20. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.d.ts +2 -0
  21. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.d.ts.map +1 -0
  22. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.js +146 -0
  23. package/payload/platform/plugins/google/mcp/dist/__tests__/loopback-listener.test.js.map +1 -0
  24. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.d.ts +2 -0
  25. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.d.ts.map +1 -0
  26. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.js +19 -0
  27. package/payload/platform/plugins/google/mcp/dist/__tests__/pending-redirect.test.js.map +1 -0
  28. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.d.ts +2 -0
  29. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.d.ts.map +1 -0
  30. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.js +41 -0
  31. package/payload/platform/plugins/google/mcp/dist/__tests__/register-redirect-chain.test.js.map +1 -0
  32. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.d.ts +44 -0
  33. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.d.ts.map +1 -0
  34. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.js +112 -0
  35. package/payload/platform/plugins/google/mcp/dist/auth/account-audit.js.map +1 -0
  36. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.d.ts +17 -0
  37. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.d.ts.map +1 -0
  38. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.js +143 -0
  39. package/payload/platform/plugins/google/mcp/dist/auth/loopback-listener.js.map +1 -0
  40. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.d.ts +9 -0
  41. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.d.ts.map +1 -1
  42. package/payload/platform/plugins/google/mcp/dist/auth/pending-store.js.map +1 -1
  43. package/payload/platform/plugins/google/mcp/dist/index.js +73 -5
  44. package/payload/platform/plugins/google/mcp/dist/index.js.map +1 -1
  45. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.d.ts +2 -0
  46. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.d.ts.map +1 -0
  47. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.js +37 -0
  48. package/payload/platform/plugins/google/mcp/dist/scripts/account-audit.js.map +1 -0
  49. package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts +20 -4
  50. package/payload/platform/plugins/google/mcp/dist/tools/account-register.d.ts.map +1 -1
  51. package/payload/platform/plugins/google/mcp/dist/tools/account-register.js +19 -9
  52. package/payload/platform/plugins/google/mcp/dist/tools/account-register.js.map +1 -1
  53. package/payload/platform/plugins/google/references/auth.md +331 -367
  54. package/payload/platform/plugins/voice-mirror/PLUGIN.md +1 -1
  55. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +3 -3
  56. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  57. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/voice-corpus.d.ts +14 -1
  58. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
  59. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/voice-corpus.js +15 -0
  60. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/voice-corpus.js.map +1 -1
  61. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  62. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +3 -1
  63. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  64. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +51 -4
  65. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +4 -0
  66. package/payload/platform/scripts/backfill-task-ids.sh +206 -0
  67. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +21 -3
  68. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
  69. package/payload/platform/services/claude-session-manager/dist/activity-rows.js +6 -2
  70. package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
  71. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  72. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +43 -5
  73. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  74. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +9 -0
  75. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
  76. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +78 -0
  77. package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
  78. package/payload/server/public/activity.html +6 -6
  79. package/payload/server/public/assets/{AdminLoginScreens-B7TQZV8u.js → AdminLoginScreens-B44KbEuC.js} +1 -1
  80. package/payload/server/public/assets/{AdminShell-DAuri4Yh.js → AdminShell-BjxV2qkb.js} +2 -2
  81. package/payload/server/public/assets/{Checkbox-B1XhnhPk.js → Checkbox-DSeZNGNA.js} +1 -1
  82. package/payload/server/public/assets/activity-Mz1BhgB1.js +1 -0
  83. package/payload/server/public/assets/{admin-BdcccSb5.js → admin-B4LpTCur.js} +1 -1
  84. package/payload/server/public/assets/{browser-B6woQ2qX.js → browser-CEFvaWwI.js} +1 -1
  85. package/payload/server/public/assets/{calendar-CalCeg5T.js → calendar-B3zo6V34.js} +1 -1
  86. package/payload/server/public/assets/chat-5tJdo9i_.js +1 -0
  87. package/payload/server/public/assets/chevron-left-BJllrPBj.js +1 -0
  88. package/payload/server/public/assets/data-Du79EgR-.js +1 -0
  89. package/payload/server/public/assets/{graph-HeZaatSQ.js → graph-RaOrnSf2.js} +1 -1
  90. package/payload/server/public/assets/{graph-labels-4W7JuOgG.js → graph-labels-GlTEIYJj.js} +1 -1
  91. package/payload/server/public/assets/{maximize-2-DKx-aTPb.js → maximize-2-CHDa8p5h.js} +1 -1
  92. package/payload/server/public/assets/{operator-DD9L2VJM.js → operator-DtqHY4jV.js} +1 -1
  93. package/payload/server/public/assets/{page-tJxw0zNO.js → page-CkhFj8GQ.js} +1 -1
  94. package/payload/server/public/assets/{page-Ceado9Kr.js → page-DxnWgPtd.js} +1 -1
  95. package/payload/server/public/assets/{public-BYFWxyeC.js → public-D5TUfQ3I.js} +1 -1
  96. package/payload/server/public/assets/{rotate-ccw-DrzdV9h7.js → rotate-ccw-BJjVAQQY.js} +1 -1
  97. package/payload/server/public/assets/tasks-CM9pYYG1.js +1 -0
  98. package/payload/server/public/assets/{time-entry-format-Bn79w6tq.js → time-entry-format-C5RYEYjM.js} +1 -1
  99. package/payload/server/public/assets/{triangle-alert-CjDVh6r-.js → triangle-alert-DLfIsuo_.js} +1 -1
  100. package/payload/server/public/assets/{useCopyFeedback-DXHkkJw-.js → useCopyFeedback-7yKeiAzV.js} +1 -1
  101. package/payload/server/public/assets/{useSelectionMode-D2H8jAhE.js → useSelectionMode-CWlh6AEq.js} +1 -1
  102. package/payload/server/public/assets/{useSubAccountSwitcher-Br7Z6Cpu.css → useSubAccountSwitcher-B2n9QPVI.css} +1 -1
  103. package/payload/server/public/assets/{useVoiceRecorder-zUvPe55I.js → useVoiceRecorder-j3ZArZ2H.js} +1 -1
  104. package/payload/server/public/browser.html +5 -5
  105. package/payload/server/public/calendar.html +6 -6
  106. package/payload/server/public/chat.html +12 -12
  107. package/payload/server/public/data.html +11 -11
  108. package/payload/server/public/graph.html +11 -11
  109. package/payload/server/public/index.html +14 -14
  110. package/payload/server/public/operator.html +14 -14
  111. package/payload/server/public/public.html +12 -12
  112. package/payload/server/public/tasks.html +5 -5
  113. package/payload/server/server.js +433 -313
  114. package/payload/server/public/assets/activity-kRwzxEKU.js +0 -1
  115. package/payload/server/public/assets/chat-9s2-L06v.js +0 -1
  116. package/payload/server/public/assets/chevron-left-C3ho8SnB.js +0 -1
  117. package/payload/server/public/assets/data-Cfr-7ERI.js +0 -1
  118. package/payload/server/public/assets/tasks-DSwoxGzh.js +0 -1
  119. /package/payload/server/public/assets/{useSubAccountSwitcher-CUJpgkKI.js → useSubAccountSwitcher-CBlKgwms.js} +0 -0
@@ -1,5 +1,6 @@
1
- // acceptance gate for GOOGLE_CLIENT_ID / GOOGLE_PUBLIC_BASE stamping in the
2
- // claude-session-manager systemd unit (Task 1761).
1
+ // acceptance gate for GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET stamping in the
2
+ // claude-session-manager systemd unit (Task 1761; Task 1791 replaced
3
+ // GOOGLE_PUBLIC_BASE with GOOGLE_CLIENT_SECRET and asserts the former is gone).
3
4
  //
4
5
  // This test exists because the first fix stamped only the darwin `.env` mirror
5
6
  // and missed this Linux unit, so on the actual (Ubuntu) install the google
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.473",
3
+ "version": "0.1.475",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -1655,7 +1655,10 @@ FOR (cr:Credit) ON (cr.accountId);
1655
1655
  // ----------------------------------------------------------
1656
1656
  // :VoiceProfile holds the distilled style card for one (AdminUser, format).
1657
1657
  // Task 462: one profile per (accountId, userId, format); format ∈
1658
- // {text, email, social-post, article, marketing-copy, note}.
1658
+ // {formal, informal, text, email, social-post, article, marketing-copy, note}.
1659
+ // Task 1795 added the broad `formal`/`informal` buckets. No constraint or
1660
+ // index change was needed — they resolve through the same `n.format =
1661
+ // $format` equality, and the uniqueness constraint already admits them.
1659
1662
  // The old (accountId, adminUserId) unique constraint is dropped and replaced.
1660
1663
  //
1661
1664
  // :VoiceEdit captures operator edits on agent drafts as feedback,
@@ -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:c15214d349505b6337668b0362b849c9c5301fd25304800cf7384888a6a21435
4
+ content-hash: sha256:3f1095125f80c4f095ed4fbe31bf54975c3ef99332fc3958138af6ad9e82b04e
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2215,7 +2215,7 @@ Source: https://docs.getmaxy.com/google-guide.md
2215
2215
  # Google Calendar
2216
2216
 
2217
2217
  Live Google Calendar for one account: read the user's real events, and create,
2218
- update, cancel, respond to and check availability against them. Eleven tools,
2218
+ update, cancel, respond to and check availability against them. Twelve tools,
2219
2219
  admin agent only.
2220
2220
 
2221
2221
  There is deliberately no Gmail surface here. Mail runs on the `email` plugin's
@@ -2228,14 +2228,23 @@ force Google's annual third-party security assessment. Calendar is only a
2228
2228
  `google-account-register` returns a consent URL. Relay it to the user: they open
2229
2229
  it on any device, sign in, and approve.
2230
2230
 
2231
- Their browser then lands on a page that **fails to load**. That is expected, not
2232
- a fault — the vendor app is a Desktop client, so Google redirects to a loopback
2233
- address on the user's own device where nothing is listening. The address bar
2234
- carries the code. Ask them to copy it and pass it to
2231
+ What happens next depends on where they approved.
2232
+
2233
+ **If they approved on the machine this install runs on**, the connection
2234
+ completes by itself and their browser shows a page naming the connected account.
2235
+ Nothing is pasted. That page asks them to return to the chat and say so — do
2236
+ that, then call `google-account-register-poll` to confirm.
2237
+
2238
+ **If they approved on another device**, such as a phone, their browser lands on
2239
+ a page that **fails to load**. That is expected, not a fault: the vendor app is
2240
+ a Desktop client, so Google redirects to a loopback address on the install, and
2241
+ a phone has no route to it. The address bar still carries the code. Ask them to
2242
+ copy the **whole URL** from the address bar and pass it to
2235
2243
  `google-account-register-submit`, which completes the connection.
2236
2244
 
2237
- If they paste the whole `code=...` fragment, or a value containing `%2F`, the
2238
- tool handles it. `google-account-register-poll` reports the outcome at any point.
2245
+ Paste the entire URL — the tool pulls the code out. A bare code, a `code=...`
2246
+ fragment, and a value containing `%2F` all work too.
2247
+ `google-account-register-poll` reports the outcome at any point.
2239
2248
 
2240
2249
  The register tool does not block and does not wait. A second register while one
2241
2250
  is already live is refused rather than overwriting it; that refusal names how
@@ -2271,9 +2280,12 @@ granted.
2271
2280
 
2272
2281
  ## When something is wrong
2273
2282
 
2274
- **"GOOGLE_CLIENT_ID is not configured"** this brand has no Google vendor app
2275
- yet. Nothing the operator does on the device fixes it; the brand needs its own
2276
- Google Cloud OAuth client. See `platform/plugins/google/references/auth.md`.
2283
+ **"GOOGLE_CLIENT_ID is not configured"**, or the same for
2284
+ `GOOGLE_CLIENT_SECRET` this brand has no Google vendor app yet. Nothing the
2285
+ operator does on the device fixes it, and no file placed on the device fixes it
2286
+ either: the id and secret ship in the brand's `brand.json` and reach the device
2287
+ through an installer publish. The brand needs its own Google Cloud Desktop OAuth
2288
+ client. See `platform/plugins/google/references/auth.md`.
2277
2289
 
2278
2290
  **A 403 naming insufficient scope** — the connected account approved something
2279
2291
  narrower than the full calendar scope. Re-run `google-account-register` so they
@@ -2285,8 +2297,9 @@ Testing, where Google caps refresh tokens at seven days and limits the app to
2285
2297
  property of the vendor app, not a fault in the connection.
2286
2298
 
2287
2299
  **A registration never completes** — the consent URL was opened but not
2288
- approved, or the callback could not be reached. `google-account-register-poll`
2289
- reports `expired` once the pending entry outlives Google's code lifetime.
2300
+ approved, or the code was never copied back to
2301
+ `google-account-register-submit`. `google-account-register-poll` reports
2302
+ `expired` once the pending entry outlives Google's code lifetime.
2290
2303
 
2291
2304
  ---
2292
2305
  # QuickBooks Online
@@ -2817,7 +2830,7 @@ In the admin chat, ask: **"Start the voice-mirror backfill."**
2817
2830
  Maxy asks which stream to backfill first — discrete documents (emails, posts, PDFs) or chat archives (WhatsApp, Telegram, etc.). **Pick chat archives first if you have any imported.** Chat is where your unguarded voice lives; email is the same voice in dress clothes.
2818
2831
 
2819
2832
  - **Chat archives** — tag a whole conversation in one click. Maxy doesn't ask about individual messages because chunks within a conversation almost always share the same voice. Options per conversation: yours, mixed (multiple authors on your side — rare), or not yours (e.g. a Slack channel where you only forwarded other people's messages).
2820
- - **Documents and posts** — paginated 10 at a time. Maxy shows each item with its detected format (email, article, note, social-post). Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, Maxy polished). Override the detected format if one is wrong.
2833
+ - **Documents and posts** — paginated 10 at a time. Maxy shows each item with its source, date, recipient or topic, and a one-line preview. You name the format on every command — Maxy never guesses it. Reach for the broad buckets, `formal` or `informal`, unless a narrower one (`email`, `article`, `note`, `social-post`, `marketing-copy`, `text`) genuinely fits. Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, Maxy polished).
2821
2834
 
2822
2835
  Skip a batch if none qualify, or stop at any time — the next session resumes where you left off.
2823
2836
 
@@ -2857,7 +2870,7 @@ Voice mirror is on by default for every drafting skill. To opt out for one, set
2857
2870
 
2858
2871
  ## Status
2859
2872
 
2860
- Voice mirror is live end-to-end. Six corpus formats (text, email, social-post, article, note, marketing-copy), five tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`, `voice-ingest-session-text`), on-demand session-turn capture, and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account for a given format, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
2873
+ Voice mirror is live end-to-end. Eight corpus formats (the broad buckets `formal` and `informal`, plus the narrower `text`, `email`, `social-post`, `article`, `note`, `marketing-copy`), five tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`, `voice-ingest-session-text`), on-demand session-turn capture, and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account for a given format, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
2861
2874
 
2862
2875
  ---
2863
2876
  # Admin UI Reference
@@ -2921,7 +2934,7 @@ either is a regression.
2921
2934
  |---|---|---|
2922
2935
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
2923
2936
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
2924
- | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss, so a Sessions row and that conversation's reader-panel row read identically. The name comes from the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line so a persistent `channelNamesResolved` shortfall is never silent. | `GET /` |
2937
+ | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss, so a Sessions row and that conversation's reader-panel row read identically. A channel row name has two sources, in precedence order. First the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, which matches the sender phone against the account's admins — that names an admin WhatsApp session and every Telegram session. Second, for a `whatsapp` row only, the WhatsApp pushName from the account's on-disk message store (`storePushNamesBySender` over `readConversationSummaries`, keyed by `normalizeE164` of the counterparty phone so a `+44 7…` sender matches a `447…` store key). The store is consulted only when the resolver reports **no binding** for that sender. That is typically a public WhatsApp customer, the case this exists for — such a sender has no admin record and so can only be named by the store, and without it those rows degraded to the bare number while the reader panel named them — though an admin missing from `users.json` qualifies too. A resolver miss caused by Neo4j being unreachable deliberately does NOT qualify: that is an outage rather than an absent binding, so the row degrades to the number and still logs `reason=neo4j-down`, keeping the outage signal intact. Telegram has no message store and is never looked up. The store read is guarded: a corrupt or absent store degrades to no pushName and logs `op=store-unreadable`, never 500s the list. Both sources are the ones the reader itself uses, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The Sessions list reads the viewed account's own store only. An account-manager's thread persists under the house account rather than the sub-account it manages, so on a multi-account install viewing a sub-account that row is not named here even though the reader panel names it — the reader additionally consults the house store, filtered to manager phones bound to that sub. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> (operator=<n> store=<n>) unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line — emitted only when **both** sources miss, so a residual shortfall means a genuinely nameless sender rather than the expected public-customer floor, and the `operator=`/`store=` split says which source is carrying the names. | `GET /` |
2925
2938
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
2926
2939
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
2927
2940
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
@@ -56,7 +56,7 @@ either is a regression.
56
56
  |---|---|---|
57
57
  | `/session` | Admin cookie session: PIN-gated mint, validate, rotate. | `GET /`, `POST /` |
58
58
  | `/sessions` | Legacy admin-server conversation routes. No UI consumer remains after the ConversationsModal was retired; the surviving handlers are deletion candidates and not described here. | (legacy, no live caller) |
59
- | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss, so a Sessions row and that conversation's reader-panel row read identically. The name comes from the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line so a persistent `channelNamesResolved` shortfall is never silent. | `GET /` |
59
+ | `/sidebar-sessions` | Sole data path for the sidebar Sessions list. One JSONL on disk equals one row. The row's delete button is the only way a row disappears. Each row carries `sessionId`, `title`, `startedAt`, `live`, `isSubagent`, `pid: number \| null` (basename of the matched `sessions/<pid>.json`), and `projectDir` (the directory holding the JSONL — consumed by the delete route). The payload also carries top-level `accountId` so the pane renders the full UUID label whose first ~8 chars prefix-match the truncated Remote Control daemon entry in claude.ai/code. The legacy `rcUrl` field is gone — the row's external-link affordance now POSTs `/session-rc-spawn` to start a fresh local `claude --remote-control <name> --session-id <sid>` PTY on every click. **WhatsApp and Telegram sessions (admin + public) appear in this list**, colour-coded by channel (WhatsApp green `#25D366`, Telegram blue `#229ED9`, via a `conv-channel-*` left accent bar) and carrying the standard row kebab (Usage & cost, Reset/re-seat, Rename, Archive, Delete) so the operator can inspect and re-seat a channel session's params. Each row's `channel` is the authoritative `.meta.json` sidecar value (`whatsapp`/`telegram`/`webchat`/`browser`/null), not the JSONL-envelope channel. Each row also carries its sidecar `senderId`. For a WhatsApp/Telegram row with a `senderId` and no operator rename or ai-title (`titleSource:'prefix'`), the route resolves a contact `personName` and the sidebar renders the reader panel's `"<name> <number>"` label via the shared `operatorConvName`, degrading to the number alone then the stripped title on a miss, so a Sessions row and that conversation's reader-panel row read identically. A channel row name has two sources, in precedence order. First the shared `resolveOperatorNamesBySender` helper the WhatsApp reader also calls, which matches the sender phone against the account's admins — that names an admin WhatsApp session and every Telegram session. Second, for a `whatsapp` row only, the WhatsApp pushName from the account's on-disk message store (`storePushNamesBySender` over `readConversationSummaries`, keyed by `normalizeE164` of the counterparty phone so a `+44 7…` sender matches a `447…` store key). The store is consulted only when the resolver reports **no binding** for that sender. That is typically a public WhatsApp customer, the case this exists for — such a sender has no admin record and so can only be named by the store, and without it those rows degraded to the bare number while the reader panel named them — though an admin missing from `users.json` qualifies too. A resolver miss caused by Neo4j being unreachable deliberately does NOT qualify: that is an outage rather than an absent binding, so the row degrades to the number and still logs `reason=neo4j-down`, keeping the outage signal intact. Telegram has no message store and is never looked up. The store read is guarded: a corrupt or absent store degrades to no pushName and logs `op=store-unreadable`, never 500s the list. Both sources are the ones the reader itself uses, so the two surfaces cannot drift; a renamed or ai-titled channel row keeps its own title. The Sessions list reads the viewed account's own store only. An account-manager's thread persists under the house account rather than the sub-account it manages, so on a multi-account install viewing a sub-account that row is not named here even though the reader panel names it — the reader additionally consults the house store, filtered to manager phones bound to that sub. The only role/channel excluded from the list is `public`+`webchat` (its dedicated reader is the sole surface); the exclusion predicate is `isSessionListExcluded`, decoupled from the reader's inclusion predicate `isReaderChannelSession` (unchanged), so the WhatsApp/Telegram reader panels are unaffected — a store-only WhatsApp conversation with no session JSONL stays reader-only. Observability rides `[admin-sessions-list] … channelInList=whatsapp:<n>,telegram:<n>,webchat:<n> channelNameCandidates=<n> channelNamesResolved=<n> (operator=<n> store=<n>) unknownChannel=<n> excludedPublicWebchat=<n>`, and each unresolved channel name logs one `[admin-sessions-list] op=channel-name-unresolved sessionId=<id8> channel=<whatsapp\|telegram> senderId=<id> reason=<no-senderId\|classify-miss\|neo4j-down>` line — emitted only when **both** sources miss, so a residual shortfall means a genuinely nameless sender rather than the expected public-customer floor, and the `operator=`/`store=` split says which source is carrying the names. | `GET /` |
60
60
  | `/session-delete` | POST `{ sessionId }`. Thin proxy over the manager: POST `/:id/stop` (idempotent) then DELETE `/:id`. Sends **no** accountId. The manager resolves the session by scanning every slug under `projects/` for its JSONL (`slugForExistingJsonl`, covering `archive/`), exactly as the sidebar list enumerates rows, and gates on file existence alone — so any session the operator can see is deletable. This replaces the earlier accountId-derived slug and the `!row` term, which narrowed resolution below the list and left a session under a non-house slug with no watcher row (e.g. a legacy `-home-admin` session on a single-account install) listed yet un-deletable. Account scope is a view filter, not a delete gate — admin access is install-wide, so there is nothing to scope against. The sibling `/session-stop` (the standalone End control) resolves identically and also forwards no accountId. | `POST /` |
61
61
  | `/session-rc-spawn` | POST `{ sessionId?, name? }`. Fire-and-forget `claude --remote-control [name] [--session-id <sid>]`. Present `sessionId` resumes; absent starts a fresh session (also used by the sidebar's "New session" button — it no longer opens claude.ai/code directly). Proxies to the manager's `/rc-spawn`, which waits up to **60 s** (raised from 12 s) for the spawned PTY to bind and returns `{ spawnedPid, sessionId, bridgeSessionId, slug, outcome, reason }`. For a webchat-bound spawn (every admin-gated host's "New session", returning a same-origin `/chat?session=<id>` target — `resolveRcSpawnOutcome` → `sameOrigin:true`) the Sidebar navigates the **current** tab via `window.location.assign`, replacing the dashboard in place (back returns to it); only a claude.ai/code slug (`sameOrigin:false`, the bare-admin resume bridge, never a new-session outcome) navigates a separately-opened tab. On `timeout` or `spawn-failed` it shows an error modal (reason + sessionId) and **never** opens a bare claude.ai/code tab. The new process registers itself as its own Remote Control entry in claude.ai/code. | `POST /` |
62
62
  | `/claude-sessions` | **Spawn surface only**. `POST /` is the Sidebar new-session-with-prompt path, cookie-auth only (the recorder loopback caller was removed; LinkedIn ingest moved to `/rc-spawn`). The former UI-facing handlers (SSE row feed, list, resume, stop, rename, archive, delete, `/:id/meta`, `/:id/input`, `/:id/log`) were removed — the maxy dashboard no longer manages or displays sessions. | `POST /` |
@@ -1,7 +1,7 @@
1
1
  # Google Calendar
2
2
 
3
3
  Live Google Calendar for one account: read the user's real events, and create,
4
- update, cancel, respond to and check availability against them. Eleven tools,
4
+ update, cancel, respond to and check availability against them. Twelve tools,
5
5
  admin agent only.
6
6
 
7
7
  There is deliberately no Gmail surface here. Mail runs on the `email` plugin's
@@ -14,14 +14,23 @@ force Google's annual third-party security assessment. Calendar is only a
14
14
  `google-account-register` returns a consent URL. Relay it to the user: they open
15
15
  it on any device, sign in, and approve.
16
16
 
17
- Their browser then lands on a page that **fails to load**. That is expected, not
18
- a fault — the vendor app is a Desktop client, so Google redirects to a loopback
19
- address on the user's own device where nothing is listening. The address bar
20
- carries the code. Ask them to copy it and pass it to
17
+ What happens next depends on where they approved.
18
+
19
+ **If they approved on the machine this install runs on**, the connection
20
+ completes by itself and their browser shows a page naming the connected account.
21
+ Nothing is pasted. That page asks them to return to the chat and say so — do
22
+ that, then call `google-account-register-poll` to confirm.
23
+
24
+ **If they approved on another device**, such as a phone, their browser lands on
25
+ a page that **fails to load**. That is expected, not a fault: the vendor app is
26
+ a Desktop client, so Google redirects to a loopback address on the install, and
27
+ a phone has no route to it. The address bar still carries the code. Ask them to
28
+ copy the **whole URL** from the address bar and pass it to
21
29
  `google-account-register-submit`, which completes the connection.
22
30
 
23
- If they paste the whole `code=...` fragment, or a value containing `%2F`, the
24
- tool handles it. `google-account-register-poll` reports the outcome at any point.
31
+ Paste the entire URL — the tool pulls the code out. A bare code, a `code=...`
32
+ fragment, and a value containing `%2F` all work too.
33
+ `google-account-register-poll` reports the outcome at any point.
25
34
 
26
35
  The register tool does not block and does not wait. A second register while one
27
36
  is already live is refused rather than overwriting it; that refusal names how
@@ -57,9 +66,12 @@ granted.
57
66
 
58
67
  ## When something is wrong
59
68
 
60
- **"GOOGLE_CLIENT_ID is not configured"** this brand has no Google vendor app
61
- yet. Nothing the operator does on the device fixes it; the brand needs its own
62
- Google Cloud OAuth client. See `platform/plugins/google/references/auth.md`.
69
+ **"GOOGLE_CLIENT_ID is not configured"**, or the same for
70
+ `GOOGLE_CLIENT_SECRET` this brand has no Google vendor app yet. Nothing the
71
+ operator does on the device fixes it, and no file placed on the device fixes it
72
+ either: the id and secret ship in the brand's `brand.json` and reach the device
73
+ through an installer publish. The brand needs its own Google Cloud Desktop OAuth
74
+ client. See `platform/plugins/google/references/auth.md`.
63
75
 
64
76
  **A 403 naming insufficient scope** — the connected account approved something
65
77
  narrower than the full calendar scope. Re-run `google-account-register` so they
@@ -71,5 +83,6 @@ Testing, where Google caps refresh tokens at seven days and limits the app to
71
83
  property of the vendor app, not a fault in the connection.
72
84
 
73
85
  **A registration never completes** — the consent URL was opened but not
74
- approved, or the callback could not be reached. `google-account-register-poll`
75
- reports `expired` once the pending entry outlives Google's code lifetime.
86
+ approved, or the code was never copied back to
87
+ `google-account-register-submit`. `google-account-register-poll` reports
88
+ `expired` once the pending entry outlives Google's code lifetime.
@@ -30,7 +30,7 @@ In the admin chat, ask: **"Start the voice-mirror backfill."**
30
30
  {{productName}} asks which stream to backfill first — discrete documents (emails, posts, PDFs) or chat archives (WhatsApp, Telegram, etc.). **Pick chat archives first if you have any imported.** Chat is where your unguarded voice lives; email is the same voice in dress clothes.
31
31
 
32
32
  - **Chat archives** — tag a whole conversation in one click. {{productName}} doesn't ask about individual messages because chunks within a conversation almost always share the same voice. Options per conversation: yours, mixed (multiple authors on your side — rare), or not yours (e.g. a Slack channel where you only forwarded other people's messages).
33
- - **Documents and posts** — paginated 10 at a time. {{productName}} shows each item with its detected format (email, article, note, social-post). Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, {{productName}} polished). Override the detected format if one is wrong.
33
+ - **Documents and posts** — paginated 10 at a time. {{productName}} shows each item with its source, date, recipient or topic, and a one-line preview. You name the format on every command — {{productName}} never guesses it. Reach for the broad buckets, `formal` or `informal`, unless a narrower one (`email`, `article`, `note`, `social-post`, `marketing-copy`, `text`) genuinely fits. Tag the whole batch, a subset by number, or per-item if you want a precise label like `human-led-agent-assisted` (you wrote the content, {{productName}} polished).
34
34
 
35
35
  Skip a batch if none qualify, or stop at any time — the next session resumes where you left off.
36
36
 
@@ -70,4 +70,4 @@ Voice mirror is on by default for every drafting skill. To opt out for one, set
70
70
 
71
71
  ## Status
72
72
 
73
- Voice mirror is live end-to-end. Six corpus formats (text, email, social-post, article, note, marketing-copy), five tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`, `voice-ingest-session-text`), on-demand session-turn capture, and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account for a given format, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
73
+ Voice mirror is live end-to-end. Eight corpus formats (the broad buckets `formal` and `informal`, plus the narrower `text`, `email`, `social-post`, `article`, `note`, `marketing-copy`), five tools (`voice-tag-content`, `voice-distil-profile`, `voice-retrieve-conditioning`, `voice-record-feedback`, `voice-ingest-session-text`), on-demand session-turn capture, and wiring into the three live drafting surfaces (email composition, property brochures, investor data room) are all live. When no voice profile exists on the account for a given format, every drafting skill degrades gracefully — the output matches what it was before voice mirror was installed.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google",
3
- "description": "Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; the user approves, lands on a page that fails to load by design, and pastes the code back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.",
3
+ "description": "Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE against a Desktop client whose id and secret ship in brand config; the plugin mints the consent URL and exchanges the code in-process, and nothing in platform/ui participates. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; approving on the install machine completes by itself and the browser confirms it, while approving on another device lands on a page that fails to load by design and the user pastes the address-bar URL back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: google
3
- description: Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE; the consent URL is minted by the plugin and the code is exchanged by platform/ui, which holds the house-only client secret. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; the user approves, lands on a page that fails to load by design, and pastes the code back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.
3
+ description: Live Google Calendar for one account via the Google Calendar API v3 — read plus control. Per-account OAuth authorization-code + PKCE against a Desktop client whose id and secret ship in brand config; the plugin mints the consent URL and exchanges the code in-process, and nothing in platform/ui participates. One account can connect several Google accounts; every operational tool takes an optional account selector (the email address) and refuses ambiguously when two or more are connected and none is named. There is deliberately no Gmail surface — mail runs on the `email` plugin's IMAP/SMTP path, and Gmail's restricted scopes would force Google's annual third-party security assessment, whereas Calendar is only a sensitive scope. Tools — google-account-register / google-account-register-submit / google-account-register-poll: connect a Google Calendar (returns a consent URL; approving on the install machine completes by itself and the browser confirms it, while approving on another device lands on a page that fails to load by design and the user pastes the address-bar URL back to submit); google-calendar-list: events in the next rangeDays days, recurring series expanded; google-calendar-event: one event by id; google-calendar-create / google-calendar-update / google-calendar-cancel: control events, attendees notified, update changes only the fields named; google-calendar-respond: accept, decline or tentatively accept an invitation, leaving other attendees' responses untouched; google-calendar-freebusy: busy blocks for this or named calendars; google-account-info: auth state for one connected Google account; google-account-list: every connected Google account with its own status.
4
4
  tools:
5
5
  - name: google-account-register
6
6
  publicAllowlist: false
@@ -50,8 +50,7 @@ mcp:
50
50
  ACCOUNT_ID: ${ACCOUNT_ID}
51
51
  SESSION_ID: ${SESSION_ID}
52
52
  GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
53
- GOOGLE_PUBLIC_BASE: ${GOOGLE_PUBLIC_BASE}
54
- MAXY_UI_INTERNAL_PORT: ${MAXY_UI_INTERNAL_PORT}
53
+ GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
55
54
  mcp-manifest: auto
56
55
  ---
57
56
 
@@ -75,16 +74,31 @@ Authorization-code + PKCE, not device-code. Google's device flow serves a fixed
75
74
  scope allowlist that excludes Calendar, and the out-of-band paste-a-code flow
76
75
  has been blocked since 31 January 2023, so consent must land on a real redirect.
77
76
 
78
- `google-account-register` mints the PKCE pair and a `state` nonce, persists them
79
- single-slot per account, and returns a consent URL. The user opens it on any
80
- device and approves. Google redirects to `/api/google/oauth/callback` on this
81
- install's public host, which verifies `state`, exchanges the code, resolves the
82
- account's email from its primary calendar, and writes encrypted tokens.
83
- `google-account-register-poll` then reports the outcome from local state.
77
+ The vendor app is a **Desktop (installed app)** client, the one type whose
78
+ secret Google sanctions shipping in distributed software. Its id and secret come
79
+ from `brand.json` and are stamped by the installer together or not at all.
84
80
 
85
- The client secret never enters this MCP process. A Web-application client must
86
- present it to refresh, so refresh is delegated to `platform/ui`, which holds it
87
- at `${PLATFORM_ROOT}/config/google-house.env`.
81
+ `google-account-register` mints the PKCE pair, binds a single-shot listener on
82
+ `127.0.0.1` at an OS-assigned port, and returns a consent URL carrying that
83
+ port. The host is always the IP literal, never `localhost` — Google's
84
+ port-agnostic loopback matching applies only to the literal.
85
+
86
+ Consent on the install's own machine reaches that listener, which verifies
87
+ `state`, completes the exchange, and renders a page naming the account. Consent
88
+ on another device cannot reach it, so the redirect fails to load and the user
89
+ pastes the address-bar URL into `google-account-register-submit`. Both paths
90
+ exchange in-process with the stored verifier, resolve the email from the primary
91
+ calendar, and write encrypted tokens. `google-account-register-poll` reports the
92
+ outcome from local state.
93
+
94
+ The pending entry stores the redirect it issued, because the port varies per
95
+ registration and Google compares the exchange's `redirect_uri` byte for byte
96
+ against the one that issued the code. A failed bind falls back to
97
+ `http://127.0.0.1:8571`, leaving the paste path exactly as it was.
98
+
99
+ Token refresh also runs in-process against Google. Nothing in `platform/ui`
100
+ mints, exchanges or refreshes a Google credential; its only Google surface is the
101
+ standing pending audit below, which reads a file's age and never decrypts.
88
102
 
89
103
  ## Scope
90
104
 
@@ -97,8 +111,53 @@ requesting `openid`/`email`/`profile`, so consent stays minimal.
97
111
  | Variable | Source | Effect when unset |
98
112
  |---|---|---|
99
113
  | `GOOGLE_CLIENT_ID` | `brand.json#googleClientId` | Plugin inert; register refuses with an actionable message |
100
- | `GOOGLE_PUBLIC_BASE` | install's public host origin | Register refuses; the redirect cannot be built |
101
- | `MAXY_ADMIN_BASE` | loopback `platform/ui` base | Defaults to `http://127.0.0.1:19301` |
114
+ | `GOOGLE_CLIENT_SECRET` | `brand.json#googleClientSecret` | Plugin inert; register refuses. Stamped with `GOOGLE_CLIENT_ID` or not at all |
115
+
116
+ Both reach this process by env inheritance from the session-manager unit, which
117
+ is why they are declared in the self-named `${FOO}` form (see
118
+ `mcp-descriptor-resolve.ts`). Boot reports `clientIdConfigured` and
119
+ `clientSecretConfigured`; either reading `false` on a brand that ships both is
120
+ the mis-stamp signature.
121
+
122
+ ## Observability
123
+
124
+ Two failures here emit no event and do not reproduce on demand: a registration
125
+ the operator abandons, and a refresh token that expires. Nothing touches either;
126
+ they simply stop being true at some future date. Two standing audits report them
127
+ without anything polling.
128
+
129
+ | Audit | Where it runs | Interval | Per-finding line | Heartbeat |
130
+ |---|---|---|---|---|
131
+ | Stranded pending registration | in-process in `platform/ui` | 5 min | `[google-oauth] op=pending-stranded account=<id> ageSec=<N>` | `[google-oauth] op=pending-audit scanned=<N> stranded=<M>` |
132
+ | Refresh-token TTL | spawned `mcp/dist/scripts/account-audit.js` | 1 hour | `[google-account-audit] account=<id> email=<addr> refreshTtlDays=<D> status=<ok\|expiring\|expired>` | `[google-account-audit] op=heartbeat accounts=<N> ok=<A> expiring=<B> expired=<C>` |
133
+
134
+ They run on different mechanisms deliberately. The pending audit needs no
135
+ decrypt — file age alone is conclusive — so it runs in-process and stays
136
+ independent of whether any spawn is healthy. The refresh audit must decrypt the
137
+ token blob with the per-account key, so it runs where `TokenStore` lives.
138
+
139
+ **The heartbeat is the point.** Each audit emits its heartbeat on every cycle,
140
+ including cycles that find nothing, because an audit that has stopped running is
141
+ otherwise indistinguishable from one reporting all-clear. The absence of a
142
+ heartbeat over more than one interval is the documented signal that the audit is
143
+ down — not silence meaning healthy. A failed pending cycle logs
144
+ `op=pending-audit-error`, deliberately a different token from the heartbeat so a
145
+ broken cycle can never be counted as a clean one.
146
+
147
+ `refreshTtlDays` is days left on Google's 180-day **inactivity** revocation
148
+ clock, measured from `lastRefresh`. Google publishes no refresh-token expiry and
149
+ the stored blob holds none, so this is the only bounded, locally-computable
150
+ clock. It does not predict the user revoking access or changing their password,
151
+ neither of which is observable from here. A connection holding no refresh token
152
+ at all reports `expired` with zero days regardless of how recently it was
153
+ written, because it dies when its access token lapses about an hour out.
154
+
155
+ Diagnostic path:
156
+
157
+ ```
158
+ ~/<brand>-code/platform/scripts/logs-read.sh --tail server 200 \
159
+ | grep -E '\[google-oauth\]|\[google-account-audit\]'
160
+ ```
102
161
 
103
162
  ## Out of scope
104
163
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=account-audit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-audit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/account-audit.test.ts"],"names":[],"mappings":""}