@rubytech/create-maxy-code 0.1.311 → 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 (85) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-style/dist/index.d.ts +5 -2
  3. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-style/dist/index.js +65 -2
  5. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +218 -0
  7. package/payload/platform/lib/graph-style/src/index.ts +53 -2
  8. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  9. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  10. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +56 -4
  11. package/payload/platform/plugins/docs/references/admin-ui.md +21 -3
  12. package/payload/platform/plugins/docs/references/quickbooks.md +29 -0
  13. package/payload/platform/plugins/quickbooks/.claude-plugin/plugin.json +21 -0
  14. package/payload/platform/plugins/quickbooks/PLUGIN.md +91 -0
  15. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +159 -0
  16. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/package.json +3 -0
  17. package/payload/platform/plugins/quickbooks/mcp/dist/index.d.ts +2 -0
  18. package/payload/platform/plugins/quickbooks/mcp/dist/index.d.ts.map +1 -0
  19. package/payload/platform/plugins/quickbooks/mcp/dist/index.js +218 -0
  20. package/payload/platform/plugins/quickbooks/mcp/dist/index.js.map +1 -0
  21. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.d.ts +37 -0
  22. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.d.ts.map +1 -0
  23. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.js +75 -0
  24. package/payload/platform/plugins/quickbooks/mcp/dist/lib/oauth.js.map +1 -0
  25. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.d.ts +39 -0
  26. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.d.ts.map +1 -0
  27. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.js +178 -0
  28. package/payload/platform/plugins/quickbooks/mcp/dist/lib/qbo-client.js.map +1 -0
  29. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.d.ts +36 -0
  30. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.d.ts.map +1 -0
  31. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.js +107 -0
  32. package/payload/platform/plugins/quickbooks/mcp/dist/lib/secrets.js.map +1 -0
  33. package/payload/platform/plugins/quickbooks/mcp/package.json +19 -0
  34. package/payload/platform/plugins/quickbooks/skills/quickbooks/SKILL.md +37 -0
  35. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  36. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +16 -0
  37. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/reseat-ledger.d.ts +10 -0
  39. package/payload/platform/services/claude-session-manager/dist/reseat-ledger.d.ts.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/reseat-ledger.js +62 -53
  41. package/payload/platform/services/claude-session-manager/dist/reseat-ledger.js.map +1 -1
  42. package/payload/platform/services/claude-session-manager/dist/scope-record.d.ts.map +1 -1
  43. package/payload/platform/services/claude-session-manager/dist/scope-record.js +35 -53
  44. package/payload/platform/services/claude-session-manager/dist/scope-record.js.map +1 -1
  45. package/payload/platform/services/claude-session-manager/dist/sidecar-store.d.ts +45 -0
  46. package/payload/platform/services/claude-session-manager/dist/sidecar-store.d.ts.map +1 -0
  47. package/payload/platform/services/claude-session-manager/dist/sidecar-store.js +118 -0
  48. package/payload/platform/services/claude-session-manager/dist/sidecar-store.js.map +1 -0
  49. package/payload/server/{chunk-J6WDAWFJ.js → chunk-Q7EWROVN.js} +4 -0
  50. package/payload/server/maxy-edge.js +1 -1
  51. package/payload/server/public/assets/{AccessGate-ChFLk-rp.js → AccessGate-Byskkf47.js} +1 -1
  52. package/payload/server/public/assets/{AdminLoginScreens-DU6zgbk5.js → AdminLoginScreens-DgwQi3HN.js} +1 -1
  53. package/payload/server/public/assets/AdminShell-DQfL2c_L.js +1 -0
  54. package/payload/server/public/assets/{Checkbox-CQGLKMXu.js → Checkbox-C54JSreC.js} +1 -1
  55. package/payload/server/public/assets/OperatorConversations-DulQyF8c.js +1 -0
  56. package/payload/server/public/assets/{admin-COHPIWdb.js → admin-B25rkI9Q.js} +1 -1
  57. package/payload/server/public/assets/{audio-attachment-mime-DL06xo72.js → audio-attachment-mime-BlwrgPQa.js} +1 -1
  58. package/payload/server/public/assets/{brand-CN7uSSKl.css → brand-TWflEl22.css} +1 -1
  59. package/payload/server/public/assets/{browser-L7N4tAOl.js → browser-C9Z5dLlN.js} +1 -1
  60. package/payload/server/public/assets/chat-CGNWDmid.js +1 -0
  61. package/payload/server/public/assets/data-DCQDh0-Y.js +1 -0
  62. package/payload/server/public/assets/graph-labels-BBtL--_M.js +1 -0
  63. package/payload/server/public/assets/{graph-Cro3-mgY.js → graph-sMzOI71g.js} +2 -2
  64. package/payload/server/public/assets/operator-hI6kJi0I.js +1 -0
  65. package/payload/server/public/assets/page-DbsqlpuX.js +1 -0
  66. package/payload/server/public/assets/{public-CX148JKi.js → public-Cs5N4-Az.js} +1 -1
  67. package/payload/server/public/assets/{public-next-BCItsUbZ.js → public-next-DoGPYX6T.js} +1 -1
  68. package/payload/server/public/assets/{useSelectionMode-BHAAhkDg.js → useSelectionMode-Cs-vtuKI.js} +1 -1
  69. package/payload/server/public/browser.html +6 -6
  70. package/payload/server/public/chat.html +8 -8
  71. package/payload/server/public/data.html +5 -5
  72. package/payload/server/public/graph.html +8 -8
  73. package/payload/server/public/index.html +8 -8
  74. package/payload/server/public/operator.html +10 -10
  75. package/payload/server/public/public-next.html +9 -9
  76. package/payload/server/public/public.html +7 -7
  77. package/payload/server/server.js +1091 -594
  78. package/payload/server/public/assets/AdminShell-BJCYb1v5.js +0 -1
  79. package/payload/server/public/assets/OperatorConversations-CHEQTz20.js +0 -1
  80. package/payload/server/public/assets/chat-B_jYIH9a.js +0 -1
  81. package/payload/server/public/assets/data-BmSTKHR1.js +0 -1
  82. package/payload/server/public/assets/graph-labels-WQQn6JIJ.js +0 -1
  83. package/payload/server/public/assets/operator-B4I25isQ.js +0 -1
  84. package/payload/server/public/assets/page-B-6ZQ4JG.js +0 -1
  85. /package/payload/server/public/assets/{brand-MKGM8WZp.js → brand-CYjs10m-.js} +0 -0
