@rubytech/create-maxy-code 0.1.312 → 0.1.313

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 (67) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  3. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  4. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +37 -3
  5. package/payload/platform/plugins/docs/references/admin-ui.md +2 -2
  6. package/payload/platform/plugins/docs/references/quickbooks.md +29 -0
  7. package/payload/platform/plugins/quickbooks/.claude-plugin/plugin.json +21 -0
  8. package/payload/platform/plugins/quickbooks/PLUGIN.md +91 -0
  9. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +159 -0
  10. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/package.json +3 -0
  11. package/payload/platform/plugins/quickbooks/mcp/dist/index.d.ts +2 -0
  12. package/payload/platform/plugins/quickbooks/mcp/dist/index.d.ts.map +1 -0
  13. package/payload/platform/plugins/quickbooks/mcp/dist/index.js +218 -0
  14. package/payload/platform/plugins/quickbooks/mcp/dist/index.js.map +1 -0
  15. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.d.ts +37 -0
  16. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.d.ts.map +1 -0
  17. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.js +75 -0
  18. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.js.map +1 -0
  19. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.d.ts +39 -0
  20. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.d.ts.map +1 -0
  21. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.js +178 -0
  22. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.js.map +1 -0
  23. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.d.ts +36 -0
  24. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.d.ts.map +1 -0
  25. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.js +107 -0
  26. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.js.map +1 -0
  27. package/payload/platform/plugins/quickbooks/mcp/package.json +19 -0
  28. package/payload/platform/plugins/quickbooks/skills/quickbooks/SKILL.md +37 -0
  29. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +16 -0
  31. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  32. package/payload/server/{chunk-J6WDAWFJ.js → chunk-Q7EWROVN.js} +4 -0
  33. package/payload/server/maxy-edge.js +1 -1
  34. package/payload/server/public/assets/{AccessGate-Dh5A79FU.js → AccessGate-Byskkf47.js} +1 -1
  35. package/payload/server/public/assets/{AdminLoginScreens-B8s2TbT7.js → AdminLoginScreens-DgwQi3HN.js} +1 -1
  36. package/payload/server/public/assets/AdminShell-DQfL2c_L.js +1 -0
  37. package/payload/server/public/assets/{Checkbox-B_sOAyv1.js → Checkbox-C54JSreC.js} +1 -1
  38. package/payload/server/public/assets/OperatorConversations-DulQyF8c.js +1 -0
  39. package/payload/server/public/assets/{admin-gZEndvJT.js → admin-B25rkI9Q.js} +1 -1
  40. package/payload/server/public/assets/{audio-attachment-mime-B0b89VnH.js → audio-attachment-mime-BlwrgPQa.js} +1 -1
  41. package/payload/server/public/assets/{brand-BcNavK6q.css → brand-TWflEl22.css} +1 -1
  42. package/payload/server/public/assets/{browser-BUlB5_MD.js → browser-C9Z5dLlN.js} +1 -1
  43. package/payload/server/public/assets/chat-CGNWDmid.js +1 -0
  44. package/payload/server/public/assets/data-DCQDh0-Y.js +1 -0
  45. package/payload/server/public/assets/{graph-labels-AZLGoVy8.js → graph-labels-BBtL--_M.js} +1 -1
  46. package/payload/server/public/assets/{graph-DMCKpW6P.js → graph-sMzOI71g.js} +2 -2
  47. package/payload/server/public/assets/operator-hI6kJi0I.js +1 -0
  48. package/payload/server/public/assets/page-DbsqlpuX.js +1 -0
  49. package/payload/server/public/assets/{public-G_-UI9nB.js → public-Cs5N4-Az.js} +1 -1
  50. package/payload/server/public/assets/{public-next-C8_nExQB.js → public-next-DoGPYX6T.js} +1 -1
  51. package/payload/server/public/assets/{useSelectionMode-Bf6Rt-sl.js → useSelectionMode-Cs-vtuKI.js} +1 -1
  52. package/payload/server/public/browser.html +6 -6
  53. package/payload/server/public/chat.html +8 -8
  54. package/payload/server/public/data.html +5 -5
  55. package/payload/server/public/graph.html +8 -8
  56. package/payload/server/public/index.html +8 -8
  57. package/payload/server/public/operator.html +10 -10
  58. package/payload/server/public/public-next.html +9 -9
  59. package/payload/server/public/public.html +7 -7
  60. package/payload/server/server.js +600 -491
  61. package/payload/server/public/assets/AdminShell-Cu1zlb6L.js +0 -1
  62. package/payload/server/public/assets/OperatorConversations-hm0Gpu_Q.js +0 -1
  63. package/payload/server/public/assets/chat-BnceaVl7.js +0 -1
  64. package/payload/server/public/assets/data-3Nl3P5wt.js +0 -1
  65. package/payload/server/public/assets/operator-BF4F7k23.js +0 -1
  66. package/payload/server/public/assets/page-DQUEst1o.js +0 -1
  67. /package/payload/server/public/assets/{brand-BQ_u9UdS.js → brand-CYjs10m-.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.312",
3
+ "version": "0.1.313",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -99,6 +99,11 @@
99
99
  "source": "./prompt-optimiser",
100
100
  "version": "0.1.0"
101
101
  },
