@loicngr/kobo 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +227 -0
  2. package/LICENSE +674 -0
  3. package/README.md +199 -0
  4. package/dist/mcp-server/kobo-tasks-handlers.js +27 -0
  5. package/dist/mcp-server/kobo-tasks-server.js +116 -0
  6. package/dist/server/db/index.js +22 -0
  7. package/dist/server/db/migrations.js +20 -0
  8. package/dist/server/db/schema.js +49 -0
  9. package/dist/server/index.js +178 -0
  10. package/dist/server/routes/dev-server.js +74 -0
  11. package/dist/server/routes/git.js +20 -0
  12. package/dist/server/routes/notion.js +24 -0
  13. package/dist/server/routes/settings.js +92 -0
  14. package/dist/server/routes/workspaces.js +730 -0
  15. package/dist/server/services/agent-manager.js +435 -0
  16. package/dist/server/services/dev-server-service.js +298 -0
  17. package/dist/server/services/notion-service.js +369 -0
  18. package/dist/server/services/pr-template-service.js +38 -0
  19. package/dist/server/services/settings-service.js +205 -0
  20. package/dist/server/services/websocket-service.js +212 -0
  21. package/dist/server/services/workspace-service.js +208 -0
  22. package/dist/server/services/worktree-service.js +117 -0
  23. package/dist/server/utils/git-ops.js +117 -0
  24. package/dist/server/utils/paths.js +95 -0
  25. package/dist/server/utils/process-tracker.js +46 -0
  26. package/package.json +84 -0
  27. package/src/client/dist/spa/assets/ActivityFeed-BveJRagX.js +60 -0
  28. package/src/client/dist/spa/assets/ActivityFeed-DBNn62g_.css +1 -0
  29. package/src/client/dist/spa/assets/CreatePage-BlgXsrJO.css +1 -0
  30. package/src/client/dist/spa/assets/CreatePage-wbOkBwYU.js +2 -0
  31. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BepdiOnY.woff +0 -0
  32. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-4ZhHFPot.woff +0 -0
  33. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-CNa4tw4G.woff +0 -0
  34. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CHKg1YId.woff +0 -0
  35. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-yBxCyPWP.woff +0 -0
  36. package/src/client/dist/spa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-3fZ6d7DD.woff +0 -0
  37. package/src/client/dist/spa/assets/MainLayout-6hzaLlYO.js +1 -0
  38. package/src/client/dist/spa/assets/MainLayout-D0OU6djX.css +1 -0
  39. package/src/client/dist/spa/assets/QBadge-Cb92Ia8-.js +1 -0
  40. package/src/client/dist/spa/assets/QDialog-B5H6ayTp.js +1 -0
  41. package/src/client/dist/spa/assets/QExpansionItem-DJgnAZg_.js +1 -0
  42. package/src/client/dist/spa/assets/QPage-CLk9i9z8.js +1 -0
  43. package/src/client/dist/spa/assets/QSpinnerDots-DcaNq8uL.js +1 -0
  44. package/src/client/dist/spa/assets/QTabPanels-DlG5TZhP.js +1 -0
  45. package/src/client/dist/spa/assets/QTooltip-637ruGFc.js +1 -0
  46. package/src/client/dist/spa/assets/SettingsPage-B9VYIQs-.css +1 -0
  47. package/src/client/dist/spa/assets/SettingsPage-KEqbLZUA.js +1 -0
  48. package/src/client/dist/spa/assets/WorkspacePage-BFuHLjou.css +1 -0
  49. package/src/client/dist/spa/assets/WorkspacePage-D0Hm21LY.js +2 -0
  50. package/src/client/dist/spa/assets/_plugin-vue_export-helper-CHpmshS7.js +1 -0
  51. package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  52. package/src/client/dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
  53. package/src/client/dist/spa/assets/index-BThMCiY7.css +1 -0
  54. package/src/client/dist/spa/assets/index-CMvo3OTb.js +5 -0
  55. package/src/client/dist/spa/assets/nodes-DeIen-kp.js +1 -0
  56. package/src/client/dist/spa/assets/use-quasar-Dq-Vjx_2.js +1 -0
  57. package/src/client/dist/spa/index.html +4 -0
  58. package/src/mcp-server/kobo-tasks-handlers.ts +54 -0
  59. package/src/mcp-server/kobo-tasks-server.ts +128 -0
@@ -0,0 +1,117 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isGitBranchExistsError } from '../utils/git-ops.js';
5
+ function git(repoPath, args) {
6
+ return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
7
+ }
8
+ function getExcludeFilePath(projectPath) {
9
+ return path.join(projectPath, '.git', 'info', 'exclude');
10
+ }
11
+ function addToExclude(projectPath, worktreePath) {
12
+ const excludeFile = getExcludeFilePath(projectPath);
13
+ // Ensure the .git/info directory exists
14
+ const infoDir = path.dirname(excludeFile);
15
+ if (!fs.existsSync(infoDir)) {
16
+ fs.mkdirSync(infoDir, { recursive: true });
17
+ }
18
+ // Make the path relative to projectPath for cleaner exclude entries
19
+ const relativePath = path.relative(projectPath, worktreePath);
20
+ const entry = `/${relativePath}`;
21
+ let current = '';
22
+ if (fs.existsSync(excludeFile)) {
23
+ current = fs.readFileSync(excludeFile, 'utf-8');
24
+ }
25
+ if (!current.split('\n').includes(entry)) {
26
+ const newContent = current.endsWith('\n') || current === '' ? `${current}${entry}\n` : `${current}\n${entry}\n`;
27
+ fs.writeFileSync(excludeFile, newContent, 'utf-8');
28
+ }
29
+ }
30
+ function removeFromExclude(projectPath, worktreePath) {
31
+ const excludeFile = getExcludeFilePath(projectPath);
32
+ if (!fs.existsSync(excludeFile))
33
+ return;
34
+ const relativePath = path.relative(projectPath, worktreePath);
35
+ const entry = `/${relativePath}`;
36
+ const lines = fs.readFileSync(excludeFile, 'utf-8').split('\n');
37
+ const filtered = lines.filter((line) => line !== entry);
38
+ // I3: ensure the file ends with exactly one newline and has no trailing empty lines
39
+ const trimmed = filtered.join('\n').replace(/\n+$/, '');
40
+ fs.writeFileSync(excludeFile, trimmed ? `${trimmed}\n` : '', 'utf-8');
41
+ }
42
+ export function createWorktree(projectPath, branchName, sourceBranch) {
43
+ const worktreesDir = path.join(projectPath, '.worktrees');
44
+ if (!fs.existsSync(worktreesDir)) {
45
+ fs.mkdirSync(worktreesDir, { recursive: true });
46
+ }
47
+ const worktreePath = path.join(worktreesDir, branchName);
48
+ try {
49
+ // Try creating a new branch + worktree
50
+ git(projectPath, ['worktree', 'add', '-b', branchName, worktreePath, sourceBranch]);
51
+ }
52
+ catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ // M3: use shared utility for branch-exists detection
55
+ // If branch already exists, add worktree without creating the branch
56
+ if (isGitBranchExistsError(message)) {
57
+ git(projectPath, ['worktree', 'add', worktreePath, branchName]);
58
+ }
59
+ else {
60
+ throw new Error(`Failed to create worktree for branch '${branchName}': ${message}`);
61
+ }
62
+ }
63
+ addToExclude(projectPath, worktreePath);
64
+ return worktreePath;
65
+ }
66
+ export function removeWorktree(projectPath, worktreePath) {
67
+ try {
68
+ git(projectPath, ['worktree', 'remove', worktreePath, '--force']);
69
+ }
70
+ catch (err) {
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ throw new Error(`Failed to remove worktree '${worktreePath}': ${message}`);
73
+ }
74
+ removeFromExclude(projectPath, worktreePath);
75
+ }
76
+ export function listWorktrees(projectPath) {
77
+ const output = git(projectPath, ['worktree', 'list', '--porcelain']);
78
+ const worktrees = [];
79
+ const blocks = output.split('\n\n').filter(Boolean);
80
+ for (const block of blocks) {
81
+ const lines = block.split('\n');
82
+ const worktree = {};
83
+ for (const line of lines) {
84
+ if (line.startsWith('worktree ')) {
85
+ worktree.path = line.slice('worktree '.length).trim();
86
+ }
87
+ else if (line.startsWith('HEAD ')) {
88
+ worktree.head = line.slice('HEAD '.length).trim();
89
+ }
90
+ else if (line.startsWith('branch ')) {
91
+ // branch refs/heads/<name>
92
+ const ref = line.slice('branch '.length).trim();
93
+ worktree.branch = ref.replace(/^refs\/heads\//, '');
94
+ }
95
+ else if (line === 'detached') {
96
+ worktree.branch = '(detached HEAD)';
97
+ }
98
+ }
99
+ if (worktree.path) {
100
+ worktrees.push({
101
+ path: worktree.path,
102
+ branch: worktree.branch ?? '',
103
+ head: worktree.head ?? '',
104
+ });
105
+ }
106
+ }
107
+ return worktrees;
108
+ }
109
+ export function worktreeExists(projectPath, branchName) {
110
+ try {
111
+ const worktrees = listWorktrees(projectPath);
112
+ return worktrees.some((wt) => wt.branch === branchName);
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
@@ -0,0 +1,117 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ function git(repoPath, args) {
3
+ return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8' }).trim();
4
+ }
5
+ export function getCurrentBranch(repoPath) {
6
+ return git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
7
+ }
8
+ export function listBranches(repoPath) {
9
+ const output = git(repoPath, ['branch', '--format=%(refname:short)']);
10
+ return output
11
+ .split('\n')
12
+ .map((b) => b.trim())
13
+ .filter(Boolean);
14
+ }
15
+ export class BranchAlreadyExistsError extends Error {
16
+ constructor(branchName) {
17
+ super(`Branch '${branchName}' already exists`);
18
+ this.name = 'BranchAlreadyExistsError';
19
+ }
20
+ }
21
+ /**
22
+ * M3: Shared utility to detect "branch already exists" git error messages
23
+ * across different locales (English, French, Russian).
24
+ */
25
+ export function isGitBranchExistsError(message) {
26
+ const lower = message.toLowerCase();
27
+ return lower.includes('already exists') || lower.includes('existe') || lower.includes('существует');
28
+ }
29
+ export function createBranch(repoPath, branchName, sourceBranch) {
30
+ try {
31
+ git(repoPath, ['branch', branchName, sourceBranch]);
32
+ }
33
+ catch (err) {
34
+ const message = err instanceof Error ? err.message : String(err);
35
+ if (isGitBranchExistsError(message)) {
36
+ throw new BranchAlreadyExistsError(branchName);
37
+ }
38
+ throw new Error(`Failed to create branch '${branchName}' from '${sourceBranch}': ${message}`);
39
+ }
40
+ }
41
+ export function getDiffStats(repoPath) {
42
+ try {
43
+ const output = git(repoPath, ['diff', '--cached', '--shortstat']);
44
+ return parseDiffShortstat(output);
45
+ }
46
+ catch {
47
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
48
+ }
49
+ }
50
+ function parseDiffShortstat(output) {
51
+ if (!output.trim()) {
52
+ return { filesChanged: 0, insertions: 0, deletions: 0 };
53
+ }
54
+ const filesMatch = output.match(/(\d+) file/);
55
+ const insertMatch = output.match(/(\d+) insertion/);
56
+ const deleteMatch = output.match(/(\d+) deletion/);
57
+ return {
58
+ filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
59
+ insertions: insertMatch ? parseInt(insertMatch[1], 10) : 0,
60
+ deletions: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
61
+ };
62
+ }
63
+ export function listRemoteBranches(repoPath) {
64
+ try {
65
+ const output = git(repoPath, ['branch', '-r', '--format=%(refname:short)']);
66
+ return output
67
+ .split('\n')
68
+ .map((b) => b.trim())
69
+ .filter(Boolean);
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ export function deleteLocalBranch(repoPath, branchName) {
76
+ try {
77
+ git(repoPath, ['branch', '-D', branchName]);
78
+ }
79
+ catch (err) {
80
+ const message = err instanceof Error ? err.message : String(err);
81
+ throw new Error(`Failed to delete local branch '${branchName}': ${message}`);
82
+ }
83
+ }
84
+ export function deleteRemoteBranch(repoPath, branchName, remote = 'origin') {
85
+ try {
86
+ git(repoPath, ['push', remote, '--delete', branchName]);
87
+ }
88
+ catch (err) {
89
+ const message = err instanceof Error ? err.message : String(err);
90
+ throw new Error(`Failed to delete remote branch '${remote}/${branchName}': ${message}`);
91
+ }
92
+ }
93
+ export function pushBranch(repoPath, branchName, remote = 'origin') {
94
+ try {
95
+ git(repoPath, ['push', '-u', remote, branchName]);
96
+ }
97
+ catch (err) {
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ throw new Error(`Failed to push branch '${branchName}' to '${remote}': ${message}`);
100
+ }
101
+ }
102
+ export function getCommitsBetween(repoPath, base, head) {
103
+ try {
104
+ return git(repoPath, ['log', `${base}..${head}`, '--pretty=format:- %s (%h)', '--no-merges']);
105
+ }
106
+ catch {
107
+ return '';
108
+ }
109
+ }
110
+ export function getDiffStatsBetween(repoPath, base, head) {
111
+ try {
112
+ return git(repoPath, ['diff', '--shortstat', `${base}...${head}`]);
113
+ }
114
+ catch {
115
+ return '';
116
+ }
117
+ }
@@ -0,0 +1,95 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ // Package root resolved from this file's location via pure path arithmetic
6
+ // (no filesystem calls, so this stays robust when tests mock `node:fs`).
7
+ // This file lives at `<root>/src/server/utils/paths.ts` in dev (tsx) and at
8
+ // `<root>/dist/server/utils/paths.js` in production (node) — both are exactly
9
+ // three directories deep from the package root.
10
+ //
11
+ // Do NOT move this file without updating the parent count. A unit test in
12
+ // src/__tests__/paths.test.ts verifies the result points at a directory that
13
+ // contains package.json.
14
+ const selfDir = path.dirname(fileURLToPath(import.meta.url));
15
+ const packageRoot = path.resolve(selfDir, '..', '..', '..');
16
+ /**
17
+ * Resolves a path inside the Kōbō package (e.g. compiled MCP server, built SPA).
18
+ * Never use this for user data — those go through getKoboHome() / getDataDir().
19
+ */
20
+ export function getPackageAssetPath(...parts) {
21
+ return path.join(packageRoot, ...parts);
22
+ }
23
+ /**
24
+ * Resolves the Kōbō home directory for user data (DB, settings). Respects
25
+ * KOBO_HOME when set, otherwise defaults to an XDG-compliant location under
26
+ * ~/.config/kobo. The directory is NOT created here — callers are responsible
27
+ * for mkdir before writing.
28
+ *
29
+ * Dev workflow: `npm run dev` sets KOBO_HOME=./data so local development uses
30
+ * the repo-relative data/ directory and never touches the user's real home.
31
+ */
32
+ export function getKoboHome() {
33
+ if (process.env.KOBO_HOME) {
34
+ return path.resolve(process.env.KOBO_HOME);
35
+ }
36
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME;
37
+ if (xdgConfigHome) {
38
+ return path.join(xdgConfigHome, 'kobo');
39
+ }
40
+ return path.join(os.homedir(), '.config', 'kobo');
41
+ }
42
+ /**
43
+ * Same as getKoboHome(), but guarantees the directory exists on disk. Call this
44
+ * before any filesystem write into the Kōbō home.
45
+ */
46
+ export function ensureKoboHome() {
47
+ const dir = getKoboHome();
48
+ if (!fs.existsSync(dir)) {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ }
51
+ return dir;
52
+ }
53
+ /**
54
+ * Absolute path to the SQLite database file under the Kōbō home.
55
+ */
56
+ export function getDbPath() {
57
+ return path.join(getKoboHome(), 'kobo.db');
58
+ }
59
+ /**
60
+ * Absolute path to settings.json under the Kōbō home.
61
+ */
62
+ export function getSettingsPath() {
63
+ return path.join(getKoboHome(), 'settings.json');
64
+ }
65
+ /**
66
+ * Absolute path to skills.json under the Kōbō home — cached list of Claude
67
+ * Code slash commands discovered from system init events.
68
+ */
69
+ export function getSkillsPath() {
70
+ return path.join(getKoboHome(), 'skills.json');
71
+ }
72
+ /**
73
+ * Absolute path to the compiled MCP server entry (shipped in the published
74
+ * package as dist/mcp-server/kobo-tasks-server.js). Returns null if not
75
+ * present — callers (agent-manager) then fall back to the TS source for dev.
76
+ */
77
+ export function getCompiledMcpServerPath() {
78
+ const compiled = getPackageAssetPath('dist', 'mcp-server', 'kobo-tasks-server.js');
79
+ return fs.existsSync(compiled) ? compiled : null;
80
+ }
81
+ /**
82
+ * Absolute path to the MCP server TypeScript source (used in dev when the
83
+ * compiled version is absent).
84
+ */
85
+ export function getMcpServerSourcePath() {
86
+ return getPackageAssetPath('src', 'mcp-server', 'kobo-tasks-server.ts');
87
+ }
88
+ /**
89
+ * Absolute path to the built Quasar SPA (src/client/dist/spa). Returns null
90
+ * if the SPA has not been built yet.
91
+ */
92
+ export function getClientSpaPath() {
93
+ const spa = getPackageAssetPath('src', 'client', 'dist', 'spa');
94
+ return fs.existsSync(spa) ? spa : null;
95
+ }
@@ -0,0 +1,46 @@
1
+ const trackedProcesses = new Map();
2
+ export function registerProcess(id, proc) {
3
+ trackedProcesses.set(id, proc);
4
+ }
5
+ export function unregisterProcess(id) {
6
+ trackedProcesses.delete(id);
7
+ }
8
+ export function getProcess(id) {
9
+ return trackedProcesses.get(id);
10
+ }
11
+ export function getTrackedCount() {
12
+ return trackedProcesses.size;
13
+ }
14
+ export function killAll() {
15
+ const procs = [...trackedProcesses.values()];
16
+ trackedProcesses.clear();
17
+ const killTimers = procs.map((proc) => {
18
+ try {
19
+ proc.kill('SIGTERM');
20
+ }
21
+ catch {
22
+ // Process may already be dead
23
+ }
24
+ return setTimeout(() => {
25
+ try {
26
+ if (!proc.killed) {
27
+ proc.kill('SIGKILL');
28
+ }
29
+ }
30
+ catch {
31
+ // Ignore
32
+ }
33
+ }, 5000);
34
+ });
35
+ // Unref timers so they don't keep the process alive
36
+ killTimers.forEach((t) => {
37
+ t.unref?.();
38
+ });
39
+ }
40
+ export function initProcessCleanup() {
41
+ process.on('exit', killAll);
42
+ process.on('SIGINT', () => {
43
+ killAll();
44
+ process.exit(0);
45
+ });
46
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@loicngr/kobo",
3
+ "version": "0.1.1",
4
+ "description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
5
+ "type": "module",
6
+ "license": "GPL-3.0-or-later",
7
+ "bin": {
8
+ "kobo": "./dist/server/index.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "src/client/dist/spa/",
13
+ "src/mcp-server/",
14
+ "AGENTS.md",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": true
21
+ },
22
+ "author": {
23
+ "name": "loicngr",
24
+ "email": "loicnogier@ik.me",
25
+ "url": "https://github.com/loicngr"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/loicngr/kobo.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/loicngr/kobo/issues"
33
+ },
34
+ "homepage": "https://github.com/loicngr/kobo#readme",
35
+ "keywords": [
36
+ "claude",
37
+ "claude-code",
38
+ "agent",
39
+ "mcp",
40
+ "workspace",
41
+ "worktree",
42
+ "hono",
43
+ "vue",
44
+ "quasar"
45
+ ],
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "scripts": {
50
+ "dev": "KOBO_HOME=./data tsx watch src/server/index.ts",
51
+ "dev:client": "cd src/client && npx quasar dev",
52
+ "dev:all": "concurrently \"npm run dev\" \"npm run dev:client\"",
53
+ "build:client": "cd src/client && npx quasar build",
54
+ "build:server": "npx tsc && chmod +x dist/server/index.js",
55
+ "build": "npm run build:client && npm run build:server",
56
+ "start": "node dist/server/index.js",
57
+ "test": "vitest run --config vitest.config.ts --root .",
58
+ "test:watch": "vitest --config vitest.config.ts --root .",
59
+ "lint": "biome check .",
60
+ "lint:fix": "biome check --write .",
61
+ "format": "biome format --write .",
62
+ "format:check": "biome format .",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "dependencies": {
66
+ "@hono/node-server": "^1.19.12",
67
+ "@modelcontextprotocol/sdk": "^1.29.0",
68
+ "better-sqlite3": "^12.8.0",
69
+ "hono": "^4.12.10",
70
+ "nanoid": "^5.1.7",
71
+ "ws": "^8.20.0"
72
+ },
73
+ "devDependencies": {
74
+ "@biomejs/biome": "2.4.10",
75
+ "@types/better-sqlite3": "^7.6.13",
76
+ "@types/node": "^25.5.2",
77
+ "@types/ws": "^8.18.1",
78
+ "@vitest/runner": "^4.1.2",
79
+ "concurrently": "^9.2.1",
80
+ "tsx": "^4.21.0",
81
+ "typescript": "^6.0.2",
82
+ "vitest": "^3.2.4"
83
+ }
84
+ }