prompt-language-shell 0.9.8 → 1.0.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.
@@ -8,6 +8,20 @@ import { saveConfigLabels } from '../configuration/labels.js';
8
8
  import { createAnswer, createConfig, createConfirm, createExecute, createFeedback, createIntrospect, createSchedule, createValidate, } from './components.js';
9
9
  import { getCancellationMessage, getConfirmationMessage, getUnknownRequestMessage, } from './messages.js';
10
10
  import { validateExecuteTasks } from './validator.js';
11
+ /**
12
+ * Task Routing Architecture
13
+ *
14
+ * Flow: Command -> SCHEDULE -> routeTasksWithConfirm() -> extractTaskGroups()
15
+ * -> routeAllGroups() -> routeGroupTasks() -> Workflow queue
16
+ *
17
+ * Key Concepts:
18
+ * - TaskGroup: Logical grouping for sequential processing
19
+ * - Routing Category: Determines which tasks can be grouped together
20
+ * - Two-Phase Routing: Config/Introspect first, then Execute/Answer
21
+ *
22
+ * Isolation Principle: Explicit Group tasks always become their own
23
+ * TaskGroup, preventing cross-contamination between user-defined groups.
24
+ */
11
25
  /**
12
26
  * Flatten inner task structure completely - removes all nested groups.
13
27
  * Used internally to flatten subtasks within a top-level group.
@@ -92,10 +106,10 @@ export function getOperationName(tasks) {
92
106
  export function routeTasksWithConfirm(tasks, message, service, userRequest, lifecycleHandlers, workflowHandlers, requestHandlers, hasDefineTask = false) {
93
107
  if (tasks.length === 0)
94
108
  return;
95
- // Filter out ignore and discard tasks early
96
- const validTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
97
- // Check if no valid tasks remain after filtering
98
- if (validTasks.length === 0) {
109
+ // Check executable tasks (ignore/discard are shown but not executed)
110
+ const executableTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
111
+ // Check if no executable tasks remain after filtering
112
+ if (executableTasks.length === 0) {
99
113
  // Use action from first ignore task if available, otherwise generic message
100
114
  const ignoreTask = tasks.find((task) => task.type === TaskType.Ignore);
101
115
  const message = ignoreTask?.action
@@ -104,7 +118,7 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
104
118
  workflowHandlers.addToQueue(createFeedback({ type: FeedbackType.Warning, message }));
105
119
  return;
106
120
  }
107
- const operation = getOperationName(validTasks);
121
+ const operation = getOperationName(executableTasks);
108
122
  // Create routing context for downstream functions
109
123
  const context = {
110
124
  service,
@@ -115,7 +129,8 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
115
129
  if (hasDefineTask) {
116
130
  // Has DEFINE tasks - add Schedule to queue for user selection
117
131
  // Refinement flow will call this function again with refined tasks
118
- const scheduleDefinition = createSchedule({ message, tasks: validTasks });
132
+ // Show all tasks (including ignore) for display
133
+ const scheduleDefinition = createSchedule({ message, tasks });
119
134
  workflowHandlers.addToQueue(scheduleDefinition);
120
135
  }
121
136
  else {
@@ -123,9 +138,10 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
123
138
  // When Schedule activates, Command moves to timeline
124
139
  // When Schedule completes, it moves to pending
125
140
  // When Confirm activates, Schedule stays pending (visible for context)
141
+ // Show all tasks (including ignore) for display
126
142
  const scheduleDefinition = createSchedule({
127
143
  message,
128
- tasks: validTasks,
144
+ tasks,
129
145
  onSelectionConfirmed: () => {
130
146
  // Schedule completed - add Confirm to queue
131
147
  const confirmDefinition = createConfirm({
@@ -133,7 +149,8 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
133
149
  onConfirmed: () => {
134
150
  // User confirmed - complete both Confirm and Schedule, then route
135
151
  lifecycleHandlers.completeActiveAndPending();
136
- executeTasksAfterConfirm(validTasks, context);
152
+ // Only execute non-ignore/non-discard tasks
153
+ executeTasksAfterConfirm(executableTasks, context);
137
154
  },
138
155
  onCancelled: () => {
139
156
  // User cancelled - complete both Confirm and Schedule, then show cancellation
@@ -230,88 +247,187 @@ function executeTasksAfterConfirm(tasks, context) {
230
247
  routeTasksAfterConfig(flatTasks, context);
231
248
  }
232
249
  /**
233
- * Task types that should appear in the upcoming display
250
+ * Collect action names for upcoming display.
251
+ * All task types are shown so users see the full queue of work ahead.
234
252
  */