102
+ {
103
+ "name": "quickbooks",
104
+ "source": "./quickbooks",
105
+ "version": "0.1.0"
106
+ },
102
107
  {
103
108
  "name": "replicate",
104
109
  "source": "./replicate",
@@ -7,6 +7,7 @@ graph
7
7
  graph-viewer
8
8
  memory
9
9
  outlook
10
+ quickbooks
10
11
  replicate
11
12
  scheduling
12
13
  telegram
@@ -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:14ea06161a5423a4a55415fac3ef50699469968a7224fa04fa9ace74757c616f
4
+ content-hash: sha256:70302074e7238ab9292e68575a41070b0f3163494e27cd31a9b455c799a9af56
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -1846,6 +1846,40 @@ Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions iss
1846
1846
 
1847
1847
  Write tools (send, draft, move, flag), OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
1848
1848
 
1849
+ ---
1850
+ # QuickBooks Online
1851
+ Source: https://docs.getmaxy.com/quickbooks.md
1852
+
1853
+ # QuickBooks Online
1854
+
1855
+ Connect QuickBooks Online so the agent can read your books and create records — invoices, customers, bills, payments, and reports — over Intuit's REST API.
1856
+
1857
+ QuickBooks Online is the cloud product. Your books live in Intuit's cloud, so every request proves two things: that the connecting app is yours, and that someone authorized it to touch a specific company. Maxy ships to your own machine and never runs a shared backend, so it cannot hold one master QuickBooks identity for everyone. Instead you bring your own Intuit app, and its keys are stored only on your install.
1858
+
1859
+ ## What you do once
1860
+
1861
+ **1. Register an Intuit app.** Go to [developer.intuit.com](https://developer.intuit.com), create an app, and turn on the Accounting scope. Copy the app's **client ID** and **client secret**. In the app's OAuth settings, add a **redirect URI**: your public agent address followed by `/api/quickbooks/callback`. Intuit does not allow `localhost` for live use, which is why the address is your public one. Start with the free **sandbox** keys (Intuit gives you up to five practice companies) before moving to real books.
1862
+
1863
+ **2. Hand the keys to the agent.** Tell the agent your client ID and secret, and whether they are sandbox or production. The agent stores them on your install only.
1864
+
1865
+ **3. Authorize each company.** Ask the agent to start a connection. It gives you a link; you open it, sign in to Intuit, and approve one company. That is the only manual step per company. An accountant with several companies under one app just repeats this for each.
1866
+
1867
+ ## What the agent does after that
1868
+
1869
+ Everything else is automatic. The agent keeps the connection alive on its own, refreshing access behind the scenes, and can:
1870
+
1871
+ - **Read** anything — "show me unpaid invoices since January", "list my customers", "what bills are due this week".
1872
+ - **Pull reports** — profit and loss, balance sheet, aged receivables, for any date range.
1873
+ - **Create and update records** — raise an invoice, add a customer, enter a bill, record a payment.
1874
+
1875
+ ## Keeping it healthy
1876
+
1877
+ A QuickBooks connection goes stale if it sits unused for about 100 days, or if you revoke access from your Intuit account. Ask the agent to run a connection audit now and then; if a company shows as not alive, just re-authorize it with a fresh link. Reading has a generous free monthly allowance and writing is free, so the agent favours precise lookups over pulling everything.
1878
+
1879
+ ## Moving to real books
1880
+
1881
+ Switching your Intuit app from sandbox to production requires passing Intuit's own production security review. That is between you and Intuit; it is not something the agent does for you.
1882
+
1849
1883
  ---
1850
1884
  # LinkedIn Extension
1851
1885
  Source: https://docs.getmaxy.com/linkedin-extension.md
@@ -2286,7 +2320,7 @@ either is a regression.
2286
2320
 
2287
2321
  **`/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. A third section, **Your specialists**, shows the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
2288
2322
 
2289
- **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble with only the message text (no label, no per-turn time); all agent output renders as plain prose (the reply-document filename line stays, as prose); a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
2323
+ **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
2290
2324
 
2291
2325
  **maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
2292
2326
 
@@ -2533,7 +2567,7 @@ authoritative. This section names the surfaces and what backs each.
2533
2567
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
2534
2568
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
2535
2569
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
2536
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). | `/claude-sessions/events`, `/claude-sessions` |
2570
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. | `/claude-sessions/events`, `/claude-sessions` |
2537
2571
  | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
2538
2572
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
2539
2573
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
@@ -55,7 +55,7 @@ either is a regression.
55
55
 
56
56
  **`/chat` empty-state greeting panel.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`) above two read-only lists from `GET /api/webchat/greeting` (`server/routes/webchat-greeting.ts`, `requireAdminSession`-gated, `accountId`-scoped): the next 5 upcoming `:Event` rows (`eventStatus IN ['scheduled','due']`, future `COALESCE(nextRun, startDate)`, soonest first) and the outstanding `:Task` rows (`status IN ['pending','active','running']`, 5 shown + `+N more`). Read failure returns HTTP 200 `{ ok: false }` and the page shows the headline alone — the panel never blocks the composer. A third section, **Your specialists**, shows the account's installed roster — every `*.md` under `data/accounts/<accountId>/specialists/agents/`, core and `--`-prefixed premium files alike — shown by frontmatter `name` + `summary` (fallback `description`), empty dir → "No specialists installed". The roster read (`app/lib/claude-agent/specialist-roster.ts`) is isolated from the graph fallback: malformed files are skipped (`op=specialist-skipped file=<name> reason=<…>`), a whole-roster failure degrades to `specialists: []` inside the `ok: true` payload (`op=specialists-read-failed`). Logs: `[webchat:greeting] op=read account=<id8> events=<n> tasks=<n> specialists=<n> ms=<n>` / `op=read-failed reason=…`; client warns `[admin-ui] greeting-unavailable …`. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
57
57
 
58
- **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble with only the message text (no label, no per-turn time); all agent output renders as plain prose (the reply-document filename line stays, as prose); a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
58
+ **`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx` unmodified, `app/chat/__tests__/transcript-render.test.tsx` new).
59
59
 
