@basaltkit/ai 1.6.0 → 1.6.1

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.
package/dist/commands.js CHANGED
@@ -180,12 +180,15 @@ export function aiCommands(options = {}) {
180
180
  io.error(error.message);
181
181
  return 1;
182
182
  }
183
- renderPlan(plan, io);
183
+ renderPlan(plan, io, { footer: false });
184
184
  io.log('');
185
- // A plan that only reuses packages has nothing to scaffold — ai:make does
186
- // not install dependencies, so the package steps above are the whole job.
187
- if (plan.entities.length === 0 && packageSteps(plan).length > 0) {
188
- io.log('Nothing to scaffold — this plan reuses ecosystem packages. Run the package steps above.');
185
+ // ai:make only scaffolds entities. A plan without any (a package to
186
+ // install or wire, config, docs) is complete as shown — say so before
187
+ // the confirmation prompt instead of failing after it.
188
+ if (plan.entities.length === 0) {
189
+ io.log(packageSteps(plan).length > 0
190
+ ? 'Nothing to scaffold — this plan reuses ecosystem packages. Run the package steps above.'
191
+ : 'Nothing to scaffold — the steps above are applied by hand.');
189
192
  return 0;
190
193
  }
191
194
  // Agent review over the generated code — read-only, uses the same provider.
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export { createPlan, parsePlan, type CreatePlanOptions } from './plan/plan.js';
16
16
  export { buildPlanContext } from './plan/context.js';
17
17
  export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
18
18
  export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog, type EcosystemPackage } from './plan/packages.js';
19
- export { renderPlan } from './plan/render.js';
19
+ export { renderPlan, type RenderPlanOptions } from './plan/render.js';
20
20
  export type { ArchitecturePlan, PlanEntity, PlanField, PlanRelation, PlanStep, PlanStepKind, } from './plan/types.js';
21
21
  export { runMake } from './make/make.js';
22
22
  export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
