@piagent/platform 1.51.0 → 1.51.2

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 (57) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/operator-manual-vietnamese.md +7 -7
  5. package/docs/publishing-for-teams.md +2 -0
  6. package/docs/release-install-policy.md +2 -2
  7. package/docs/team-onboarding.md +2 -2
  8. package/docs-site/architecture.html +1 -1
  9. package/docs-site/assets/search-en.json +1 -1
  10. package/docs-site/assets/search-vi.json +1 -1
  11. package/docs-site/benchmark.html +1 -1
  12. package/docs-site/commands.html +1 -1
  13. package/docs-site/context-engine.html +1 -1
  14. package/docs-site/ecosystem.html +1 -1
  15. package/docs-site/en/architecture.html +1 -1
  16. package/docs-site/en/benchmark.html +1 -1
  17. package/docs-site/en/commands.html +1 -1
  18. package/docs-site/en/context-engine.html +1 -1
  19. package/docs-site/en/ecosystem.html +1 -1
  20. package/docs-site/en/index.html +15 -15
  21. package/docs-site/en/mcp-approval.html +1 -1
  22. package/docs-site/en/mcp-auth.html +1 -1
  23. package/docs-site/en/mcp-servers.html +1 -1
  24. package/docs-site/en/mcp.html +1 -1
  25. package/docs-site/en/permissions.html +1 -1
  26. package/docs-site/en/profiles.html +1 -1
  27. package/docs-site/en/quickstart.html +1 -1
  28. package/docs-site/en/runtime.html +1 -1
  29. package/docs-site/en/scope.html +1 -1
  30. package/docs-site/en/security.html +1 -1
  31. package/docs-site/en/team.html +1 -1
  32. package/docs-site/en/update.html +3 -3
  33. package/docs-site/en/whats-new.html +52 -33
  34. package/docs-site/en/windows.html +1 -1
  35. package/docs-site/en/workflows.html +1 -1
  36. package/docs-site/index.html +15 -15
  37. package/docs-site/mcp-approval.html +1 -1
  38. package/docs-site/mcp-auth.html +1 -1
  39. package/docs-site/mcp-servers.html +1 -1
  40. package/docs-site/mcp.html +1 -1
  41. package/docs-site/permissions.html +1 -1
  42. package/docs-site/profiles.html +1 -1
  43. package/docs-site/quickstart.html +1 -1
  44. package/docs-site/releases.json +47 -0
  45. package/docs-site/runtime.html +1 -1
  46. package/docs-site/scope.html +1 -1
  47. package/docs-site/security.html +1 -1
  48. package/docs-site/team.html +1 -1
  49. package/docs-site/update.html +3 -3
  50. package/docs-site/whats-new.html +52 -33
  51. package/docs-site/windows.html +1 -1
  52. package/docs-site/workflows.html +1 -1
  53. package/evals/private-holdout-v1/public-exposure.v2.json +1 -1
  54. package/package.json +1 -1
  55. package/packages/piagent-core/managed/check-questions.mjs +23 -57
  56. package/packages/piagent-core/managed/test-scope.mjs +170 -0
  57. package/packages/piagent-core/package.json +1 -1
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.51.2" title="v1.51.2 docs · VI"><span class="dot"></span>v1.51.2</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new" aria-current="page">Changelog</a>
@@ -108,7 +108,28 @@
108
108
  <div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
109
109
  <section class="section releases" data-releases>
110
110
  <h2 id="latest" class="sr-only">Mới nhất</h2>
111
- <article class="release latest" id="v1-51-0" data-release>
111
+ <article class="release latest" id="v1-51-2" data-release>
112
+ <div class="release-meta"><a class="release-version" href="#v1-51-2">v1.51.2</a><time class="release-date" datetime="2026-11-03">03/11/2026</time></div>
113
+ <div class="release-body">
114
+ <h3 data-toc-skip id="v1-51-2-title">Chạy riêng phần test liên quan dùng được với mọi công cụ test</h3>
115
+ <p>Khi bạn chọn chỉ chạy phần test liên quan, harness hiểu nhầm nhiều lệnh chạy một test là chạy toàn bộ rồi chặn lại. Giờ lệnh chạy một file, một bộ lọc hay một module được đọc đúng ở từng công cụ.</p>
116
+ <ul class="release-items">
117
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Maven -Dtest và -Dit.test đặt trước goal, Gradle --tests và :module:test, node --import tsx --test, flutter, cargo test TÊN và các lệnh khác giờ được tính là một phần.</span></li>
118
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lệnh chỉ biên dịch hay liệt kê test (test-compile, -DskipTests, --listTests) không còn bị tính là chạy test; dòng lệnh có kèm chạy toàn bộ vẫn được hỏi.</span></li>
119
+ </ul>
120
+ </div>
121
+ </article>
122
+ <article class="release" id="v1-51-1" data-release>
123
+ <div class="release-meta"><a class="release-version" href="#v1-51-1">v1.51.1</a><time class="release-date" datetime="2026-11-03">03/11/2026</time></div>
124
+ <div class="release-body">
125
+ <h3 data-toc-skip id="v1-51-1-title">Task để chạy qua đêm không còn đứng chờ câu trả lời tới sáng</h3>
126
+ <p>Ở chế độ công ty, harness hỏi chạy test phần nào hay có push không, rồi chờ bạn mãi. Giờ câu hỏi không ai trả lời trong mười phút sẽ tự rút và lựa chọn an toàn được dùng.</p>
127
+ <ul class="release-items">
128
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu hỏi về test hay push không ai trả lời trong mười phút sẽ tự rút: chỉ chạy test liên quan tới thay đổi, không push gì. Task không còn đứng yên cả đêm.</span></li>
129
+ </ul>
130
+ </div>
131
+ </article>
132
+ <article class="release" id="v1-51-0" data-release>
112
133
  <div class="release-meta"><a class="release-version" href="#v1-51-0">v1.51.0</a><time class="release-date" datetime="2026-11-02">02/11/2026</time></div>
113
134
  <div class="release-body">
114
135
  <h3 data-toc-skip id="v1-51-0-title">Theo dõi mọi session từ bên ngoài, thêm quyền Always bypass</h3>
@@ -400,42 +421,40 @@
400
421
  </ul>
401
422
  </div>
402
423
  </article>
403
- <article class="release" id="v1-43-9" data-release>
404
- <div class="release-meta"><a class="release-version" href="#v1-43-9">v1.43.9</a><time class="release-date" datetime="2026-10-27">27/10/2026</time></div>
405
- <div class="release-body">
406
- <h3 data-toc-skip id="v1-43-9-title">Đủ skill đã cài, và mở file agent gửi lại</h3>
407
- <p>Skill trong plugin Claude và Codex, như superpowers, đến được agent; file agent gửi lại mở ngay trong web; hộp chọn thư mục chạy trên Windows.</p>
408
- <ul class="release-items">
409
- <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
410
- <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
411
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
412
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
413
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
414
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
415
- </ul>
416
- </div>
417
- </article>
418
- <article class="release" id="v1-43-8" data-release>
419
- <div class="release-meta"><a class="release-version" href="#v1-43-8">v1.43.8</a><time class="release-date" datetime="2026-10-27">27/10/2026</time></div>
420
- <div class="release-body">
421
- <h3 data-toc-skip id="v1-43-8-title">Đủ skill đã cài, và mở file agent gửi lại <span class="tag">không phát hành</span></h3>
422
- <p>Đã tag nhưng chưa publish: test mới chỉ chạy trên Linux đọc sai trường trên CI. Thay bằng 1.43.9.</p>
423
- <ul class="release-items">
424
- <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
425
- <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
426
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
427
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
428
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
429
- <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
430
- </ul>
431
- </div>
432
- </article>
433
424
  </section>
434
425
  <section class="section archive">
435
426
  <h2 id="archive">Các bản trước</h2>
436
427
  <p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
437
428
  <details class="release-archive">
438
- <summary><span>Tháng 10/2026</span><span class="archive-count">72</span></summary>
429
+ <summary><span>Tháng 10/2026</span><span class="archive-count">74</span></summary>
430
+ <div class="archive-row" id="v1-43-9">
431
+ <div class="archive-head"><a class="release-version" href="#v1-43-9">v1.43.9</a> <span class="release-date">27/10/2026</span></div>
432
+ <div class="archive-body">
433
+ <p><strong>Đủ skill đã cài, và mở file agent gửi lại</strong> — Skill trong plugin Claude và Codex, như superpowers, đến được agent; file agent gửi lại mở ngay trong web; hộp chọn thư mục chạy trên Windows.</p>
434
+ <ul class="release-items">
435
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
436
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
437
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
438
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
439
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
440
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
441
+ </ul>
442
+ </div>
443
+ </div>
444
+ <div class="archive-row" id="v1-43-8">
445
+ <div class="archive-head"><a class="release-version" href="#v1-43-8">v1.43.8</a> <span class="release-date">27/10/2026</span></div>
446
+ <div class="archive-body">
447
+ <p><strong>Đủ skill đã cài, và mở file agent gửi lại</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: test mới chỉ chạy trên Linux đọc sai trường trên CI. Thay bằng 1.43.9.</p>
448
+ <ul class="release-items">
449
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
450
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
451
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
452
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt chỉ sửa tài liệu hay file Spec Kit không bị harness bắt chạy check của repo, và approval cho check đã bị từ chối thì không bị hỏi lại.</span></li>
453
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Câu trả lời agent lặp lại sau mỗi lần harness trả về chỉ hiện một lần, không lặp theo từng vòng.</span></li>
454
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Trên Linux và Windows (WSL2), check tìm được JDK ngoài PATH: SDKMAN, /usr/lib/jvm hoặc ~/.jdks.</span></li>
455
+ </ul>
456
+ </div>
457
+ </div>
439
458
  <div class="archive-row" id="v1-43-7">
440
459
  <div class="archive-head"><a class="release-version" href="#v1-43-7">v1.43.7</a> <span class="release-date">27/10/2026</span></div>