@@ -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
 
@@ -78,7 +78,7 @@ either is a regression.
78
78
  | `/sidebar-artefact-content` | Reads a single artefact's bytes for the artefact pane. |
79
79
  | `/sidebar-artefact-save` | Persists an artefact edit. |
80
80
  | `/attachment` | Per-attachment binary fetch (images, PDFs, etc.). |
81
- | `/files` | File browser CRUD (list, download, upload, delete). Listings put directories first, then files newest-first by `mtime` (name tie-break) so a just-changed file leads the panel. |
81
+ | `/files` | File browser CRUD (list, download, upload, delete). Listings put directories first, then files newest-first by `mtime` (name tie-break) so a just-changed file leads the panel. Upload is a raw-body stream (file bytes are the request body; `session_key`/`path`/`filename` in the query) written to disk without buffering — ceiling 2 GB (`MAX_DATA_UPLOAD_BYTES`), over-ceiling bodies abort mid-stream with `413`; the client shows a determinate progress bar. Distinct from the 50 MB in-memory chat-attachment cap. |
82
82
 
83
83
  **Artefact download resolution.** Clicking a sidebar Artefacts row
84
84
  streams the `KnowledgeDocument`'s real backing file, which can live in one of
@@ -106,6 +106,24 @@ that folder instead of snapping back to the data root; the hash never reaches
106
106
  the server and `/data` has no client router, so the channel is isolated.
107
107
  `path=` is cleared at the root for a clean URL.
108
108
 