@@ -11,7 +11,7 @@ export const BASALT_KNOWLEDGE = `You are the Architect agent for Basalt — a ba
11
11
 
12
12
  ABSOLUTE RULES — always use the framework's official APIs and generators, never invent new ones:
13
13
 
14
- 0. ECOSYSTEM FIRST. Before planning any resource, check whether an official package already covers the capability (catalog below). If one does, use it: a "package" step whose "command" installs it (\`pnpm add <package>\`, plus its Prisma store when the app uses Prisma) and whose "detail" says how to wire its plugin/routes in src/app.ts — or, when the project already has it installed, a step that only wires/uses it. NEVER generate a resource, model or module that duplicates a package (no Comment model when @basaltkit/comments exists, no Notification table, no File/Attachment model, no Team/Invitation model, no AuditLog). Do NOT list a package-covered concept in "entities" — entities are only the domain models no package provides. Only generate from scratch what the catalog does not cover, and say so in "warnings" when a package covers it only partly.
14
+ 0. ECOSYSTEM FIRST. Before planning any resource, check whether an official package already covers the capability (catalog below). If one does, use it: a "package" step whose "command" installs it (\`pnpm add <package>\`, plus its Prisma store when the app uses Prisma) and whose "detail" says how to wire its plugin/routes in src/app.ts — or, when the project already has it installed, a "package" step with no "command" that only wires/uses it. NEVER generate a resource, model or module that duplicates a package (no Comment model when @basaltkit/comments exists, no Notification table, no File/Attachment model, no Team/Invitation model, no AuditLog). Do NOT list a package-covered concept in "entities" — entities are only the domain models no package provides. Only generate from scratch what the catalog does not cover, and say so in "warnings" when a package covers it only partly.
15
15
 
16
16
  ECOSYSTEM CATALOG:
17
17
  ${renderEcosystemCatalog()}
@@ -17,7 +17,17 @@ export interface EcosystemPackage {
17
17
  routes?: string;
18
18
  /** The Prisma-backed store package, when the default store is in-memory. */
19
19
  prisma?: string;
20
+ /**
21
+ * The wiring as the package README shows it, one line — so a plan names the
22
+ * real options instead of guessing them (it once wrote
23
+ * `app.register(mailerPlugin, { provider: 'smtp' })`).
24
+ */
25
+ example?: string;
26
+ /** The same wiring with the Prisma store, for apps on Prisma. */
27
+ prismaExample?: string;
28
+ /** The DI token a service resolves with `ctx().container.get(TOKEN)`. */
29
+ token?: string;
20
30
  }
21
31
  export declare const ECOSYSTEM_PACKAGES: readonly EcosystemPackage[];
22
- /** One catalog line per package, for the planner's system prompt. */
32
+ /** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
23
33
  export declare function renderEcosystemCatalog(packages?: readonly EcosystemPackage[]): string;
@@ -1,39 +1,166 @@
1
1
  export const ECOSYSTEM_PACKAGES = [
2
- { name: '@basaltkit/auth', covers: 'login, signup, JWT/sessions, password reset, MFA, OAuth, passkeys, API keys', plugin: 'authPlugin', prisma: '@basaltkit/auth-prisma' },
3
- { name: '@basaltkit/permissions', covers: 'roles, permissions, policies, route guards (RBAC)', plugin: 'permissionsPlugin', prisma: '@basaltkit/permissions-prisma' },
4
- { name: '@basaltkit/tenancy', covers: 'multi-tenancy: tenant resolution and tenant context', plugin: 'tenancyPlugin', prisma: '@basaltkit/tenancy-prisma' },
5
- { name: '@basaltkit/teams', covers: 'team members, roles inside a tenant, email invitations', plugin: 'teamsPlugin', routes: 'teamRoutes', prisma: '@basaltkit/teams-prisma' },
6
- { name: '@basaltkit/comments', covers: 'comment threads on any resource, @mentions, resolve/reopen', plugin: 'commentsPlugin', routes: 'commentRoutes', prisma: '@basaltkit/comments-prisma' },
7
- { name: '@basaltkit/activity', covers: 'user-facing activity feed / history of changes', plugin: 'activityPlugin', prisma: '@basaltkit/activity-prisma' },
8
- { name: '@basaltkit/audit', covers: 'append-only audit trail of state changes', plugin: 'auditPlugin', prisma: '@basaltkit/audit-prisma' },
9
- { name: '@basaltkit/notifications', covers: 'in-app and email notifications, per-user preferences', plugin: 'notificationsPlugin', routes: 'inAppRoutes', prisma: '@basaltkit/notifications-prisma' },
10
- { name: '@basaltkit/files', covers: 'file uploads and attachments, quotas, signed URLs', plugin: 'filesPlugin', routes: 'fileRoutes', prisma: '@basaltkit/files-prisma' },
11
- { name: '@basaltkit/files-versions', covers: 'document versions / revisions on top of files', plugin: 'fileVersionsPlugin' },
12
- { name: '@basaltkit/storage', covers: 'disk/object storage (local, S3, Azure, GCS)', plugin: 'storagePlugin' },
13
- { name: '@basaltkit/drives', covers: 'import from Google Drive, OneDrive/SharePoint, Dropbox', plugin: 'drivesPlugin' },
14
- { name: '@basaltkit/subscriptions', covers: 'billing: plans, subscriptions, trials, usage limits, payment gateways', plugin: 'subscriptionsPlugin', prisma: '@basaltkit/subscriptions-prisma' },
15
- { name: '@basaltkit/webhooks', covers: 'outbound webhooks with signing and retries', plugin: 'webhooksPlugin', prisma: '@basaltkit/webhooks-prisma' },
16
- { name: '@basaltkit/events', covers: 'typed domain events, listeners, transactional outbox', plugin: 'eventsPlugin', prisma: '@basaltkit/events-prisma' },
17
- { name: '@basaltkit/queue', covers: 'background jobs (BullMQ, RabbitMQ, SQS, Kafka)', plugin: 'queuePlugin' },
18
- { name: '@basaltkit/scheduler', covers: 'scheduled / recurring tasks (cron-like)', plugin: 'schedulerPlugin' },
19
- { name: '@basaltkit/mailer', covers: 'sending email (SMTP, Resend, SES, Mailgun)', plugin: 'mailerPlugin' },
20
- { name: '@basaltkit/inbound-mail', covers: 'receiving email', routes: 'inboundMailRoutes' },
21
- { name: '@basaltkit/realtime', covers: 'live updates over WebSocket/SSE, presence', plugin: 'realtimePlugin' },
22
- { name: '@basaltkit/search', covers: 'full-text search (Postgres, Elasticsearch, Meilisearch)', plugin: 'searchPlugin' },
23
- { name: '@basaltkit/cache', covers: 'caching with tags and tenant scoping', plugin: 'cachePlugin' },
24
- { name: '@basaltkit/flags', covers: 'feature flags and percentage rollouts', plugin: 'flagsPlugin' },
25
- { name: '@basaltkit/exports', covers: 'CSV/JSON/XLSX exports and CSV import', plugin: 'exportsPlugin' },
26
- { name: '@basaltkit/i18n', covers: 'translations, locale, number/date formatting' },
27
- { name: '@basaltkit/backup', covers: 'PostgreSQL backups', plugin: 'backupPlugin' },
28
- { name: '@basaltkit/mcp', covers: 'expose routes as MCP tools for AI agents', plugin: 'mcpPlugin' },
2
+ {
3
+ name: '@basaltkit/auth', covers: 'login, signup, JWT/sessions, password reset, MFA, OAuth, passkeys, API keys',
4
+ plugin: 'authPlugin', routes: 'authRoutes', prisma: '@basaltkit/auth-prisma', token: 'AUTH',
5
+ example: 'authPlugin({ users: new MemoryUserSource(), secret: env.AUTH_SECRET }) + fastifyPlugin({ routes: [...authRoutes()] })',
6
+ prismaExample: 'const s = prismaAuthStores(prisma); authPlugin({ secret: env.AUTH_SECRET, users: s.users, sessions: s.sessions, refreshTokens: s.refreshTokens, tokens: s.tokens, mfa: s.mfa })',
7
+ },
8
+ {
9
+ name: '@basaltkit/permissions', covers: 'roles, permissions, policies, route guards (RBAC)',
10
+ plugin: 'permissionsPlugin', prisma: '@basaltkit/permissions-prisma', token: 'GATE',
11
+ example: 'permissionsPlugin({ store })',
12
+ prismaExample: 'const p = prismaAccessStore(prisma); permissionsPlugin({ store: p.store })',
13
+ },
14
+ {
15
+ name: '@basaltkit/tenancy', covers: 'multi-tenancy: tenant resolution and tenant context',
16
+ plugin: 'tenancyPlugin', prisma: '@basaltkit/tenancy-prisma', token: 'TENANCY',
17
+ example: 'tenancyPlugin({ source, resolvers: [headerResolver()] })',
18
+ prismaExample: 'tenancyPlugin({ source: prismaTenantSource(prisma), resolvers: [subdomainResolver({ base: env.APP_DOMAIN })] })',
19
+ },
20
+ {
21
+ name: '@basaltkit/teams', covers: 'team members, roles inside a tenant, email invitations',
22
+ plugin: 'teamsPlugin', routes: 'teamRoutes', prisma: '@basaltkit/teams-prisma', token: 'TEAMS',
23
+ example: 'teamsPlugin() + fastifyPlugin({ routes: [...teamRoutes()] })',
24
+ prismaExample: 'const t = prismaTeamsStores(prisma); teamsPlugin({ memberships: t.memberships, invitations: t.invitations })',
25
+ },
26
+ {
27
+ name: '@basaltkit/comments', covers: 'comment threads on any resource, @mentions, resolve/reopen',
28
+ plugin: 'commentsPlugin', routes: 'commentRoutes', prisma: '@basaltkit/comments-prisma', token: 'COMMENTS',
29
+ example: 'commentsPlugin() + fastifyPlugin({ routes: [...commentRoutes()] })',
30
+ prismaExample: 'commentsPlugin({ store: prismaCommentsStore(prisma).store })',
31
+ },
32
+ {
33
+ name: '@basaltkit/activity', covers: 'user-facing activity feed / history of changes',
34
+ plugin: 'activityPlugin', prisma: '@basaltkit/activity-prisma', token: 'ACTIVITY',
35
+ example: "activityPlugin(); then container.get(ACTIVITY).in('project').performedOn('project', id).log('published')",
36
+ prismaExample: 'activityPlugin({ store: prismaActivityStore(prisma).store })',
37
+ },
38
+ {
39
+ name: '@basaltkit/audit', covers: 'append-only audit trail of state changes',
40
+ plugin: 'auditPlugin', prisma: '@basaltkit/audit-prisma', token: 'AUDIT',
41
+ example: 'eventsPlugin() + auditPlugin()',
42
+ prismaExample: 'auditPlugin({ store: prismaAuditStore(prisma).store })',
43
+ },
44
+ {
45
+ name: '@basaltkit/notifications', covers: 'in-app and email notifications, per-user preferences',
46
+ plugin: 'notificationsPlugin', routes: 'inAppRoutes', prisma: '@basaltkit/notifications-prisma', token: 'NOTIFIER',
47
+ example: 'mailerPlugin({ driver: \'log\', from: env.MAIL_FROM }) + notificationsPlugin()',
48
+ prismaExample: 'notificationsPlugin({ inApp: prismaInAppStore(prisma).store, preferences: prismaPreferenceStore(prisma) })',
49
+ },
50
+ {
51
+ name: '@basaltkit/files', covers: 'file uploads and attachments, quotas, signed URLs',
52
+ plugin: 'filesPlugin', routes: 'fileRoutes', prisma: '@basaltkit/files-prisma', token: 'FILES',
53
+ example: "storagePlugin({ disks: { uploads: { driver: 'local', root: './storage' } } }) + filesPlugin({ disk: 'uploads', validate: { maxSize: 5_000_000 } }) + fastifyPlugin({ routes: [...fileRoutes()] })",
54
+ prismaExample: "filesPlugin({ disk: 'uploads', store: prismaFilesStore(prisma).store })",
55
+ },
56
+ {
57
+ name: '@basaltkit/files-versions', covers: 'document versions / revisions on top of files',
58
+ plugin: 'fileVersionsPlugin', token: 'FILE_VERSIONS',
59
+ example: 'fileVersionsPlugin({ store: prismaFileVersionsStore(prisma).store }) // store from @basaltkit/files-prisma/versions',
60
+ },
61
+ {
62
+ name: '@basaltkit/storage', covers: 'disk/object storage (local, S3, Azure, GCS)',
63
+ plugin: 'storagePlugin', token: 'STORAGE',
64
+ example: "storagePlugin({ default: 'uploads', disks: { uploads: { driver: 'local', root: './storage' } } })",
65
+ },
66
+ {
67
+ name: '@basaltkit/drives', covers: 'import from Google Drive, OneDrive/SharePoint, Dropbox',
68
+ plugin: 'drivesPlugin', token: 'DRIVES',
69
+ example: "drivesPlugin({ providers: [dropboxDrive({ clientId, clientSecret })], keys: [{ id: '2026-09', key: env.DRIVES_ENCRYPTION_KEY }], secret: env.APP_SECRET })",
70
+ },
71
+ {
72
+ name: '@basaltkit/subscriptions', covers: 'billing: plans, subscriptions, trials, usage limits, payment gateways',
73
+ plugin: 'subscriptionsPlugin', prisma: '@basaltkit/subscriptions-prisma', token: 'SUBSCRIPTIONS',
74
+ example: "subscriptionsPlugin({ plans, fallbackPlan: 'free', gateway }) + routes billingRoutes({ successUrl, cancelUrl }), invoiceRoutes(); guard with meta: { subscribed: 'pro' } / { feature: 'api' }",
75
+ prismaExample: 'const s = prismaSubscriptionsStores(prisma); subscriptionsPlugin({ plans, store: s.store, usage: s.usage, webhooks: s.webhooks })',
76
+ },
77
+ {
78
+ name: '@basaltkit/webhooks', covers: 'outbound webhooks with signing and retries',
79
+ plugin: 'webhooksPlugin', prisma: '@basaltkit/webhooks-prisma', token: 'WEBHOOKS',
80
+ example: "eventsPlugin() + webhooksPlugin({ secret: env.WEBHOOK_SECRET, events: ['invoice.*'] })",
81
+ prismaExample: 'webhooksPlugin({ store: prismaWebhookStore(prisma).store, secret: env.WEBHOOK_SECRET })',
82
+ },
83
+ {
84
+ name: '@basaltkit/events', covers: 'typed domain events, listeners, transactional outbox',
85
+ plugin: 'eventsPlugin', prisma: '@basaltkit/events-prisma', token: 'EVENTS',
86
+ example: "eventsPlugin() + outboxPlugin({ store, captureEvents: ['invoice.*'], dispatch: async (entry) => { … } })",
87
+ prismaExample: 'outboxPlugin({ store: prismaOutboxStore(prisma, { claim: true }).store, dispatch })',
88
+ },
89
+ {
90
+ name: '@basaltkit/queue', covers: 'background jobs (BullMQ, RabbitMQ, SQS, Kafka)',
91
+ plugin: 'queuePlugin', token: 'QUEUE',
92
+ example: 'queuePlugin({ jobs: [SendWelcomeEmail] }) // no connection = sync driver; bullmqQueuePlugin({ jobs, connection: env.REDIS_URL }) from @basaltkit/queue-bullmq',
93
+ },
94
+ {
95
+ name: '@basaltkit/scheduler', covers: 'scheduled / recurring tasks (cron-like)',
96
+ plugin: 'schedulerPlugin', token: 'SCHEDULER',
97
+ example: "schedulerPlugin({ define: (schedule) => { schedule.call('backup', async () => { … }).daily().at('03:00') } })",
98
+ },
99
+ {
100
+ name: '@basaltkit/mailer', covers: 'sending email (SMTP, Resend, SES, Mailgun)',
101
+ plugin: 'mailerPlugin', token: 'MAILER',
102
+ example: "mailerPlugin({ driver: 'log', from: env.MAIL_FROM }) // or driver: 'resend', resend: { apiKey }; SMTP: driver: smtpMailer({ url }) from @basaltkit/mailer-smtp; send with container.get(MAILER)",
103
+ },
104
+ {
105
+ name: '@basaltkit/inbound-mail', covers: 'receiving email',
106
+ routes: 'inboundMailRoutes',
107
+ example: "fastifyPlugin({ routes: [...inboundMailRoutes({ driver: signedDriver({ secret: env.INBOUND_SECRET }), routes: [{ address: '{tenant}@in.example.com', handler }] })] })",
108
+ },
109
+ {
110
+ name: '@basaltkit/realtime', covers: 'live updates over WebSocket/SSE, presence',
111
+ plugin: 'realtimePlugin', token: 'REALTIME',
112
+ example: "realtimePlugin({ bridge: [bridgeRule({ hook: 'note:created', channel: 'notes', event: 'created' })] })",
113
+ },
114
+ {
115
+ name: '@basaltkit/search', covers: 'full-text search (Postgres, Elasticsearch, Meilisearch)',
116
+ plugin: 'searchPlugin', token: 'SEARCH',
117
+ example: "searchPlugin({ indexes: [defineIndex({ name: 'notes', fields: ['title', 'body'] })] })",
118
+ },
119
+ {
120
+ name: '@basaltkit/cache', covers: 'caching with tags and tenant scoping',
121
+ plugin: 'cachePlugin', token: 'CACHE',
122
+ example: "cachePlugin({ driver: 'memory' }) // shared: driver: redisCache(env.REDIS_URL) from @basaltkit/cache-redis",
123
+ },
124
+ {
125
+ name: '@basaltkit/flags', covers: 'feature flags and percentage rollouts',
126
+ plugin: 'flagsPlugin', token: 'FLAGS',
127
+ example: 'flagsPlugin(defineFlags({ newDashboard: { default: false, tenants: { acme: true } }, betaSearch: { default: false, rollout: 25 } }))',
128
+ },
129
+ {
130
+ name: '@basaltkit/exports', covers: 'CSV/JSON/XLSX exports and CSV import',
131
+ plugin: 'exportsPlugin', token: 'EXPORTS',
132
+ example: "exportsPlugin({ formatters: [createCsvFormatter({ delimiter: ';' })] })",
133
+ },
134
+ {
135
+ name: '@basaltkit/i18n', covers: 'translations, locale, number/date formatting',
136
+ plugin: 'i18nPlugin', token: 'I18N',
137
+ example: "i18nPlugin({ locales: { en, pt }, defaultLocale: 'en' })",
138
+ },
139
+ {
140
+ name: '@basaltkit/backup', covers: 'PostgreSQL backups',
141
+ plugin: 'backupPlugin', token: 'BACKUP',
142
+ example: "storagePlugin({ disks: { backups: { driver: 'local', root: './backups', scope: null } } }) + backupPlugin({ connectionUrl: env.DATABASE_URL, disk: 'backups', retention: 7 })",
143
+ },
144
+ {
145
+ name: '@basaltkit/mcp', covers: 'expose routes as MCP tools for AI agents',
146
+ plugin: 'mcpPlugin', token: 'MCP',
147
+ example: "mcpPlugin({ routes, serverInfo: { name: 'app', version: '1.0.0' } }) + fastifyPlugin({ routes: [...routes, ...mcpRoutes()] }); opt a route in with meta: { mcp: { name } }",
148
+ },
29
149
  ];
30
- /** One catalog line per package, for the planner's system prompt. */
150
+ /** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
31
151
  export function renderEcosystemCatalog(packages = ECOSYSTEM_PACKAGES) {
32
152
  return packages
33
153
  .map((p) => {
34
- const wiring = [p.plugin, p.routes ? `${p.routes}()` : undefined].filter(Boolean).join(' + ');
35
- const store = p.prisma ? `; Prisma store: ${p.prisma}` : '';
36
- return `- ${p.name} — ${p.covers}${wiring ? ` (wire: ${wiring}${store})` : store ? ` (${store.slice(2)})` : ''}`;
154
+ const lines = [`- ${p.name} — ${p.covers}`];
155
+ if (p.example)
156
+ lines.push(` wire: ${p.example}`);
157
+ if (p.prismaExample)
158
+ lines.push(` with Prisma (${p.prisma}): ${p.prismaExample}`);
159
+ else if (p.prisma)
160
+ lines.push(` Prisma store: ${p.prisma}`);
161
+ if (p.token)
162
+ lines.push(` use: ctx().container.get(${p.token})`);
163
+ return lines.join('\n');
37
164
  })
38
165
  .join('\n');
39
166
  }
@@ -1,4 +1,8 @@
1
1
  import type { LineWriter } from '../render.js';
2
2
  import type { ArchitecturePlan } from './types.js';
3
+ export interface RenderPlanOptions {
4
+ /** Print the "what next" footer (default `true`); ai:make, which acts next itself, turns it off. */
5
+ footer?: boolean;
6
+ }
3
7
  /** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
4
- export declare function renderPlan(plan: ArchitecturePlan, io: LineWriter): void;
8
+ export declare function renderPlan(plan: ArchitecturePlan, io: LineWriter, options?: RenderPlanOptions): void;
@@ -1,5 +1,5 @@
1
1
  /** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
2
- export function renderPlan(plan, io) {
2
+ export function renderPlan(plan, io, options = {}) {
3
3
  io.log('ARCHITECTURE PLAN');
4
4
  io.log('');
5
5
  io.log(`Request: "${plan.request}"`);
@@ -49,7 +49,13 @@ export function renderPlan(plan, io) {
49
49
  for (const warning of plan.warnings)
50
50
  io.log(` - ${warning}`);
51
51
  }
52
+ if (options.footer === false)
53
+ return;
52
54
  io.log('');
53
55
  io.log('This is a plan only — nothing was changed.');
54
- io.log(`Next: basalt ai:make "${plan.request}" implements it.`);
56
+ // ai:make only scaffolds entities: suggesting it for a plan without any
57
+ // would end in "nothing to generate" after a confirmation prompt.
58
+ io.log(plan.entities.length > 0
59
+ ? `Next: basalt ai:make "${plan.request}" implements it.`
60
+ : 'Nothing for ai:make to generate — the steps above are applied by hand.');
55
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/ai",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },