@pivanov/claude-wire 0.0.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.
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +28 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +15 -0
- package/dist/cost.d.ts +12 -0
- package/dist/cost.js +36 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +16 -0
- package/dist/parser/content.d.ts +4 -0
- package/dist/parser/content.js +46 -0
- package/dist/parser/ndjson.d.ts +2 -0
- package/dist/parser/ndjson.js +12 -0
- package/dist/parser/translator.d.ts +7 -0
- package/dist/parser/translator.js +127 -0
- package/dist/pipeline.d.ts +8 -0
- package/dist/pipeline.js +47 -0
- package/dist/process.d.ts +15 -0
- package/dist/process.js +179 -0
- package/dist/reader.d.ts +13 -0
- package/dist/reader.js +94 -0
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.js +136 -0
- package/dist/session.d.ts +8 -0
- package/dist/session.js +239 -0
- package/dist/stream.d.ts +9 -0
- package/dist/stream.js +130 -0
- package/dist/tools/handler.d.ts +9 -0
- package/dist/tools/handler.js +18 -0
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.js +42 -0
- package/dist/types/events.d.ts +41 -0
- package/dist/types/events.js +1 -0
- package/dist/types/options.d.ts +41 -0
- package/dist/types/options.js +1 -0
- package/dist/types/protocol.d.ts +42 -0
- package/dist/types/protocol.js +1 -0
- package/dist/types/results.d.ts +17 -0
- package/dist/types/results.js +1 -0
- package/dist/writer.d.ts +7 -0
- package/dist/writer.js +26 -0
- package/package.json +61 -0
package/dist/stream.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createCostTracker } from "./cost.js";
|
|
2
|
+
import { AbortError, ClaudeError, ProcessError } from "./errors.js";
|
|
3
|
+
import { createTranslator } from "./parser/translator.js";
|
|
4
|
+
import { buildResult, extractText } from "./pipeline.js";
|
|
5
|
+
import { spawnClaude } from "./process.js";
|
|
6
|
+
import { readNdjsonEvents } from "./reader.js";
|
|
7
|
+
import { createToolHandler } from "./tools/handler.js";
|
|
8
|
+
const drainStderr = (proc) => {
|
|
9
|
+
const chunks = [];
|
|
10
|
+
const stderrReader = proc.stderr.getReader();
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
const done = (async () => {
|
|
13
|
+
try {
|
|
14
|
+
while (true) {
|
|
15
|
+
const { done: isDone, value } = await stderrReader.read();
|
|
16
|
+
if (isDone) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// process exited
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
stderrReader.releaseLock();
|
|
27
|
+
}
|
|
28
|
+
})().catch(() => { });
|
|
29
|
+
return { chunks, done };
|
|
30
|
+
};
|
|
31
|
+
export const createStream = (prompt, options = {}) => {
|
|
32
|
+
if (options.signal?.aborted) {
|
|
33
|
+
throw new AbortError();
|
|
34
|
+
}
|
|
35
|
+
const proc = spawnClaude({ prompt, ...options });
|
|
36
|
+
const stderr = drainStderr(proc);
|
|
37
|
+
const translator = createTranslator();
|
|
38
|
+
const toolHandler = options.tools ? createToolHandler(options.tools) : undefined;
|
|
39
|
+
const costTracker = createCostTracker({
|
|
40
|
+
maxCostUsd: options.maxCostUsd,
|
|
41
|
+
onCostUpdate: options.onCostUpdate,
|
|
42
|
+
});
|
|
43
|
+
let cachedGenerator;
|
|
44
|
+
const generate = async function* () {
|
|
45
|
+
const stdoutReader = proc.stdout.getReader();
|
|
46
|
+
let turnComplete = false;
|
|
47
|
+
try {
|
|
48
|
+
for await (const event of readNdjsonEvents({
|
|
49
|
+
reader: stdoutReader,
|
|
50
|
+
translator,
|
|
51
|
+
toolHandler,
|
|
52
|
+
proc,
|
|
53
|
+
signal: options.signal,
|
|
54
|
+
})) {
|
|
55
|
+
if (event.type === "turn_complete") {
|
|
56
|
+
costTracker.update(event.costUsd ?? 0, event.inputTokens ?? 0, event.outputTokens ?? 0);
|
|
57
|
+
costTracker.checkBudget();
|
|
58
|
+
turnComplete = true;
|
|
59
|
+
}
|
|
60
|
+
yield event;
|
|
61
|
+
}
|
|
62
|
+
if (!turnComplete) {
|
|
63
|
+
const exitCode = await proc.exited;
|
|
64
|
+
if (exitCode !== 0) {
|
|
65
|
+
let errorMessage = `Claude process exited with code ${exitCode}`;
|
|
66
|
+
await stderr.done;
|
|
67
|
+
const stderrText = stderr.chunks.join("").trim();
|
|
68
|
+
if (stderrText) {
|
|
69
|
+
errorMessage = stderrText;
|
|
70
|
+
}
|
|
71
|
+
throw new ProcessError(errorMessage, exitCode);
|
|
72
|
+
}
|
|
73
|
+
throw new ProcessError("Process exited without completing the turn");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
stdoutReader.releaseLock();
|
|
78
|
+
proc.kill();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const bufferedEvents = [];
|
|
82
|
+
let consumePromise;
|
|
83
|
+
const ensureConsumed = () => {
|
|
84
|
+
if (!consumePromise) {
|
|
85
|
+
if (cachedGenerator) {
|
|
86
|
+
throw new ClaudeError("Cannot call text()/cost()/result() after iterating with for-await. Use one or the other.");
|
|
87
|
+
}
|
|
88
|
+
consumePromise = (async () => {
|
|
89
|
+
cachedGenerator = generate();
|
|
90
|
+
for await (const event of { [Symbol.asyncIterator]: () => cachedGenerator }) {
|
|
91
|
+
bufferedEvents.push(event);
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
}
|
|
95
|
+
return consumePromise;
|
|
96
|
+
};
|
|
97
|
+
const text = async () => {
|
|
98
|
+
await ensureConsumed();
|
|
99
|
+
return extractText(bufferedEvents);
|
|
100
|
+
};
|
|
101
|
+
const cost = async () => {
|
|
102
|
+
await ensureConsumed();
|
|
103
|
+
return costTracker.snapshot();
|
|
104
|
+
};
|
|
105
|
+
const result = async () => {
|
|
106
|
+
await ensureConsumed();
|
|
107
|
+
const sessionId = bufferedEvents.find((e) => e.type === "session_meta")?.sessionId;
|
|
108
|
+
return buildResult(bufferedEvents, costTracker, sessionId);
|
|
109
|
+
};
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
if (!cachedGenerator) {
|
|
112
|
+
proc.kill();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
[Symbol.asyncIterator]: () => {
|
|
117
|
+
if (consumePromise) {
|
|
118
|
+
throw new ClaudeError("Cannot iterate after calling text()/cost()/result(). Use one or the other.");
|
|
119
|
+
}
|
|
120
|
+
cachedGenerator ??= generate();
|
|
121
|
+
return cachedGenerator;
|
|
122
|
+
},
|
|
123
|
+
text,
|
|
124
|
+
cost,
|
|
125
|
+
result,
|
|
126
|
+
[Symbol.asyncDispose]: async () => {
|
|
127
|
+
cleanup();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TToolUseEvent } from "../types/events.js";
|
|
2
|
+
import type { IToolHandler } from "../types/options.js";
|
|
3
|
+
export type TToolDecision = "approve" | "deny" | {
|
|
4
|
+
result: string;
|
|
5
|
+
};
|
|
6
|
+
export interface IToolHandlerInstance {
|
|
7
|
+
decide: (tool: TToolUseEvent) => Promise<TToolDecision>;
|
|
8
|
+
}
|
|
9
|
+
export declare const createToolHandler: (options?: IToolHandler) => IToolHandlerInstance;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const createToolHandler = (options = {}) => {
|
|
2
|
+
const { allowed, blocked, onToolUse } = options;
|
|
3
|
+
const allowedSet = allowed ? new Set(allowed) : undefined;
|
|
4
|
+
const blockedSet = blocked ? new Set(blocked) : undefined;
|
|
5
|
+
const decide = async (tool) => {
|
|
6
|
+
if (blockedSet?.has(tool.toolName)) {
|
|
7
|
+
return "deny";
|
|
8
|
+
}
|
|
9
|
+
if (allowedSet && !allowedSet.has(tool.toolName)) {
|
|
10
|
+
return "deny";
|
|
11
|
+
}
|
|
12
|
+
if (onToolUse) {
|
|
13
|
+
return onToolUse(tool);
|
|
14
|
+
}
|
|
15
|
+
return "approve";
|
|
16
|
+
};
|
|
17
|
+
return { decide };
|
|
18
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Best-effort snapshot of known Claude Code tools. May not be exhaustive.
|
|
2
|
+
// For the authoritative list, check session_meta.tools from a live session.
|
|
3
|
+
export const BUILT_IN_TOOLS = new Set([
|
|
4
|
+
"Read",
|
|
5
|
+
"Write",
|
|
6
|
+
"Edit",
|
|
7
|
+
"Bash",
|
|
8
|
+
"Glob",
|
|
9
|
+
"Grep",
|
|
10
|
+
"Agent",
|
|
11
|
+
"NotebookEdit",
|
|
12
|
+
"WebFetch",
|
|
13
|
+
"WebSearch",
|
|
14
|
+
"TodoRead",
|
|
15
|
+
"TodoWrite",
|
|
16
|
+
"TaskCreate",
|
|
17
|
+
"TaskUpdate",
|
|
18
|
+
"TaskGet",
|
|
19
|
+
"TaskList",
|
|
20
|
+
"TaskStop",
|
|
21
|
+
"TaskOutput",
|
|
22
|
+
"ToolSearch",
|
|
23
|
+
"Monitor",
|
|
24
|
+
"EnterPlanMode",
|
|
25
|
+
"ExitPlanMode",
|
|
26
|
+
"SendMessage",
|
|
27
|
+
"LSP",
|
|
28
|
+
"AskUserQuestion",
|
|
29
|
+
"Skill",
|
|
30
|
+
"CronCreate",
|
|
31
|
+
"CronDelete",
|
|
32
|
+
"CronList",
|
|
33
|
+
"RemoteTrigger",
|
|
34
|
+
"TeamCreate",
|
|
35
|
+
"TeamDelete",
|
|
36
|
+
"EnterWorktree",
|
|
37
|
+
"ExitWorktree",
|
|
38
|
+
"ScheduleWakeup",
|
|
39
|
+
]);
|
|
40
|
+
export const isBuiltInTool = (name) => {
|
|
41
|
+
return BUILT_IN_TOOLS.has(name);
|
|
42
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type TTextEvent = {
|
|
2
|
+
type: "text";
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
export type TThinkingEvent = {
|
|
6
|
+
type: "thinking";
|
|
7
|
+
content: string;
|
|
8
|
+
};
|
|
9
|
+
export type TToolUseEvent = {
|
|
10
|
+
type: "tool_use";
|
|
11
|
+
toolUseId: string;
|
|
12
|
+
toolName: string;
|
|
13
|
+
input: string;
|
|
14
|
+
};
|
|
15
|
+
export type TToolResultEvent = {
|
|
16
|
+
type: "tool_result";
|
|
17
|
+
toolUseId: string;
|
|
18
|
+
output: string;
|
|
19
|
+
isError: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type TSessionMetaEvent = {
|
|
22
|
+
type: "session_meta";
|
|
23
|
+
sessionId: string;
|
|
24
|
+
model: string;
|
|
25
|
+
tools: string[];
|
|
26
|
+
};
|
|
27
|
+
export type TTurnCompleteEvent = {
|
|
28
|
+
type: "turn_complete";
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
costUsd?: number;
|
|
31
|
+
inputTokens?: number;
|
|
32
|
+
outputTokens?: number;
|
|
33
|
+
contextWindow?: number;
|
|
34
|
+
durationMs?: number;
|
|
35
|
+
};
|
|
36
|
+
export type TErrorEvent = {
|
|
37
|
+
type: "error";
|
|
38
|
+
message: string;
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
};
|
|
41
|
+
export type TRelayEvent = TTextEvent | TThinkingEvent | TToolUseEvent | TToolResultEvent | TSessionMetaEvent | TTurnCompleteEvent | TErrorEvent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TToolDecision } from "../tools/handler.js";
|
|
2
|
+
import type { TToolUseEvent } from "./events.js";
|
|
3
|
+
import type { TCostSnapshot } from "./results.js";
|
|
4
|
+
export interface IToolHandler {
|
|
5
|
+
allowed?: string[];
|
|
6
|
+
blocked?: string[];
|
|
7
|
+
onToolUse?: (tool: TToolUseEvent) => Promise<TToolDecision>;
|
|
8
|
+
}
|
|
9
|
+
export interface IClaudeOptions {
|
|
10
|
+
cwd?: string;
|
|
11
|
+
model?: "opus" | "sonnet" | "haiku" | (string & {});
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
appendSystemPrompt?: string;
|
|
14
|
+
allowedTools?: string[];
|
|
15
|
+
disallowedTools?: string[];
|
|
16
|
+
tools?: IToolHandler;
|
|
17
|
+
maxCostUsd?: number;
|
|
18
|
+
maxBudgetUsd?: number;
|
|
19
|
+
onCostUpdate?: (cost: TCostSnapshot) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
resume?: string;
|
|
22
|
+
verbose?: boolean;
|
|
23
|
+
mcpConfig?: string;
|
|
24
|
+
continueSession?: boolean;
|
|
25
|
+
permissionMode?: "default" | "plan" | "auto" | "bypassPermissions" | "acceptEdits" | "dontAsk" | (string & {});
|
|
26
|
+
configDir?: string;
|
|
27
|
+
env?: Record<string, string>;
|
|
28
|
+
addDirs?: string[];
|
|
29
|
+
effort?: "low" | "medium" | "high" | "max";
|
|
30
|
+
includeHookEvents?: boolean;
|
|
31
|
+
includePartialMessages?: boolean;
|
|
32
|
+
bare?: boolean;
|
|
33
|
+
jsonSchema?: string;
|
|
34
|
+
forkSession?: boolean;
|
|
35
|
+
noSessionPersistence?: boolean;
|
|
36
|
+
sessionId?: string;
|
|
37
|
+
settingSources?: string;
|
|
38
|
+
disableSlashCommands?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface ISessionOptions extends IClaudeOptions {
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type TModelUsageEntry = {
|
|
2
|
+
inputTokens: number;
|
|
3
|
+
outputTokens: number;
|
|
4
|
+
cacheReadInputTokens?: number;
|
|
5
|
+
cacheCreationInputTokens?: number;
|
|
6
|
+
contextWindow: number;
|
|
7
|
+
};
|
|
8
|
+
export type TClaudeContentType = "text" | "thinking" | "tool_use" | "tool_result" | (string & {});
|
|
9
|
+
export type TClaudeContent = {
|
|
10
|
+
type: TClaudeContentType;
|
|
11
|
+
text?: string;
|
|
12
|
+
thinking?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
input?: unknown;
|
|
16
|
+
content?: unknown;
|
|
17
|
+
tool_use_id?: string;
|
|
18
|
+
is_error?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type TClaudeMessage = {
|
|
21
|
+
content: TClaudeContent[];
|
|
22
|
+
role?: string;
|
|
23
|
+
stop_reason?: string;
|
|
24
|
+
};
|
|
25
|
+
export type TClaudeEventType = "system" | "assistant" | "user" | "result" | "progress" | "rate_limit_event" | (string & {});
|
|
26
|
+
export type TClaudeEvent = {
|
|
27
|
+
type: TClaudeEventType;
|
|
28
|
+
subtype?: string;
|
|
29
|
+
message?: TClaudeMessage;
|
|
30
|
+
result?: unknown;
|
|
31
|
+
session_id?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
tools?: string[];
|
|
34
|
+
duration_ms?: number;
|
|
35
|
+
duration_api_ms?: number;
|
|
36
|
+
cost_usd?: number;
|
|
37
|
+
total_cost_usd?: number;
|
|
38
|
+
is_error?: boolean;
|
|
39
|
+
num_turns?: number;
|
|
40
|
+
modelUsage?: Record<string, TModelUsageEntry>;
|
|
41
|
+
usage?: unknown;
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TRelayEvent } from "./events.js";
|
|
2
|
+
export type TCostSnapshot = {
|
|
3
|
+
totalUsd: number;
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
};
|
|
7
|
+
export type TAskResult = {
|
|
8
|
+
text: string;
|
|
9
|
+
costUsd: number;
|
|
10
|
+
tokens: {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
};
|
|
14
|
+
duration: number;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
events: TRelayEvent[];
|
|
17
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/writer.d.ts
ADDED
package/dist/writer.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ClaudeError } from "./errors.js";
|
|
2
|
+
const ABORT_LINE = `${JSON.stringify({ type: "abort" })}\n`;
|
|
3
|
+
const requireNonEmpty = (value, name) => {
|
|
4
|
+
if (!value) {
|
|
5
|
+
throw new ClaudeError(`${name} must be a non-empty string`);
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
export const writer = {
|
|
9
|
+
user: (content) => {
|
|
10
|
+
requireNonEmpty(content, "content");
|
|
11
|
+
return `${JSON.stringify({ type: "user", message: { role: "user", content } })}\n`;
|
|
12
|
+
},
|
|
13
|
+
approve: (toolUseId) => {
|
|
14
|
+
requireNonEmpty(toolUseId, "toolUseId");
|
|
15
|
+
return `${JSON.stringify({ type: "approve", tool_use_id: toolUseId })}\n`;
|
|
16
|
+
},
|
|
17
|
+
deny: (toolUseId) => {
|
|
18
|
+
requireNonEmpty(toolUseId, "toolUseId");
|
|
19
|
+
return `${JSON.stringify({ type: "deny", tool_use_id: toolUseId })}\n`;
|
|
20
|
+
},
|
|
21
|
+
toolResult: (toolUseId, content) => {
|
|
22
|
+
requireNonEmpty(toolUseId, "toolUseId");
|
|
23
|
+
return `${JSON.stringify({ type: "tool_result", tool_use_id: toolUseId, content })}\n`;
|
|
24
|
+
},
|
|
25
|
+
abort: () => ABORT_LINE,
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pivanov/claude-wire",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Run Claude Code programmatically. Typed SDK for spawning, streaming, and controlling the CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22",
|
|
8
|
+
"bun": ">=1.0"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Pavel Ivanov <iweb.ivanov@gmail.com>",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/pivanov/claude-wire",
|
|
27
|
+
"directory": "packages/claude-wire"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://pivanov.github.io/claude-wire/",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/pivanov/claude-wire/issues"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"claude",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"anthropic",
|
|
37
|
+
"sdk",
|
|
38
|
+
"stream-json",
|
|
39
|
+
"ndjson",
|
|
40
|
+
"streaming",
|
|
41
|
+
"typescript",
|
|
42
|
+
"cli",
|
|
43
|
+
"agent",
|
|
44
|
+
"ai",
|
|
45
|
+
"tool-use",
|
|
46
|
+
"process-management",
|
|
47
|
+
"async-iterable"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"test": "bun test",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"knip": "knip-bun",
|
|
54
|
+
"prepublishOnly": "cp ../../README.md ./README.md && bun run build"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/bun": "^1.3.12",
|
|
58
|
+
"knip": "^6.4.1",
|
|
59
|
+
"typescript": "^6.0.2"
|
|
60
|
+
}
|
|
61
|
+
}
|