@open-agent-toolkit/cli 0.1.27 → 0.1.29

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.
@@ -104,14 +104,15 @@ Key behavior:
104
104
  - Same pack selection and install flow as `oat init tools`
105
105
  - Pack-oriented install subcommands: `core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`, `brainstorm`
106
106
  - Interactive installs show each pack's current install location in the picker so already-installed packs are visible before you submit
107
- - User-scope follow-up choices are prepopulated from the current install state for user-eligible packs (`ideas`, `docs`, `utility`, `research`, `brainstorm`)
108
- - The `brainstorm` pack defaults to user scope on fresh installs (driven by `PACK_METADATA[brainstorm].defaultScope = 'user'`); existing project-scope installs still take precedence on re-install so users do not get unexpected scope migrations
109
- - If a user-eligible pack is already installed in both project and user scope, the installer asks whether to keep both installs or normalize the pack to user scope before it makes any cleanup changes
110
- - Changing a user-eligible pack from project scope to user scope, or back again, is treated as a migration: the old canonical copy is removed so the pack ends in the selected scope instead of accumulating duplicate installs
107
+ - Installing is **additive**: choosing a scope for a pack never removes it from another scope. A user-eligible pack installed at user scope plus a project install ends up at `project + user`, not moved
108
+ - For each user-eligible pack (`ideas`, `docs`, `utility`, `research`, `brainstorm`), the interactive flow offers a per-pack end-state selector (`project`, `user`, or `both`) defaulting to the pack's current placement; leaving the default makes no changes for that pack
109
+ - The `brainstorm` pack defaults to user scope on fresh installs (driven by `PACK_METADATA[brainstorm].defaultScope = 'user'`); existing installs keep their current placement on re-install, so a re-install never moves a pack between scopes
110
+ - Removing a pack from a scope happens only when you explicitly choose a narrower end-state in the interactive flow (e.g. a pack at `both` set to `project` only). All staged removals are shown in a single change summary and applied only after one batch confirmation — declining makes no changes
111
+ - Non-interactive installs (including `--scope project`, `--scope user`, and the default pack set) are strictly additive and never remove a pack from a scope. Removal is interactive-only
111
112
  - Tracks installed vs bundled skill versions and reports outdated skills
112
113
  - Records installed pack state in shared repo config as `tools.<pack>: true` so other OAT workflows can detect installed capabilities without relying on filesystem heuristics
113
114
  - Interactive runs can prompt to update selected outdated skills
114
- - Successful installs report the final scope chosen for each pack, including `project + user` when both installs are preserved, and auto-sync every scope touched by the install or migration
115
+ - Successful installs report the final scope chosen for each pack, including `project + user` when a pack is installed in both, and auto-sync only the scopes actually changed by the install so untouched scopes are never re-synced or pruned
115
116
  - Install-triggered auto-sync limits removal planning to the canonical entries from the pack that was just installed, so stale manifest drift in unrelated packs does not delete other provider views
116
117
  - Use `--no-sync` to skip auto-sync
117
118
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.1.27",
3
- "docs-config": "0.1.27",
4
- "docs-theme": "0.1.27",
5
- "docs-transforms": "0.1.27"
2
+ "cli": "0.1.29",
3
+ "docs-config": "0.1.29",
4
+ "docs-theme": "0.1.29",
5
+ "docs-transforms": "0.1.29"
6
6
  }
@@ -2,6 +2,4 @@ import { createDocsConfig } from '@open-agent-toolkit/docs-config';
2
2
 
3
3
  const basePath = process.env.NEXT_PUBLIC_BASE_PATH || undefined;
4
4
 