60
60
  **maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
61
61
 
@@ -302,7 +302,7 @@ authoritative. This section names the surfaces and what backs each.
302
302
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
303
303
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
304
304
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
305
- | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). | `/claude-sessions/events`, `/claude-sessions` |
305
+ | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. Every listed row carries two launch actions: the claude.ai/code resume (`ExternalLink`) and a chat-launch (`MessageSquare`) that opens `/chat?session=<full sessionId>` — the admin webchat pointed at that exact session; sending there resumes a stopped session with the webchat channel bound to that id (see `admin-webchat-native-channel.md`, "Session-targeted mode"). At the ≤720 px drawer breakpoint each row stacks into two lines — dot + title + id/stamp, then the action icons — with a divider between rows and actions at full opacity (the desktop hover-gated 0.5 opacity never resolves on touch). Below a 400 px list width each row's action icons collapse to a single `MoreHorizontal` ellipsis trigger; its popover menu lists the same actions as the inline icons but labels them with concise verbs (`Resume in claude.ai/code`, `Open in webchat`, `Stop`, `Archive` / `Unarchive`, `Rename`, `Delete`) rather than the inline icon buttons' verbose per-session aria-labels — the inline buttons keep those verbose labels because an icon-only control needs the full descriptive accessible name. | `/claude-sessions/events`, `/claude-sessions` |
306
306
  | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session**. Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows a **display name** resolved by precedence `operatorName ?? whatsappName ?? senderId ?? title`: the bound `Person givenName familyName` when a `senderId`→`userId` binding exists, else the persisted WhatsApp display name (`pushName`, read from the latest `:Message:WhatsAppMessage.senderName`), else the raw `senderId`, else the agent title; the agent session title is the hover tooltip only. Under the name each row shows a **last-message-time breadcrumb** (`conv-timestamp`, same markup as the Sessions rows) formatted from the row's `lastMessageAt` (the last parseable turn's `ts`); a session with no parseable turn shows no time, never "Invalid Date". A click on a conversation **opens its transcript on the home surface**: on `/` it selects in place, and from any other route (`/graph`, `/data`, `/browser`) it navigates to `/?wa=<sessionId>&projectDir=<dir>`, which the root shell hydrates on mount (then strips the query so a later refresh does not re-pin a closed conversation). Server logs `[wa-reader] op=operator-name-fallback senderId=… source=whatsapp-pushname` when the pushName fallback fires and `[wa-reader] op=conversations count=<n> withTimestamp=<m>` per list build; the client logs `[admin-ui] wa-open route=… via=<in-place\|navigate> …` and `[admin-ui] wa-hydrate route=/ …`. | `/whatsapp-reader/conversations` |
