noxctl 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +279 -29
  2. package/dist/auth.d.ts +15 -7
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +128 -22
  5. package/dist/auth.js.map +1 -1
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +1640 -70
  9. package/dist/cli.js.map +1 -1
  10. package/dist/completions.d.ts +13 -0
  11. package/dist/completions.d.ts.map +1 -0
  12. package/dist/completions.js +150 -0
  13. package/dist/completions.js.map +1 -0
  14. package/dist/config-paths.d.ts +2 -0
  15. package/dist/config-paths.d.ts.map +1 -0
  16. package/dist/config-paths.js +7 -0
  17. package/dist/config-paths.js.map +1 -0
  18. package/dist/credentials-store.d.ts +13 -3
  19. package/dist/credentials-store.d.ts.map +1 -1
  20. package/dist/credentials-store.js +197 -62
  21. package/dist/credentials-store.js.map +1 -1
  22. package/dist/date-periods.d.ts +14 -0
  23. package/dist/date-periods.d.ts.map +1 -0
  24. package/dist/date-periods.js +103 -0
  25. package/dist/date-periods.js.map +1 -0
  26. package/dist/formatter.d.ts +11 -2
  27. package/dist/formatter.d.ts.map +1 -1
  28. package/dist/formatter.js +27 -4
  29. package/dist/formatter.js.map +1 -1
  30. package/dist/fortnox-client.d.ts +1 -0
  31. package/dist/fortnox-client.d.ts.map +1 -1
  32. package/dist/fortnox-client.js +28 -5
  33. package/dist/fortnox-client.js.map +1 -1
  34. package/dist/index.d.ts +15 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +92 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/keychain-target.d.ts +32 -0
  39. package/dist/keychain-target.d.ts.map +1 -0
  40. package/dist/keychain-target.js +349 -0
  41. package/dist/keychain-target.js.map +1 -0
  42. package/dist/operations/analytics.d.ts +84 -0
  43. package/dist/operations/analytics.d.ts.map +1 -0
  44. package/dist/operations/analytics.js +154 -0
  45. package/dist/operations/analytics.js.map +1 -0
  46. package/dist/operations/contracts.d.ts +23 -0
  47. package/dist/operations/contracts.d.ts.map +1 -0
  48. package/dist/operations/contracts.js +52 -0
  49. package/dist/operations/contracts.js.map +1 -0
  50. package/dist/operations/costcenters.d.ts +20 -0
  51. package/dist/operations/costcenters.d.ts.map +1 -0
  52. package/dist/operations/costcenters.js +38 -0
  53. package/dist/operations/costcenters.js.map +1 -0
  54. package/dist/operations/customers.d.ts.map +1 -1
  55. package/dist/operations/customers.js +12 -2
  56. package/dist/operations/customers.js.map +1 -1
  57. package/dist/operations/financial-years.d.ts +16 -0
  58. package/dist/operations/financial-years.d.ts.map +1 -0
  59. package/dist/operations/financial-years.js +32 -0
  60. package/dist/operations/financial-years.js.map +1 -0
  61. package/dist/operations/invoice-payments.d.ts +21 -0
  62. package/dist/operations/invoice-payments.d.ts.map +1 -0
  63. package/dist/operations/invoice-payments.js +40 -0
  64. package/dist/operations/invoice-payments.js.map +1 -0
  65. package/dist/operations/offers.d.ts +25 -0
  66. package/dist/operations/offers.d.ts.map +1 -0
  67. package/dist/operations/offers.js +48 -0
  68. package/dist/operations/offers.js.map +1 -0
  69. package/dist/operations/orders.d.ts +24 -0
  70. package/dist/operations/orders.d.ts.map +1 -0
  71. package/dist/operations/orders.js +44 -0
  72. package/dist/operations/orders.js.map +1 -0
  73. package/dist/operations/pricelists.d.ts +36 -0
  74. package/dist/operations/pricelists.d.ts.map +1 -0
  75. package/dist/operations/pricelists.js +52 -0
  76. package/dist/operations/pricelists.js.map +1 -0
  77. package/dist/operations/projects.d.ts +19 -0
  78. package/dist/operations/projects.d.ts.map +1 -0
  79. package/dist/operations/projects.js +33 -0
  80. package/dist/operations/projects.js.map +1 -0
  81. package/dist/operations/supplier-invoice-payments.d.ts +20 -0
  82. package/dist/operations/supplier-invoice-payments.d.ts.map +1 -0
  83. package/dist/operations/supplier-invoice-payments.js +34 -0
  84. package/dist/operations/supplier-invoice-payments.js.map +1 -0
  85. package/dist/operations/taxreductions.d.ts +19 -0
  86. package/dist/operations/taxreductions.d.ts.map +1 -0
  87. package/dist/operations/taxreductions.js +28 -0
  88. package/dist/operations/taxreductions.js.map +1 -0
  89. package/dist/operations/vouchers.d.ts +15 -0
  90. package/dist/operations/vouchers.d.ts.map +1 -1
  91. package/dist/operations/vouchers.js +97 -0
  92. package/dist/operations/vouchers.js.map +1 -1
  93. package/dist/profile-name.d.ts +9 -0
  94. package/dist/profile-name.d.ts.map +1 -0
  95. package/dist/profile-name.js +58 -0
  96. package/dist/profile-name.js.map +1 -0
  97. package/dist/profiles.d.ts +55 -0
  98. package/dist/profiles.d.ts.map +1 -0
  99. package/dist/profiles.js +210 -0
  100. package/dist/profiles.js.map +1 -0
  101. package/dist/tools/analytics.d.ts +3 -0
  102. package/dist/tools/analytics.d.ts.map +1 -0
  103. package/dist/tools/analytics.js +52 -0
  104. package/dist/tools/analytics.js.map +1 -0
  105. package/dist/tools/bookkeeping.d.ts.map +1 -1
  106. package/dist/tools/bookkeeping.js +38 -2
  107. package/dist/tools/bookkeeping.js.map +1 -1
  108. package/dist/tools/contracts.d.ts +3 -0
  109. package/dist/tools/contracts.d.ts.map +1 -0
  110. package/dist/tools/contracts.js +136 -0
  111. package/dist/tools/contracts.js.map +1 -0
  112. package/dist/tools/costcenters.d.ts +3 -0
  113. package/dist/tools/costcenters.d.ts.map +1 -0
  114. package/dist/tools/costcenters.js +79 -0
  115. package/dist/tools/costcenters.js.map +1 -0
  116. package/dist/tools/financial-years.d.ts +3 -0
  117. package/dist/tools/financial-years.d.ts.map +1 -0
  118. package/dist/tools/financial-years.js +33 -0
  119. package/dist/tools/financial-years.js.map +1 -0
  120. package/dist/tools/invoice-payments.d.ts +3 -0
  121. package/dist/tools/invoice-payments.d.ts.map +1 -0
  122. package/dist/tools/invoice-payments.js +60 -0
  123. package/dist/tools/invoice-payments.js.map +1 -0
  124. package/dist/tools/offers.d.ts +3 -0
  125. package/dist/tools/offers.d.ts.map +1 -0
  126. package/dist/tools/offers.js +130 -0
  127. package/dist/tools/offers.js.map +1 -0
  128. package/dist/tools/orders.d.ts +3 -0
  129. package/dist/tools/orders.d.ts.map +1 -0
  130. package/dist/tools/orders.js +114 -0
  131. package/dist/tools/orders.js.map +1 -0
  132. package/dist/tools/pricelists.d.ts +3 -0
  133. package/dist/tools/pricelists.d.ts.map +1 -0
  134. package/dist/tools/pricelists.js +104 -0
  135. package/dist/tools/pricelists.js.map +1 -0
  136. package/dist/tools/projects.d.ts +3 -0
  137. package/dist/tools/projects.d.ts.map +1 -0
  138. package/dist/tools/projects.js +80 -0
  139. package/dist/tools/projects.js.map +1 -0
  140. package/dist/tools/status.d.ts.map +1 -1
  141. package/dist/tools/status.js +6 -1
  142. package/dist/tools/status.js.map +1 -1
  143. package/dist/tools/supplier-invoice-payments.d.ts +3 -0
  144. package/dist/tools/supplier-invoice-payments.d.ts.map +1 -0
  145. package/dist/tools/supplier-invoice-payments.js +60 -0
  146. package/dist/tools/supplier-invoice-payments.js.map +1 -0
  147. package/dist/tools/taxreductions.d.ts +3 -0
  148. package/dist/tools/taxreductions.d.ts.map +1 -0
  149. package/dist/tools/taxreductions.js +51 -0
  150. package/dist/tools/taxreductions.js.map +1 -0
  151. package/dist/views.d.ts +28 -0
  152. package/dist/views.d.ts.map +1 -1
  153. package/dist/views.js +234 -0
  154. package/dist/views.js.map +1 -1
  155. package/package.json +1 -1
