@cmgmyr/hive 0.1.0 → 1.1.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.
- package/README.md +7 -8
- package/dist/build-info.json +1 -1
- package/dist/cli.js +38 -2
- package/dist/db.js +11 -0
- package/dist/help.js +3 -1
- package/dist/projectYml.js +17 -0
- package/dist/tools/agents.js +43 -5
- package/dist/transcript.js +6 -3
- package/dist/turnCount.js +26 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center"><img src="https://raw.githubusercontent.com/cmgmyr/hive/main/docs/assets/logo-card.svg" alt="hive" height="112"></p>
|
|
2
|
-
<p align="center"><a href="https://github.com/cmgmyr/hive/actions/workflows/ci.yml"><img src="https://github.com/cmgmyr/hive/actions/workflows/ci.yml/badge.svg" alt="CI"></a></p>
|
|
2
|
+
<p align="center"><a href="https://www.npmjs.com/package/@cmgmyr/hive"><img src="https://img.shields.io/npm/v/%40cmgmyr%2Fhive?label=npm" alt="npm"></a> <a href="https://github.com/cmgmyr/hive/actions/workflows/ci.yml"><img src="https://github.com/cmgmyr/hive/actions/workflows/ci.yml/badge.svg" alt="CI"></a></p>
|
|
3
3
|
|
|
4
4
|
Shared, persistent memory and a visible crew of tmux workers for Claude Code and Codex sessions on one project.
|
|
5
5
|
|
|
@@ -14,17 +14,16 @@ Shared, persistent memory and a visible crew of tmux workers for Claude Code and
|
|
|
14
14
|
Requirements: macOS, Node `^22.14.0 || >=23.6.0`, [Claude Code](https://claude.com/claude-code), and tmux for the agent tools. codex is optional, only needed if a project opts a worker into it; see [docs/install.md](docs/install.md#codex-workers).
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
npm run build
|
|
20
|
-
npm link # puts the hive command on your PATH
|
|
21
|
-
hive setup # pins that command to one interpreter
|
|
17
|
+
npm install -g @cmgmyr/hive
|
|
18
|
+
hive setup # pins the hive command to one interpreter, prints the MCP line
|
|
22
19
|
brew install tmux
|
|
23
|
-
claude mcp add --scope user hive -- "$(command -v node)" "$(
|
|
24
|
-
ln -s "$(
|
|
20
|
+
claude mcp add --scope user hive -- "$(command -v node)" "$(npm root -g)/@cmgmyr/hive/dist/index.js"
|
|
21
|
+
ln -s "$(npm root -g)/@cmgmyr/hive/claude-plugin" ~/.claude/skills/hive # optional: session-start kickoff
|
|
25
22
|
hive doctor # verify: node, ABI, tmux, claude, database, hooks all green
|
|
26
23
|
```
|
|
27
24
|
|
|
25
|
+
Working from a clone instead? See [docs/install.md](docs/install.md#from-source).
|
|
26
|
+
|
|
28
27
|
Put `~/.local/bin` on your PATH below your version manager's block. See [Node version and the interpreter pin](docs/install.md#node-version-and-the-interpreter-pin) for why the order matters.
|
|
29
28
|
|
|
30
29
|
## First run
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1.1.0","sha":"38f535a","dirty":false}
|
package/dist/cli.js
CHANGED
|
@@ -13,12 +13,13 @@ import { ATTACH_MODES, AUTO_ATTACH_MODES, attachMode, isAttachMode, isAutoAttach
|
|
|
13
13
|
import { cliPath, dispatcherDir, dispatcherPath, dispatcherScript, durabilityLines, firstHiveOnPath, linkedWorktreePin, pathAdvice, readDispatcher, } from "./dispatcher.js";
|
|
14
14
|
import { codexConfigPath, codexHiveRegistrations, hiveRegistrations, registrationOffer, registrationProblem, } from "./mcpConfig.js";
|
|
15
15
|
import { DEFAULT_DATA_DIR } from "./dataDir.js";
|
|
16
|
-
import { dataDir, db, migrate } from "./db.js";
|
|
16
|
+
import { dataDir, db, migrate, storeSchemaAhead } from "./db.js";
|
|
17
17
|
import { isLowHeadroom, orphanLoginShellDetails, ptyHeadroom } from "./ptys.js";
|
|
18
18
|
import { addProject, agentProjectPin, currentActor, effectiveProjectId, findProjectForCwd, getProjectByPath, gitPrimaryRoot, getProject, listProjects, registeredAncestor, takeRegistrationNotice, } from "./context.js";
|
|
19
19
|
import { ensureHooksFile } from "./hooks.js";
|
|
20
20
|
import { errorMessage, parseTags, registrationNoticeText, withTrailingNewline } from "./result.js";
|
|
21
21
|
import { ACTIVE_TIMER_WHERE, dashboardFileContained, describeStall, HELD_REASON_CONVERSATION, HELD_REASON_LEAD_PANE_DEAD, HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX, isUnclassifiablePaneHold, HELD_REASON_UNSUBMITTED_INPUT_PREFIX, isUnsubmittedInputHold, janitor, resolveDashboardFile, transcriptStaleness, wasHeldForPaneReissue, } from "./scheduler.js";
|
|
22
|
+
import { readTurnCount } from "./turnCount.js";
|
|
22
23
|
import { STALL_BOUND_SECONDS } from "./backgroundTasks.js";
|
|
23
24
|
import { probeSessionInterpreter, reexecTarget, sessionStartVerdict, } from "./sessionProbe.js";
|
|
24
25
|
import { backupHealth, backupNow, backupsDir, formatBytes, listSnapshots, previewRestore, restoreSnapshot, totalSizeBytes, } from "./backup.js";
|
|
@@ -562,6 +563,8 @@ placement: split # placement for workers and visible processes: s
|
|
|
562
563
|
|
|
563
564
|
# context_checkpoint_percent: null # unset means off; integer 1-100 to enable
|
|
564
565
|
|
|
566
|
+
# lead_turn_budget: {warn: 300, stop: 600} # optional lead statusline thresholds
|
|
567
|
+
|
|
565
568
|
# review_tags: [from-review] # todo tags \`hive doctor\` counts as review findings and
|
|
566
569
|
# reports as triaged (has a comment, completed, or archived)
|
|
567
570
|
# or untriaged. Absent means doctor tracks none.
|
|
@@ -1978,6 +1981,10 @@ function cmdDoctor(argv) {
|
|
|
1978
1981
|
check("claude", () => execFileSync("which", ["claude"], { encoding: "utf8" }).trim());
|
|
1979
1982
|
check("database", () => {
|
|
1980
1983
|
const n = db.prepare("SELECT COUNT(*) AS n FROM migrations").get().n;
|
|
1984
|
+
const ahead = storeSchemaAhead(db);
|
|
1985
|
+
if (ahead) {
|
|
1986
|
+
warn("database", `store schema v${ahead.store} is ahead of this build (v${ahead.build}): a newer hive has migrated your shared store. Upgrade this install before relying on it.`);
|
|
1987
|
+
}
|
|
1981
1988
|
return `${dataDir} (schema v${n})`;
|
|
1982
1989
|
});
|
|
1983
1990
|
check("hooks file", () => ensureHooksFile());
|
|
@@ -2388,6 +2395,7 @@ function cmdStatusline() {
|
|
|
2388
2395
|
AND NOT EXISTS (${OPEN_BLOCKERS_SQL})`);
|
|
2389
2396
|
const pads = count("SELECT COUNT(*) AS n FROM pads WHERE project_id = ? AND archived = 0");
|
|
2390
2397
|
const wakes = count(`SELECT COUNT(*) AS n FROM wakes WHERE project_id = ? AND ${ACTIVE_TIMER_WHERE}`);
|
|
2398
|
+
const schemaAhead = storeSchemaAhead(db);
|
|
2391
2399
|
// One statement, not two: the count and the chosen row must come from the same read, or a wake that
|
|
2392
2400
|
// delivers/re-arms between two separate queries leaves the second with no row to read.
|
|
2393
2401
|
const held = db
|
|
@@ -2403,7 +2411,7 @@ function cmdStatusline() {
|
|
|
2403
2411
|
(SELECT held_reason FROM chosen) AS held_reason,
|
|
2404
2412
|
(SELECT first_held_at FROM chosen) AS first_held_at`)
|
|
2405
2413
|
.get(project.id, `${HELD_REASON_UNSUBMITTED_INPUT_PREFIX}%`, `${HELD_REASON_UNCLASSIFIABLE_PANE_PREFIX}%`, project.id);
|
|
2406
|
-
if (agents + commands + todos + pads + wakes === 0)
|
|
2414
|
+
if (agents + commands + todos + pads + wakes === 0 && !schemaAhead)
|
|
2407
2415
|
return;
|
|
2408
2416
|
const s = (n) => (n === 1 ? "" : "s");
|
|
2409
2417
|
const parts = [
|
|
@@ -2419,6 +2427,34 @@ function cmdStatusline() {
|
|
|
2419
2427
|
const age = held.first_held_at ? humanizeAge(ageSecondsSince(held.first_held_at)) : "?";
|
|
2420
2428
|
parts.push(`${held.n} held (${age}, ${heldReasonLabel(held.held_reason)})`);
|
|
2421
2429
|
}
|
|
2430
|
+
if (schemaAhead)
|
|
2431
|
+
parts.push("store ahead (update hive)");
|
|
2432
|
+
let turns = null;
|
|
2433
|
+
let budget = null;
|
|
2434
|
+
try {
|
|
2435
|
+
const actorId = process.env.HIVE_AGENT_ID;
|
|
2436
|
+
if (actorId) {
|
|
2437
|
+
const lead = db.prepare("SELECT 1 AS lead FROM agents WHERE project_id = ? AND actor_id = ? AND kind = 'lead' AND status = 'running'").get(project.id, actorId);
|
|
2438
|
+
if (lead) {
|
|
2439
|
+
let inputPath = "";
|
|
2440
|
+
if (!process.stdin.isTTY) {
|
|
2441
|
+
try {
|
|
2442
|
+
const raw = readFileSync(0, "utf8").trim();
|
|
2443
|
+
const input = raw ? JSON.parse(raw) : null;
|
|
2444
|
+
inputPath = typeof input?.transcript_path === "string" ? input.transcript_path : "";
|
|
2445
|
+
}
|
|
2446
|
+
catch { }
|
|
2447
|
+
}
|
|
2448
|
+
turns = readTurnCount(inputPath);
|
|
2449
|
+
budget = loadProjectYml(project.path).config?.lead_turn_budget ?? null;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
catch { }
|
|
2454
|
+
if (turns !== null) {
|
|
2455
|
+
const color = budget === null ? "" : turns >= budget.stop ? "\x1b[31m" : turns >= budget.warn ? "\x1b[33m" : "";
|
|
2456
|
+
parts.push(`${color}turns ${turns}${color ? "\x1b[0m" : ""}`);
|
|
2457
|
+
}
|
|
2422
2458
|
console.log(`\x1b[33m⬡\x1b[0m \x1b[2mhive:\x1b[0m ${parts.join(" \x1b[2m·\x1b[0m ")}`);
|
|
2423
2459
|
}
|
|
2424
2460
|
// test/docs.test.mjs fails if a label here is missing from docs/install.md. It checks
|
package/dist/db.js
CHANGED
|
@@ -370,6 +370,17 @@ CREATE INDEX idx_wakes_active ON wakes(project_id, kind)
|
|
|
370
370
|
CREATE INDEX idx_wakes_parent ON wakes(parent_wake_id) WHERE parent_wake_id IS NOT NULL;
|
|
371
371
|
`,
|
|
372
372
|
];
|
|
373
|
+
export function storeSchemaAhead(database) {
|
|
374
|
+
try {
|
|
375
|
+
const { version } = database.prepare("SELECT MAX(version) AS version FROM migrations").get();
|
|
376
|
+
if (version == null || !Number.isFinite(version) || version <= MIGRATIONS.length)
|
|
377
|
+
return null;
|
|
378
|
+
return { store: version, build: MIGRATIONS.length };
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
373
384
|
function readAppliedVersions() {
|
|
374
385
|
return new Set(db.prepare("SELECT version FROM migrations").all().map((r) => r.version));
|
|
375
386
|
}
|
package/dist/help.js
CHANGED
|
@@ -153,7 +153,9 @@ runbook (fork it first), not a pad write.`,
|
|
|
153
153
|
agent_status(name|agent_id, include_brief?) — liveness, current command,
|
|
154
154
|
short tail, and the path to the brief this worker was given
|
|
155
155
|
(include_brief=true returns its text; no transcript records it).
|
|
156
|
-
agent_list(include_closed?) —
|
|
156
|
+
agent_list(include_closed?, limit?, before_id?) — agents with live status.
|
|
157
|
+
include_closed adds closed/parked rows, newest first, limit 20/max 100;
|
|
158
|
+
page with before_id set to the receipt's next_before_id.
|
|
157
159
|
agent_rename(name|agent_id, new_name) — change the display name. actor_id
|
|
158
160
|
stays agent:N, so older pad writes and todo comments still point here.
|
|
159
161
|
A live claude worker is told to retitle its own session, which arrives as
|
package/dist/projectYml.js
CHANGED
|
@@ -65,6 +65,22 @@ export function loadProjectYml(projectPath) {
|
|
|
65
65
|
warnings.push("context_checkpoint_percent must be an integer from 1 through 100; ignoring it.");
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
let lead_turn_budget = null;
|
|
69
|
+
if (root.lead_turn_budget != null) {
|
|
70
|
+
const value = root.lead_turn_budget;
|
|
71
|
+
const budget = typeof value === "object" && value !== null && !Array.isArray(value)
|
|
72
|
+
? value
|
|
73
|
+
: null;
|
|
74
|
+
const warn = budget?.warn;
|
|
75
|
+
const stop = budget?.stop;
|
|
76
|
+
if (typeof warn === "number" && Number.isInteger(warn) && warn > 0 &&
|
|
77
|
+
typeof stop === "number" && Number.isInteger(stop) && stop > warn) {
|
|
78
|
+
lead_turn_budget = { warn, stop };
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
warnings.push("lead_turn_budget must contain positive integer warn and stop values, with stop greater than warn; ignoring it.");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
68
84
|
const lead = typeof root.lead === "string" && root.lead.trim() !== "" ? root.lead.trim() : null;
|
|
69
85
|
let placement = null;
|
|
70
86
|
if (root.placement != null) {
|
|
@@ -231,6 +247,7 @@ export function loadProjectYml(projectPath) {
|
|
|
231
247
|
review_tags,
|
|
232
248
|
lead_branches,
|
|
233
249
|
context_checkpoint_percent,
|
|
250
|
+
lead_turn_budget,
|
|
234
251
|
dashboard,
|
|
235
252
|
vars,
|
|
236
253
|
processes,
|
package/dist/tools/agents.js
CHANGED
|
@@ -837,18 +837,53 @@ export function registerAgents(server) {
|
|
|
837
837
|
};
|
|
838
838
|
}));
|
|
839
839
|
server.registerTool("agent_list", {
|
|
840
|
-
description: "List this project's agents with live status.",
|
|
840
|
+
description: "List this project's agents with live status. Without include_closed, this is every running agent, in full. With include_closed, it is every agent (running and closed/parked), newest first, bounded by limit (default 20, max 100); when the receipt carries next_before_id, page through the rest by passing it back as before_id.",
|
|
841
841
|
inputSchema: {
|
|
842
842
|
include_closed: z.boolean().optional(),
|
|
843
|
+
limit: z
|
|
844
|
+
.number()
|
|
845
|
+
.int()
|
|
846
|
+
.min(1)
|
|
847
|
+
.max(100)
|
|
848
|
+
.optional()
|
|
849
|
+
.describe("Max rows to return when include_closed is true, newest first. Default 20, max 100. Ignored otherwise."),
|
|
850
|
+
before_id: z
|
|
851
|
+
.number()
|
|
852
|
+
.int()
|
|
853
|
+
.positive()
|
|
854
|
+
.optional()
|
|
855
|
+
.describe("Page cursor for include_closed: only rows with id below this value. Pass the previous receipt's next_before_id to get the next page."),
|
|
843
856
|
project_id: projectIdParam,
|
|
844
857
|
},
|
|
845
858
|
}, (args) => run(() => {
|
|
846
859
|
const project = resolveProject(args.project_id);
|
|
847
|
-
let sql = "SELECT * FROM agents WHERE project_id = ?";
|
|
848
|
-
if (!args.include_closed)
|
|
849
|
-
sql += " AND status = 'running'";
|
|
850
|
-
const rows = db.prepare(`${sql} ORDER BY id`).all(project.id);
|
|
851
860
|
const snapshot = liveTargets();
|
|
861
|
+
let rows;
|
|
862
|
+
let total;
|
|
863
|
+
let nextBeforeId;
|
|
864
|
+
if (args.include_closed) {
|
|
865
|
+
const limit = Math.min(args.limit ?? 20, 100);
|
|
866
|
+
total = db.prepare("SELECT COUNT(*) AS n FROM agents WHERE project_id = ?").get(project.id).n;
|
|
867
|
+
let sql = "SELECT * FROM agents WHERE project_id = ?";
|
|
868
|
+
const params = [project.id];
|
|
869
|
+
if (args.before_id != null) {
|
|
870
|
+
sql += " AND id < ?";
|
|
871
|
+
params.push(args.before_id);
|
|
872
|
+
}
|
|
873
|
+
sql += " ORDER BY id DESC LIMIT ?";
|
|
874
|
+
params.push(limit + 1);
|
|
875
|
+
const page = db.prepare(sql).all(...params);
|
|
876
|
+
const truncated = page.length > limit;
|
|
877
|
+
rows = truncated ? page.slice(0, limit) : page;
|
|
878
|
+
if (truncated)
|
|
879
|
+
nextBeforeId = rows[rows.length - 1].id;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
rows = db
|
|
883
|
+
.prepare("SELECT * FROM agents WHERE project_id = ? AND status = 'running' ORDER BY id")
|
|
884
|
+
.all(project.id);
|
|
885
|
+
total = rows.length;
|
|
886
|
+
}
|
|
852
887
|
return {
|
|
853
888
|
project_id: project.id,
|
|
854
889
|
project_name: project.name,
|
|
@@ -857,6 +892,9 @@ export function registerAgents(server) {
|
|
|
857
892
|
note: `${PROBE_FAILED_NOTE} These rows are what the store holds; do not conclude a worker died.`,
|
|
858
893
|
}
|
|
859
894
|
: {}),
|
|
895
|
+
total,
|
|
896
|
+
returned: rows.length,
|
|
897
|
+
...(nextBeforeId !== undefined ? { next_before_id: nextBeforeId } : {}),
|
|
860
898
|
agents: rows.map((r) => {
|
|
861
899
|
const summary = agentSummary(r, snapshot);
|
|
862
900
|
return {
|
package/dist/transcript.js
CHANGED
|
@@ -8,6 +8,9 @@ export function transcriptDirName(cwd) {
|
|
|
8
8
|
export function transcriptDir(cwd) {
|
|
9
9
|
return join(claudeConfigDir(), "projects", transcriptDirName(cwd));
|
|
10
10
|
}
|
|
11
|
+
export function transcriptPath(cwd, sessionId, explicitPath = "") {
|
|
12
|
+
return explicitPath || (sessionId ? join(transcriptDir(cwd), `${sessionId}.jsonl`) : "");
|
|
13
|
+
}
|
|
11
14
|
export function resolveTranscriptDir(cwd) {
|
|
12
15
|
const dir = transcriptDir(cwd);
|
|
13
16
|
return existsSync(dir) ? dir : null;
|
|
@@ -116,8 +119,8 @@ export function readContextFill(kind, worker) {
|
|
|
116
119
|
used_tokens: used, window_tokens: window, used_percent: Math.round(used * 100 / window),
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
|
-
export function readContextTokens(cwd, sessionId,
|
|
120
|
-
if (!sessionId && !
|
|
122
|
+
export function readContextTokens(cwd, sessionId, explicitPath = "") {
|
|
123
|
+
if (!sessionId && !explicitPath)
|
|
121
124
|
return null;
|
|
122
|
-
return claudeTokens(tailRecords(transcriptPath
|
|
125
|
+
return claudeTokens(tailRecords(transcriptPath(cwd, sessionId, explicitPath)));
|
|
123
126
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
export function readTurnCount(path) {
|
|
3
|
+
if (!path)
|
|
4
|
+
return null;
|
|
5
|
+
try {
|
|
6
|
+
const ids = new Set();
|
|
7
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
8
|
+
if (!line.includes('"type":"assistant"'))
|
|
9
|
+
continue;
|
|
10
|
+
try {
|
|
11
|
+
const record = JSON.parse(line);
|
|
12
|
+
if (record.type !== "assistant")
|
|
13
|
+
continue;
|
|
14
|
+
const message = record.message;
|
|
15
|
+
if (typeof message?.id !== "string" || message.id === "" || message.model === "<synthetic>")
|
|
16
|
+
continue;
|
|
17
|
+
ids.add(message.id);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
return ids.size;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmgmyr/hive",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Shared memory and coordination for Claude Code sessions: one lead, many tmux workers, one project-scoped store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc && node scripts/gen-version.mjs",
|
|
12
|
+
"check:mermaid": "node scripts/check-mermaid.mjs",
|
|
12
13
|
"prepublishOnly": "npm run build",
|
|
13
14
|
"watch": "tsc --watch",
|
|
14
15
|
"start": "node dist/index.js",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"url": "git+https://github.com/cmgmyr/hive.git"
|
|
32
33
|
},
|
|
33
34
|
"homepage": "https://github.com/cmgmyr/hive#readme",
|
|
35
|
+
"funding": "https://github.com/sponsors/cmgmyr",
|
|
34
36
|
"bugs": {
|
|
35
37
|
"url": "https://github.com/cmgmyr/hive/issues"
|
|
36
38
|
},
|
|
@@ -48,12 +50,12 @@
|
|
|
48
50
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
49
51
|
"better-sqlite3": "^13.0.3",
|
|
50
52
|
"smol-toml": "^1.8.0",
|
|
51
|
-
"yaml": "^2.9.
|
|
52
|
-
"zod": "^4.
|
|
53
|
+
"yaml": "^2.9.1",
|
|
54
|
+
"zod": "^4.6.5"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@types/better-sqlite3": "^9.6.0",
|
|
56
|
-
"@types/node": "^26.
|
|
58
|
+
"@types/node": "^26.6.2",
|
|
57
59
|
"typescript": "^7.0.2"
|
|
58
60
|
},
|
|
59
61
|
"allowScripts": {
|