307
307
  | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. Every turn shows its time-of-day (HH:MM from the turn's `ts`; a null/unparseable `ts` shows no time, never "Invalid Date"). `tool-call`/`tool-result` turns render as **collapsed cards** (chevron + label + time, default collapsed, expandable per card; one card's state never affects another) so the human↔agent text is not buried in JSON; the three conversation kinds always render in full. The thread **follows the tail**: it opens at the newest turn and pins to the bottom on each live append **only while the operator is already within 32 px of the bottom** — a scroll up suppresses the jump so reading history is never yanked. While the operator is scrolled up a **jump-to-bottom control** (`.wa-jump`, bottom-right of the viewport) appears; clicking it scrolls to the newest turn and re-arms follow-tail, and it hides again at the bottom. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. The thread also interleaves a collapsed `⚙ directive injected (N B)` row per turn, sorted by timestamp just before the turn it informed; expanding one fetches the exact stored `prompt-optimiser-directive` bytes for that turn. The entries are listed by `GET /whatsapp-reader/directives?sessionId=` and the bytes served by `GET /whatsapp-reader/directive?sessionId=&name=`, both admin-gated and account-scoped; a missing store degrades to no rows. | `/whatsapp-reader/stream`, `/whatsapp-reader/directives`, `/whatsapp-reader/directive` |
