@neeter/server 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dan Leeper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ export type { CustomEvent, SSEEvent } from "@neeter/types";
2
+ export { PermissionGate } from "./permission-gate.js";
3
+ export { PushChannel } from "./push-channel.js";
4
+ export { createAgentRouter } from "./router.js";
5
+ export { type Session, type SessionInit, SessionManager } from "./session.js";
6
+ export { MessageTranslator, sseEncode, streamSession, type TranslatorConfig, } from "./translator.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { PermissionGate } from "./permission-gate.js";
2
+ export { PushChannel } from "./push-channel.js";
3
+ export { createAgentRouter } from "./router.js";
4
+ export { SessionManager } from "./session.js";
5
+ export { MessageTranslator, sseEncode, streamSession, } from "./translator.js";
@@ -0,0 +1,12 @@
1
+ import type { PermissionRequest, PermissionResponse } from "@neeter/types";
2
+ type RequestListener = (request: PermissionRequest) => void;
3
+ export declare class PermissionGate {
4
+ private pending;
5
+ private listeners;
6
+ request(permissionRequest: PermissionRequest): Promise<PermissionResponse>;
7
+ respond(response: PermissionResponse): boolean;
8
+ onRequest(listener: RequestListener): () => void;
9
+ getPending(): PermissionRequest[];
10
+ cancelAll(message: string): void;
11
+ }
12
+ export {};
@@ -0,0 +1,41 @@
1
+ export class PermissionGate {
2
+ pending = new Map();
3
+ listeners = new Set();
4
+ request(permissionRequest) {
5
+ return new Promise((resolve) => {
6
+ this.pending.set(permissionRequest.requestId, {
7
+ request: permissionRequest,
8
+ resolve,
9
+ });
10
+ for (const listener of this.listeners) {
11
+ listener(permissionRequest);
12
+ }
13
+ });
14
+ }
15
+ respond(response) {
16
+ const entry = this.pending.get(response.requestId);
17
+ if (!entry)
18
+ return false;
19
+ this.pending.delete(response.requestId);
20
+ entry.resolve(response);
21
+ return true;
22
+ }
23
+ onRequest(listener) {
24
+ this.listeners.add(listener);
25
+ return () => this.listeners.delete(listener);
26
+ }
27
+ getPending() {
28
+ return [...this.pending.values()].map((p) => p.request);
29
+ }
30
+ cancelAll(message) {
31
+ for (const [id, entry] of this.pending) {
32
+ if (entry.request.kind === "tool_approval") {
33
+ entry.resolve({ kind: "tool_approval", requestId: id, behavior: "deny", message });
34
+ }
35
+ else {
36
+ entry.resolve({ kind: "user_question", requestId: id, answers: {} });
37
+ }
38
+ }
39
+ this.pending.clear();
40
+ }
41
+ }
@@ -0,0 +1,8 @@
1
+ export declare class PushChannel<T> implements AsyncIterable<T> {
2
+ private queue;
3
+ private resolve;
4
+ private done;
5
+ push(value: T): void;
6
+ close(): void;
7
+ [Symbol.asyncIterator](): AsyncIterator<T>;
8
+ }
@@ -0,0 +1,40 @@
1
+ export class PushChannel {
2
+ queue = [];
3
+ resolve = null;
4
+ done = false;
5
+ push(value) {
6
+ if (this.done)
7
+ return;
8
+ if (this.resolve) {
9
+ const r = this.resolve;
10
+ this.resolve = null;
11
+ r({ value, done: false });
12
+ }
13
+ else {
14
+ this.queue.push(value);
15
+ }
16
+ }
17
+ close() {
18
+ this.done = true;
19
+ if (this.resolve) {
20
+ const r = this.resolve;
21
+ this.resolve = null;
22
+ r({ value: undefined, done: true });
23
+ }
24
+ }
25
+ [Symbol.asyncIterator]() {
26
+ return {
27
+ next: () => {
28
+ if (this.queue.length > 0) {
29
+ return Promise.resolve({ value: this.queue.shift(), done: false });
30
+ }
31
+ if (this.done) {
32
+ return Promise.resolve({ value: undefined, done: true });
33
+ }
34
+ return new Promise((resolve) => {
35
+ this.resolve = resolve;
36
+ });
37
+ },
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,8 @@
1
+ import { Hono } from "hono";
2
+ import type { SessionManager } from "./session.js";
3
+ import { type MessageTranslator } from "./translator.js";
4
+ export declare function createAgentRouter<TCtx>(config: {
5
+ sessions: SessionManager<TCtx>;
6
+ translator: MessageTranslator<TCtx>;
7
+ basePath?: string;
8
+ }): Hono;
package/dist/router.js ADDED
@@ -0,0 +1,67 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { sseEncode, streamSession } from "./translator.js";
4
+ export function createAgentRouter(config) {
5
+ const { sessions, translator, basePath = "/api" } = config;
6
+ const app = new Hono();
7
+ app.use(`${basePath}/*`, cors({ origin: "*" }));
8
+ app.post(`${basePath}/sessions`, (c) => {
9
+ const session = sessions.create();
10
+ return c.json({ sessionId: session.id });
11
+ });
12
+ app.post(`${basePath}/sessions/:id/messages`, async (c) => {
13
+ const session = sessions.get(c.req.param("id"));
14
+ if (!session)
15
+ return c.json({ error: "Session not found" }, 404);
16
+ const body = await c.req.json();
17
+ if (!body.text?.trim())
18
+ return c.json({ error: "Message text required" }, 400);
19
+ session.pushMessage(body.text.trim());
20
+ return c.json({ ok: true });
21
+ });
22
+ app.get(`${basePath}/sessions/:id/events`, (c) => {
23
+ const session = sessions.get(c.req.param("id"));
24
+ if (!session)
25
+ return c.json({ error: "Session not found" }, 404);
26
+ const stream = new ReadableStream({
27
+ async start(controller) {
28
+ const encoder = new TextEncoder();
29
+ try {
30
+ for await (const evt of streamSession(session, translator)) {
31
+ controller.enqueue(encoder.encode(sseEncode(evt)));
32
+ }
33
+ }
34
+ catch (err) {
35
+ const message = err instanceof Error ? err.message : "Unknown error";
36
+ controller.enqueue(encoder.encode(sseEncode({ event: "error", data: JSON.stringify({ message }) })));
37
+ }
38
+ finally {
39
+ controller.close();
40
+ }
41
+ },
42
+ });
43
+ return new Response(stream, {
44
+ headers: {
45
+ "Content-Type": "text/event-stream",
46
+ "Cache-Control": "no-cache",
47
+ Connection: "keep-alive",
48
+ },
49
+ });
50
+ });
51
+ app.post(`${basePath}/sessions/:id/permissions`, async (c) => {
52
+ const session = sessions.get(c.req.param("id"));
53
+ if (!session)
54
+ return c.json({ error: "Session not found" }, 404);
55
+ const body = await c.req.json();
56
+ if (!body.requestId || !body.kind) {
57
+ return c.json({ error: "Invalid permission response" }, 400);
58
+ }
59
+ const resolved = session.permissionGate.respond(body);
60
+ if (!resolved) {
61
+ return c.json({ error: "No pending request with this ID" }, 404);
62
+ }
63
+ session.lastActivityAt = Date.now();
64
+ return c.json({ ok: true });
65
+ });
66
+ return app;
67
+ }
@@ -0,0 +1,40 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { PermissionGate } from "./permission-gate.js";
3
+ type SDKMessage = ReturnType<typeof query> extends AsyncGenerator<infer T> ? T : never;
4
+ export interface SessionInit<TCtx> {
5
+ context: TCtx;
6
+ model: string;
7
+ systemPrompt: string;
8
+ mcpServers?: Record<string, unknown>;
9
+ tools?: unknown[];
10
+ allowedTools?: string[];
11
+ maxTurns?: number;
12
+ permissionMode?: "default" | "acceptEdits" | "plan" | "bypassPermissions";
13
+ thinking?: {
14
+ type: "enabled";
15
+ budgetTokens: number;
16
+ } | {
17
+ type: "disabled";
18
+ };
19
+ }
20
+ export interface Session<TCtx> {
21
+ id: string;
22
+ context: TCtx;
23
+ pushMessage(text: string): void;
24
+ messageIterator: AsyncIterable<SDKMessage>;
25
+ permissionGate: PermissionGate;
26
+ abort(): void;
27
+ createdAt: number;
28
+ lastActivityAt: number;
29
+ }
30
+ export declare class SessionManager<TCtx> {
31
+ private sessions;
32
+ private factory;
33
+ private idleTimeoutMs;
34
+ constructor(factory: () => SessionInit<TCtx>, idleTimeoutMs?: number);
35
+ create(): Session<TCtx>;
36
+ get(id: string): Session<TCtx> | undefined;
37
+ delete(id: string): void;
38
+ cleanup(): void;
39
+ }
40
+ export {};
@@ -0,0 +1,117 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+ import { PermissionGate } from "./permission-gate.js";
3
+ import { PushChannel } from "./push-channel.js";
4
+ function userMessage(content) {
5
+ return {
6
+ type: "user",
7
+ message: { role: "user", content },
8
+ parent_tool_use_id: null,
9
+ session_id: "",
10
+ };
11
+ }
12
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
13
+ export class SessionManager {
14
+ sessions = new Map();
15
+ factory;
16
+ idleTimeoutMs;
17
+ constructor(factory, idleTimeoutMs) {
18
+ this.factory = factory;
19
+ this.idleTimeoutMs = idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
20
+ }
21
+ create() {
22
+ const id = crypto.randomUUID();
23
+ const init = this.factory();
24
+ const channel = new PushChannel();
25
+ const abortController = new AbortController();
26
+ const permissionGate = new PermissionGate();
27
+ const permissionMode = init.permissionMode ?? "bypassPermissions";
28
+ const isBypass = permissionMode === "bypassPermissions";
29
+ const canUseTool = isBypass
30
+ ? undefined
31
+ : async (toolName, input, options) => {
32
+ const requestId = crypto.randomUUID();
33
+ if (toolName === "AskUserQuestion") {
34
+ const questions = (input.questions ?? []);
35
+ const response = await permissionGate.request({
36
+ kind: "user_question",
37
+ requestId,
38
+ questions,
39
+ });
40
+ if (response.kind === "user_question") {
41
+ return {
42
+ behavior: "allow",
43
+ updatedInput: { ...input, answers: response.answers },
44
+ };
45
+ }
46
+ return { behavior: "deny", message: "Cancelled" };
47
+ }
48
+ const response = await permissionGate.request({
49
+ kind: "tool_approval",
50
+ requestId,
51
+ toolName,
52
+ toolUseId: options.toolUseID,
53
+ input,
54
+ description: input.description,
55
+ });
56
+ if (response.kind === "tool_approval" && response.behavior === "allow") {
57
+ return { behavior: "allow", updatedInput: input };
58
+ }
59
+ const message = response.kind === "tool_approval" ? (response.message ?? "Denied by user") : "Denied";
60
+ return { behavior: "deny", message };
61
+ };
62
+ const messageIterator = query({
63
+ prompt: channel,
64
+ options: {
65
+ systemPrompt: init.systemPrompt,
66
+ model: init.model,
67
+ tools: init.tools ?? [],
68
+ mcpServers: init.mcpServers,
69
+ allowedTools: init.allowedTools,
70
+ maxTurns: init.maxTurns ?? 200,
71
+ permissionMode,
72
+ ...(isBypass ? { allowDangerouslySkipPermissions: true } : {}),
73
+ includePartialMessages: true,
74
+ abortController,
75
+ ...(canUseTool ? { canUseTool } : {}),
76
+ ...(init.thinking ? { thinking: init.thinking } : {}),
77
+ },
78
+ });
79
+ const session = {
80
+ id,
81
+ context: init.context,
82
+ pushMessage: (text) => {
83
+ session.lastActivityAt = Date.now();
84
+ channel.push(userMessage(text));
85
+ },
86
+ messageIterator,
87
+ permissionGate,
88
+ abort: () => {
89
+ permissionGate.cancelAll("Session aborted");
90
+ abortController.abort();
91
+ },
92
+ createdAt: Date.now(),
93
+ lastActivityAt: Date.now(),
94
+ };
95
+ this.sessions.set(id, session);
96
+ return session;
97
+ }
98
+ get(id) {
99
+ return this.sessions.get(id);
100
+ }
101
+ delete(id) {
102
+ const session = this.sessions.get(id);
103
+ if (session) {
104
+ session.abort();
105
+ this.sessions.delete(id);
106
+ }
107
+ }
108
+ cleanup() {
109
+ const now = Date.now();
110
+ for (const [id, session] of this.sessions) {
111
+ if (now - session.lastActivityAt > this.idleTimeoutMs) {
112
+ session.abort();
113
+ this.sessions.delete(id);
114
+ }
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,15 @@
1
+ import type { CustomEvent, SSEEvent } from "@neeter/types";
2
+ import type { Session } from "./session.js";
3
+ export interface TranslatorConfig<TCtx> {
4
+ onToolResult?: (toolName: string, result: string, session: Session<TCtx>) => CustomEvent[];
5
+ }
6
+ export declare class MessageTranslator<TCtx> {
7
+ private config;
8
+ private toolNames;
9
+ private hadStreamThinking;
10
+ constructor(config?: TranslatorConfig<TCtx>);
11
+ translate(message: Record<string, unknown>, session: Session<TCtx>): SSEEvent[];
12
+ private lastToolId;
13
+ }
14
+ export declare function sseEncode(evt: SSEEvent): string;
15
+ export declare function streamSession<TCtx>(session: Session<TCtx>, translator: MessageTranslator<TCtx>): AsyncGenerator<SSEEvent>;
@@ -0,0 +1,236 @@
1
+ import { PushChannel } from "./push-channel.js";
2
+ export class MessageTranslator {
3
+ config;
4
+ toolNames = new Map();
5
+ hadStreamThinking = false;
6
+ constructor(config) {
7
+ this.config = config ?? {};
8
+ }
9
+ translate(message, session) {
10
+ const events = [];
11
+ const type = message.type;
12
+ switch (type) {
13
+ case "stream_event": {
14
+ if (!("event" in message))
15
+ break;
16
+ const event = message.event;
17
+ switch (event.type) {
18
+ case "message_start": {
19
+ this.hadStreamThinking = false;
20
+ events.push({ event: "message_start", data: "{}" });
21
+ break;
22
+ }
23
+ case "content_block_start": {
24
+ const block = event.content_block;
25
+ switch (block?.type) {
26
+ case "tool_use":
27
+ case "server_tool_use": {
28
+ const id = block.id;
29
+ const name = block.name;
30
+ this.toolNames.set(id, name);
31
+ events.push({
32
+ event: "tool_start",
33
+ data: JSON.stringify({ id, name }),
34
+ });
35
+ break;
36
+ }
37
+ case "thinking": {
38
+ events.push({ event: "thinking_start", data: "{}" });
39
+ break;
40
+ }
41
+ case "web_search_tool_result": {
42
+ const toolUseId = block.tool_use_id;
43
+ const toolName = this.toolNames.get(toolUseId);
44
+ const result = JSON.stringify(block.content);
45
+ events.push({
46
+ event: "tool_result",
47
+ data: JSON.stringify({ toolUseId, result }),
48
+ });
49
+ if (this.config.onToolResult && toolName) {
50
+ for (const c of this.config.onToolResult(toolName, result, session)) {
51
+ events.push({ event: "custom", data: JSON.stringify(c) });
52
+ }
53
+ }
54
+ break;
55
+ }
56
+ }
57
+ break;
58
+ }
59
+ case "content_block_delta": {
60
+ const delta = event.delta;
61
+ if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
62
+ this.hadStreamThinking = true;
63
+ events.push({
64
+ event: "thinking_delta",
65
+ data: JSON.stringify({ text: delta.thinking }),
66
+ });
67
+ }
68
+ else if (delta.type === "text_delta" && typeof delta.text === "string") {
69
+ events.push({
70
+ event: "text_delta",
71
+ data: JSON.stringify({ text: delta.text }),
72
+ });
73
+ }
74
+ else if (delta.type === "input_json_delta" &&
75
+ typeof delta.partial_json === "string") {
76
+ const id = this.lastToolId();
77
+ if (id) {
78
+ events.push({
79
+ event: "tool_input_delta",
80
+ data: JSON.stringify({ id, partialJson: delta.partial_json }),
81
+ });
82
+ }
83
+ }
84
+ break;
85
+ }
86
+ }
87
+ break;
88
+ }
89
+ case "assistant": {
90
+ const msg = message.message;
91
+ if (msg?.content) {
92
+ for (const block of msg.content) {
93
+ switch (block.type) {
94
+ case "thinking": {
95
+ if (!this.hadStreamThinking && typeof block.thinking === "string") {
96
+ events.push({
97
+ event: "thinking_delta",
98
+ data: JSON.stringify({ text: block.thinking }),
99
+ });
100
+ }
101
+ break;
102
+ }
103
+ case "tool_use":
104
+ case "server_tool_use": {
105
+ const name = block.name;
106
+ const id = block.id;
107
+ this.toolNames.set(id, name);
108
+ events.push({
109
+ event: "tool_call",
110
+ data: JSON.stringify({ id, name, input: block.input }),
111
+ });
112
+ break;
113
+ }
114
+ case "web_search_tool_result": {
115
+ const toolUseId = block.tool_use_id;
116
+ const toolName = this.toolNames.get(toolUseId);
117
+ const result = JSON.stringify(block.content);
118
+ events.push({
119
+ event: "tool_result",
120
+ data: JSON.stringify({ toolUseId, result }),
121
+ });
122
+ if (this.config.onToolResult && toolName) {
123
+ for (const c of this.config.onToolResult(toolName, result, session)) {
124
+ events.push({ event: "custom", data: JSON.stringify(c) });
125
+ }
126
+ }
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ break;
133
+ }
134
+ case "user": {
135
+ const msg = message.message;
136
+ if (Array.isArray(msg?.content)) {
137
+ for (const block of msg.content) {
138
+ if (block.type === "tool_result") {
139
+ const text = extractToolResultText(block);
140
+ const toolName = this.toolNames.get(block.tool_use_id);
141
+ events.push({
142
+ event: "tool_result",
143
+ data: JSON.stringify({ toolUseId: block.tool_use_id, result: text }),
144
+ });
145
+ if (this.config.onToolResult && toolName) {
146
+ for (const c of this.config.onToolResult(toolName, text, session)) {
147
+ events.push({ event: "custom", data: JSON.stringify(c) });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ break;
154
+ }
155
+ case "tool_progress": {
156
+ events.push({
157
+ event: "tool_progress",
158
+ data: JSON.stringify({
159
+ toolName: message.tool_name,
160
+ elapsed: message.elapsed_time_seconds,
161
+ }),
162
+ });
163
+ break;
164
+ }
165
+ case "result": {
166
+ if (message.subtype === "success") {
167
+ events.push({
168
+ event: "turn_complete",
169
+ data: JSON.stringify({
170
+ numTurns: message.num_turns ?? 0,
171
+ cost: message.total_cost_usd ?? 0,
172
+ }),
173
+ });
174
+ }
175
+ else {
176
+ events.push({
177
+ event: "session_error",
178
+ data: JSON.stringify({ subtype: message.subtype }),
179
+ });
180
+ }
181
+ break;
182
+ }
183
+ }
184
+ return events;
185
+ }
186
+ lastToolId() {
187
+ const entries = [...this.toolNames.entries()];
188
+ return entries.length > 0 ? entries[entries.length - 1][0] : undefined;
189
+ }
190
+ }
191
+ export function sseEncode(evt) {
192
+ return `event: ${evt.event}\ndata: ${evt.data}\n\n`;
193
+ }
194
+ export async function* streamSession(session, translator) {
195
+ const output = new PushChannel();
196
+ const unsubscribe = session.permissionGate.onRequest((request) => {
197
+ output.push({ event: "permission_request", data: JSON.stringify(request) });
198
+ });
199
+ for (const pending of session.permissionGate.getPending()) {
200
+ yield { event: "permission_request", data: JSON.stringify(pending) };
201
+ }
202
+ const driveMessages = async () => {
203
+ try {
204
+ for await (const message of session.messageIterator) {
205
+ const events = translator.translate(message, session);
206
+ for (const evt of events) {
207
+ output.push(evt);
208
+ }
209
+ }
210
+ }
211
+ finally {
212
+ unsubscribe();
213
+ output.close();
214
+ }
215
+ };
216
+ driveMessages();
217
+ for await (const evt of output) {
218
+ yield evt;
219
+ }
220
+ }
221
+ function extractToolResultText(block) {
222
+ const content = block.content;
223
+ if (typeof content === "string")
224
+ return content;
225
+ if (Array.isArray(content)) {
226
+ const textParts = content
227
+ .filter((p) => p.type === "text" && typeof p.text === "string")
228
+ .map((p) => p.text);
229
+ if (textParts.length > 0)
230
+ return textParts.join("");
231
+ // Non-text content blocks (e.g. web_search_tool_result) — serialize so
232
+ // downstream consumers (widgets, onToolResult) can still parse the data.
233
+ return JSON.stringify(content);
234
+ }
235
+ return "";
236
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@neeter/server",
3
+ "version": "0.6.0",
4
+ "description": "Hono server toolkit for building chat UIs on top of the Claude Agent SDK",
5
+ "license": "MIT",
6
+ "author": "Dan Leeper",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/quantumleeps/neeter.git",
10
+ "directory": "packages/server"
11
+ },
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "dependencies": {
23
+ "@neeter/types": "0.6.0"
24
+ },
25
+ "peerDependencies": {
26
+ "@anthropic-ai/claude-agent-sdk": ">=0.2.0",
27
+ "hono": ">=4.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@anthropic-ai/claude-agent-sdk": "latest",
31
+ "hono": "^4.11.9"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json"
35
+ }
36
+ }