235
- const UPCOMING_TASK_TYPES = [TaskType.Execute, TaskType.Answer, TaskType.Group];
253
+ function collectUpcomingNames(tasks) {
254
+ return tasks.map((t) => t.action);
255
+ }
236
256
  /**
237
- * Collect action names for tasks that appear in upcoming display.
238
- * Groups are included with their group name (not individual subtask names).
257
+ * Get the routing category for a task type.
258
+ * Tasks in the same category can be grouped together.
259
+ * Config and Introspect are special categories that should be isolated.
239
260
  */
240
- function collectUpcomingNames(tasks) {
241
- return tasks
242
- .filter((t) => UPCOMING_TASK_TYPES.includes(t.type))
243
- .map((t) => t.action);
261
+ export function getRoutingCategory(task) {
262
+ // Groups with subtasks get their own category (based on subtask types)
263
+ if (task.type === TaskType.Group &&
264
+ task.subtasks &&
265
+ task.subtasks.length > 0) {
266
+ // Check what types of subtasks this group has
267
+ const hasConfig = task.subtasks.some((t) => t.type === TaskType.Config);
268
+ const hasIntrospect = task.subtasks.some((t) => t.type === TaskType.Introspect);
269
+ const hasExecute = task.subtasks.some((t) => t.type === TaskType.Execute);
270
+ const hasAnswer = task.subtasks.some((t) => t.type === TaskType.Answer);
271
+ // Mixed types get unique category to ensure isolation
272
+ const typeCount = (hasConfig ? 1 : 0) +
273
+ (hasIntrospect ? 1 : 0) +
274
+ (hasExecute ? 1 : 0) +
275
+ (hasAnswer ? 1 : 0);
276
+ if (typeCount > 1) {
277
+ return `mixed:${task.action}`;
278
+ }
279
+ // Single type groups use that type's category
280
+ if (hasConfig)
281
+ return 'config';
282
+ if (hasIntrospect)
283
+ return 'introspect';
284
+ if (hasExecute)
285
+ return 'execute';
286
+ if (hasAnswer)
287
+ return 'answer';
288
+ return 'group';
289
+ }
290
+ // Standalone tasks use their type as category
291
+ switch (task.type) {
292
+ case TaskType.Config:
293
+ return 'config';
294
+ case TaskType.Introspect:
295
+ return 'introspect';
296
+ case TaskType.Execute:
297
+ return 'execute';
298
+ case TaskType.Answer:
299
+ return 'answer';
300
+ default:
301
+ return task.type;
302
+ }
244
303
  }
245
304
  /**
246
- * Route tasks after config is complete (or when no config is needed)
247
- * Processes task list, routing each task type to its handler.
248
- * Top-level groups are preserved: their subtasks are routed with the group name.
249
- * Config tasks are grouped together; Execute/Answer are routed individually.
305
+ * Extract logical task groups from a flat task list.
306
+ * Each explicit Group (TaskType.Group) becomes its own TaskGroup for isolation.
307
+ * Consecutive standalone tasks of the same routing category are grouped together.
250
308
  */
251
- function routeTasksAfterConfig(tasks, context) {
252
- if (tasks.length === 0)
253
- return;
254
- // Collect all upcoming names for display (Execute, Answer, and Group tasks)
255
- const allUpcomingNames = collectUpcomingNames(tasks);
256
- let upcomingIndex = 0;
257
- // Task types that should be grouped together (one component for all tasks)
258
- const groupedTypes = [TaskType.Config, TaskType.Introspect];
259
- // Route grouped task types together (collect from all tasks including subtasks)
260
- for (const groupedType of groupedTypes) {
261
- const typeTasks = [];
262
- for (const task of tasks) {
263
- if (task.type === groupedType) {
264
- typeTasks.push(task);
309
+ export function extractTaskGroups(tasks) {
310
+ const groups = [];
311
+ let currentGroup = null;
312
+ let currentCategory = null;
313
+ for (const task of tasks) {
314
+ // Skip empty groups
315
+ if (task.type === TaskType.Group &&
316
+ (!task.subtasks || task.subtasks.length === 0)) {
317
+ continue;
318
+ }
319
+ // Explicit Groups always become their own TaskGroup for isolation
320
+ if (task.type === TaskType.Group) {
321
+ // Save current group if exists
322
+ if (currentGroup !== null) {
323
+ groups.push(currentGroup);
324
+ currentGroup = null;
325
+ currentCategory = null;
265
326
  }
266
- else if (task.type === TaskType.Group && task.subtasks) {
267
- typeTasks.push(...task.subtasks.filter((t) => t.type === groupedType));
327
+ // Create standalone TaskGroup for this Group
328
+ groups.push({
329
+ name: task.action,
330
+ tasks: [task],
331
+ });
332
+ continue;
333
+ }
334
+ const category = getRoutingCategory(task);
335
+ // Start new group when category changes (or first task)
336
+ if (currentGroup === null || category !== currentCategory) {
337
+ if (currentGroup !== null) {
338
+ groups.push(currentGroup);
268
339
  }
340
+ currentGroup = {
341
+ name: task.action,
342
+ tasks: [task],
343
+ };
344
+ currentCategory = category;
269
345
  }
270
- if (typeTasks.length > 0) {
271
- routeTasksByType(groupedType, typeTasks, context, []);
346
+ else {
347
+ // Same category - add to current group
348
+ currentGroup.tasks.push(task);
272
349
  }
273
350
  }
274
- // Process Execute, Answer, and Group tasks individually (with upcoming support)
351
+ // Don't forget the last group
352
+ if (currentGroup !== null) {
353
+ groups.push(currentGroup);
354
+ }
355
+ return groups;
356
+ }
357
+ /**
358
+ * Route all groups in order, calculating correct upcoming for each.
359
+ * Groups are processed sequentially by the Workflow's queue mechanism.
360
+ */
361
+ function routeAllGroups(groups, context) {
362
+ for (let i = 0; i < groups.length; i++) {
363
+ const group = groups[i];
364
+ // Calculate upcoming from LATER groups (not current group)
365
+ const laterGroups = groups.slice(i + 1);
366
+ const laterUpcoming = laterGroups.flatMap((g) => collectUpcomingNames(g.tasks));
367
+ // Route this group's tasks with upcoming from later groups
368
+ routeGroupTasks(group, context, laterUpcoming);
369
+ }
370
+ }
371
+ /**
372
+ * Route all tasks within a single group in the order received from LLM.
373
+ * Standalone tasks become individual components.
374
+ * Group subtasks are batched by type (Execute subtasks together, etc.).
375
+ */
376
+ function routeGroupTasks(group, context, groupUpcoming) {
377
+ const tasks = group.tasks;
378
+ const withinGroupUpcoming = collectUpcomingNames(tasks);
275
379
  for (let i = 0; i < tasks.length; i++) {
276
380
  const task = tasks[i];
277
381
  const taskType = task.type;
278
- // Skip grouped task types (already routed above)
279
- if (groupedTypes.includes(taskType))
280
- continue;
382
+ // Calculate upcoming: remaining tasks in this group + tasks from later groups
383
+ const remainingInGroup = withinGroupUpcoming.slice(i + 1);
384
+ const taskUpcoming = [...remainingInGroup, ...groupUpcoming];
385
+ // Handle Group tasks with subtasks - batch subtasks by type
281
386
  if (taskType === TaskType.Group && task.subtasks) {
282
- // Route group's subtasks - Execute tasks get group label, others routed normally
283
- const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
284
- upcomingIndex++;
285
- // Separate subtasks by type
387
+ // Batch Execute subtasks together (sent to LLM as single request)
286
388
  const executeSubtasks = task.subtasks.filter((t) => t.type === TaskType.Execute);
287
- const answerSubtasks = task.subtasks.filter((t) => t.type === TaskType.Answer);
288
- // Route Execute subtasks with group name as label
289
389
  if (executeSubtasks.length > 0) {
290
- routeExecuteTasks(executeSubtasks, context, upcoming, task.action);
390
+ routeExecuteTasks(executeSubtasks, context, taskUpcoming, task.action);
291
391
  }
292
- // Route Answer subtasks individually
392
+ // Route Answer subtasks (each becomes its own component)
393
+ const answerSubtasks = task.subtasks.filter((t) => t.type === TaskType.Answer);
293
394
  if (answerSubtasks.length > 0) {
294
- routeAnswerTasks(answerSubtasks, context, upcoming);
395
+ routeAnswerTasks(answerSubtasks, context, taskUpcoming);
396
+ }
397
+ // Route other subtask types individually
398
+ for (const subtask of task.subtasks) {
399
+ if (subtask.type !== TaskType.Execute &&
400
+ subtask.type !== TaskType.Answer) {
401
+ routeTasksByType(subtask.type, [subtask], context, taskUpcoming);
402
+ }
295
403
  }
296
404
  }
297
405
  else if (taskType === TaskType.Execute) {
298
- // Calculate upcoming for this Execute task
299
- const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
300
- upcomingIndex++;
301
- routeExecuteTasks([task], context, upcoming);
406
+ routeExecuteTasks([task], context, taskUpcoming);
302
407
  }
303
408
  else if (taskType === TaskType.Answer) {
304
- // Calculate upcoming for this Answer task
305
- const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
306
- upcomingIndex++;
307
- routeTasksByType(taskType, [task], context, upcoming);
409
+ routeAnswerTasks([task], context, taskUpcoming);
308
410
  }
309
411
  else {
310
- // For other types (Report, etc.), route without upcoming
311
- routeTasksByType(taskType, [task], context, []);
412
+ routeTasksByType(taskType, [task], context, taskUpcoming);
312
413
  }
313
414
  }
314
415
  }
416
+ /**
417
+ * Route tasks after config is complete (or when no config is needed)
418
+ * Processes task groups in order - groups with different task types are
419
+ * kept separate to ensure proper lifecycle handling.
420
+ */
421
+ function routeTasksAfterConfig(tasks, context) {
422
+ if (tasks.length === 0)
423
+ return;
424
+ // Extract logical task groups
425
+ const groups = extractTaskGroups(tasks);
426
+ if (groups.length === 0)
427
+ return;
428
+ // Route all groups in order
429
+ routeAllGroups(groups, context);
430
+ }
315
431
  /**
316
432
  * Route Answer tasks - creates separate Answer component for each question
317
433
  */
@@ -335,48 +451,60 @@ function routeIntrospectTasks(tasks, context, _upcoming) {
335
451
  context.workflowHandlers.addToQueue(createIntrospect({ tasks, service: context.service }));
336
452
  }
337
453
  /**
338
- * Route Config tasks - extracts keys, caches labels, creates Config component
454
+ * Route Config tasks - extracts keys or uses query, creates Config component
339
455
  */
340
456
  function routeConfigTasks(tasks, context, _upcoming) {
457
+ // Extract specific keys from task params
341
458
  const configKeys = tasks
342
459
  .map((task) => task.params?.key)
343
460
  .filter((key) => key !== undefined);
344
- // Extract and cache labels from task descriptions
345
- // Only cache labels for dynamically discovered keys (not in schema)
346
- const schema = getConfigSchema();
347
- const labels = {};
348
- for (const task of tasks) {
349
- const key = task.params?.key;
350
- if (key && task.action && !(key in schema)) {
351
- labels[key] = task.action;
461
+ // Handler for saving config values
462
+ const onFinished = (config) => {
463
+ try {
464
+ const configBySection = unflattenConfig(config);
465
+ for (const [section, sectionConfig] of Object.entries(configBySection)) {
466
+ saveConfig(section, sectionConfig);
467
+ }
468
+ }
469
+ catch (error) {
470
+ const errorMessage = error instanceof Error ? error.message : 'Failed to save configuration';
471
+ throw new Error(errorMessage);
472
+ }
473
+ };
474
+ const onAborted = (operation) => {
475
+ context.requestHandlers.onAborted(operation);
476
+ };
477
+ if (configKeys.length > 0) {
478
+ // Has specific keys - create steps directly
479
+ const schema = getConfigSchema();
480
+ const labels = {};
481
+ for (const task of tasks) {
482
+ const key = task.params?.key;
483
+ if (key && task.action && !(key in schema)) {
484
+ labels[key] = task.action;
485
+ }
352
486
  }
487
+ if (Object.keys(labels).length > 0) {
488
+ saveConfigLabels(labels);
489
+ }
490
+ context.workflowHandlers.addToQueue(createConfig({
491
+ steps: createConfigStepsFromSchema(configKeys),
492
+ onFinished,
493
+ onAborted,
494
+ }));
353
495
  }
354
- if (Object.keys(labels).length > 0) {
355
- saveConfigLabels(labels);
496
+ else {
497
+ // No keys - use query (Config will resolve via CONFIGURE tool)
498
+ const query = tasks[0]?.params?.query;
499
+ if (query) {
500
+ context.workflowHandlers.addToQueue(createConfig({
501
+ query,
502
+ service: context.service,
503
+ onFinished,
504
+ onAborted,
505
+ }));
506
+ }
356
507
  }
357
- context.workflowHandlers.addToQueue(createConfig({
358
- steps: createConfigStepsFromSchema(configKeys),
359
- onFinished: (config) => {
360
- // Save config - Config component will handle completion and feedback
361
- try {
362
- // Convert flat dotted keys to nested structure grouped by section
363
- const configBySection = unflattenConfig(config);
364
- // Save each section
365
- for (const [section, sectionConfig] of Object.entries(configBySection)) {
366
- saveConfig(section, sectionConfig);
367
- }
368
- }
369
- catch (error) {
370
- const errorMessage = error instanceof Error
371
- ? error.message
372
- : 'Failed to save configuration';
373
- throw new Error(errorMessage);
374
- }
375
- },
376
- onAborted: (operation) => {
377
- context.requestHandlers.onAborted(operation);
378
- },
379
- }));
380
508
  }
381
509
  /**
382
510
  * Route Execute tasks - creates Execute component (validation already done)
@@ -105,8 +105,10 @@ export class OutputStreamer {
105
105
  // Collapse when we have too many chunks to prevent memory growth
106
106
  if (this.chunks.length > 16) {
107
107
  const accumulated = this.chunks.join('');
108
- this.chunks = [limitLines(accumulated)];
109
- this.emittedLength = 0;
108
+ const limited = limitLines(accumulated);
109
+ this.chunks = [limited];
110
+ // Mark all collapsed content as emitted to prevent re-emission
111
+ this.emittedLength = limited.length;
110
112
  }
111
113
  if (!this.callback)
112
114
  return;
@@ -146,6 +148,7 @@ export class OutputStreamer {
146
148
  */
147
149
  export class RealExecutor {
148
150
  outputCallback;
151
+ memoryCallback;
149
152
  constructor(outputCallback) {
150
153
  this.outputCallback = outputCallback;
151
154
  }
@@ -155,6 +158,12 @@ export class RealExecutor {
155
158
  setOutputCallback(callback) {
156
159
  this.outputCallback = callback;
157
160
  }
161
+ /**
162
+ * Set or update the memory callback
163
+ */
164
+ setMemoryCallback(callback) {
165
+ this.memoryCallback = callback;
166
+ }
158
167
  execute(cmd, onProgress, _ = 0) {
159
168
  return new Promise((resolve) => {
160
169
  onProgress?.(ExecutionStatus.Running);
@@ -197,7 +206,7 @@ export class RealExecutor {
197
206
  if (cmd.memoryLimit) {
198
207
  memoryMonitor = new MemoryMonitor(child, cmd.memoryLimit, (info) => {
199
208
  memoryInfo = info;
200
- });
209
+ }, undefined, this.memoryCallback);
201
210
  memoryMonitor.start();
202
211
  }
203
212
  // Use OutputStreamer for buffered stdout streaming
@@ -278,6 +287,12 @@ const executor = realExecutor;
278
287
  export function setOutputCallback(callback) {
279
288
  realExecutor.setOutputCallback(callback);
280
289
  }
290
+ /**
291
+ * Set a callback to receive memory updates during execution
292
+ */
293
+ export function setMemoryCallback(callback) {
294
+ realExecutor.setMemoryCallback(callback);
295
+ }
281
296
  /**
282
297
  * Execute a single shell command
283
298
  */
@@ -25,6 +25,17 @@ export function formatDuration(ms) {
25
25
  }
26
26
  return parts.join(' ');
27
27
  }
28
+ /**
29
+ * Formats memory in megabytes to a human-readable string.
30
+ * Uses MB for values under 1024 MB, GB for larger values.
31
+ */
32
+ export function formatMemory(mb) {
33
+ if (mb >= 1024) {
34
+ const gb = mb / 1024;
35
+ return `${gb.toFixed(1)} GB`;
36
+ }
37
+ return `${mb} MB`;
38
+ }
28
39
  /**
29
40
  * Recursively extracts all leaf tasks from a hierarchical task structure.
30
41
  * Leaf tasks are tasks without subtasks.
@@ -129,6 +129,13 @@ Before creating tasks, evaluate the request type:
129
129
  capabilities", "show skills"
130
130
  - Example: "flex" → introspect type
131
131
 
132
+ **CRITICAL - Introspection is ALWAYS a single task:**
133
+ - Introspection requests MUST result in exactly ONE introspect leaf task
134
+ - NEVER create multiple introspect tasks for a single request
135
+ - NEVER nest introspect tasks within groups
136
+ - NEVER break down capabilities into separate introspect tasks
137
+ - The single introspect task will list ALL capabilities
138
+
132
139
  2. **Information requests** (questions) - Use question keywords:
133
140
  - "explain", "describe", "tell me", "what is", "how does", "find",
134
141
  "search"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.9.8",
3
+ "version": "1.0.0",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,8 +17,8 @@
17
17
  "dev": "npm run build && tsc --watch",
18
18
  "prepare": "husky",
19
19
  "prepublishOnly": "npm run check",
20
- "test": "vitest run --exclude 'tests/tools/schedule/*.test.tsx' --exclude 'tests/shell/*.test.ts'",
21
- "test:watch": "vitest --exclude 'tests/tools/schedule/*.test.tsx' --exclude 'tests/shell/*.test.ts'",
20
+ "test": "vitest run --exclude 'tests/tools/' --exclude 'tests/shell/'",
21
+ "test:watch": "vitest --exclude 'tests/tools/' --exclude 'tests/shell/'",
22
22
  "test:llm": "vitest run tests/tools/schedule/*.test.tsx",
23
23
  "test:shell": "vitest run tests/shell/*.test.ts",
24
24
  "format": "prettier --write '**/*.{ts,tsx}'",
@@ -53,18 +53,18 @@
53
53
  "ink-text-input": "^6.0.0",
54
54
  "react": "^19.2.3",
55
55
  "yaml": "^2.8.2",
56
- "zod": "^4.2.1"
56
+ "zod": "^4.3.6"
57
57
  },
58
58
  "devDependencies": {
59
- "@types/node": "^25.0.3",
60
- "@types/react": "^19.2.7",
61
- "@vitest/coverage-v8": "^4.0.16",
59
+ "@types/node": "^25.0.10",
60
+ "@types/react": "^19.2.9",
61
+ "@vitest/coverage-v8": "^4.0.18",
62
62
  "eslint": "^9.39.2",
63
63
  "husky": "^9.1.7",
64
64
  "ink-testing-library": "^4.0.0",
65
- "prettier": "^3.7.4",
65
+ "prettier": "^3.8.1",
66
66
  "typescript": "^5.9.3",
67
- "typescript-eslint": "^8.50.1",
68
- "vitest": "^4.0.16"
67
+ "typescript-eslint": "^8.53.1",
68
+ "vitest": "^4.0.18"
69
69
  }
70
70
  }