Files
accounted/.claude/skills/create-extension/SKILL.md
T
Jakob Wennberg ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

4.6 KiB

name, description
name description
create-extension Generate and implement extensions for Accounted: scaffold files, configure manifests, write event handlers, API routes, services, workspace UIs, settings panels, and testing. Use when creating new extensions, adding surfaces to existing extensions, or understanding the extension architecture. Covers the full lifecycle from scaffolding to registration.

Extension Generator

Quick Start

1. Scaffold:

npx tsx scripts/create-extension.ts \
  --name my-extension --sector general --category operations \
  --description "Short description"

Sectors: general, restaurant, construction, hotel, tech, ecommerce, export Categories: import, operations, reports, accounting

2. Enable: add "my-extension" to extensions.config.json

3. Regenerate: npm run setup:extensions (auto-runs on dev/build)

4. Implement: edit index.ts to add surfaces


Extension Object Template

import type { Extension, ExtensionContext } from '@/lib/extensions/types'
import type { EventPayload } from '@/lib/events/types'
import { myExtApiRoutes } from './api-routes'

interface MyExtSettings { featureEnabled: boolean; threshold: number }
const DEFAULT_SETTINGS: MyExtSettings = { featureEnabled: true, threshold: 0.8 }

async function handleSomeEvent(
  payload: EventPayload<'transaction.synced'>, ctx?: ExtensionContext
): Promise<void> {
  const { transactions, userId } = payload
  const log = ctx?.log ?? console
  const settings = ctx
    ? { ...DEFAULT_SETTINGS, ...(await ctx.settings.get<Partial<MyExtSettings>>() || {}) }
    : DEFAULT_SETTINGS
  if (!settings.featureEnabled) return

  try {
    const supabase = ctx?.supabase ?? await (await import('@/lib/supabase/server')).createClient()
    // ... business logic ...
  } catch (error) { log.error('Handler failed:', error) }
}

export const myExtensionExtension: Extension = {
  id: 'my-extension',
  name: 'My Extension',
  version: '1.0.0',
  sector: 'general',
  apiRoutes: myExtApiRoutes,
  eventHandlers: [
    { eventType: 'transaction.synced', handler: handleSomeEvent },
  ],
  settingsPanel: { label: 'My Extension', path: '/settings/extensions/my-extension' },
  async onInstall(ctx) { await ctx.settings.set('settings', DEFAULT_SETTINGS) },
}

Export name convention: {camelCaseId}Extension (e.g., myExtensionExtension).


Manifest (Minimal)

{
  "id": "my-extension",
  "sector": "general",
  "exportName": "myExtensionExtension",
  "entryPoint": "@/extensions/general/my-extension",
  "workspace": null,
  "requiredEnvVars": [],
  "optionalEnvVars": [],
  "npmDependencies": [],
  "definition": {
    "name": "My Extension",
    "category": "operations",
    "icon": "Box",
    "dataPattern": "core",
    "description": "Short marketplace description",
    "longDescription": "Longer detail page description."
  }
}

For a workspace page, set "workspace": "@/components/extensions/general/MyExtensionWorkspace". See Manifest Reference.

Extension Context

interface ExtensionContext {
  userId: string; extensionId: string
  supabase: SupabaseClient         // Pre-authenticated
  emit(event: CoreEvent): void     // Publish core events
  settings: ExtensionSettings      // get<T>(key?) / set<T>(key, value): stored in extension_data table
  storage: ExtensionStorage        // download / upload / getPublicUrl
  log: ExtensionLogger             // info / warn / error (scoped)
  services: ExtensionServices      // Core services (e.g., ingestTransactions)
}

Available Surfaces

Surface Reference
eventHandlers: react to core events Event Handlers
apiRoutes: HTTP endpoints API Routes
services: named functions for core Services
settingsPanel / workspace UI Workspace & UI
mappingRuleTypes, onInstall/onUninstall Extension Interface

Common Mistakes

  1. Forgetting npm run setup:extensions after config change: extension won't load
  2. Editing _generated/ files: overwritten on next setup
  3. Not handling ctx = undefined: context is undefined in cron jobs; always fallback
  4. Wrong export name: must match exportName in manifest exactly
  5. Importing from other extensions: only import from core (@/lib/, @/types)
  6. Lazy imports in module scope: static imports for extension code; await import() only inside handler functions