308
308
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
@@ -0,0 +1,29 @@
1
+ # QuickBooks Online
2
+
3
+ Connect QuickBooks Online so the agent can read your books and create records — invoices, customers, bills, payments, and reports — over Intuit's REST API.
4
+
5
+ QuickBooks Online is the cloud product. Your books live in Intuit's cloud, so every request proves two things: that the connecting app is yours, and that someone authorized it to touch a specific company. {{productName}} ships to your own machine and never runs a shared backend, so it cannot hold one master QuickBooks identity for everyone. Instead you bring your own Intuit app, and its keys are stored only on your install.
6
+
7
+ ## What you do once
8
+
9
+ **1. Register an Intuit app.** Go to [developer.intuit.com](https://developer.intuit.com), create an app, and turn on the Accounting scope. Copy the app's **client ID** and **client secret**. In the app's OAuth settings, add a **redirect URI**: your public agent address followed by `/api/quickbooks/callback`. Intuit does not allow `localhost` for live use, which is why the address is your public one. Start with the free **sandbox** keys (Intuit gives you up to five practice companies) before moving to real books.
10
+
11
+ **2. Hand the keys to the agent.** Tell the agent your client ID and secret, and whether they are sandbox or production. The agent stores them on your install only.
12
+
13
+ **3. Authorize each company.** Ask the agent to start a connection. It gives you a link; you open it, sign in to Intuit, and approve one company. That is the only manual step per company. An accountant with several companies under one app just repeats this for each.
14
+
15
+ ## What the agent does after that
16
+
17
+ Everything else is automatic. The agent keeps the connection alive on its own, refreshing access behind the scenes, and can:
18
+
19
+ - **Read** anything — "show me unpaid invoices since January", "list my customers", "what bills are due this week".
20
+ - **Pull reports** — profit and loss, balance sheet, aged receivables, for any date range.
21
+ - **Create and update records** — raise an invoice, add a customer, enter a bill, record a payment.
22
+
23
+ ## Keeping it healthy
24
+
25
+ A QuickBooks connection goes stale if it sits unused for about 100 days, or if you revoke access from your Intuit account. Ask the agent to run a connection audit now and then; if a company shows as not alive, just re-authorize it with a fresh link. Reading has a generous free monthly allowance and writing is free, so the agent favours precise lookups over pulling everything.
26
+
27
+ ## Moving to real books
28
+
29
+ Switching your Intuit app from sandbox to production requires passing Intuit's own production security review. That is between you and Intuit; it is not something the agent does for you.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "quickbooks",
3
+ "description": "QuickBooks Online connector — operator-owned Intuit app, OAuth consent relay, then agent-driven read/write over the QBO REST API.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "quickbooks": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/lib/mcp-spawn-tee/index.js",
14
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
15
+ ],
16
+ "env": {
17
+ "MCP_SPAWN_TEE_NAME": "quickbooks"
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: quickbooks
3
+ description: "QuickBooks Online connector — operator-owned Intuit app, OAuth consent relay, then agent-driven read/write over the QBO REST API."
4
+ tools:
5
+ - name: quickbooks-credentials-set
6
+ publicAllowlist: false
7
+ adminAllowlist: true
8
+ - name: quickbooks-authorize-url
9
+ publicAllowlist: false
10
+ adminAllowlist: true
11
+ - name: quickbooks-connections
12
+ publicAllowlist: false
13
+ adminAllowlist: true
14
+ - name: quickbooks-token-audit
15
+ publicAllowlist: false
16
+ adminAllowlist: true
17
+ - name: quickbooks-disconnect
18
+ publicAllowlist: false
19
+ adminAllowlist: true
20
+ - name: quickbooks-query
21
+ publicAllowlist: false
22
+ adminAllowlist: false
23
+ - name: quickbooks-report
24
+ publicAllowlist: false
25
+ adminAllowlist: false
26
+ - name: quickbooks-invoice-create
27
+ publicAllowlist: false
28
+ adminAllowlist: false
29
+ - name: quickbooks-invoice-update
30
+ publicAllowlist: false
31
+ adminAllowlist: false
32
+ - name: quickbooks-customer-create
33
+ publicAllowlist: false
34
+ adminAllowlist: false
35
+ - name: quickbooks-customer-update
36
+ publicAllowlist: false
37
+ adminAllowlist: false
38
+ - name: quickbooks-bill-create
39
+ publicAllowlist: false
40
+ adminAllowlist: false
41
+ - name: quickbooks-payment-create
42
+ publicAllowlist: false
43
+ adminAllowlist: false
44
+ - name: quickbooks-entity-create
45
+ publicAllowlist: false
46
+ adminAllowlist: false
47
+ - name: quickbooks-entity-update
48
+ publicAllowlist: false
49
+ adminAllowlist: false
50
+ skills:
51
+ - skills/quickbooks/SKILL.md
52
+ metadata: {"platform":{"optional":true,"pluginKey":"quickbooks"}}
53
+ mcp:
54
+ command: node
55
+ args:
56
+ - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
57
+ - ${PLATFORM_ROOT}/plugins/quickbooks/mcp/dist/index.js
58
+ env:
59
+ MCP_SPAWN_TEE_NAME: quickbooks
60
+ LOG_DIR: ${LOG_DIR}
61
+ PLATFORM_ROOT: ${PLATFORM_ROOT}
62
+ ACCOUNT_ID: ${ACCOUNT_ID}
63
+ SESSION_ID: ${SESSION_ID}
64
+ mcp-manifest: auto
65
+ ---
66
+
67
+ # QuickBooks Online
68
+
69
+ Connects to QuickBooks **Online** — the cloud accounting product (REST/JSON, OAuth 2.0). There is no local company file; the books live in Intuit's cloud, so every call proves app identity plus per-company consent.
70
+
71
+ Maxy ships installs and walks away, so Maxy is **never** the OAuth app owner. The operator registers their **own** Intuit app; its client ID/secret is a per-brand secret like the Cloudflare token. Two steps are irreducibly human and are relayed, never auto-clicked: registering the app at developer.intuit.com, and clicking the consent link for each company. Everything after — code→token exchange, access-token refresh, every REST call — is agent-driven.
72
+
73
+ ## Capabilities
74
+
75
+ - **quickbooks-credentials-set** — store the operator's Intuit app client ID + secret (sandbox or production).
76
+ - **quickbooks-authorize-url** — mint a consent URL for a human to click; one company per click.
77
+ - **quickbooks-connections** — list connected companies and the age of each refresh token.
78
+ - **quickbooks-token-audit** — probe each refresh token; surfaces a dead connection before a customer-facing call hits it.
79
+ - **quickbooks-disconnect** — drop one company's stored token.
80
+ - **quickbooks-query** — read any entity via the QBO query language (invoices, customers, bills, …).
81
+ - **quickbooks-report** — fetch a named report (ProfitAndLoss, BalanceSheet, …).
82
+ - **quickbooks-invoice-create / -update**, **quickbooks-customer-create / -update**, **quickbooks-bill-create**, **quickbooks-payment-create** — entity-specific writes.
83
+ - **quickbooks-entity-create / -update** — generic write for any other entity (Vendor, Item, Estimate, …).
84
+
85
+ ## Setup
86
+
87
+ Connecting QuickBooks runs through the `quickbooks` skill (`skills/quickbooks/SKILL.md`): register the Intuit app, paste the credentials, then relay the consent link. The skill is the operator-facing procedure; load it and follow its instructions.
88
+
89
+ ## Credentials and isolation
90
+
91
+ Credentials and per-company refresh tokens live only in this account's secrets directory (`secrets/quickbooks.json`, mode `0600`), inside the brand-isolated install. No Intuit app identity is shared across brands. Token refresh is automatic until a refresh token expires (~100 days of inactivity) or is revoked.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * MCP spawn-tee — parent-side stderr capture + lifecycle observability.
5
+ *
6
+ * Claude Code spawns each MCP server itself; the platform never holds a
7
+ * ChildProcess handle. This wrapper sits between Claude Code and the real
8
+ * MCP server: Claude Code runs `node <this> <real-entry>`, and the wrapper
9
+ * spawns the real entry with `stdio:['inherit','inherit','pipe']` so stdin
10
+ * and stdout (the JSON-RPC channel) pass through byte-identical. Only stderr
11
+ * is intercepted, and only for logging.
12
+ *
13
+ * Because the wrapper IS the helper's parent, it observes the helper's true
14
+ * exit code, signal, and lifetime for every death mode — including a SIGKILL
15
+ * or a transport-layer crash that an in-process handler would miss.
16
+ *
17
+ * Claude Code CLI → wrapper (this file) → child = node <real-entry>
18
+ * child stdin/stdout : inherited (Claude Code pipe, untouched)
19
+ * child stderr : piped → per-date log + per-session log + passthrough
20
+ *
21
+ * Destinations (Task 706):
22
+ * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` — per-date raw (back-compat;
23
+ * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
24
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
25
+ * lines; wrapper-only, so it never mixes the enumeration copy with the
26
+ * live copy. Authoritative sink. Absent when SESSION_ID is unset
27
+ * (enumeration spawn) — then the per-date file is the fallback.
28
+ * - `server.log` via the loopback log-ingest route — best-effort mirror of
29
+ * the [mcp-helper] lifecycle lines. A dropped POST loses nothing because
30
+ * the per-session file already holds the line (written synchronously).
31
+ *
32
+ * Lifecycle lines, correlation key `session=<id8> server=<name>`:
33
+ * [mcp-helper] op=spawn ... pid= entry=
34
+ * [mcp-helper] op=boot ... head=<first stderr bytes>
35
+ * [mcp-helper] op=exit ... code= signal= lifetimeMs= stderr-tail= (always)
36
+ *
37
+ * The wrapper never writes to fd 1 (stdout) — that is the JSON-RPC channel.
38
+ * SIGTERM/SIGINT are forwarded to the child so a Claude Code shutdown does
39
+ * not orphan it; the child exit code/signal is propagated verbatim.
40
+ */
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ const node_child_process_1 = require("node:child_process");
43
+ const node_fs_1 = require("node:fs");
44
+ const node_path_1 = require("node:path");
45
+ const SERVER_NAME = process.env.MCP_SPAWN_TEE_NAME ?? "unknown";
46
+ const LOG_DIR = process.env.LOG_DIR;
47
+ const SESSION_ID = process.env.SESSION_ID;
48
+ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
+ const ENTRY = process.argv[2];
50
+ const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
+ const spawnStamp = Date.now();
52
+ // Per-session raw + lifecycle log (Task 706). Wrapper-only. Empty SESSION_ID
53
+ // (enumeration spawn) → undefined, and the per-date file is the fallback.
54
+ const perSessionPath = LOG_DIR && SESSION_ID
55
+ ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
+ : undefined;
57
+ const perDatePath = LOG_DIR
58
+ ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
59
+ : undefined;
60
+ // Rolling tail of child stderr for op=exit. Capped so a chatty child cannot
61
+ // grow the buffer without bound.
62
+ const TAIL_CAP = 2048;
63
+ let stderrTail = "";
64
+ let bootEmitted = false;
65
+ function appendSafe(path, data) {
66
+ if (!path || !LOG_DIR)
67
+ return;
68
+ try {
69
+ (0, node_fs_1.mkdirSync)(LOG_DIR, { recursive: true });
70
+ (0, node_fs_1.appendFileSync)(path, data);
71
+ }
72
+ catch {
73
+ /* unwritable destination — never mask primary output */
74
+ }
75
+ }
76
+ // Best-effort mirror of a lifecycle line to server.log via the loopback
77
+ // log-ingest route. Fire-and-forget: the per-session file is authoritative,
78
+ // so a dropped POST (unreachable port, process exiting) loses nothing.
79
+ function postToServerLog(suffix, level) {
80
+ if (!PLATFORM_PORT)
81
+ return;
82
+ try {
83
+ const ctrl = new AbortController();
84
+ const t = setTimeout(() => ctrl.abort(), 500);
85
+ t.unref?.(); // never let the abort timer keep the wrapper's event loop alive
86
+ void fetch(`http://127.0.0.1:${PLATFORM_PORT}/api/admin/log-ingest`, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json" },
89
+ body: JSON.stringify({ tag: "mcp-helper", level, line: suffix }),
90
+ signal: ctrl.signal,
91
+ }).then(() => clearTimeout(t)).catch(() => clearTimeout(t));
92
+ }
93
+ catch {
94
+ /* fetch threw synchronously — best-effort only */
95
+ }
96
+ }
97
+ // Emit one [mcp-helper] lifecycle line: authoritative per-session file (sync,
98
+ // survives process.exit), the wrapper's own stderr (journald / Claude Code
99
+ // visibility), and a best-effort server.log mirror. `suffix` is the line body
100
+ // after the tag and carries no newline (the log-ingest route rejects newlines).
101
+ function emitLifecycle(suffix, level) {
102
+ const line = `[mcp-helper] ${suffix}\n`;
103
+ appendSafe(perSessionPath, line);
104
+ try {
105
+ process.stderr.write(line);
106
+ }
107
+ catch { /* stderr closed */ }
108
+ postToServerLog(suffix, level);
109
+ }
110
+ if (!ENTRY) {
111
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason="no entry given (argv[2] missing)"`, "error");
112
+ process.exit(2);
113
+ }
114
+ const child = (0, node_child_process_1.spawn)(process.execPath, [ENTRY], {
115
+ stdio: ["inherit", "inherit", "pipe"],
116
+ env: process.env,
117
+ });
118
+ emitLifecycle(`op=spawn session=${SESSION_ID8} server=${SERVER_NAME} pid=${child.pid ?? -1} entry=${ENTRY}`, "info");
119
+ child.on("error", (err) => {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ emitLifecycle(`op=error session=${SESSION_ID8} server=${SERVER_NAME} reason=${JSON.stringify(`spawn error: ${msg}`)}`, "error");
122
+ process.exit(127);
123
+ });
124
+ if (child.stderr) {
125
+ child.stderr.on("data", (chunk) => {
126
+ appendSafe(perDatePath, chunk); // per-date raw (back-compat)
127
+ appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
128
+ try {
129
+ process.stderr.write(chunk);
130
+ }
131
+ catch { /* stderr closed */ } // passthrough
132
+ stderrTail = (stderrTail + chunk.toString("utf8")).slice(-TAIL_CAP);
133
+ if (!bootEmitted) {
134
+ bootEmitted = true;
135
+ const head = chunk.toString("utf8").split("\n")[0].slice(0, 200);
136
+ emitLifecycle(`op=boot session=${SESSION_ID8} server=${SERVER_NAME} head=${JSON.stringify(head)}`, "info");
137
+ }
138
+ });
139
+ }
140
+ const forward = (signal) => { if (!child.killed)
141
+ child.kill(signal); };
142
+ process.on("SIGTERM", () => forward("SIGTERM"));
143
+ process.on("SIGINT", () => forward("SIGINT"));
144
+ // Signal → number, so a signal-killed child propagates as 128+signum (the
145
+ // shell convention). `code` is null on a signal exit, so `128 + (code ?? 0)`
146
+ // would collapse every signal to 128 and lose the signal identity in the
147
+ // wrapper's own exit status.
148
+ const SIGNUM = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 };
149
+ child.on("exit", (code, signal) => {
150
+ const lifetimeMs = Date.now() - spawnStamp;
151
+ const tail = stderrTail.slice(-200);
152
+ const level = signal || (code ?? 0) !== 0 ? "error" : "info";
153
+ emitLifecycle(`op=exit session=${SESSION_ID8} server=${SERVER_NAME} pid=${child.pid ?? -1} ` +
154
+ `code=${code ?? "—"} signal=${signal ?? "—"} lifetimeMs=${lifetimeMs} stderr-tail=${JSON.stringify(tail)}`, level);
155
+ if (signal)
156
+ process.exit(128 + (SIGNUM[signal] ?? 0));
157
+ process.exit(code ?? 0);
158
+ });
159
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}