@piagent/platform 1.35.0 → 1.36.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/extensions/repository-memory.js +27 -2
  58. package/packages/piagent-core/managed/continuation.mjs +23 -2
  59. package/packages/piagent-core/managed/memory-lessons.mjs +34 -6
  60. package/packages/piagent-core/managed/memory-serves.mjs +17 -0
  61. package/packages/piagent-core/managed/memory.mjs +12 -10
  62. package/packages/piagent-core/managed/request-stream.mjs +8 -2
  63. package/packages/piagent-core/managed/skill-feedback.mjs +79 -0
  64. package/packages/piagent-core/managed/studio-link.mjs +18 -0
  65. package/packages/piagent-core/package.json +1 -1
  66. package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
  67. package/packages/piagent-webui/package.json +1 -1
@@ -6,7 +6,7 @@ import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { MemoryOutbox, memoryScope, scopeID } from './memory-outbox.mjs';
9
- import { CheckLessons } from './memory-lessons.mjs';
9
+ import { CheckLessons } from './memory-lessons.mjs'; import { SkillFeedback } from './skill-feedback.mjs'; import { RoundServes, FEEDBACK_PROTOCOL } from './memory-serves.mjs';
10
10
  import { CLIENT_AGENT } from './client-agent.mjs'; import { knownCapabilities, refreshCapabilities } from './studio-capabilities.mjs';
11
11
  import { redactedText } from '../runtime/managed-failure.mjs';
12
12
  import { git, repoKey, repositoryAt, findRepositories, topDirs, eventFor, commitChanges } from './memory-repos.mjs';
@@ -43,7 +43,7 @@ export function turnRecord(messages, changedFiles = [], lessons = []) {
43
43
  };
44
44
  const parts = [];
45
45
  // Preserve evidence and the final answer before budgeting member input.
46
- const lessonText = lessons.slice(0, 3).map(l => `${l.line} Evidence: ${l.failed_call} → ${l.passed_call}; fingerprint ${l.fingerprint}; ${l.at}.`).join('\n');
46
+ const lessonText = lessons.slice(0, 3).map(l => `${l.line} Evidence: ${l.failed_call} ${l.kind === 'tool_error' ? 'and' : '→'} ${l.passed_call}; fingerprint ${l.fingerprint}; ${l.at}.`).join('\n');
47
47
  const memberBudget = MAX_TURN - 13000 - Buffer.byteLength(lessonText);
48
48
  const member = safeClip(asked.map(a => `Member: ${a}`).join('\n\n'), memberBudget);
49
49
  if (member) parts.push(member);