package/README.md CHANGED
@@ -39,14 +39,21 @@ Fortnox product plans, API activation requirements, and integration licensing ca
39
39
  - Copy your **Client ID** and **Client Secret**
40
40
  4. On the **Integration** tab, enable these scopes under **Behörigheter** / **Permissions**:
41
41
 
42
- | Swedish (SV) | English (EN) |
43
- |----------------------|---------------------|
44
- | Artikel | Article |
45
- | Bokföring | Bookkeeping |
46
- | Faktura | Invoice |
47
- | Företagsinformation | Company Information |
48
- | Inställningar | Settings |
49
- | Kund | Customer |
42
+ | Swedish (SV) | English (EN) | Needed for |
43
+ |----------------------|---------------------|------------------------------------------------|
44
+ | Artikel | Article | Articles, prices, price lists |
45
+ | Bokföring | Bookkeeping | Vouchers, accounts, financial reports |
46
+ | Faktura | Invoice | Invoices, invoice payments, offers, orders, tax reductions |
47
+ | Företagsinformation | Company Information | Company info |
48
+ | Inställningar | Settings | Financial year, locked period |
49
+ | Kund | Customer | Customers |
50
+ | Leverantör | Supplier | Suppliers |
51
+ | Leverantörsfaktura | Supplier Invoice | Supplier invoices, supplier invoice payments |
52
+ | Projekt | Project | Projects |
53
+ | Kostnadsställe | Cost Center | Cost centers |
54
+ | Priser | Price | Price lists, prices |
55
+
56
+ Enable every scope for the resources you intend to use. Missing scopes surface as `403 Forbidden` with a hint pointing at the right one.
50
57
 
51
58
  5. Save the integration
52
59
 
@@ -108,45 +115,283 @@ node dist/cli.js company info
108
115
 
109
116
  You should see your company name, organisation number, and address. If this works, you're all set.
110
117
 
118
+ ## Profiles (multi-tenant)
119
+
120
+ noxctl supports multiple Fortnox tenants from a single installation — useful if you bookkeep for several companies, or want to keep a sandbox tenant separate from production. Each profile has its own OAuth credentials in the OS secure store (macOS Keychain / Linux Secret Service / Windows DPAPI), keyed by profile name.
121
+
122
+ ### Running against a specific profile
123
+
124
+ Three ways to pick the profile, in precedence order (highest wins):
125
+
126
+ 1. **`--profile <name>` flag** — explicit, per-command
127
+ 2. **`NOXCTL_PROFILE` environment variable** — scoped to a shell session
128
+ 3. **Active pointer** — `~/.fortnox-mcp/active-profile`, set by `noxctl profile use`
129
+
130
+ If none of these is set, noxctl uses the `default` profile (what existing users have been using all along — no opt-in required).
131
+
132
+ ```bash
133
+ noxctl init --profile staging # authorize a second tenant
134
+ noxctl --profile staging invoices list # one-off against staging
135
+ NOXCTL_PROFILE=staging noxctl company info # shell-scoped
136
+ noxctl profile use staging # sticky — persists to the pointer
137
+ noxctl profile current # show resolved profile + source
138
+ noxctl profile list # list known profiles
139
+ ```
140
+
141
+ ### MCP server
142
+
143
+ When launched by Claude Desktop / claude.ai, the MCP server resolves the profile from `NOXCTL_PROFILE` and the active pointer at startup, and binds for the session. When launched via `noxctl --profile <name> serve`, the CLI forwards the flag.
144
+
145
+ To run multiple MCP servers (one per tenant) in parallel, register them with distinct names and scoped environments:
146
+
147
+ ```bash
148
+ claude mcp add fortnox-prod -- npx noxctl serve
149
+ claude mcp add fortnox-staging -e NOXCTL_PROFILE=staging -- npx noxctl serve
150
+ ```
151
+
152
+ Non-default sessions print a `[profile: <name>]` stderr banner on startup and prefix every Fortnox API error and token-refresh failure with the same tag so mis-bound sessions are diagnosable from a single error line.
153
+
154
+ ### Fail-closed pointer semantics
155
+
156
+ If the active pointer becomes unreadable or corrupt and no explicit `--profile` flag or `NOXCTL_PROFILE` is set, `noxctl serve` **refuses to start** rather than silently falling back to `default`. This prevents a corrupted pointer from routing production MCP sessions to the wrong tenant. The CLI's `doctor` and `profile use` commands are exempt — they can still run against a broken pointer so you can repair it.
157
+
158
+ ## YubiKey-locked keychain (macOS)
159
+
160
+ By default, credentials live in your macOS **login keychain**, which unlocks automatically when you log in. That's convenient, but it means an AI agent (or anything running as you) can read the tokens without a per-session gesture from you.
161
+
162
+ The optional **dedicated keychain** moves credentials into a separate, lock-on-sleep keychain whose password is derived from your YubiKey via HMAC-SHA1 challenge-response. You unlock it once per session with a single tap; it re-locks when your Mac sleeps. The keychain password is never typed or stored — it only exists on the YubiKey.
163
+
164
+ This is opt-in and macOS-only. It does not change how credentials are stored on Linux or Windows.
165
+
166
+ ### One-time YubiKey setup
167
+
168
+ Program OTP slot 2 for challenge-response (requires touch). **This writes only the empty OTP slot 2** — FIDO2, PIV, OATH, OpenPGP, and slot 1 are untouched, and it is reversible with `ykman otp delete 2`.
169
+
170
+ ```bash
171
+ brew install ykman # if not already installed
172
+ ykman otp chalresp --generate --touch 2 # program slot 2
173
+ ```
174
+
175
+ ### Enable it
176
+
177
+ ```bash
178
+ noxctl keychain init # generate challenge, tap to derive the password,
179
+ # create the locked keychain, copy existing creds in
180
+ ```
181
+
182
+ `init` uses **copy-and-keep**: your existing login-keychain credentials are copied into the new keychain but left in place as a rollback. Once you've confirmed the new flow works, remove the originals:
183
+
184
+ ```bash
185
+ noxctl keychain seal # delete the login-keychain copies (irreversible)
186
+ ```
187
+
188
+ Until you `seal`, the login copies remain readable without a tap — so the per-session protection isn't fully in effect.
189
+
190
+ ### Daily use
191
+
192
+ ```bash
193
+ noxctl keychain unlock # tap your YubiKey — open until the Mac next sleeps
194
+ noxctl keychain status # mode, lock state, ykman/YubiKey presence
195
+ noxctl keychain lock # lock immediately
196
+ ```
197
+
198
+ When the keychain is locked, any command that needs credentials fails fast with a message telling you to run `noxctl keychain unlock` — it never pops a macOS password dialog (the challenge-response password can't be typed into one).
199
+
200
+ ### Recovery if you lose the YubiKey
201
+
202
+ The keychain password lives only on the key, so a lost or re-programmed key means the dedicated keychain can't be unlocked. As long as you have **not** run `seal`, your credentials are still in the login keychain — delete the dedicated keychain and challenge file to fall back:
203
+
204
+ ```bash
205
+ security delete-keychain ~/Library/Keychains/fortnox-mcp.keychain-db
206
+ rm ~/.fortnox-mcp/keychain-challenge
207
+ ```
208
+
209
+ If you *have* sealed, re-run `noxctl init` to re-authenticate from scratch.
210
+
111
211
  ## Tools
112
212
 
113
- Every operation is available both as a CLI command and as an MCP tool. The CLI is the primary interface; the MCP server exposes the same operations to AI agents.
213
+ Every operation is available both as a CLI command and as an MCP tool. The CLI is the primary interface; the MCP server exposes the same operations to AI agents. All mutations — every row labeled `(mutation)` — prompt for confirmation on a TTY and require `--yes` (CLI) or `confirm: true` (MCP) when piped. See [Mutation safety](#mutation-safety).
114
214
 
115
215
  ### Customers
116
216
 
117
217
  | CLI | MCP tool | Description |
118
218
  |-----|----------|-------------|
119
219
  | `noxctl customers list [--search <term>]` | `fortnox_list_customers` | List/search customers |
120
- | `noxctl customers get <number>` | `fortnox_get_customer` | Get a single customer by customer number |
121
- | `noxctl customers create --name <name>` | `fortnox_create_customer` | Create a new customer (mutation) |
122
- | `noxctl customers update <number> --input <file>` | `fortnox_update_customer` | Update an existing customer (mutation) |
220
+ | `noxctl customers get <number>` | `fortnox_get_customer` | Get a single customer |
221
+ | `noxctl customers create --name <name>` | `fortnox_create_customer` | Create a customer (mutation) |
222
+ | `noxctl customers update <number> --input <file>` | `fortnox_update_customer` | Update a customer (mutation) |
223
+
224
+ ### Suppliers
225
+
226
+ | CLI | MCP tool | Description |
227
+ |-----|----------|-------------|
228
+ | `noxctl suppliers list [--search <term>]` | `fortnox_list_suppliers` | List/search suppliers |
229
+ | `noxctl suppliers get <number>` | `fortnox_get_supplier` | Get a single supplier |
230
+ | `noxctl suppliers create --name <name>` | `fortnox_create_supplier` | Create a supplier (mutation) |
231
+ | `noxctl suppliers update <number> --input <file>` | `fortnox_update_supplier` | Update a supplier (mutation) |
232
+
233
+ ### Articles
234
+
235
+ | CLI | MCP tool | Description |
236
+ |-----|----------|-------------|
237
+ | `noxctl articles list [--search <term>]` | `fortnox_list_articles` | List/search articles |
238
+ | `noxctl articles get <number>` | `fortnox_get_article` | Get a single article |
239
+ | `noxctl articles create --description <text>` | `fortnox_create_article` | Create an article (mutation) |
240
+ | `noxctl articles update <number> --input <file>` | `fortnox_update_article` | Update an article (mutation) |
123
241
 
124
242
  ### Invoices
125
243
 
126
244
  | CLI | MCP tool | Description |
127
245
  |-----|----------|-------------|
128
246
  | `noxctl invoices list [--filter <status>] [--customer <number>]` | `fortnox_list_invoices` | List/filter invoices. Filters: `cancelled`, `fullypaid`, `unpaid`, `unpaidoverdue`, `unbooked` |
129
- | `noxctl invoices get <docNumber>` | `fortnox_get_invoice` | Get a single invoice by document number |
130
- | `noxctl invoices create --customer <number> --input <file>` | `fortnox_create_invoice` | Create an invoice with line items (mutation) |
247
+ | `noxctl invoices get <docNumber>` | `fortnox_get_invoice` | Get a single invoice |
248
+ | `noxctl invoices create --customer <number> --input <file>` | `fortnox_create_invoice` | Create an invoice (mutation) |
131
249
  | `noxctl invoices update <docNumber> --input <file>` | `fortnox_update_invoice` | Update an invoice that has not been bookkeept (mutation) |
132
- | `noxctl invoices send <docNumber> [--method email\|print\|einvoice] [--subject <s>] [--body <s>] [--bcc <email>]` | `fortnox_send_invoice` | Send invoice via email (default), print, or e-invoice. Supports email subject, body, and BCC (Blind Carbon Copy) (mutation) |
133
- | `noxctl invoices bookkeep <docNumber>` | `fortnox_bookkeep_invoice` | Book an invoice (mutation) |
250
+ | `noxctl invoices send <docNumber> [--method email\|print\|einvoice] [--subject <s>] [--body <s>] [--bcc <email>]` | `fortnox_send_invoice` | Send via email (default), print, or e-invoice (mutation) |
251
+ | `noxctl invoices bookkeep <docNumber>` | `fortnox_bookkeep_invoice` | Bookkeep an invoice (mutation) |
134
252
  | `noxctl invoices credit <docNumber>` | `fortnox_credit_invoice` | Credit an invoice (mutation) |
135
253
 
254
+ ### Invoice payments (inbetalningar)
255
+
256
+ | CLI | MCP tool | Description |
257
+ |-----|----------|-------------|
258
+ | `noxctl invoice-payments list [--invoice <number>]` / alias `noxctl ip list` | `fortnox_list_invoice_payments` | List invoice payments |
259
+ | `noxctl invoice-payments get <number>` | `fortnox_get_invoice_payment` | Get a single invoice payment |
260
+ | `noxctl invoice-payments create --invoice <n> --amount <a> --date <date>` | `fortnox_create_invoice_payment` | Register a payment against an invoice (mutation) |
261
+ | `noxctl invoice-payments bookkeep <number>` | — | Bookkeep an invoice payment (mutation) |
262
+ | `noxctl invoice-payments delete <number>` | `fortnox_delete_invoice_payment` | Delete an invoice payment (mutation) |
263
+
264
+ ### Supplier invoices (leverantörsfakturor)
265
+
266
+ | CLI | MCP tool | Description |
267
+ |-----|----------|-------------|
268
+ | `noxctl supplier-invoices list [--filter <status>] [--supplier <number>]` / alias `si list` | `fortnox_list_supplier_invoices` | List/filter supplier invoices |
269
+ | `noxctl supplier-invoices get <givenNumber>` | `fortnox_get_supplier_invoice` | Get a single supplier invoice |
270
+ | `noxctl supplier-invoices create --supplier <n> --input <file>` | `fortnox_create_supplier_invoice` | Create a supplier invoice (mutation) |
271
+ | `noxctl supplier-invoices bookkeep <givenNumber>` | `fortnox_bookkeep_supplier_invoice` | Bookkeep a supplier invoice (mutation) |
272
+
273
+ ### Supplier invoice payments (utbetalningar)
274
+
275
+ | CLI | MCP tool | Description |
276
+ |-----|----------|-------------|
277
+ | `noxctl supplier-invoice-payments list [--invoice <number>]` / alias `sip list` | `fortnox_list_supplier_invoice_payments` | List supplier invoice payments |
278
+ | `noxctl supplier-invoice-payments get <number>` | `fortnox_get_supplier_invoice_payment` | Get a single supplier invoice payment |
279
+ | `noxctl supplier-invoice-payments create --invoice <n> --amount <a> --date <date>` | `fortnox_create_supplier_invoice_payment` | Register a payment against a supplier invoice (mutation) |
280
+ | `noxctl supplier-invoice-payments delete <number>` | `fortnox_delete_supplier_invoice_payment` | Delete a supplier invoice payment (mutation) |
281
+
282
+ ### Offers (offerter)
283
+
284
+ | CLI | MCP tool | Description |
285
+ |-----|----------|-------------|
286
+ | `noxctl offers list [--filter <status>] [--customer <number>]` | `fortnox_list_offers` | List/filter offers. Filters: `cancelled`, `expired`, `ordercreated`, `invoicecreated` |
287
+ | `noxctl offers get <docNumber>` | `fortnox_get_offer` | Get a single offer |
288
+ | `noxctl offers create --customer <number> --input <file>` | `fortnox_create_offer` | Create an offer (mutation) |
289
+ | `noxctl offers update <docNumber> --input <file>` | `fortnox_update_offer` | Update an offer (mutation) |
290
+ | `noxctl offers create-invoice <docNumber>` | `fortnox_create_invoice_from_offer` | Convert offer → invoice (mutation) |
291
+ | `noxctl offers create-order <docNumber>` | `fortnox_create_order_from_offer` | Convert offer → order (mutation) |
292
+
293
+ ### Orders (ordrar)
294
+
295
+ | CLI | MCP tool | Description |
296
+ |-----|----------|-------------|
297
+ | `noxctl orders list [--filter <status>] [--customer <number>]` | `fortnox_list_orders` | List/filter orders. Filters: `cancelled`, `invoicecreated`, `invoicenotcreated` |
298
+ | `noxctl orders get <docNumber>` | `fortnox_get_order` | Get a single order |
299
+ | `noxctl orders create --customer <number> --input <file>` | `fortnox_create_order` | Create an order (mutation) |
300
+ | `noxctl orders update <docNumber> --input <file>` | `fortnox_update_order` | Update an order (mutation) |
301
+ | `noxctl orders create-invoice <docNumber>` | `fortnox_create_invoice_from_order` | Convert order → invoice (mutation) |
302
+
136
303
  ### Bookkeeping
137
304
 
138
305
  | CLI | MCP tool | Description |
139
306
  |-----|----------|-------------|
140
307
  | `noxctl vouchers list [--series <s>] [--from <date>] [--to <date>]` | `fortnox_list_vouchers` | List vouchers, optionally filtered by series and date range |
308
+ | `noxctl vouchers get <series> <number>` | `fortnox_get_voucher` | Get a single voucher with rows |
141
309
  | `noxctl vouchers create --input <file>` | `fortnox_create_voucher` | Create a voucher with debit/credit rows (mutation) |
310
+ | `noxctl vouchers attach <series> <number> <file...> [--year]` | `fortnox_attach_voucher_files` | Upload receipt/underlag files and link them to a voucher (mutation; needs the Fortnox archive scope) |
142
311
  | `noxctl accounts list [--search <term>]` | `fortnox_list_accounts` | View chart of accounts, search by name or number |
143
312
 
313
+ ### Financial reports
314
+
315
+ | CLI | MCP tool | Description |
316
+ |-----|----------|-------------|
317
+ | `noxctl reports income [--year <n>] [--from <date>] [--to <date>]` / alias `reports resultat` | `fortnox_income_statement` | Income statement (resultaträkning) |
318
+ | `noxctl reports balance [--year <n>] [--to <date>]` / alias `reports balans` | `fortnox_balance_sheet` | Balance sheet (balansräkning) |
319
+
144
320
  ### Tax
145
321
 
146
322
  | CLI | MCP tool | Description |
147
323
  |-----|----------|-------------|
148
324
  | `noxctl tax report --from <date> --to <date>` | `fortnox_tax_report` | Informational VAT summary for a period. Reconcile against Fortnox before filing. Dates in `YYYY-MM-DD` format |
149
325
 
326
+ ### Tax reductions (ROT/RUT)
327
+
328
+ | CLI | MCP tool | Description |
329
+ |-----|----------|-------------|
330
+ | `noxctl tax-reductions list [--filter <type>]` | `fortnox_list_taxreductions` | List tax reductions (ROT/RUT) |
331
+ | `noxctl tax-reductions get <id>` | `fortnox_get_taxreduction` | Get a single tax reduction |
332
+ | `noxctl tax-reductions create --reference <n> --type <rot\|rut> --document-type <type> --customer-name <name> --amount <öre>` | `fortnox_create_taxreduction` | Create a ROT/RUT tax reduction (mutation) |
333
+
334
+ ### Projects
335
+
336
+ | CLI | MCP tool | Description |
337
+ |-----|----------|-------------|
338
+ | `noxctl projects list` | `fortnox_list_projects` | List projects |
339
+ | `noxctl projects get <number>` | `fortnox_get_project` | Get a single project |
340
+ | `noxctl projects create --description <text>` | `fortnox_create_project` | Create a project (mutation) |
341
+ | `noxctl projects update <number> --input <file>` | `fortnox_update_project` | Update a project (mutation) |
342
+
343
+ ### Cost centers (kostnadsställen)
344
+
345
+ | CLI | MCP tool | Description |
346
+ |-----|----------|-------------|
347
+ | `noxctl costcenters list` | `fortnox_list_costcenters` | List cost centers |
348
+ | `noxctl costcenters get <code>` | `fortnox_get_costcenter` | Get a single cost center |
349
+ | `noxctl costcenters create --code <code> --description <text>` | `fortnox_create_costcenter` | Create a cost center (mutation) |
350
+ | `noxctl costcenters update <code> --input <file>` | `fortnox_update_costcenter` | Update a cost center (mutation) |
351
+ | `noxctl costcenters delete <code>` | `fortnox_delete_costcenter` | Delete a cost center (mutation) |
352
+
353
+ ### Price lists and prices
354
+
355
+ | CLI | MCP tool | Description |
356
+ |-----|----------|-------------|
357
+ | `noxctl pricelists list` | `fortnox_list_pricelists` | List price lists |
358
+ | `noxctl pricelists get <code>` | `fortnox_get_pricelist` | Get a single price list |
359
+ | `noxctl pricelists create --code <code> --description <text>` | `fortnox_create_pricelist` | Create a price list (mutation) |
360
+ | `noxctl pricelists update <code> --input <file>` | `fortnox_update_pricelist` | Update a price list (mutation) |
361
+ | `noxctl prices list --pricelist <code> [--article <number>]` | `fortnox_list_prices` | List prices within a price list |
362
+ | `noxctl prices get --pricelist <code> --article <number>` | `fortnox_get_price` | Get a specific price |
363
+ | `noxctl prices update --pricelist <code> --article <number> --input <file>` | `fortnox_update_price` | Update a price (mutation) |
364
+
365
+ ### Contracts (avtal — recurring invoicing)
366
+
367
+ | CLI | MCP tool | Description |
368
+ |-----|----------|-------------|
369
+ | `noxctl contracts list [--filter active\|inactive\|finished]` | `fortnox_list_contracts` | List/filter contracts |
370
+ | `noxctl contracts get <number>` | `fortnox_get_contract` | Get a single contract |
371
+ | `noxctl contracts create --customer <number> --input <file>` | `fortnox_create_contract` | Create a contract (mutation) |
372
+ | `noxctl contracts update <number> --input <file>` | `fortnox_update_contract` | Update a contract (mutation) |
373
+ | `noxctl contracts finish <number>` | `fortnox_finish_contract` | Finish a contract (mutation) |
374
+ | `noxctl contracts create-invoice <number>` | `fortnox_create_invoice_from_contract` | Create the next invoice now (mutation) |
375
+ | `noxctl contracts increase-invoice-count <number>` | `fortnox_increase_contract_invoice_count` | Extend by one invoice (mutation) |
376
+
377
+ ### Financial years and locked period
378
+
379
+ | CLI | MCP tool | Description |
380
+ |-----|----------|-------------|
381
+ | `noxctl financial-years list [--date <date>]` | `fortnox_list_financialyears` | List financial years (räkenskapsår) |
382
+ | `noxctl financial-years get <id>` | `fortnox_get_financialyear` | Get a single financial year |
383
+ | `noxctl financial-years locked-period` | `fortnox_get_lockedperiod` | Show through which date bookkeeping is locked |
384
+
385
+ ### Analytics and dashboard
386
+
387
+ | CLI | MCP tool | Description |
388
+ |-----|----------|-------------|
389
+ | `noxctl analytics overdue` | `fortnox_overdue_invoices` | Overdue invoices summary |
390
+ | `noxctl analytics unpaid` | `fortnox_unpaid_totals` | Outstanding receivables, with overdue split |
391
+ | `noxctl analytics top-customers [--period <period>]` | `fortnox_top_customers` | Top customers by invoiced amount |
392
+ | `noxctl analytics vat --period <period>` | `fortnox_vat_summary` | VAT summary with net VAT position |
393
+ | `noxctl dashboard` | — | At-a-glance: outstanding, overdue, recent invoices, monthly revenue |
394
+
150
395
  ### Company
151
396
 
152
397
  | CLI | MCP tool | Description |
@@ -160,6 +405,10 @@ Every operation is available both as a CLI command and as an MCP tool. The CLI i
160
405
  | `noxctl init` | — | Interactive setup wizard — connects to Fortnox, stores credentials, optionally registers MCP server |
161
406
  | `noxctl doctor` | `fortnox_status` | Validate setup: Node version, credentials, token status, API connectivity, and scopes |
162
407
  | `noxctl logout` | — | Remove stored credentials from the OS keychain |
408
+ | `noxctl profile use <name>` | — | Set the active profile (writes `~/.fortnox-mcp/active-profile`) |
409
+ | `noxctl profile current` | — | Show the currently resolved profile and where it came from |
410
+ | `noxctl profile list` | — | List known profiles from the index |
411
+ | `noxctl completion <bash\|zsh\|fish>` | — | Generate a shell completion script |
163
412
 
164
413
  ## CLI output
165
414
 
@@ -172,6 +421,14 @@ noxctl -o table invoices list # force table
172
421
  noxctl invoices list | jq . # auto-JSON (piped)
173
422
  ```
174
423
 
424
+ JSON output has a stable envelope: list commands wrap under the plural resource key (`{"Invoices": [...], "MetaInformation": {...}}`) and single-resource commands under the singular key (`{"Invoice": {...}}`), so scripted callers can rely on a fixed accessor. Failures in JSON mode are emitted to stderr as a structured envelope:
425
+
426
+ ```json
427
+ { "error": { "status": 400, "message": "...", "source": "fortnox-api" } }
428
+ ```
429
+
430
+ Natural date periods are accepted wherever `--from`/`--to` work, via `--period`: `Q1`, `2025-Q3`, `march`/`mars`, `this-quarter`, `last-quarter`, `this-month`, `last-month`, `ytd`, `this-year`, `last-year`, or a bare year. Periods are **calendar-year** based (broken fiscal years are not yet considered).
431
+
175
432
  When running from a local clone instead of an installed binary, replace `noxctl` with `node dist/cli.js`.
176
433
 
177
434
  ## Mutation safety
@@ -228,16 +485,18 @@ Ask Claude naturally — works in both Swedish and English:
228
485
 
229
486
  ## Troubleshooting
230
487
 
231
- **"FORTNOX_CLIENT_ID and FORTNOX_CLIENT_SECRET must be set"**
488
+ **"stdin is not a TTY. Set FORTNOX_CLIENT_ID and FORTNOX_CLIENT_SECRET env vars to run non-interactively"**
232
489
 
233
- Environment variables were not passed to the command. Use `export` to set them in your shell first:
490
+ `noxctl init` is normally interactive it prompts for the Client ID and Secret. In CI or other non-TTY contexts it falls back to reading them from environment variables:
234
491
 
235
492
  ```bash
236
493
  export FORTNOX_CLIENT_ID=<your-id>
237
494
  export FORTNOX_CLIENT_SECRET=<your-secret>
495
+ export FORTNOX_SERVICE_ACCOUNT=1 # optional, enables service account mode
496
+ noxctl init
238
497
  ```
239
498
 
240
- Then run setup again. This avoids issues with long commands wrapping across lines.
499
+ Once authorized, the tokens are stored in the OS keychain. No env vars are needed afterwards only for re-running `init` non-interactively.
241
500
 
242
501
  **"Not authenticated. Run `noxctl init`"**
243
502
 
@@ -245,16 +504,7 @@ Credentials are missing or were not saved. Re-run the setup step. On macOS, chec
245
504
 
246
505
  **403 Forbidden from Fortnox API**
247
506
 
248
- Your app is missing required scopes. Go to [developer.fortnox.se](https://developer.fortnox.se/), open your app, and check that these are enabled under **Behörigheter** / **Permissions**:
249
-
250
- | Swedish (SV) | English (EN) |
251
- |----------------------|---------------------|
252
- | Artikel | Article |
253
- | Bokföring | Bookkeeping |
254
- | Faktura | Invoice |
255
- | Företagsinformation | Company Information |
256
- | Inställningar | Settings |
257
- | Kund | Customer |
507
+ Your app is missing one or more scopes. The error message names the specific scope needed (e.g. `Missing "supplier" scope`). Go to [developer.fortnox.se](https://developer.fortnox.se/), open your app, and enable the matching permission under **Behörigheter** / **Permissions** — see the full table in [Setup → Create a Fortnox app](#1-create-a-fortnox-app). Then re-run `noxctl init`.
258
508
 
259
509
  **"Token refresh failed"**
260
510
 
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export declare const SCOPES = "article customer invoice supplier supplierinvoice bookkeeping companyinformation settings";
1
+ export declare const SCOPES = "article customer invoice payment supplier supplierinvoice bookkeeping companyinformation settings inbox connectfile";
2
+ export declare const CREDENTIAL_SCHEMA_VERSION = 2;
2
3
  export interface FortnoxCredentials {
3
4
  client_id: string;
4
5
  client_secret: string;
@@ -6,24 +7,31 @@ export interface FortnoxCredentials {
6
7
  refresh_token: string;
7
8
  expires_at: number;
8
9
  tenant_id?: string;
10
+ company_name?: string;
11
+ schema_version?: number;
12
+ last_write_epoch?: number;
9
13
  }
10
14
  export interface FortnoxAppConfig {
11
15
  clientId: string;
12
16
  clientSecret: string;
13
17
  serviceAccount?: boolean;
14
18
  }
15
- export declare function loadCredentials(): Promise<FortnoxCredentials | null>;
16
- export declare function saveCredentials(creds: FortnoxCredentials): Promise<void>;
19
+ export declare function setResolvedProfile(name: string): void;
20
+ export declare function getResolvedProfile(): string;
21
+ export declare function __resetLegacyObservedForDefault(): void;
22
+ export declare function loadCredentials(profile?: string): Promise<FortnoxCredentials | null>;
23
+ export declare function saveCredentials(creds: FortnoxCredentials, profile?: string): Promise<void>;
17
24
  export declare function exchangeCodeForTokens(code: string, redirectUri: string, config: FortnoxAppConfig): Promise<{
18
25
  access_token: string;
19
26
  refresh_token: string;
20
27
  expires_in: number;
21
28
  }>;
22
- export declare function getTokenViaClientCredentials(creds: FortnoxCredentials): Promise<FortnoxCredentials>;
23
- export declare function refreshAccessToken(creds: FortnoxCredentials): Promise<FortnoxCredentials>;
24
- export declare function getValidToken(): Promise<string>;
29
+ export declare function getTokenViaClientCredentials(creds: FortnoxCredentials, profile?: string): Promise<FortnoxCredentials>;
30
+ export declare function refreshAccessToken(creds: FortnoxCredentials, profile?: string): Promise<FortnoxCredentials>;
31
+ export declare function getValidToken(profile?: string): Promise<string>;
25
32
  export declare function fetchTenantId(accessToken: string): Promise<string | undefined>;
33
+ export declare function fetchCompanyNameSafe(accessToken: string): Promise<string | undefined>;
26
34
  export declare function escapeHtml(value: string): string;
27
35
  export declare function buildAuthorizationUrl(config: FortnoxAppConfig, redirectUri: string, state: string): string;
28
- export declare function runOAuthSetup(config: FortnoxAppConfig): Promise<void>;
36
+ export declare function runOAuthSetup(config: FortnoxAppConfig, profile?: string): Promise<void>;
29
37
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,MAAM,8FAC0E,CAAC;AAE9F,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAQ1E;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B9E;AAED,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAuC7B;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoC/F;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC,CAwBrD;AAED,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAepF;AAgBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,CAeR;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6G3E"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,MAAM,wHACoG,CAAC;AAExH,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAcD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAErD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAGD,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAuBD,wBAAsB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA2B1F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhG;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B9E;AAED,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC,CAwC7B;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,CAAC,CAqC7B;AAED,wBAAsB,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BrE;AAED,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAepF;AAED,wBAAsB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAgB3F;AAgBD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,gBAAgB,EACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,CAeR;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,MAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAgIf"}