109
+ **`/data` upload — button and drag-drop.** Besides the Upload button (one OS-picked
110
+ file, stored under a timestamped name), files and whole folders can be dragged
111
+ onto the file-browser body to upload into the open folder. A dropped folder's
112
+ subtree is recreated under the open folder and every contained file uploaded,
113
+ preserving source names and relative paths (a same-named file in an existing
114
+ folder is overwritten; empty source subfolders are skipped). The drop region
115
+ shows an active outline while a drag is over it and is inert wherever the open
116
+ folder is not writable (outside the operator's own `accounts/<id>/…` subtree),
117
+ rejecting the drop with the same "Open a folder inside your account to upload"
118
+ guidance as the button. Each drop reports an uploaded/skipped summary; a file
119
+ that fails the 50 MB limit, the MIME allowlist, or its HTTP write is skipped
120
+ individually without aborting the rest, and every uploaded file is indexed for
121
+ search exactly as the button path is. The server `/files/upload` endpoint takes
122
+ an optional `relpath` form field (the file's path relative to the drop root); it
123
+ recreates that path's directories under the open folder, scope-checked level by
124
+ level, and writes the file under its original basename. A flat upload (no
125
+ `relpath`) is unchanged.
126
+
109
127
  **`/data` operator surface.** `/data` mounts the operator
110
128
  `HeaderMenu` (logo-left / name-centre / burger-right; flyout = Chat / Data /
111
129
  Log out) above the toolbar — the burger's Chat item returns to `/`, replacing
@@ -284,7 +302,7 @@ authoritative. This section names the surfaces and what backs each.
284
302
  | `+ New session` button | Opens `NewSessionModal`, which POSTs `{channel, permissionMode, model, initialMessage}` to `/api/admin/claude-sessions`. | `claude-sessions.ts` |
285
303
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
286
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` |
287
- | 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` |
288
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` |
289
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` |
290
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":""}
@@ -0,0 +1,218 @@
1
+ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
2
+ initStderrTee("quickbooks");
3
+ import { randomBytes } from "node:crypto";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { readStore, setCredentials, removeConnection, writePending, } from "./lib/secrets.js";
8
+ import { buildAuthorizeUrl } from "./lib/oauth.js";
9
+ import { qboQuery, qboCreate, qboUpdate, qboReport, auditConnection } from "./lib/qbo-client.js";
10
+ // Plugin-system boot tolerance — see Task 202. Module-init must never throw;
11
+ // the plugin-system enumerates tools/list with no env, so per-account context
12
+ // is resolved per call and a missing account refuses, never crashes.
13
+ const ACCOUNT_ID = process.env.ACCOUNT_ID ?? null;
14
+ const PLATFORM_ROOT = process.env.PLATFORM_ROOT ?? null;
15
+ process.stderr.write(`[quickbooks] boot accountId=${ACCOUNT_ID ?? "null"} platformRoot=${PLATFORM_ROOT ?? "null"}\n`);
16
+ const CONSENT_TTL_MS = 15 * 60 * 1000;
17
+ function refuseNoAccount(toolName) {
18
+ process.stderr.write(`[quickbooks] tool=${toolName} refuse reason=no-account-context\n`);
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text: `${toolName} cannot execute: this MCP instance was launched without ACCOUNT_ID/PLATFORM_ROOT (plugin-system metadata-only mode). Per-account access requires both at spawn.`,
24
+ },
25
+ ],
26
+ isError: true,
27
+ };
28
+ }
29
+ function ok(data) {
30
+ return {
31
+ content: [
32
+ { type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) },
33
+ ],
34
+ };
35
+ }
36
+ function fail(message) {
37
+ return { content: [{ type: "text", text: message }], isError: true };
38
+ }
39
+ const server = new McpServer({ name: "quickbooks", version: "0.1.0" });
40
+ // Arbitrary QBO resource body (the caller assembles it per Intuit's entity
41
+ // schema; the connector does not model every field).
42
+ const resourceBody = z.record(z.unknown());
43
+ // ---------------------------------------------------------------------------
44
+ // Connection management
45
+ // ---------------------------------------------------------------------------
46
+ server.tool("quickbooks-credentials-set", "Store the operator's own Intuit app credentials (client ID + secret) for this account. Get these by registering an app at developer.intuit.com — Maxy is never the OAuth app owner. environment is 'sandbox' for an Intuit sandbox company or 'production' for real books.", {
47
+ clientId: z.string().describe("Intuit app client ID"),
48
+ clientSecret: z.string().describe("Intuit app client secret"),
49
+ environment: z.enum(["sandbox", "production"]).describe("Which QBO environment these credentials target"),
50
+ }, async ({ clientId, clientSecret, environment }) => {
51
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
52
+ return refuseNoAccount("quickbooks-credentials-set");
53
+ try {
54
+ setCredentials(PLATFORM_ROOT, ACCOUNT_ID, { clientId, clientSecret, environment });
55
+ return ok(`Stored Intuit ${environment} credentials. Next: run quickbooks-authorize-url and have a human authorize each company.`);
56
+ }
57
+ catch (err) {
58
+ return fail(`quickbooks-credentials-set failed: ${err.message}`);
59
+ }
60
+ });
61
+ server.tool("quickbooks-authorize-url", "Generate the Intuit consent URL for a human to click. Returns a URL — hand it to a person; they sign in and authorize one company. Intuit then redirects to the callback, which stores the refresh token. redirectUri MUST exactly match the redirect URI registered in the Intuit app (the brand's public host + /api/quickbooks/callback).", {
62
+ redirectUri: z.string().describe("The exact redirect URI registered in the Intuit app, e.g. https://<public-host>/api/quickbooks/callback"),
63
+ realmHint: z.string().optional().describe("Optional human label for which company is being connected (recorded for correlation only)"),
64
+ }, async ({ redirectUri, realmHint }) => {
65
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
66
+ return refuseNoAccount("quickbooks-authorize-url");
67
+ try {
68
+ const store = readStore(PLATFORM_ROOT, ACCOUNT_ID);
69
+ if (!store)
70
+ return fail("No credentials set. Run quickbooks-credentials-set first.");
71
+ const state = randomBytes(24).toString("base64url");
72
+ const now = Date.now();
73
+ writePending(PLATFORM_ROOT, ACCOUNT_ID, {
74
+ state,
75
+ redirectUri,
76
+ realmHint,
77
+ createdAt: now,
78
+ expiresAt: now + CONSENT_TTL_MS,
79
+ });
80
+ const url = buildAuthorizeUrl({ clientId: store.clientId, redirectUri, state });
81
+ process.stderr.write(`[quickbooks] op=authorize-url state=${state} realmHint=${realmHint ?? ""}\n`);
82
+ return ok(`Hand this URL to a person to authorize the company (valid 15 minutes):\n\n${url}`);
83
+ }
84
+ catch (err) {
85
+ return fail(`quickbooks-authorize-url failed: ${err.message}`);
86
+ }
87
+ });
88
+ server.tool("quickbooks-connections", "List the QuickBooks companies (realmIds) this account is connected to, with the age of each stored refresh token.", {}, async () => {
89
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
90
+ return refuseNoAccount("quickbooks-connections");
91
+ const store = readStore(PLATFORM_ROOT, ACCOUNT_ID);
92
+ if (!store)
93
+ return ok("No credentials set.");
94
+ const now = Date.now();
95
+ const connections = Object.entries(store.connections).map(([realmId, c]) => ({
96
+ realmId,
97
+ lastRefresh: c.lastRefresh,
98
+ ageDays: c.lastRefresh > 0 ? Math.floor((now - c.lastRefresh) / 86_400_000) : null,
99
+ refreshTokenExpiry: c.refreshTokenExpiry,
100
+ }));
101
+ return ok({ environment: store.environment, connections });
102
+ });
103
+ server.tool("quickbooks-token-audit", "Probe every connected company's refresh token and report which are still alive. Run periodically: a refresh token dies silently after ~100 days of inactivity or operator revocation. alive=false means that company needs operator re-consent before any customer-facing call.", {
104
+ realmId: z.string().optional().describe("Audit just this company; omit to audit all"),
105
+ }, async ({ realmId }) => {
106
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
107
+ return refuseNoAccount("quickbooks-token-audit");
108
+ const store = readStore(PLATFORM_ROOT, ACCOUNT_ID);
109
+ if (!store)
110
+ return ok("No credentials set.");
111
+ const realms = realmId ? [realmId] : Object.keys(store.connections);
112
+ const results = [];
113
+ for (const r of realms) {
114
+ results.push({ realmId: r, ...(await auditConnection(PLATFORM_ROOT, ACCOUNT_ID, r)) });
115
+ }
116
+ return ok({ audited: results });
117
+ });
118
+ server.tool("quickbooks-disconnect", "Remove the stored refresh token for one company. The operator can re-authorize later with quickbooks-authorize-url.", { realmId: z.string().describe("The company realmId to disconnect") }, async ({ realmId }) => {
119
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
120
+ return refuseNoAccount("quickbooks-disconnect");
121
+ removeConnection(PLATFORM_ROOT, ACCOUNT_ID, realmId);
122
+ return ok(`Disconnected realmId=${realmId}.`);
123
+ });
124
+ // ---------------------------------------------------------------------------
125
+ // Read
126
+ // ---------------------------------------------------------------------------
127
+ server.tool("quickbooks-query", "Run a QuickBooks query (the QBO SQL-like language) against a company. Covers every readable entity: e.g. SELECT * FROM Invoice WHERE TxnDate > '2026-01-01', SELECT * FROM Customer, SELECT * FROM Bill.", {
128
+ realmId: z.string().describe("The company realmId"),
129
+ query: z.string().describe("A QBO query, e.g. \"SELECT * FROM Invoice MAXRESULTS 20\""),
130
+ }, async ({ realmId, query }) => {
131
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
132
+ return refuseNoAccount("quickbooks-query");
133
+ try {
134
+ return ok(await qboQuery({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, query }));
135
+ }
136
+ catch (err) {
137
+ return fail(`quickbooks-query failed: ${err.message}`);
138
+ }
139
+ });
140
+ server.tool("quickbooks-report", "Fetch a named QuickBooks report for a company, e.g. ProfitAndLoss, BalanceSheet, CustomerBalance, AgedReceivables. Params are report-specific (start_date, end_date, etc).", {
141
+ realmId: z.string().describe("The company realmId"),
142
+ report: z.string().describe("Report name, e.g. ProfitAndLoss"),
143
+ params: z.record(z.string()).optional().describe("Report parameters, e.g. { start_date: '2026-01-01', end_date: '2026-03-31' }"),
144
+ }, async ({ realmId, report, params }) => {
145
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
146
+ return refuseNoAccount("quickbooks-report");
147
+ try {
148
+ return ok(await qboReport({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, report, params }));
149
+ }
150
+ catch (err) {
151
+ return fail(`quickbooks-report failed: ${err.message}`);
152
+ }
153
+ });
154
+ // ---------------------------------------------------------------------------
155
+ // Write — entity-specific convenience tools + a generic fallback.
156
+ // All updates require Id + SyncToken in the body (QBO's optimistic-lock token).
157
+ // ---------------------------------------------------------------------------
158
+ function createTool(toolName, entity, description) {
159
+ server.tool(toolName, description, { realmId: z.string().describe("The company realmId"), body: resourceBody.describe(`${entity} resource body`) }, async ({ realmId, body }) => {
160
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
161
+ return refuseNoAccount(toolName);
162
+ try {
163
+ return ok(await qboCreate({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, entity, body }));
164
+ }
165
+ catch (err) {
166
+ return fail(`${toolName} failed: ${err.message}`);
167
+ }
168
+ });
169
+ }
170
+ function updateTool(toolName, entity, description) {
171
+ server.tool(toolName, description, { realmId: z.string().describe("The company realmId"), body: resourceBody.describe(`${entity} resource body — MUST include Id and SyncToken`) }, async ({ realmId, body }) => {
172
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
173
+ return refuseNoAccount(toolName);
174
+ try {
175
+ return ok(await qboUpdate({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, entity, body }));
176
+ }
177
+ catch (err) {
178
+ return fail(`${toolName} failed: ${err.message}`);
179
+ }
180
+ });
181
+ }
182
+ createTool("quickbooks-invoice-create", "Invoice", "Create an invoice in a QuickBooks company.");
183
+ updateTool("quickbooks-invoice-update", "Invoice", "Update an invoice (sparse or full) in a QuickBooks company.");
184
+ createTool("quickbooks-customer-create", "Customer", "Create a customer in a QuickBooks company.");
185
+ updateTool("quickbooks-customer-update", "Customer", "Update a customer in a QuickBooks company.");
186
+ createTool("quickbooks-bill-create", "Bill", "Create a bill (vendor payable) in a QuickBooks company.");
187
+ createTool("quickbooks-payment-create", "Payment", "Record a customer payment in a QuickBooks company.");
188
+ server.tool("quickbooks-entity-create", "Create any QuickBooks entity not covered by a dedicated tool (Vendor, Item, Estimate, CreditMemo, etc). entity is the QBO resource name.", {
189
+ realmId: z.string().describe("The company realmId"),
190
+ entity: z.string().describe("QBO entity name, e.g. Vendor, Item, Estimate"),
191
+ body: resourceBody.describe("The resource body"),
192
+ }, async ({ realmId, entity, body }) => {
193
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
194
+ return refuseNoAccount("quickbooks-entity-create");
195
+ try {
196
+ return ok(await qboCreate({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, entity, body }));
197
+ }
198
+ catch (err) {
199
+ return fail(`quickbooks-entity-create failed: ${err.message}`);
200
+ }
201
+ });
202
+ server.tool("quickbooks-entity-update", "Update any QuickBooks entity not covered by a dedicated tool. body MUST include Id and SyncToken.", {
203
+ realmId: z.string().describe("The company realmId"),
204
+ entity: z.string().describe("QBO entity name, e.g. Vendor, Item, Estimate"),
205
+ body: resourceBody.describe("The resource body — MUST include Id and SyncToken"),
206
+ }, async ({ realmId, entity, body }) => {
207
+ if (!ACCOUNT_ID || !PLATFORM_ROOT)
208
+ return refuseNoAccount("quickbooks-entity-update");
209
+ try {
210
+ return ok(await qboUpdate({ platformRoot: PLATFORM_ROOT, accountId: ACCOUNT_ID, realmId, entity, body }));
211
+ }
212
+ catch (err) {
213
+ return fail(`quickbooks-entity-update failed: ${err.message}`);
214
+ }
215
+ });
216
+ const transport = new StdioServerTransport();
217
+ await server.connect(transport);
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}