5
- export default createDocsConfig({
6
- ...(basePath ? { basePath } : {}),
7
- });
5
+ export default createDocsConfig(basePath ? { basePath } : {});
@@ -52,6 +52,16 @@ export interface InitToolsDependencies {
52
52
  interface InitToolsRunMetadata {
53
53
  affectedScopes: ConcreteScope[];
54
54
  }
55
+ interface PackScopeChange {
56
+ pack: ToolPack;
57
+ scope: ConcreteScope;
58
+ }
59
+ /**
60
+ * Render a batch change summary (`+ pack@scope` adds / `- pack@scope` removes)
61
+ * for the interactive removal-confirmation gate. Pure for snapshot-friendly
62
+ * testing.
63
+ */
64
+ export declare function formatReconcileSummary(adds: PackScopeChange[], removes: PackScopeChange[]): string;
55
65
  export declare function consumeInitToolsRunMetadata(): InitToolsRunMetadata | null;
56
66
  interface PackScopeInfo {
57
67
  pack: ToolPack;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/init/tools/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/init/tools/index.ts"],"names":[],"mappings":"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"}
@@ -117,6 +117,77 @@ const USER_ELIGIBLE_PACKS = new Set([
117
117
  function isUserEligiblePack(pack) {
118
118
  return USER_ELIGIBLE_PACKS.has(pack);
119
119
  }
120
+ /**
121
+ * Concrete scopes implied by a desired end-state placement.
122
+ */
123
+ function scopesForEndState(end) {
124
+ switch (end) {
125
+ case 'project':
126
+ return ['project'];
127
+ case 'user':
128
+ return ['user'];
129
+ case 'both':
130
+ return ['project', 'user'];
131
+ }
132
+ }
133
+ /**
134
+ * Concrete scopes a pack is currently installed at.
135
+ */
136
+ function scopesForLocation(location) {
137
+ switch (location) {
138
+ case 'project':
139
+ return ['project'];
140
+ case 'user':
141
+ return ['user'];
142
+ case 'both':
143
+ return ['project', 'user'];
144
+ default:
145
+ return [];
146
+ }
147
+ }
148
+ /**
149
+ * Pure diff of current placement vs a desired end-state into the set of
150
+ * scopes to add (install into) and remove (delete from). This is the single
151
+ * source of truth that the reconciliation loop drives off of, so installs and
152
+ * removals only ever touch scopes that actually change.
153
+ */
154
+ function reconcilePackScope(current, desired) {
155
+ const currentScopes = new Set(scopesForLocation(current));
156
+ const desiredScopes = new Set(scopesForEndState(desired));
157
+ const adds = [...desiredScopes].filter((scope) => !currentScopes.has(scope));
158
+ const removes = [...currentScopes].filter((scope) => !desiredScopes.has(scope));
159
+ return { adds, removes };
160
+ }
161
+ /**
162
+ * Render a batch change summary (`+ pack@scope` adds / `- pack@scope` removes)
163
+ * for the interactive removal-confirmation gate. Pure for snapshot-friendly
164
+ * testing.
165
+ */
166
+ export function formatReconcileSummary(adds, removes) {
167
+ const lines = ['Review pending changes:'];
168
+ for (const { pack, scope } of adds) {
169
+ lines.push(` + ${pack}@${scope}`);
170
+ }
171
+ for (const { pack, scope } of removes) {
172
+ lines.push(` - ${pack}@${scope}`);
173
+ }
174
+ return lines.join('\n');
175
+ }
176
+ /**
177
+ * Additive union of a pack's current placement with a requested scope. Never
178
+ * returns a placement narrower than current — installing at one scope can
179
+ * never remove the pack from another scope.
180
+ */
181
+ function unionScopeWithCurrent(current, requested) {
182
+ const scopes = new Set([
183
+ ...scopesForLocation(current),
184
+ requested,
185
+ ]);
186
+ if (scopes.has('project') && scopes.has('user')) {
187
+ return 'both';
188
+ }
189
+ return scopes.has('user') ? 'user' : 'project';
190
+ }
120
191
  async function loadInstalledPackStates(projectRoot, userRoot, assetsRoot, dependencies) {
121
192
  const [projectTools, userTools] = await Promise.all([
122
193
  dependencies.scanTools({
@@ -191,43 +262,6 @@ function buildPackChoices(installedPackStates) {
191
262
  },
192
263
  ];
193
264
  }
194
- function buildUserScopeChoices(packs, installedPackStates) {
195
- return packs.map((pack) => {
196
- const location = installedPackStates[pack].location;
197
- // Existing-install detection wins over PACK_METADATA defaultScope so
198
- // re-installs of an already-placed pack do not silently migrate the
199
- // user across scopes. When the pack is not yet installed at any
200
- // scope, fall back to PACK_METADATA[pack]?.defaultScope (defaults
201
- // to 'project' when absent).
202
- const checked = location === 'user' || location === 'both'
203
- ? true
204
- : location === 'not-installed'
205
- ? resolvePackDefaultScope(pack) === 'user'
206
- : false;
207
- return {
208
- label: location === 'not-installed'
209
- ? pack
210
- : `${pack} (current: ${formatInstalledLocation(location)})`,
211
- value: pack,
212
- checked,
213
- };
214
- });
215
- }
216
- async function resolveBothScopeTarget(pack, dependencies, interactive) {
217
- const selection = await dependencies.selectWithAbort(`${pack} is currently installed in project and user scope. Keep both installs or normalize to user scope?`, [
218
- {
219
- label: 'Keep project + user (recommended)',
220
- value: 'both',
221
- description: 'Preserve both installed copies',
222
- },
223
- {
224
- label: 'User only',
225
- value: 'user',
226
- description: 'Remove the project-scoped copy',
227
- },
228
- ], { interactive });
229
- return selection ?? 'both';
230
- }
231
265
  export function consumeInitToolsRunMetadata() {
232
266
  const metadata = lastRunInitToolsMetadata;
233
267
  lastRunInitToolsMetadata = null;
@@ -258,16 +292,13 @@ async function resolvePackScopes(context, selections, installedPackStates, depen
258
292
  if (eligiblePacks.length === 0) {
259
293
  return scopes;
260
294
  }
261
- // Explicit --scope overrides per-pack selection
262
- if (context.scope === 'project') {
295
+ // Explicit --scope is additive: union the requested scope with the pack's
296
+ // current placement so installing at one scope never removes another. A pack
297
+ // currently at `user` installed with `--scope project` becomes `both`.
298
+ if (context.scope === 'project' || context.scope === 'user') {
299
+ const requested = context.scope;
263
300
  for (const pack of eligiblePacks) {
264
- scopes[pack] = 'project';
265
- }
266
- return scopes;
267
- }
268
- if (context.scope === 'user') {
269
- for (const pack of eligiblePacks) {
270
- scopes[pack] = 'user';
301
+ scopes[pack] = unionScopeWithCurrent(installedPackStates[pack].location, requested);
271
302
  }
272
303
  return scopes;
273
304
  }
@@ -298,21 +329,70 @@ async function resolvePackScopes(context, selections, installedPackStates, depen
298
329
  }
299
330
  return scopes;
300
331
  }
301
- // Interactive: let user pick which packs go to user scope
302
- const userScopePacks = (await dependencies.selectManyWithAbort('Which packs should install at user scope? (unselected go to project scope)', buildUserScopeChoices(eligiblePacks, installedPackStates), { interactive: context.interactive })) ?? [];
303
- const userScopeSet = new Set(userScopePacks);
332
+ // Interactive: for each user-eligible pack, choose its desired end-state
333
+ // placement (project / user / both). The default offered is the pack's
334
+ // current placement (or its default scope when not yet installed), so
335
+ // breezing through accepting defaults is a no-op and never removes a scope.
304
336
  for (const pack of eligiblePacks) {
305
- if (!userScopeSet.has(pack)) {
306
- scopes[pack] = 'project';
307
- continue;
308
- }
309
- scopes[pack] =
310
- installedPackStates[pack].location === 'both'
311
- ? await resolveBothScopeTarget(pack, dependencies, context.interactive)
312
- : 'user';
337
+ const currentLocation = installedPackStates[pack].location;
338
+ const defaultEndState = resolvePackDefaultEndState(pack, currentLocation);
339
+ const selection = await dependencies.selectWithAbort(`Where should ${pack} install?`, buildPackEndStateChoices(pack, currentLocation, defaultEndState), { interactive: context.interactive });
340
+ scopes[pack] = selection ?? defaultEndState;
313
341
  }
314
342
  return scopes;
315
343
  }
344
+ /**
345
+ * Default end-state offered for a pack in the interactive selector: its
346
+ * current placement when installed, or its configured default scope when not
347
+ * yet present.
348
+ */
349
+ function resolvePackDefaultEndState(pack, currentLocation) {
350
+ switch (currentLocation) {
351
+ case 'project':
352
+ return 'project';
353
+ case 'user':
354
+ return 'user';
355
+ case 'both':
356
+ return 'both';
357
+ default:
358
+ return resolvePackDefaultScope(pack);
359
+ }
360
+ }
361
+ /**
362
+ * Per-pack end-state options (project / user / both) for the interactive
363
+ * selector. The default end-state is listed first so the underlying
364
+ * single-select highlights it.
365
+ */
366
+ function buildPackEndStateChoices(pack, currentLocation, defaultEndState) {
367
+ const currentLabel = currentLocation === 'not-installed'
368
+ ? 'not installed'
369
+ : `current: ${formatInstalledLocation(currentLocation)}`;
370
+ const baseChoices = [
371
+ {
372
+ label: `Project scope (${pack})`,
373
+ value: 'project',
374
+ description: 'Install at project scope only',
375
+ },
376
+ {
377
+ label: `User scope (${pack})`,
378
+ value: 'user',
379
+ description: 'Install at user scope only',
380
+ },
381
+ {
382
+ label: `Project + user (${pack})`,
383
+ value: 'both',
384
+ description: 'Install at both scopes',
385
+ },
386
+ ];
387
+ // Annotate the option matching the default so the prompt communicates the
388
+ // current placement, and order it first so it is the highlighted default.
389
+ const annotated = baseChoices.map((choice) => choice.value === defaultEndState
390
+ ? { ...choice, label: `${choice.label} [${currentLabel}]` }
391
+ : choice);
392
+ const defaultChoice = annotated.find((choice) => choice.value === defaultEndState);
393
+ const rest = annotated.filter((choice) => choice.value !== defaultEndState);
394
+ return defaultChoice ? [defaultChoice, ...rest] : annotated;
395
+ }
316
396
  function buildInstalledToolsConfig(selectedPacks, installedPackStates, existingTools) {
317
397
  const selectedPackSet = new Set(selectedPacks);
318
398
  const tools = { ...existingTools };
@@ -428,37 +508,89 @@ export async function runInitTools(context, dependencies) {
428
508
  return [];
429
509
  }
430
510
  const packScopes = await resolvePackScopes(context, selectedPacks, initialPackStates, dependencies);
431
- function packRoot(pack) {
432
- return packScopes[pack] === 'user' ? userRoot : projectRoot;
511
+ function scopeRoot(scope) {
512
+ return scope === 'user' ? userRoot : projectRoot;
513
+ }
514
+ // Reconcile current placement vs the desired end-state per user-eligible
515
+ // pack into adds/removes. Installs copy idempotently into the full desired
516
+ // end-state, but only the scopes that actually changed (an add or a
517
+ // confirmed remove) are recorded in `affectedScopes`, so auto-sync never
518
+ // prunes a preserved scope.
519
+ const reconciliationByPack = new Map();
520
+ for (const pack of selectedPacks) {
521
+ if (!isUserEligiblePack(pack)) {
522
+ continue;
523
+ }
524
+ reconciliationByPack.set(pack, reconcilePackScope(initialPackStates[pack].location, packScopes[pack]));
433
525
  }
526
+ // Scopes a pack should install into (its full desired end-state for
527
+ // user-eligible packs; the resolved scope otherwise). Re-installing an
528
+ // already-present scope is an idempotent copy.
434
529
  function packTargets(pack) {
435
530
  return packScopes[pack] === 'both'
436
531
  ? [projectRoot, userRoot]
437
- : [packRoot(pack)];
532
+ : [packScopes[pack] === 'user' ? userRoot : projectRoot];
533
+ }
534
+ // Only scopes that received a new add for this pack should be auto-synced.
535
+ function addedScopes(pack) {
536
+ return new Set(reconciliationByPack.get(pack)?.adds);
438
537
  }
439
538
  const outdatedSkills = [];
440
539
  const affectedScopes = new Set();
441
- for (const pack of selectedPacks) {
442
- if (!isUserEligiblePack(pack)) {
443
- continue;
540
+ // Collect all staged removals across packs. Removals are interactive-only
541
+ // and gated behind a single batch confirmation (added in p01-t03);
542
+ // non-interactive paths are strictly additive and can never remove.
543
+ const stagedRemovals = [];
544
+ for (const [pack, reconciliation] of reconciliationByPack) {
545
+ for (const scope of reconciliation.removes) {
546
+ stagedRemovals.push({ pack, scope });
444
547
  }
445
- const desiredScope = packScopes[pack];
446
- const currentLocation = initialPackStates[pack].location;
447
- if (desiredScope === 'both') {
448
- continue;
449
- }
450
- if (desiredScope === 'user') {
451
- if (currentLocation === 'project' || currentLocation === 'both') {
452
- await removePackFromScope(pack, projectRoot, dependencies);
453
- affectedScopes.add('project');
548
+ }
549
+ if (stagedRemovals.length > 0 && !context.interactive) {
550
+ // Strictly-additive guard: removals must never be applied
551
+ // non-interactively. This should be unreachable because non-interactive
552
+ // resolution unions with current placement, but we fail loud rather than
553
+ // silently delete a user's install.
554
+ throw new Error('Non-interactive install attempted to remove a pack from a scope; ' +
555
+ 'install is strictly additive in non-interactive mode.');
556
+ }
557
+ // Interactive removal gate: if any pack would lose a scope, surface one
558
+ // batch change summary and require a single confirmation before mutating
559
+ // anything. Declining aborts with zero changes (no installs, no removals).
560
+ if (stagedRemovals.length > 0 && context.interactive) {
561
+ const stagedAdds = [];
562
+ for (const [pack, reconciliation] of reconciliationByPack) {
563
+ for (const scope of reconciliation.adds) {
564
+ stagedAdds.push({ pack, scope });
454
565
  }
455
- continue;
456
566
  }
457
- if (currentLocation === 'user' || currentLocation === 'both') {
458
- await removePackFromScope(pack, userRoot, dependencies);
459
- affectedScopes.add('user');
567
+ context.logger.info(formatReconcileSummary(stagedAdds, stagedRemovals));
568
+ const confirmation = await dependencies.selectWithAbort('Apply these changes? Removals will delete the listed scoped installs.', [
569
+ {
570
+ label: 'No, cancel (recommended)',
571
+ value: 'no',
572
+ description: 'Make no changes',
573
+ },
574
+ {
575
+ label: 'Yes, apply adds and removals',
576
+ value: 'yes',
577
+ description: 'Install adds and delete the listed removals',
578
+ },
579
+ ], { interactive: context.interactive });
580
+ if (confirmation !== 'yes') {
581
+ lastRunInitToolsMetadata = { affectedScopes: [] };
582
+ if (!context.json) {
583
+ context.logger.info('No changes applied.');
584
+ }
585
+ process.exitCode = 0;
586
+ return [];
460
587
  }
461
588
  }
589
+ // Removals are deferred until after the add phase below: for a confirmed
590
+ // move (e.g. user -> project), the replacement install must succeed before
591
+ // the preserved scope is deleted. If any installer throws, the catch
592
+ // handler aborts and these removals never run, so the pack is never left
593
+ // installed in neither scope (review I1; design.md:86/114).
462
594
  if (selectedPacks.includes('core')) {
463
595
  // Core pack always installs at user scope, regardless of userEligibleScope
464
596
  affectedScopes.add('user');
@@ -475,8 +607,12 @@ export async function runInitTools(context, dependencies) {
475
607
  }
476
608
  }
477
609
  if (selectedPacks.includes('ideas')) {
610
+ const ideasAdded = addedScopes('ideas');
478
611
  for (const targetRoot of packTargets('ideas')) {
479
- affectedScopes.add(targetRoot === userRoot ? 'user' : 'project');
612
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
613
+ if (ideasAdded.has(targetScope)) {
614
+ affectedScopes.add(targetScope);
615
+ }
480
616
  const ideasResult = await dependencies.installIdeas({
481
617
  assetsRoot,
482
618
  targetRoot,
@@ -491,8 +627,12 @@ export async function runInitTools(context, dependencies) {
491
627
  }
492
628
  }
493
629
  if (selectedPacks.includes('docs')) {
630
+ const docsAdded = addedScopes('docs');
494
631
  for (const targetRoot of packTargets('docs')) {
495
- affectedScopes.add(targetRoot === userRoot ? 'user' : 'project');
632
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
633
+ if (docsAdded.has(targetScope)) {
634
+ affectedScopes.add(targetScope);
635
+ }
496
636
  const docsResult = await dependencies.installDocs({
497
637
  assetsRoot,
498
638
  targetRoot,
@@ -557,8 +697,12 @@ export async function runInitTools(context, dependencies) {
557
697
  }
558
698
  }
559
699
  if (selectedPacks.includes('utility')) {
700
+ const utilityAdded = addedScopes('utility');
560
701
  for (const targetRoot of packTargets('utility')) {
561
- affectedScopes.add(targetRoot === userRoot ? 'user' : 'project');
702
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
703
+ if (utilityAdded.has(targetScope)) {
704
+ affectedScopes.add(targetScope);
705
+ }
562
706
  const utilityResult = await dependencies.installUtility({
563
707
  assetsRoot,
564
708
  targetRoot,
@@ -589,8 +733,12 @@ export async function runInitTools(context, dependencies) {
589
733
  }
590
734
  }
591
735
  if (selectedPacks.includes('research')) {
736
+ const researchAdded = addedScopes('research');
592
737
  for (const targetRoot of packTargets('research')) {
593
- affectedScopes.add(targetRoot === userRoot ? 'user' : 'project');
738
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
739
+ if (researchAdded.has(targetScope)) {
740
+ affectedScopes.add(targetScope);
741
+ }
594
742
  const researchResult = await dependencies.installResearch({
595
743
  assetsRoot,
596
744
  targetRoot,
@@ -606,8 +754,12 @@ export async function runInitTools(context, dependencies) {
606
754
  }
607
755
  }
608
756
  if (selectedPacks.includes('brainstorm')) {
757
+ const brainstormAdded = addedScopes('brainstorm');
609
758
  for (const targetRoot of packTargets('brainstorm')) {
610
- affectedScopes.add(targetRoot === userRoot ? 'user' : 'project');
759
+ const targetScope = targetRoot === userRoot ? 'user' : 'project';
760
+ if (brainstormAdded.has(targetScope)) {
761
+ affectedScopes.add(targetScope);
762
+ }
611
763
  const brainstormResult = await dependencies.installBrainstorm({
612
764
  assetsRoot,
613
765
  targetRoot,
@@ -621,6 +773,12 @@ export async function runInitTools(context, dependencies) {
621
773
  }
622
774
  }
623
775
  }
776
+ // Apply confirmed removals only after every add has succeeded, so a failed
777
+ // replacement install can never leave a pack uninstalled in both scopes.
778
+ for (const { pack, scope } of stagedRemovals) {
779
+ await removePackFromScope(pack, scopeRoot(scope), dependencies);
780
+ affectedScopes.add(scope);
781
+ }
624
782
  if (outdatedSkills.length > 0) {
625
783
  reportOutdatedSkills(context, outdatedSkills);
626
784
  if (context.interactive) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.1.27"
37
+ "@open-agent-toolkit/control-plane": "0.1.29"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",