13 Commits

Author SHA1 Message Date
Jakob Wennberg 678acfe7ef fix(mcp): eager-auth flag so claude.ai's connector dialog detects OAuth, not "None" (#2159)
claude.ai's two-step "Add custom connector" dialog probes the server URL
without credentials and pre-fills the Authentication choice from the
answer. Our lazy-auth endpoint (issue #1814) answers 200 on an anonymous
initialize, which the dialog reads as an authless server: it suggests
"None", and a connector added with that default never opens the sign-in
when the challenge arrives later. Per Anthropic's connector docs a 401 is
the only answer it reads as OAuth ("Claude does not honor a
WWW-Authenticate header on a 200 response").

- `auth=required` on the endpoint URL (extensions/general/mcp-server/
  auth-mode.ts) turns lazy auth off for that URL: every tokenless
  request, initialize included, answers the 401 + WWW-Authenticate
  challenge. Callers with a token are unaffected; the bare URL keeps
  lazy auth for Claude Code, the plugin, Cursor and ChatGPT, and existing
  connector records are untouched.
- The links we control carry the flag: Settings -> API & MCP (install
  link and copy block), the onboarding checklist, both docs pages and
  claude-plugin/CONNECTORS.md (plugin 1.2.3). The docs' Path A now
  describes the eager flow (sign-in opens on Add) instead of telling
  users to override the dialog's "None".
- Tests: eager-auth.test.ts (401 on initialize/tools/list/public tools,
  namespaced metadata pointer, token no-op, exact-flag only); checklist
  link shape updated.

Companion: gnubok-website PR (Kom igång connector link + regenerated
connect-claude / anslut-claude pages).


Claude-Session: https://claude.ai/code/session_013yw62FMXGSzo6icFDiBwP3

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 14:51:32 +02:00
Jakob Wennberg b160f7290d docs(plugin): starter prompt nudges the connector's onboarding guide (#1965)
Several E2E runs composed the first reply from tool descriptions before
the skill loaded (questionnaire instead of the guided round). Four words
in the published prompt point the agent at the guide from message one;
the rest of the prompt stays memory-first and universal.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 22:48:10 +02:00
Jakob Wennberg cd9fb5b717 feat(mcp): byte-exact SIE upload path + brevity and memory-first onboarding (#1954)
From the fourth E2E run: the agent correctly refused to reproduce a
104 KB SIE file token by token (silent mid-verifikat truncation) and
dead-ended to the web wizard, and its replies were walls of compliance
prose.

1. gnubok_create_sie_upload: signed same-origin upload URL (reuses the
   pending-document infra; .se/.sie/.si only, 50 MB HTTP cap).
   gnubok_sie_preflight and gnubok_import_sie accept upload_id as the
   byte-exact source, plus optional sha256 (hex of the raw bytes)
   verified on the upload_id/base64 paths so truncation is DETECTED,
   never silent. Inline content above 120k chars is refused with a
   pointer to the upload flow. Scope bookkeeping:write (same intent as
   import_sie).

2. Skill: brevity rule (max ~8 short lines per reply, one warning per
   step, no legal essays), memory-first rule (check what is already
   known before asking the opening questions), the upload-first SIE
   step, and gnubok_explain_voucher_gap after import for skipped
   voucher numbers.

3. CONNECTORS.md starter prompt rewritten memory-first so it stays
   copy-paste ready without the user's own data in it. Plugin v1.2.2.

tools/list ceiling 62K to 62.4K documented in the bench.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:11:11 +02:00
Jakob Wennberg 4af7469523 feat(onboarding): minimal input: orgnr + moms period is the whole ask (#1952)
* feat(onboarding): minimal input: orgnr + moms period is the whole ask

Two fixes from the third E2E attempt (2026-08-26):

1. accounting_method is now optional in CompanySetupSchema and defaults by
   form in planCompanySetup: aktiebolag = accrual (the norm), enskild
   firma = cash (the common small-EF choice; legal under 3 MSEK, BFL 4
   kap 4 paragraf). The plan flags the default (resolved.accountingMethodDefaulted)
   and gnubok_create_company's preview carries accounting_method_defaulted
   so the readback names it and the user overrides in the same 'ja'.
   Never silent: the preview is the checkpoint. Applies to the MCP tool
   and POST /api/v1/companies (additive; response shows the resolved
   value). The lookup tool's still_to_ask no longer lists it.

2. The agent refused a real orgnr because the user said 'nytt bolag' and
   the registry showed an established company ('Stopp. Numret matchar
   inte ett nytt bolag'): lookup instructions now state that an
   established company with F-skatt/VAT is the NORMAL case (new = new to
   Accounted) and the orgnr is never second-guessed for looking
   established.

Skill + plugin (v1.2.1) updated; API skill regenerated; DECISIONS.md entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): surface the kontantmetod 3-MSEK condition on the defaulted cash method

Compliance-review finding on #1952: the EF cash default carries a legal
eligibility condition (turnover normally under 3 MSEK, BFL 4 kap 4 §)
that a client not reading the onboarding skill would never see. The
create preview now carries accounting_method_note with the condition
whenever cash was defaulted, and the v1 pitfall states it for API
integrators. The registry cannot verify turnover, so the confirm-time
human check is the gate; the default itself stays (a brand-new EF has
zero turnover by definition).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 15:15:09 +02:00
Jakob Wennberg e7e4efbfbc feat(oauth): one-click consent: all scopes pre-selected, list collapsed, Allow above the fold (#1953)
The read-only default forced every agent-first user to scroll a scope
list and hand-tick write rows before the flow could work. Founder call
2026-08-26: pre-check ALL scopes when the client requests none (Claude's
connector case), collapse the scope list into an expandable details fold
('Alla förvalda, visa och justera'), and keep the Allow button visible
without scrolling.

Why this is defensible: every write is STAGED for explicit approval
before anything touches the ledger, each scope row stays individually
untickable inside the fold, the warn line states the staging rule right
above the button, and the grant is revocable under Inställningar >
API-nycklar. A client that requests explicit scopes still gets exactly
that set (RFC 6749 3.3 least-privilege unchanged), and the tampered/empty
POST fallback stays read-only.

CONNECTORS.md gains the share link (connectorName/connectorUrl params)
plus the starter prompt to pair with it.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 15:06:46 +02:00
Jakob Wennberg 188816652d docs(api,mcp): tool counts, changelog backfill, version-header honesty, lazy auth, endpoint map (#1929)
Brings the developer-facing API and MCP docs back in line with origin/main
(audit 2026-08-26). Docs only; no runtime behaviour changes.

- Tool counts: the server registers 153 tools; docs said 90+/100+/120.
  All now say "150+" (connect-claude, gnubok-mcp README, plugin README,
  mcp-server rules, CLAUDE.md, registry entry with refreshed updatedAt).
  Not derived from the tools array: lib/ must not import @/extensions/.
- REST changelog: backfilled the additive 2026-08 changes (#1909 report
  date ranges + PDFs, #1864 POST /companies, #1773 vat-declarations,
  #1405 PATCH settings, #1724/#1788 customer personal_number, #1809
  cash_account_id filter). API version date unchanged.
- Version headers: Gnubok-Deprecation is planned, not emitted; the
  Gnubok-Version request header is not read today (version.ts comment,
  versioning page, conventions overlay, regenerated skills/accounted-api).
- connect-claude Path A documents lazy auth (connector works before an
  account exists; sign-in on the first company-scoped call).
- MCP server README: real Anthropic SDK call sites, real resource URIs,
  pending-operations widget, public-tools/tasks/origin-guard/pii-guard.
  Rules file gains Lazy auth + feedback/tasks paragraphs.
- api-routes endpoint map regenerated from the filesystem (560 routes,
  55 families incl. v1, agent, reconciliation account-keyed, dimensions,
  peppol, rot-rut, webshop-orders, mileage, billing, skatteverket,
  receipt-hunt).
- gnubok-mcp/accounted-mcp: /settings?tab=api is the pre-redesign URL;
  now /settings/api (README + help hints, no version bump).

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 13:35:54 +02:00
Jakob Wennberg 8ddc77fdfd feat(mcp): org-number-first onboarding: gnubok_lookup_company prefills the company from the registry (#1940)
The onboarding flow now mirrors the web wizard: ask for the
organisationsnummer first, look the company up in the public registry (one
TIC Lens call through the extracted extensions/general/tic/lib/lookup.ts,
shared with the /lookup HTTP route), and present the facts for confirmation
instead of interrogating the user.

The new gnubok_lookup_company tool (companies:read, company-independent,
default catalog) returns the registry facts, a prefilled
suggested_create_company_input, and a still_to_ask list that encodes the
same fact-vs-question rules as lib/onboarding-journey/reducer.ts: F-skatt
is a fact both ways, VAT is a fact only when positively registered (ML 17
kap 24 paragraf), moms period and accounting method are always asked, an
enskild firma's verksamhetsnamn is the user's choice, and a known fiscal
year becomes a confirm question. Registry outages degrade to the full
question list instead of failing onboarding.

The onboarding skill and the plugin's /accounted:setup command are updated
to the orgnr-first flow (plugin 1.2.0). tools/list ceiling bumped 61.2K to
61.5K with the reason documented in the bench.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 13:03:02 +02:00
Jakob Wennberg a1af9adb05 docs(connector): name the Claude.ai auth mode and OAuth client to pick (#1914)
Claude.ai's Add custom connector dialog auto-detects Authentication
"None" for a server that answers the handshake without credentials,
which is exactly what lazy auth does; a user who accepts that default
gets an error instead of the sign-in on the first company-scoped call.
State the two correct choices ("Required when the server asks", DCR
client registration) in the bridge README and the plugin's CONNECTORS.md.


Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 08:58:02 +02:00
Jakob Wennberg 159823583c feat(plugin): /accounted:setup command, CONNECTORS.md, v1.1.0 (#1902)
The Claude plugin is the one-click install for Cowork and Claude Code,
so it should also be the entry to agent-first onboarding (#1814).
/accounted:setup connects the bundled connector (creating the account on
the sign-in screen if needed), hands off to the server-side onboarding
skill when the account has no company, then the bank and Skatteverket
links. CONNECTORS.md documents the single bundled connector the way
Anthropic's own plugins do. Version 1.1.0 so marketplaces that sync on
version bumps pick it up. The plugin-refs guard now also validates
commands/*.md against the server.


Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 18:39:01 +02:00
Jakob Wennberg e92365b86e feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint (#1814 PR 4) (#1866)
* feat(mcp): distribution polish for agent-first onboarding: CIMD, plugin start skill, bridge hint

Fourth PR of agent-first onboarding (#1814).

- The OAuth AS metadata advertises client_id_metadata_document_supported
  next to the existing `none` token auth, the pair Claude.ai, Claude Code
  and Codex look for to use CIMD instead of registering a DCR client per
  connection. authorize/token never keyed on client_id (the redirect-URI
  allowlist is the trust boundary), so nothing else changes; DCR stays
  for ChatGPT.
- The plugin's start skill no longer sends a user without an account to
  the website: the /mcp OAuth screen creates the account, and a
  NO_COMPANY_YET briefing failure routes to the onboarding skill and
  accounted_create_company. README updated to match.
- `npx accounted-mcp` without ACCOUNTED_API_KEY prints the OAuth
  alternative (Claude Code, Codex, Claude.ai connector) and that the
  account can be created on the sign-in screen; package README too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

* fix(oauth): do not advertise CIMD until redirect URIs are matched against the client document

CodeRabbit on #1866: advertising client_id_metadata_document_supported
makes Claude and Codex send URL client_ids and expects an exact
redirect_uri match against that document; the authorize endpoint only
checks the global allowlist and never fetches client metadata. The flag
is withheld until an SSRF-safe, cached CIMD fetch with exact redirect
matching exists. DCR stays the registration path (stateless, so free).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 13:05:59 +02:00
Jakob Wennberg 4dfdb35e13 feat(plugin): ready the Claude Code plugin for directory submission (#1245)
The plugin has been installable from erp-mafia/accounted since #1088, but
three things would fail review at the Claude plugin directory:

- `homepage` pointed at https://app.gnubok.se/docs/api/connect-claude, which
  404s. next.config.ts redirects /docs/api/* to the separate docs.gnubok.se
  repo, where that page was never ported, so app/docs/api/connect-claude is
  unreachable dead code. Point homepage at the plugin README instead.
- `license: MIT` was a bare claim with no artifact next to it. Add the MIT
  text and make the README explicit that the plugin is MIT while the platform
  it connects to is a separate AGPL-3.0 work.
- Version stayed at 0.1.0 for a plugin that has been live and working.

Also give /accounted:start a path for a user who installs from the directory
with no Accounted account: previously it only handled a not-yet-authenticated
MCP server, and a cold user would hit OAuth with nowhere to go.

Both manifests pass `claude plugin validate`, the same check the review
pipeline runs on every submission.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 19:18:28 +02:00
Mattsson 53e343ee92 Bug/invalid imports (#1146)
* feat: add Accounted MCP namespace

* fix(bookkeeping): stop flagging verifikat whose underlag lives on a referenced supplier invoice

The missing-underlag surfaces only accepted a document directly linked to
the entry, so payment verifikat for supplier invoices (doc on the
registration entry per design) and entries whose doc was pinned to the
bank transaction before matching were falsely flagged; opening the entry
showed the referenced doc and cleared the warning client-side, and it
came back on reload.

- verifikat_without_documents + transactions_without_documents now treat
  an entry as covered when a supplier invoice referencing it (registration
  or payment FK, or a supplier_invoice_payments row) carries a document
  anchored to a journal entry (BFL 5 kap 7 paragraf hänvisning till
  underlag; anchoring required because the WORM deletion guards key on
  document_attachments.journal_entry_id)
- match-supplier-invoice routes (dashboard + v1) propagate the
  transaction's pinned document onto the payment verifikat, mirroring the
  categorize route; migration backfills rows already written (open
  unlocked periods, company-guarded, never steals a linked doc)
- /api/documents/counts, the transactions-page badges, the bulk "Inget
  underlag krävs" count and the push-notification scheduler share the
  same reference-aware predicate, so every surface agrees with the RPC
- counts route validates journal_entry_ids as UUIDs (they are
  interpolated into a PostgREST or-filter)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): align table columns flush with page edges

Collapse the checkbox gutter column to zero width and hang the
hover-revealed checkbox/expand chevron in the page margins, drop the
outer padding so DATUM sits flush left and STATUS flush right, and
tuck the overflow-menu dots under the middle of the STATUS header.
Applied to both the inbox and history tables so they stay identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(arsredovisning): tie anlaggningstillgangar note to booked depreciation

The ARL 5:8 roll-forward note recomputed depreciation from its own
day-based linear formula (365.25/12 month length, non-inclusive day
count, linear only), drifting ~20 kr per year per asset from the
ledger-driven resultat- and balansrakning and misstating non-linear
methods entirely. Note figures now come from posted
depreciation_schedules rows (the same source disposeAsset reverses),
falling back to the engine's computeAnnualDepreciation when nothing is
posted; pre-onboarding opening balances iterate prior years through
the engine. Adds a note-vs-trial-balance tie-out warning (accounts
1000-1299, over 1 kr) surfaced before download.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(stripe): move connect and sync surface from settings to import page

Stripe's transaction feed is a continuous import source in the same
category as the PSD2 bank connection, so its connect/sync surface now
lives on the import page as a source card (mode=stripe), gated
"kommer snart" on hosted like before; self-hosted keeps the full panel.

- Import page: Stripe card after Koppla bank, renders the existing
  StripeSettingsPanel via the settings-panel registry
- OAuth callback and panel cleanup return to /import?mode=stripe
- Settings > Betalningar retired: nav item removed, route redirects,
  PaymentsSettingsContent deleted, legacy ?tab=payments mapped
- New import.stripe_* strings in sv+en; dead settings_nav.payments removed

Crons and sync logic unchanged; payment-link settings stay in the
invoicing section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(underlag): paginate missing-underlag cron and harden doc-surface queries

Resolve PR review findings on bug/invalid-imports:
- notification-scheduler: fetchAllRows on all 5 global reads; past 1000 rows
  the capped reads produced false "saknade underlag" notifications
- bulk-missing: LOOKUP_CHUNK 300->150 so the twice-embedded .or() id list
  stays under the PostgREST URL limit
- bulk-missing + transactions page: UUID-guard the .or()-interpolated id
  lists, matching documents/counts
- match-supplier-invoice (dashboard + v1): log documentId/journalEntryId on
  the non-fatal doc-link warning
- well-known/oauth-protected-resource: document the tool_namespace allow-list
- messages/en: reword stripe_description
- DECISIONS.md: record the asset ibAck tie-out and Tailwind !important calls

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tic): convert registrationDate from Unix seconds to millisecond epoch in lookup and profile tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:03:50 +02:00
Jakob Wennberg e33cc2428d feat(plugin): official Claude Code plugin with marketplace and seven workflow skills (#1088)
Ships an installable Claude Code plugin (/plugin marketplace add
erp-mafia/accounted) that bundles the MCP connection (OAuth, zero-key)
with seven short workflow skills following the Swedish bookkeeping
rhythm: start, bookkeep, check, month-close, vat, payroll, year-end.

Wrappers are deliberately thin: they ground in the agent briefing and
Accounted:// resources, load server-side workflow skills and regulatory
atoms via gnubok_load_skill at need, and stage every write for user
approval. No knowledge is duplicated into the plugin.

A vitest cross-checks every skill slug, atom id, resource URI, and tool
name the wrappers reference against the MCP server source, so a server
rename fails CI instead of a user's chat session.

Assessment and follow-ups in dev_docs/claude_plugin.md (local, dev_docs
is unpublished by design).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 12:59:57 +02:00