441
460
  <div class="archive-body">
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.51.2" title="v1.51.2 docs · VI"><span class="dot"></span>v1.51.2</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.51.2" title="v1.51.2 docs · VI"><span class="dot"></span>v1.51.2</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -257,7 +257,7 @@
257
257
  {
258
258
  "path": "tests",
259
259
  "fileCount": 651,
260
- "sha256": "02e75d8917cda9a4abc8833a33058700d7ee0baf9553b2efc54d894ff845023b"
260
+ "sha256": "87bed56a6089ee73778f5f9ae3beeb95b1fc5b807f0a124ea5d52126968e29f3"
261
261
  }
262
262
  ],
263
263
  "custodianComparisonRules": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.51.0",
3
+ "version": "1.51.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { memberQuestions } from './member-questions.mjs';
3
3
  import { alwaysBypass, effectivePermission } from './permission.mjs';
4
+ import { checkScope, suitePart, unquoted } from './test-scope.mjs';
4
5
  import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pushRepositories, reviewBeforePush } from './check-evidence.mjs';
5
6
 
6
7
  // How much to test is the member's decision: a one-line fix should not cost a
@@ -8,53 +9,12 @@ import { digestsOnce, knownFast, markCheckStart, missingSuites, pushQuestion, pu
8
9
  // suite and before a push, on the dashboard card or in the Terminal. Bypass skips
9
10
  // the questions about the work (it runs the tests that cover the change) but not
10
11
  // the one before a push. With no screen nobody can answer: nothing is asked.
12
+ // A question nobody answers in UNANSWERED_MS (the member left the task running
13
+ // overnight) is taken back and the cheap answer goes on: only the tests that
14
+ // cover the change, and no push. Before this a turn waited until morning.
15
+ export const UNANSWERED_MS = 10 * 60_000;
16
+ export { checkScope, suitePart };
11
17
 
12
- // How much of the suite a test command covers: 'partial' (a filter, one file, one
13
- // package), 'suite' (the whole test command) or null when it runs no tests.
14
- const VERSION_ONLY = /(?:^|\s)(?:--version|-V|--help)(?:\s|$)/;
15
- const FILTERED = /(?:^|\s)(?:-k|-t|--grep|--testNamePattern|--test-name-pattern|--filter|--only|--testPathPattern)(?:[\s=]|$)|\bgo\s+test\b.*\s-run[\s=]|\bpytest\b.*\s::|\.only\b/;
16
- const TEST_RUNNER = /\b(npm|pnpm|yarn|bun)\s+(run\s+)?test(:\S+)?(?=\s|$)|\bnode\s+--test\b|\b(vitest|jest|mocha|pytest|rspec|phpunit)\b|\bplaywright\s+test\b|\bpython3?\s+-m\s+(pytest|unittest)\b|\b(go|cargo|swift|dotnet|deno)\s+test\b|\bmake\s+test\b|\bxcodebuild\b.*\btest\b|\b(gradle|gradlew|mvn|mvnw)\b.*\b(test|verify)\b/;
17
- // Test commands start a command: `npm install -D vitest` or `rm jest.config.ts` only name one.
18
- const TEST_START = new RegExp(`^(?:${TEST_RUNNER.source})`);
19
- // A function body, a group or a loop body opens a part as well.
20
- const LAUNCHER = /^(?:[\w-]+\s*\(\)\s*\{?\s*|\{\s+|\(\s*|(?:do|then|else)\s+|\w+=\S*\s+|time\s+|nohup\s+|timeout\s+\S+\s+|npx\s+(?:-\S+\s+)*|bunx\s+|pnpm\s+(?:exec|dlx)\s+|uv\s+run\s+|poetry\s+run\s+|yarn\s+(?=jest|vitest|mocha|playwright)|node_modules\/\.bin\/|\.\/)+/;
21
- const partsOf = bare => bare.split(/\s*(?:\|\|?|&&|;)\s*/).map(part => part.replace(/\s\d*>.*$/, '').replace(/[\s)}]+$/, '').trim());
22
- const NOT_TESTS = /\b(lint|typecheck|type-check|tsc|eslint|biome|ruff|mypy|pyright|vet|clippy|format|build)\b/;
23
- const TARGET_FLAG = /^(-p|--package|-w|--workspace|--tests)$|^(-Dtest=|-only-testing:|--workspace=|--package=)/;
24
- // `go test -p 4` sets how many packages run at once; for cargo -p names a package.
25
- const GO_TEST = /^go\s+test\b/;
26
- const VALUE_FLAG = /^(-c|--config|--reporter|--project|--workers|--timeout|--rootDir|--root|-f|--file|-o|--output)$/;
27
- // A file or folder named on the command line; a flag's own value names none.
28
- const targetsPart = (args, go = false) => args.some((a, i) => {
29
- // A variable: a function's arguments, a loop's file, $(go list …). The caller chose them, so this is not the whole command.
30
- if (a.startsWith('$')) return true;
31
- if (go && a === '-p') return false;
32
- if (TARGET_FLAG.test(a)) return true;
33
- if (go && args[i - 1] === '-p') return false;
34
- if (a.startsWith('-') || VALUE_FLAG.test(args[i - 1] ?? '') || a.includes('*') || a.endsWith('...')) return false;
35
- return a.includes('/') || /\.(test|spec)\.\w+$|_test\.\w+$|^test_\w+\.\w+$/.test(a);
36
- });
37
- // What a command runs, without the text it merely quotes (a commit message, an echo).
38
- const unquoted = text => text.replace(/"[^"]*"|'[^']*'/g, quoted => quoted.startsWith('"$') ? '$' : '""');
39
- export function checkScope(command, declared = []) {
40
- const text = String(command ?? '').replace(/\s+/g, ' ').trim();
41
- if (!text || /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/.test(text) || VERSION_ONLY.test(text) || /--collect-only/.test(text)) return null;
42
- const bare = unquoted(text);
43
- for (const part of partsOf(bare)) {
44
- const own = part.replace(LAUNCHER, ''), runner = own.match(TEST_START);
45
- if (!runner) continue;
46
- if (FILTERED.test(own)) return 'partial';
47
- return !declared.includes(text) && targetsPart(own.slice(runner[0].length).split(' ').filter(Boolean), GO_TEST.test(own)) ? 'partial' : 'suite';
48
- }
49
- // A script the repository declares: it cannot be narrowed from here.
50
- const named = declared.find(d => partsOf(bare).some(part => part.startsWith(unquoted(d))));
51
- return named && !NOT_TESTS.test(named) ? 'suite' : null;
52
- }
53
- // The test step of a command, without what surrounds it: the same step costs the same.
54
- export function suitePart(command) {
55
- for (const part of partsOf(unquoted(String(command ?? '').replace(/\s+/g, ' ').trim()))) { const own = part.replace(LAUNCHER, ''); if (TEST_START.test(own)) return own; }
56
- return String(command ?? '').trim();
57
- }
58
18
  // The member's own words asking for the whole suite: nothing to ask them then.
59
19
  const RUN_ALL = String.raw`(?:full|all(?:\s+the)?|whole(?:\s+the)?|entire(?:\s+the)?|the\s+(?:full|whole|entire))`;
60
20
  const FULL_WORDS = new RegExp([
@@ -82,14 +42,20 @@ function surfaceOf(self, ctx) {
82
42
  const runner = self.session?.extensionRunner, ui = ctx ? ctx.hasUI && ctx.ui : runner?.hasUI?.() && runner.getUIContext?.();
83
43
  return ui?.select && !ui[GATEWAY_UI] ? { ui } : null;
84
44
  }
85
- // The label picked, { other } for the member's own words, undefined for no answer (Esc, stopped).
86
- async function ask(surface, question, labels, signal) {
87
- if (surface.ui) return surface.ui.select(question, labels, { signal });
88
- const questions = [{ header: 'Test', question, options: labels.map(label => ({ label, description: '' })), multiSelect: false }];
89
- const answer = await memberQuestions.ask({ sessionId: surface.sessionId, toolCallId: `check-scope-${randomUUID()}`, questions, signal }).catch(() => null);
90
- if (!answer || answer.skipped) return undefined;
91
- const { selected, other } = answer.answers[0];
92
- return other ? { other } : labels[selected[0]];
45
+ // The label picked, { other } for the member's own words, undefined for no
46
+ // answer (Esc, stopped, or nobody answered in time).
47
+ async function ask(surface, question, labels, signal, waitMs = UNANSWERED_MS) {
48
+ // A plain timer, not AbortSignal.timeout: the wait itself keeps the process up.
49
+ const expired = new AbortController(), timer = setTimeout(() => expired.abort(), waitMs);
50
+ const until = signal ? AbortSignal.any([signal, expired.signal]) : expired.signal;
51
+ try {
52
+ if (surface.ui) return await surface.ui.select(question, labels, { signal: until });
53
+ const questions = [{ header: 'Test', question, options: labels.map(label => ({ label, description: '' })), multiSelect: false }];
54
+ const answer = await memberQuestions.ask({ sessionId: surface.sessionId, toolCallId: `check-scope-${randomUUID()}`, questions, signal: until }).catch(() => null);
55
+ if (!answer || answer.skipped) return undefined;
56
+ const { selected, other } = answer.answers[0];
57
+ return other ? { other } : labels[selected[0]];
58
+ } finally { clearTimeout(timer); }
93
59
  }
94
60
 
95
61
  // At the end of a change: { choice: 'partial' | 'full' | 'skip', words? }, or null
@@ -99,7 +65,7 @@ export async function askCheckScope(self, { partPassed = false, signal } = {}) {
99
65
  if (!surface) return null;
100
66
  if (['bypass', 'always-bypass'].includes(effectivePermission(self))) return { choice: 'partial' };
101
67
  const part = partPassed ? KEEP : PART;
102
- const picked = await ask(surface, partPassed ? 'Code đã đổi, phần test liên quan đã pass. Có chạy toàn bộ test không?' : 'Code đã đổi, chưa chạy test. Chạy phần nào?', [part, FULL, NONE], signal);
68
+ const picked = await ask(surface, partPassed ? 'Code đã đổi, phần test liên quan đã pass. Có chạy toàn bộ test không?' : 'Code đã đổi, chưa chạy test. Chạy phần nào?', [part, FULL, NONE], signal, self.unansweredMs);
103
69
  if (signal?.aborted) return null;
104
70
  if (picked?.other) return { choice: 'partial', words: picked.other };
105
71
  return { choice: picked === FULL ? 'full' : picked === NONE ? 'skip' : 'partial' };
@@ -133,7 +99,7 @@ async function holdOrLet(self, run, command, ctx, signal) {
133
99
  const now = await digestOf('.'), repositories = pushRepositories(self, command), single = !repositories.length || repositories[0] === '.';
134
100
  const missing = await missingSuites(digestOf, run, repositories.length ? repositories : ['.']);
135
101
  if ((single ? run.suitePassedOn(now) : false) || !missing.length || (run.pushAllowedOn !== undefined && run.pushAllowedOn === now)) return;
136
- const picked = await ask(surface, await pushQuestion(self, digestOf, run, repositories, missing), [PUSH_FULL, PUSH_NOW], signal);
102
+ const picked = await ask(surface, await pushQuestion(self, digestOf, run, repositories, missing), [PUSH_FULL, PUSH_NOW], signal, self.unansweredMs);
137
103
  if (picked === PUSH_NOW) { run.pushAllowedOn = now; return; }
138
104
  if (picked?.other) throw Error(WORDS_REASON(picked.other));
139
105
  if (picked !== PUSH_FULL) throw Error(STOPPED_REASON);
@@ -144,7 +110,7 @@ async function holdOrLet(self, run, command, ctx, signal) {
144
110
  const held = run.scopeHeld ??= new Set(), again = held.has(command); held.add(command);
145
111
  if (effectivePermission(self) === 'bypass') { if (again || knownFast(self, command, suitePart(command))) return; throw Error(BYPASS_REASON); }
146
112
  if (run.scopeChoice === 'partial' && !again) throw Error(PARTIAL_REASON);
147
- const picked = await ask(surface, `Agent muốn chạy toàn bộ test: ${String(command).slice(0, 160)}`, [PART, FULL], signal);
113
+ const picked = await ask(surface, `Agent muốn chạy toàn bộ test: ${String(command).slice(0, 160)}`, [PART, FULL], signal, self.unansweredMs);
148
114
  if (picked === FULL) { run.scopeChoice = 'full'; return; }
149
115
  run.scopeChoice = 'partial';
150
116
  throw Error(picked?.other ? WORDS_REASON(picked.other) : PARTIAL_REASON);
@@ -0,0 +1,170 @@
1
+ // How much of the suite a test command covers: 'partial' (a filter, one file, one
2
+ // package or module), 'suite' (the whole test command) or null when it runs no tests.
3
+ // A member who chose "only the related part" is stuck when a part reads as the
4
+ // whole suite, so each tool says how its own commands narrow: the flags that pick
5
+ // tests, the flags that take a value, and the words that still run everything
6
+ // (a Maven goal, `vitest run`). Any other word on the line names a part.
7
+
8
+ // What a command runs, without the text it merely quotes (a commit message, an
9
+ // echo): a quoted word stays one word with nothing in it that splits a command.
10
+ export const unquoted = text => text.replace(/"[^"]*"|'[^']*'/g, quoted => quoted.startsWith('"$') ? '$' : quoted.length > 2 ? quoted.slice(1, -1).replace(/[\s;&|<>()]/g, '_') : '""');
11
+ export const partsOf = bare => bare.split(/\s*(?:\|\|?|&&|;)\s*/).map(part => part.replace(/\s\d*[<>].*$/, '').replace(/[\s)}]+$/, '').trim());
12
+
13
+ const VERSION_ONLY = /(?:^|\s)(?:--version|-V|--help)(?:\s|$)/;
14
+ // Listing the tests runs none of them.
15
+ const LIST_ONLY = /(?:^|\s)(?:--collect-only|--co|--listTests|--list-tests|--list|-list|--dry-run|--dryRun|-showTestPlans)(?=[\s=]|$)/;
16
+ const NOT_A_RUN = /^(echo|printf|grep|rg|cat|sed|awk|head|tail|less|git|ls|find)\b/;
17
+ const NOT_TESTS = /\b(lint|typecheck|type-check|tsc|eslint|biome|ruff|mypy|pyright|vet|clippy|format|build)\b/;
18
+ // What starts a command before the tool itself: a function body, a group, a loop body, env, a runner of binaries.
19
+ const LAUNCHER = /^(?:[\w-]+\s*\(\)\s*\{?\s*|\{\s+|\(\s*|(?:do|then|else)\s+|\w+=\S*\s+|env\s+|time\s+|nohup\s+|timeout\s+\S+\s+|npx\s+(?:-\S+\s+)*|bunx\s+|bun\s+x\s+|pnpm\s+(?:exec|dlx)\s+|(?:yarn|pnpm|bun)\s+(?=(?:jest|vitest|mocha|ava|playwright|cypress|nx|turbo|lerna)\b)|uv\s+run\s+|poetry\s+run\s+|pipenv\s+run\s+|hatch\s+run\s+|bundle\s+exec\s+|php\s+(?=\S*(?:phpunit|pest)\b)|node_modules\/\.bin\/|(?:vendor\/)?bin\/|\.\/)+/;
20
+
21
+ const NONE = /$^/;
22
+ // A word that names everything there is: the test folder, every package, a glob, a count.
23
+ const ALL_OF_IT = /^(?:\.\/)?(?:tests?|specs?|__tests__|e2e|src)\/?$|^(?:\.\/)?\.\.\.$|\*|^\d+[smh]?$|^auto$/;
24
+ // Does this command pick part of the tests? `narrow` flags pick tests (by name before
25
+ // `=`), `value` flags take the next word, `whole` words still run everything; any
26
+ // other word names a part, unless the tool takes none (`words: false`).
27
+ function narrows(args, { narrow = NONE, value = NONE, whole = NONE, words = true }) {
28
+ for (let i = 0; i < args.length; i++) {
29
+ const a = args[i];
30
+ // A variable: a function's arguments, a loop's file. The caller chose them, so this is not the whole command.
31
+ if (a.startsWith('$')) return true;
32
+ if (a === '--') continue;
33
+ if (a.startsWith('-')) {
34
+ const name = a.split('=')[0];
35
+ if (narrow.test(name)) return true;
36
+ if (!a.includes('=') && value.test(name)) i++;
37
+ continue;
38
+ }
39
+ if (words && !whole.test(a) && !ALL_OF_IT.test(a)) return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ const JS = {
45
+ narrow: /^(?:-t|--testNamePattern|--test-name-pattern|-g|--grep|--fgrep|--testPathPattern|--testPathPatterns|--filter|--spec|--project|--selectProjects|--findRelatedTests|--onlyChanged|-o|--changed|--changedSince|--lastCommit|--only|--test-only|--match|-m)$/,
46
+ value: /^(?:-w|--maxWorkers|--workers|-j|--jobs|--timeout|--reporter|-r|--require|--import|--loader|--config|-c|--rootDir|--root|--outputFile|--coverageDirectory|--test-concurrency|--test-reporter|--test-reporter-destination|--retries|--repeat-each|--env|--browser|--shard|--seed|--slow|--file|--mode|--environment|--pool|--dir)$/,
47
+ whole: /^(?:run|watch|dev|test)$/,
48
+ };
49
+ const PYTEST = { narrow: /^(?:-k|-m|--lf|--last-failed|--sw|--stepwise|--deselect|--pyargs)$/, value: /^(?:-n|--numprocesses|-c|-p|--maxfail|--rootdir|--durations|-o|--junitxml|--cov|--timeout|--dist|-W|--basetemp|--log-level|--confcutdir|--ignore|--ignore-glob|--tb|-r|--reruns)$/ };
50
+ const tool = (start, spec, test = () => true) => ({ start, scope: args => test(args) ? (narrows(args, spec) ? 'partial' : 'suite') : null });
51
+
52
+ // npm/pnpm/yarn/bun: the workspace or filter before the script, the script, then its own arguments (Jest, Vitest, …).
53
+ function packageScript(args) {
54
+ let picked = false;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (/^(?:-w|--workspace|--filter|-F)$/.test(a)) { picked = true; i++; continue; }
58
+ if (/^(?:--workspace|--filter)=/.test(a)) { picked = true; continue; }
59
+ if (a.startsWith('-')) continue;
60
+ if (a === 'workspace') { picked = true; i++; continue; }
61
+ // `npm t` is `npm test`; `npm run t` is a script of that name.
62
+ const run = /^(?:run|run-script)$/.test(a), script = run ? args[++i] : a;
63
+ if (!(run ? /^test(?::\S+)?$/ : /^(?:test|t|tst)(?::\S+)?$/).test(script ?? '')) return null;
64
+ // Before `--`, `-w web` is still npm's workspace; a number is Jest's worker count.
65
+ const rest = args.slice(i + 1), end = rest.includes('--') ? rest.indexOf('--') : rest.length;
66
+ picked ||= rest.slice(0, end).some((a, j) => /^--workspace=/.test(a) || (/^(?:-w|--workspace)$/.test(a) && !/^\d+$/.test(rest[j + 1] ?? '0')));
67
+ return picked || narrows(rest, JS) ? 'partial' : 'suite';
68
+ }
69
+ return null;
70
+ }
71
+ // Maven: every word is a goal; it tests when a goal reaches `test`. The flags pick classes, groups or modules.
72
+ function maven(args) {
73
+ if (!args.some(a => /^(?:[\w.-]+:)*(?:test|verify|integration-test)$/.test(a))) return null;
74
+ if (args.some(a => /^-D(?:skipTests|maven\.test\.skip)(?:=true)?$/.test(a))) return null;
75
+ const file = args.findIndex(a => a === '-f' || a === '--file');
76
+ if (file >= 0 && args[file + 1]?.includes('/')) return 'partial';
77
+ return narrows(args, { narrow: /^(?:-Dtest|-Dit\.test|-Dgroups|-Dcucumber\.filter\.tags|-Dcucumber\.options|-pl|--projects|-rf|--resume-from)$/, value: /^(?:-T|--threads|-s|--settings|-gs|--global-settings|-P|--activate-profiles|-l|--log-file|-t|--toolchains|-b|--builder|-f|--file)$/, words: false }) ? 'partial' : 'suite';
78
+ }
79
+ // Gradle: the words are tasks; `:cart:test` is one module's.
80
+ function gradle(args) {
81
+ const value = /^(?:-x|--exclude-task|--max-workers|-I|--init-script|-c|--settings-file|--console|-g|--gradle-user-home)$/;
82
+ const tasks = args.filter((a, i) => !a.startsWith('-') && !value.test(args[i - 1] ?? ''));
83
+ const testTasks = tasks.filter(t => /^(?:test|check|[a-zA-Z]*Test)$/.test(t.split(':').pop()));
84
+ if (!testTasks.length) return null;
85
+ if (testTasks.some(t => /^:?[\w-]+(?::[\w-]+)*:\w+$/.test(t) && t.replace(/^:/, '').includes(':'))) return 'partial';
86
+ return narrows(args, { narrow: /^(?:--tests|-p|--project-dir|-b|--build-file)$/, value, words: false }) ? 'partial' : 'suite';
87
+ }
88
+ // Go: the words are packages; none is the current one, `./...` is all of them.
89
+ function goTest(args) {
90
+ const end = args.indexOf('-args');
91
+ const own = end >= 0 ? args.slice(0, end) : args;
92
+ const spec = { narrow: /^-{1,2}run$/, value: /^-{1,2}(?:p|count|timeout|parallel|cpu|tags|exec|covermode|coverpkg|coverprofile|o|bench|benchtime|shuffle|vet|C|fuzz|fuzztime|skip|json)$/ };
93
+ const packages = own.filter((a, i) => !a.startsWith('-') && !(spec.value.test(own[i - 1] ?? '') && !own[i - 1].includes('=')));
94
+ return !packages.length || narrows(own, spec) ? 'partial' : 'suite';
95
+ }
96
+ // make: a test target; a variable naming tests narrows it (`make test TEST=./cart`).
97
+ function make(args) {
98
+ if (!args.some(a => /^(?:test|check)(?:[-_:]\w+)*$/.test(a))) return null;
99
+ return args.some(a => /^\w*(?:TEST|RUN|FILTER|PKG|SPEC|ONLY|PATTERN|FILE)\w*=/i.test(a) || a.startsWith('$')) ? 'partial' : 'suite';
100
+ }
101
+ // Nx, Turborepo, Lerna run a target across projects; naming projects or `affected` narrows.
102
+ function monorepo(name, args) {
103
+ const words = args.filter(a => !a.startsWith('-'));
104
+ const target = args.find((a, i) => /^(?:-t|--targets?)$/.test(args[i - 1] ?? '')) ?? args.find(a => /^--targets?=/.test(a))?.split('=')[1];
105
+ if (name === 'nx') {
106
+ const [command, project] = words;
107
+ if (command === 'run-many') return /test|e2e/.test(target ?? '') ? (args.some(a => /^(?:-p|--projects)(?:=|$)/.test(a)) ? 'partial' : 'suite') : null;
108
+ if (command === 'affected') return /test|e2e/.test(target ?? words[1] ?? '') ? 'partial' : null;
109
+ if (command === 'run') return /:(?:\w*test|e2e)\w*$/.test(project ?? '') ? 'partial' : null;
110
+ return /test|e2e/.test(command ?? '') ? (project ? 'partial' : 'suite') : null;
111
+ }
112
+ const tasks = words[0] === 'run' ? words.slice(1) : words;
113
+ if (!tasks.some(t => /test|e2e/.test(t))) return null;
114
+ return args.some(a => /^(?:--filter|-F|--scope|--since|--affected)(?:=|$)/.test(a)) ? 'partial' : 'suite';
115
+ }
116
+
117
+ const TOOLS = [
118
+ { start: /^(?:npm|pnpm|yarn|bun)(?=\s|$)/, scope: packageScript },
119
+ tool(/^(?:jest|vitest|mocha|ava|tap|uvu)(?=\s|$)/, JS),
120
+ tool(/^node(?=\s)/, JS, args => args.includes('--test')),
121
+ tool(/^playwright\s+test(?=\s|$)/, JS),
122
+ tool(/^cypress\s+run(?=\s|$)/, JS),
123
+ tool(/^(?:pytest|py\.test|python[\d.]*\s+-m\s+pytest)(?=\s|$)/, PYTEST),
124
+ tool(/^python[\d.]*\s+-m\s+unittest(?=\s|$)/, { narrow: /^(?:-k|-p|--pattern)$/, value: /^(?:-s|--start-directory|-t|--top-level-directory)$/, whole: /^discover$/ }),
125
+ tool(/^python[\d.]*\s+manage\.py\s+test(?=\s|$)/, { narrow: /^(?:-k|--tag)$/, value: /^(?:--parallel|--settings|-v|--verbosity|--exclude-tag|--testrunner)$/ }),
126
+ tool(/^tox(?=\s|$)/, { value: /^(?:-e|-c|--conf|-p|--parallel|-x|--override)$/ }),
127
+ { start: /^go\s+test(?=\s|$)/, scope: goTest },
128
+ tool(/^cargo\s+test(?=\s|$)/, { narrow: /^(?:-p|--package|--test|--lib|--bin|--bins|--example|--examples|--doc|--bench|--manifest-path|--ignored|--exact)$/, value: /^(?:-j|--jobs|--features|-F|--target|--profile|--test-threads|--color|-Z|--target-dir|--message-format|--config|--exclude)$/ }),
129
+ tool(/^cargo\s+nextest\s+run(?=\s|$)/, { narrow: /^(?:-E|--filterset|--filter-expr|-p|--package|--test|--lib|--bin)$/, value: /^(?:-j|--jobs|--profile|--test-threads|--retries|--partition|--features|-F)$/ }),
130
+ { start: /^mvnw?(?=\s|$)/, scope: maven },
131
+ { start: /^gradlew?(?=\s|$)/, scope: gradle },
132
+ tool(/^dotnet\s+test(?=\s|$)/, { narrow: /^--filter$/, value: /^(?:-c|--configuration|-f|--framework|-r|--runtime|-l|--logger|-s|--settings|-v|--verbosity|--results-directory|-a|--test-adapter-path|--collect|-d|--diag|-o|--output|--arch|--os|--blame-hang-timeout|--blame-crash-dump-type|-e|--environment)$/, whole: /\.slnx?$/ }),
133
+ tool(/^swift\s+test(?=\s|$)/, { narrow: /^(?:--filter|--package-path)$/, value: /^(?:-c|--configuration|-j|--jobs|--num-workers|--scratch-path|--skip)$/ }),
134
+ tool(/^xcodebuild(?=\s)/, { narrow: /^-only-testing(?::|$)/, value: /^-(?:scheme|destination|project|workspace|sdk|configuration|derivedDataPath|resultBundlePath|testPlan|skip-testing|parallel-testing-worker-count|xctestrun|target|arch|toolchain|destination-timeout|maximum-parallel-testing-workers|test-iterations|testLanguage|testRegion|only-test-configuration|skip-test-configuration)$/, words: false }, args => args.some(a => /^test(?:-without-building)?$/.test(a))),
135
+ tool(/^rspec(?=\s|$)/, { narrow: /^(?:-t|--tag|-e|--example|-E|--example-matches|--only-failures|--next-failure|-P|--pattern)$/, value: /^(?:-f|--format|-o|--out|--seed|--order|-r|--require|-I|--default-path|-O|--options)$/ }),
136
+ tool(/^(?:rails|rake)\s+test(?::\w+)?(?=\s|$)/, { narrow: /^(?:-n|--name)$/, value: /^(?:--seed|-s|-e|--environment|-j)$/ }),
137
+ tool(/^(?:phpunit|pest|php\s+artisan\s+test)(?=\s|$)/, { narrow: /^(?:--filter|--group|--testsuite)$/, value: /^(?:-c|--configuration|--bootstrap|--log-junit|--exclude-group|--coverage-html|--coverage-clover|--order-by|--cache-directory|--processes|-d)$/ }),
138
+ tool(/^mix\s+test(?=\s|$)/, { narrow: /^(?:--only|--failed|--stale)$/, value: /^(?:--seed|--max-failures|--exclude|--include|--formatter|--partitions|--max-cases)$/ }),
139
+ tool(/^(?:flutter|dart)\s+test(?=\s|$)/, { narrow: /^(?:--name|-n|--plain-name|-N|--tags|-t)$/, value: /^(?:-j|--concurrency|-r|--reporter|-p|--platform|-x|--exclude-tags|--file-reporter|--coverage-path|--flavor|--total-shards|--shard-index)$/ }),
140
+ tool(/^deno\s+test(?=\s|$)/, { narrow: /^--filter$/, value: /^(?:-c|--config|--import-map|--reporter|--seed)$/ }),
141
+ { start: /^make(?=\s)/, scope: make },
142
+ { start: /^(?:nx|turbo|lerna)(?=\s)/, scope: (args, name) => monorepo(name, args) },
143
+ ];
144
+ function testRun(part) {
145
+ const own = part.replace(LAUNCHER, '');
146
+ for (const { start, scope } of TOOLS) {
147
+ const hit = own.match(start);
148
+ if (!hit) continue;
149
+ const result = scope(own.slice(hit[0].length).split(' ').filter(Boolean), hit[0]);
150
+ return result ? { own, result } : null;
151
+ }
152
+ return null;
153
+ }
154
+
155
+ export function checkScope(command, declared = []) {
156
+ const text = String(command ?? '').replace(/\s+/g, ' ').trim();
157
+ if (!text || NOT_A_RUN.test(text) || VERSION_ONLY.test(text) || LIST_ONLY.test(text)) return null;
158
+ const bare = unquoted(text), runs = partsOf(bare).map(testRun).filter(Boolean);
159
+ // A command the repository declares is its whole check, however it is written;
160
+ // a line that also runs a whole suite is a whole run.
161
+ if (runs.length) return declared.includes(text) || runs.some(run => run.result === 'suite') ? 'suite' : 'partial';
162
+ // A script the repository declares: it cannot be narrowed from here.
163
+ const named = declared.find(d => partsOf(bare).some(part => part.startsWith(unquoted(d))));
164
+ return named && !NOT_TESTS.test(named) ? 'suite' : null;
165
+ }
166
+ // The test step of a command, without what surrounds it: the same step costs the same.
167
+ export function suitePart(command) {
168
+ const runs = partsOf(unquoted(String(command ?? '').replace(/\s+/g, ' ').trim())).map(testRun).filter(Boolean);
169
+ return (runs.find(run => run.result === 'suite') ?? runs[0])?.own ?? String(command ?? '').trim();
170
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.51.0",
3
+ "version": "1.51.2",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",