@@ -68,7 +68,7 @@ function bounded(promise, signal, late = () => {}) {
68
68
 
69
69
  export class TeamMemory {
70
70
  constructor({ origin, cwd, stateDir, conversationId, scope = () => null, lease = null, renew = null, fetchImpl = fetch, now = () => Date.now(), changed = () => [] }) {
71
- this.renew = renew; this.scope = scope; this.lease = lease; this.protocol = 0; this.background = null; this.stats = {};
71
+ this.renew = renew; this.scope = scope; this.lease = lease; this.protocol = 0; this.background = null; this.stats = {}; this.serves = new RoundServes();
72
72
  Object.assign(this, { origin, cwd, stateDir, conversationId, fetchImpl, now, changed, repo: undefined, repos: undefined, folder: null, heads: {}, offUntil: 0, retain: true, briefed: false });
73
73
  this.syncScope();
74
74
  this.knownPath = stateDir ? path.join(stateDir, 'memory-known.json') : null;
@@ -215,7 +215,7 @@ export class TeamMemory {
215
215
  this.retain = answered.every(([, b]) => b.retain_sessions !== false);
216
216
  this.count('brief_successes'); if (this.lastBrief === 'ok') this.count('brief_nonempty');
217
217
  const heads = {};
218
- for (const [r, b] of answered) heads[r.key] = b.git_head ?? null;
218
+ for (const [r, b] of answered) { heads[r.key] = b.git_head ?? null; this.serves.add(r.key, b.serve_id); }
219
219
  this.startBackground(grant, heads);
220
220
  const several = repos.length > 1, pages = new Set(), parts = [];
221
221
  let facts = 0;
@@ -309,11 +309,12 @@ export class TeamMemory {
309
309
  if (own.length) item.files = own.slice(0, 12).map(f => String(f).slice(0, 200));
310
310
  if (checks) item.checks = checks;
311
311
  }
312
+ if (this.protocol >= FEEDBACK_PROTOCOL && this.serves.of(r.key).length) item.serves = this.serves.of(r.key);
312
313
  if (!box.put(item)) { this.count('queue_rejected'); continue; }
313
314
  this.count('queued'); queued = true;
314
315
  }
315
- // A rebind cannot send the old round using the new key.
316
- if (!queued || scopeID(source) !== scopeID(this.scope())) return;
316
+ // A serve belongs to the round that used it. A rebind cannot send the old round using the new key.
317
+ this.serves.clear(); if (!queued || scopeID(source) !== scopeID(this.scope())) return;
317
318
  await this.#drain(grant, deadline);
318
319
  }
319
320
 
@@ -331,7 +332,7 @@ export class TeamMemory {
331
332
  try {
332
333
  if (claim.record.attempts) this.count('write_retries');
333
334
  // A round queued for protocol 3 meets a Studio that went back to 2: send the fields 2 knows.
334
- const item = this.protocol >= PROTOCOL ? claim.record.item : (({ context, files, checks, ...v2 }) => v2)(claim.record.item);
335
+ const { serves, ...v3 } = claim.record.item, item = this.protocol >= FEEDBACK_PROTOCOL ? claim.record.item : this.protocol >= PROTOCOL ? v3 : (({ context, files, checks, ...v2 }) => v2)(v3);
335
336
  const ack = await this.#post(grant, 'retain', item, WRITE_MS, this.renew, signal);
336
337
  if (!ack || ack.status === 'retain_disabled') { this.retain = false; outcome = 'discard'; }
337
338
  else if (ack.event_id === claim.record.item.event_id && ack.operation_id && ['pending','processing','completed','failed','cancelled','unknown'].includes(ack.status)) {
@@ -369,7 +370,7 @@ export class TeamMemory {
369
370
  if (repository && !picked && this.repos.length > 1) return this.#none(repository);
370
371
  this.count(renew?.role && renew.role !== 'main' ? 'helper_search_calls' : 'search_calls');
371
372
  return this.#each(picked ? [picked] : this.#about(query), async r => {
372
- try { return (await this.#post(grant, 'search', await this.#body(r, { query }), TOOL_MS, renew ?? this.renew))?.text ?? 'Team memory is off for this team.'; }
373
+ try { const answer = await this.#post(grant, 'search', await this.#body(r, { query }), TOOL_MS, renew ?? this.renew); this.serves.add(r.key, answer?.serve_id); return answer?.text ?? 'Team memory is off for this team.'; }
373
374
  catch { return 'Team memory did not answer; go on without it.'; }
374
375
  });
375
376
  }
@@ -447,7 +448,7 @@ export function attachMemory(self, manager, agentDir, verify) {
447
448
  return self.memory;
448
449
  }
449
450
  export function trackMemoryLessons(self) {
450
- self.session.subscribe(event => { try { self.memoryTurn?.checks?.event(event); } catch { /* optional evidence */ } });
451
+ self.session.subscribe(event => { try { self.memoryTurn?.checks?.event(event); self.memoryTurn?.skills?.event(event); } catch { /* optional evidence */ } });
451
452
  }
452
453
  // Whether this conversation has team memory now: Studio answered memory for
453
454
  // this key recently and has not switched it off since (the Terminal's header).
@@ -464,7 +465,7 @@ export const helperMemoryTools = (managed, grant, renew) => managed.memory?.stat
464
465
  export async function openRound(self, manager, text) {
465
466
  if (!self.memory) return;
466
467
  self.memory.stats = {};
467
- const turn = self.memoryTurn = { id: randomUUID(), at: new Date().toISOString(), scope: memoryScope(self.origin, self.manifest), request: text, users: [text], checks: new CheckLessons(self.checks?.commands ?? []) };
468
+ const turn = self.memoryTurn = { id: randomUUID(), at: new Date().toISOString(), scope: memoryScope(self.origin, self.manifest), request: text, users: [text], checks: new CheckLessons(self.checks?.commands ?? []), skills: SkillFeedback.forRound(self, text) };
468
469
  turn.entry = manager.appendCustomEntry('agent-watch-memory-turn', { id: turn.id, at: turn.at, scope: turn.scope });
469
470
  try {
470
471
  // Preserve the first brief in history; later refreshes are injected by the
@@ -479,6 +480,7 @@ export async function openRound(self, manager, text) {
479
480
  } catch { /* memory is optional */ }
480
481
  }
481
482
  export async function closeRound(self) {
483
+ try { const t = self.memoryTurn; t?.skills?.close({ checks: t.checks?.last, lessons: t.checks?.lessons ?? [] }); } catch { /* optional evidence */ }
482
484
  if (!self.grant || self.preflight || !self.memory || !self.memoryTurn) return;
483
485
  const turn = self.memoryTurn, closing = AbortSignal.timeout(CLOSE_MS), started = Date.now();
484
486
  let outcome = 'unknown';
@@ -257,7 +257,7 @@ function capacityStatus(owner) {
257
257
  // run) fails at once, as before. Pi decides by the words of an error, which
258
258
  // for a company failure would match "rate limited" or "timeout" in sentences
259
259
  // that must not be retried; here it decides by Studio's code only.
260
- export const TURN_RETRY_CODES = new Set(['upstream_interrupted', 'upstream_incomplete', 'upstream_timeout', 'upstream_unavailable', 'output_limit_exceeded',
260
+ export const TURN_RETRY_CODES = new Set(['upstream_interrupted', 'upstream_incomplete', 'upstream_timeout', 'upstream_unavailable', 'upstream_server_error', 'output_limit_exceeded',
261
261
  'connector_unavailable', 'connector_execution_failed', 'inference_unavailable', 'studio-unreachable']);
262
262
  export const TURN_RETRY = { maxRetries: 2, baseDelayMs: 2_000 };
263
263
 
@@ -278,7 +278,13 @@ export function enableCompanyTurnRetry(session, settings, link = null) {
278
278
  settings.applyOverrides({ retry: { enabled: true, ...TURN_RETRY } });
279
279
  const prepare = session._prepareRetry;
280
280
  if (link && typeof prepare === 'function') session._prepareRetry = async function (message) {
281
- if (!studio(message)) return prepare.call(this, message);
281
+ // Each ask again goes to the member's local log (studio-link.jsonl), so a
282
+ // task that stopped or went on can be read back later.
283
+ if (!studio(message)) {
284
+ const asked = await prepare.call(this, message);
285
+ if (asked) link.log({ kind: 'turn_retry', code: failureCode(message.errorMessage), attempt: this._retryAttempt });
286
+ return asked;
287
+ }
282
288
  // Studio answers here and was not away lately (Agent Watch or Studio's
283
289
  // connector failed on a running Studio): Pi's own asks again, as before.
284
290
  if (!(await link.needed(failureCode(message.errorMessage)).catch(() => false))) return companyTurnRetryable(message) ? prepare.call(this, message) : false;
@@ -0,0 +1,79 @@
1
+ // Skill feedback (after Cognee's proposal-first skill improvement): which
2
+ // skills a round used, and whether the round ended badly (its last check
3
+ // failed, or a command failed the same way twice). A skill that keeps ending
4
+ // badly gets a proposal file to review under .pi/piagent-state/skill-proposals;
5
+ // Piagent never edits a skill itself and makes no model call for this.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ const MIN_FAILURES = 3, MIN_SHARE = 0.5, KEEP = 10, REPROPOSE_MS = 7 * 86400_000;
10
+ const safeName = name => String(name).replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 80);
11
+
12
+ export class SkillFeedback {
13
+ // A managed round's tracker, when the conversation has skills.
14
+ static forRound(self, text) {
15
+ if (!self.resources?.skills?.length) return null;
16
+ const feedback = new SkillFeedback({ cwd: self.cwd, skills: self.resources.skills });
17
+ feedback.request(text);
18
+ return feedback;
19
+ }
20
+ constructor({ cwd, skills = [], now = Date.now }) {
21
+ this.cwd = cwd; this.now = now; this.used = new Set();
22
+ this.skills = skills.filter(s => s?.name && s?.filePath).map(s => ({ name: s.name, file: path.resolve(s.filePath) }));
23
+ }
24
+ // A member's /skill:name arrives expanded as a <skill name="..."> block.
25
+ request(text) {
26
+ for (const m of String(text ?? '').matchAll(/<skill name="([^"]{1,80})"/g)) if (this.skills.some(s => s.name === m[1])) this.used.add(m[1]);
27
+ }
28
+ // The model reading a skill's SKILL.md is the skill being used.
29
+ event(event) {
30
+ if (event?.type !== 'tool_execution_start' || event.toolName !== 'read' || typeof event.args?.path !== 'string') return;
31
+ const file = path.resolve(this.cwd, event.args.path);
32
+ for (const s of this.skills) if (s.file === file) this.used.add(s.name);
33
+ }
34
+ // The round's end: counts for every skill it used; returns the proposals written.
35
+ close({ checks = 'none', lessons = [] } = {}) {
36
+ if (!this.used.size || !this.cwd) return [];
37
+ const errors = lessons.filter(l => l.kind === 'tool_error');
38
+ const bad = checks === 'failed' || errors.length > 0;
39
+ const written = [];
40
+ let dir;
41
+ // Inside the project's own state folder only: never through a link.
42
+ try {
43
+ const state = path.join(fs.realpathSync(this.cwd), '.pi', 'piagent-state');
44
+ dir = path.join(state, 'skill-proposals');
45
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
46
+ if (fs.realpathSync(dir) !== dir || fs.lstatSync(dir).isSymbolicLink()) return [];
47
+ } catch { return []; }
48
+ for (const name of this.used) {
49
+ const file = path.join(dir, `${safeName(name)}.json`);
50
+ let state = { name, uses: 0, failures: [], proposed_at: null };
51
+ try { state = { ...state, ...JSON.parse(fs.readFileSync(file, 'utf8')) }; } catch { /* first use */ }
52
+ state.uses++;
53
+ if (bad) {
54
+ const detail = checks === 'failed' ? 'the round\'s last check failed' : errors[0].line;
55
+ state.failures = [...state.failures, { at: new Date(this.now()).toISOString(), detail: String(detail).slice(0, 400) }].slice(-KEEP);
56
+ }
57
+ const recent = state.failures.length, share = recent / Math.min(state.uses, KEEP);
58
+ const due = recent >= MIN_FAILURES && share >= MIN_SHARE && (!state.proposed_at || this.now() - Date.parse(state.proposed_at) > REPROPOSE_MS);
59
+ if (due) {
60
+ state.proposed_at = new Date(this.now()).toISOString();
61
+ const skill = this.skills.find(s => s.name === name);
62
+ const proposal = path.join(dir, `${safeName(name)}.md`);
63
+ fs.writeFileSync(proposal, [
64
+ `# Review the skill "${name}"`, '',
65
+ `Proposed ${state.proposed_at} by Piagent. Nothing was changed: read the evidence, then edit the skill yourself or delete this file.`, '',
66
+ `Skill file: ${skill ? path.relative(this.cwd, skill.file) || skill.file : 'unknown'}`, '',
67
+ `${recent} of the last ${Math.min(state.uses, KEEP)} rounds that used it ended badly:`, '',
68
+ ...state.failures.map(f => `- ${f.at}: ${f.detail}`), '',
69
+ 'Look for: a step the skill tells the agent to run that no longer works here (a command, flag, path or check),',
70
+ 'instructions that contradict the repository\'s current conventions, or a missing prerequisite.', ''].join('\n'), { mode: 0o600 });
71
+ written.push(proposal);
72
+ state.failures = [];
73
+ }
74
+ fs.writeFileSync(file, JSON.stringify(state, null, 1), { mode: 0o600 });
75
+ }
76
+ this.used.clear();
77
+ return written;
78
+ }
79
+ }
@@ -151,6 +151,16 @@ export class StudioLink {
151
151
  }
152
152
  justBack() { return !!this.recoveredAt && this.now() - this.recoveredAt < (this.brokerGraceMs ?? BROKER_GRACE_MS); }
153
153
 
154
+ // A pause Stop can end (a round asked again after a provider's passing failure).
155
+ async hold(ms, signal) {
156
+ const controller = new AbortController(), stop = () => controller.abort(signal?.reason);
157
+ if (signal?.aborted) throw signal.reason ?? Error('aborted');
158
+ signal?.addEventListener('abort', stop, { once: true });
159
+ this.waiting.add(controller);
160
+ try { await this.pause(ms, controller.signal); }
161
+ finally { signal?.removeEventListener('abort', stop); this.waiting.delete(controller); if (!this.waiting.size) this.status(undefined); }
162
+ }
163
+
154
164
  // Stop ends every wait at once.
155
165
  cancel() { for (const controller of this.waiting) controller.abort(Error('managed-request-aborted')); }
156
166
 
@@ -211,6 +221,14 @@ export class StudioLink {
211
221
  }
212
222
  }
213
223
 
224
+ // What the main agent reads when its work goes on after the model provider
225
+ // failed for a passing reason in the middle of the round.
226
+ export function providerResumeMessage(code, attempt, max) {
227
+ return { customType: 'agent-watch-process', display: true,
228
+ content: `Harness: your last step was cut off because the model provider failed while answering [${code}]; that is a passing failure on its side (attempt ${attempt} of ${max} to go on). Nothing you asked for is lost: the files and the checklist are as you left them. Check where you were (the checklist, git status, the last command you ran) and continue the member's task from there; do not redo work that is already done.`,
229
+ details: { phase: 'resume', cause: 'provider', code, attempt, max } };
230
+ }
231
+
214
232
  // What the main agent reads when its work goes on after Studio was back.
215
233
  export function resumeMessage(wait, code) {
216
234
  const why = { studio_update: `Studio was updated${wait.version_before && wait.version_after ? ` (${wait.version_before} → ${wait.version_after})` : ''}`,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -52,6 +52,10 @@ const CODES = {
52
52
  managed_service_unavailable: ['service', 'Studio could not start the run'],
53
53
  upstream_interrupted: ['service', 'the model response was interrupted'],
54
54
  upstream_incomplete: ['service', 'the model returned no response'],
55
+ // The provider's own error event in the middle of an answer (OpenAI
56
+ // server_error / api_error, Anthropic api_error / overloaded_error): a
57
+ // passing failure on its side, asked again.
58
+ upstream_server_error: ['service', 'the model provider failed while answering (a passing error on its side)'],
55
59
  // Studio ended the stream: the model wrote past the request's output limit
56
60
  // (a degenerate answer), or the request was ended for the member.
57
61
  output_limit_exceeded: ['service', 'the model kept writing past its output limit and was stopped'],
@@ -104,6 +108,7 @@ const CODES = {
104
108
  };
105
109
  const ROLES = { main: 'main agent', scout: 'scout subagent', research: 'research subagent', verify: 'verify subagent', review: 'review subagent' };
106
110
  const REQUEST = /\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/;
111
+ const PROVIDER_SERVER_ERROR = /^(?:Error Code )?(?:server_error|api_error|internal_error|internal_server_error|overloaded_error|undefined):|An error occurred while processing your request|"type"\s*:\s*"(?:api_error|overloaded_error)"|\bOverloaded\b/i;
107
112
  const NETWORK = /fetch failed|econnrefused|econnreset|enotfound|ehostunreach|etimedout|socket hang up|network|connection error|terminated|timed? ?out|aborted due to timeout/i;
108
113
 
109
114
  export function failureKind(code) { return CODES[code]?.[0] ?? 'failed'; }
@@ -151,6 +156,9 @@ export function failureCode(raw) {
151
156
  const local = /(managed-broker:[A-Za-z_]{1,80}|managed-[a-z-]{1,80}|web-search-empty)/.exec(text)?.[1];
152
157
  if (known(local)) return local;
153
158
  if (!studio && /\bAPI error \((?:50[234]|52\d|530)\)|^(?:50[234]|52\d|530)\b/.test(text)) return 'studio-gateway-unavailable';
159
+ // How Pi's providers word the provider's error event inside a streamed
160
+ // answer (seen 2026-10-21: gpt-6-sol sent api_error mid-answer and the task stopped).
161
+ if (PROVIDER_SERVER_ERROR.test(text)) return 'upstream_server_error';
154
162
  if (studio || /\bAPI error \(\d{3}\)|^\d{3} \{/.test(text)) return 'studio-request-failed';
155
163
  // The provider declined the request itself (its usage policy), not Studio:
156
164
  // the same request is declined again, so it is said in words.
@@ -28,7 +28,7 @@
28
28
  "@vitejs/plugin-react": "6.0.5",
29
29
  "json-schema-to-typescript": "15.0.4",
30
30
  "typescript": "7.0.2",
31
- "vite": "8.2.1"
31
+ "vite": "8.2.4"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=22.19.0"