threenative-engine-mcp 0.2.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 +21 -0
- package/README.md +14 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +253 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João Paulo Furtado
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# threenative-engine-mcp
|
|
2
|
+
|
|
3
|
+
An offline MCP server that lets a ThreeNative authoring agent search the committed engine
|
|
4
|
+
capability manifest by situation and inspect one capability in detail.
|
|
5
|
+
|
|
6
|
+
The server exposes exactly two read-only tools: `engine_search_capabilities` and
|
|
7
|
+
`engine_capability_detail`.
|
|
8
|
+
|
|
9
|
+
First infer the concrete mechanics implied by the request. Call `engine_search_capabilities` once
|
|
10
|
+
with that mechanically explicit request and `scope: "request"`, then once per mechanic with
|
|
11
|
+
`scope: "mechanic"`. Complete requests return up to 15 deduplicated capabilities; focused
|
|
12
|
+
situations return up to five. A genre label alone returns no guessed recipe. Every result includes
|
|
13
|
+
`matchedSituation`, so an authoring agent can verify why it was selected instead of treating a
|
|
14
|
+
lexical coincidence as engine guidance.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
interface ICapabilityEntry {
|
|
2
|
+
readonly symbol: string;
|
|
3
|
+
readonly package: string;
|
|
4
|
+
readonly importPath: string;
|
|
5
|
+
readonly kind: string;
|
|
6
|
+
readonly signature: string;
|
|
7
|
+
readonly summary: string;
|
|
8
|
+
readonly situations: readonly string[];
|
|
9
|
+
readonly example: string;
|
|
10
|
+
readonly constraints: readonly string[];
|
|
11
|
+
readonly overrides?: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
interface ICapabilityManifest {
|
|
14
|
+
readonly version: number;
|
|
15
|
+
readonly entries: readonly ICapabilityEntry[];
|
|
16
|
+
}
|
|
17
|
+
interface ICapabilitySearchResult {
|
|
18
|
+
readonly symbol: string;
|
|
19
|
+
readonly importPath: string;
|
|
20
|
+
readonly summary: string;
|
|
21
|
+
readonly example: string;
|
|
22
|
+
readonly constraints: readonly string[];
|
|
23
|
+
readonly matchedSituation: string;
|
|
24
|
+
}
|
|
25
|
+
interface ICapabilityDetail extends ICapabilityEntry {
|
|
26
|
+
}
|
|
27
|
+
interface IEngineTool {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly description: string;
|
|
30
|
+
readonly annotations: {
|
|
31
|
+
readonly destructiveHint: false;
|
|
32
|
+
readonly openWorldHint: false;
|
|
33
|
+
readonly readOnlyHint: true;
|
|
34
|
+
};
|
|
35
|
+
readonly inputSchema: {
|
|
36
|
+
readonly type: "object";
|
|
37
|
+
readonly properties: Record<string, {
|
|
38
|
+
readonly description?: string;
|
|
39
|
+
readonly enum?: readonly string[];
|
|
40
|
+
readonly type: "string";
|
|
41
|
+
}>;
|
|
42
|
+
readonly required: readonly string[];
|
|
43
|
+
readonly additionalProperties: false;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
declare const ENGINE_MCP_TOOL_NAMES: readonly ["engine_search_capabilities", "engine_capability_detail"];
|
|
47
|
+
declare function defaultManifestPath(cwd?: string): string;
|
|
48
|
+
declare function loadCapabilityManifest(file?: string): ICapabilityManifest;
|
|
49
|
+
declare function searchCapabilities(situation: string, manifestFile?: string, scope?: "mechanic" | "request"): readonly ICapabilitySearchResult[];
|
|
50
|
+
declare function capabilityDetail(symbol: string, manifestFile?: string): ICapabilityDetail;
|
|
51
|
+
declare function toolDefinitions(): readonly IEngineTool[];
|
|
52
|
+
/**
|
|
53
|
+
* Handles one raw stdin line and returns the JSON-RPC response to write, or `undefined` when the
|
|
54
|
+
* line must be consumed silently. Extracted so the stdio contract is testable without a live
|
|
55
|
+
* `process.stdin`: this framing is the whole surface an authoring agent's MCP client speaks.
|
|
56
|
+
*/
|
|
57
|
+
declare function handleLine(line: string, manifestFile: string): string | undefined;
|
|
58
|
+
declare function runServer(manifestFile?: string): void;
|
|
59
|
+
|
|
60
|
+
export { ENGINE_MCP_TOOL_NAMES, type ICapabilityDetail, type ICapabilityEntry, type ICapabilityManifest, type ICapabilitySearchResult, type IEngineTool, capabilityDetail, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync, readFileSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
var ENGINE_MCP_TOOL_NAMES = [
|
|
8
|
+
"engine_search_capabilities",
|
|
9
|
+
"engine_capability_detail"
|
|
10
|
+
];
|
|
11
|
+
var DEFAULT_MANIFEST_FILE = "capabilities.json";
|
|
12
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
13
|
+
"a",
|
|
14
|
+
"add",
|
|
15
|
+
"an",
|
|
16
|
+
"and",
|
|
17
|
+
"around",
|
|
18
|
+
"build",
|
|
19
|
+
"by",
|
|
20
|
+
"create",
|
|
21
|
+
"for",
|
|
22
|
+
"from",
|
|
23
|
+
"game",
|
|
24
|
+
"in",
|
|
25
|
+
"into",
|
|
26
|
+
"it",
|
|
27
|
+
"make",
|
|
28
|
+
"of",
|
|
29
|
+
"on",
|
|
30
|
+
"that",
|
|
31
|
+
"the",
|
|
32
|
+
"to",
|
|
33
|
+
"use",
|
|
34
|
+
"with"
|
|
35
|
+
]);
|
|
36
|
+
var MAX_COMPLETE_REQUEST_RESULTS = 15;
|
|
37
|
+
var MAX_SITUATION_RESULTS = 5;
|
|
38
|
+
var AUTHORING_INSTRUCTIONS = `Before authoring, infer concrete gameplay mechanics. Preserve the request's distinctive fantasy: choose the smallest loop that uses its characteristic setting, traversal medium, or simulation instead of a generic character game with themed props, and search those implied mechanics even when the user did not name engine terms. Search the mechanically explicit complete request with scope "request", then each mechanic with scope "mechanic". A genre label alone is not a capability query: clarify or decompose it; do not assume a preset. Inspect capability detail and obey constraints before implementing. Capability detail is authoritative on platform support: never invent a platform limitation it does not state.`;
|
|
39
|
+
function defaultManifestPath(cwd = process.cwd()) {
|
|
40
|
+
return path.resolve(cwd, process.env.THREENATIVE_CAPABILITIES_MANIFEST ?? DEFAULT_MANIFEST_FILE);
|
|
41
|
+
}
|
|
42
|
+
function manifestError(file, reason) {
|
|
43
|
+
return new Error(`TN_ENGINE_CAPABILITIES_MANIFEST: ${file}: ${reason}`);
|
|
44
|
+
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function validateManifest(value, file) {
|
|
49
|
+
if (!isRecord(value) || typeof value.version !== "number" || !Array.isArray(value.entries)) {
|
|
50
|
+
throw manifestError(file, "root must contain a numeric version and entries array");
|
|
51
|
+
}
|
|
52
|
+
for (const [index, raw] of value.entries.entries()) {
|
|
53
|
+
if (!isRecord(raw) || typeof raw.symbol !== "string" || typeof raw.package !== "string" || typeof raw.importPath !== "string" || typeof raw.kind !== "string" || typeof raw.signature !== "string" || typeof raw.summary !== "string" || typeof raw.example !== "string" || !Array.isArray(raw.situations) || !Array.isArray(raw.constraints) || !raw.situations.every((situation) => typeof situation === "string") || !raw.constraints.every((constraint) => typeof constraint === "string")) {
|
|
54
|
+
throw manifestError(file, `entry ${index} is malformed`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function loadCapabilityManifest(file = defaultManifestPath()) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = readFileSync(file, "utf8");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw manifestError(file, `cannot read committed manifest: ${String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return validateManifest(JSON.parse(raw), file);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof Error && error.message.startsWith("TN_ENGINE_CAPABILITIES_MANIFEST:"))
|
|
70
|
+
throw error;
|
|
71
|
+
throw manifestError(file, `cannot parse JSON: ${String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function tokens(value) {
|
|
75
|
+
return value.toLocaleLowerCase().split(/[^a-z0-9]+/u).filter((token) => token.length > 1 && !STOP_WORDS.has(token)).map((token) => {
|
|
76
|
+
if (token.length > 4 && token.endsWith("ies")) return `${token.slice(0, -3)}y`;
|
|
77
|
+
if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss"))
|
|
78
|
+
return token.slice(0, -1);
|
|
79
|
+
return token;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function situationScore(query, situations) {
|
|
83
|
+
if (query.length === 0) return { matchedSituation: "", score: 0 };
|
|
84
|
+
const queryText = query.join(" ");
|
|
85
|
+
let best = 0;
|
|
86
|
+
let matchedSituation = "";
|
|
87
|
+
for (const situation of situations) {
|
|
88
|
+
const phrase = tokens(situation);
|
|
89
|
+
const overlap = new Set(phrase.filter((token) => query.includes(token))).size;
|
|
90
|
+
const score = overlap / Math.max(query.length, phrase.length);
|
|
91
|
+
const phraseBonus = phrase.join(" ").includes(queryText) || queryText.includes(phrase.join(" ")) ? 1 : 0;
|
|
92
|
+
if (overlap < (query.length >= 4 ? 2 : 1) && phraseBonus === 0) continue;
|
|
93
|
+
const candidate = score + phraseBonus;
|
|
94
|
+
if (candidate > best) {
|
|
95
|
+
best = candidate;
|
|
96
|
+
matchedSituation = situation;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { matchedSituation, score: best };
|
|
100
|
+
}
|
|
101
|
+
function capabilitySearchKey(entry) {
|
|
102
|
+
return `${entry.importPath}
|
|
103
|
+
${entry.signature}
|
|
104
|
+
${entry.summary}
|
|
105
|
+
${entry.situations.join("\n")}`;
|
|
106
|
+
}
|
|
107
|
+
function searchCapabilities(situation, manifestFile = defaultManifestPath(), scope = "mechanic") {
|
|
108
|
+
if (typeof situation !== "string" || situation.trim().length === 0)
|
|
109
|
+
throw new Error("engine_search_capabilities requires a non-empty situation string.");
|
|
110
|
+
const manifest = loadCapabilityManifest(manifestFile);
|
|
111
|
+
const query = tokens(situation);
|
|
112
|
+
const limit = scope === "request" ? MAX_COMPLETE_REQUEST_RESULTS : MAX_SITUATION_RESULTS;
|
|
113
|
+
return manifest.entries.map((entry) => ({ entry, ...situationScore(query, entry.situations) })).filter(({ score }) => score > 0).sort(
|
|
114
|
+
(left, right) => right.score - left.score || `${left.entry.importPath}:${left.entry.symbol}`.localeCompare(
|
|
115
|
+
`${right.entry.importPath}:${right.entry.symbol}`
|
|
116
|
+
)
|
|
117
|
+
).filter(
|
|
118
|
+
(candidate, index, candidates) => candidates.findIndex(
|
|
119
|
+
(other) => capabilitySearchKey(other.entry) === capabilitySearchKey(candidate.entry)
|
|
120
|
+
) === index
|
|
121
|
+
).slice(0, limit).map(({ entry, matchedSituation }) => ({
|
|
122
|
+
constraints: entry.constraints,
|
|
123
|
+
example: entry.example,
|
|
124
|
+
importPath: entry.importPath,
|
|
125
|
+
matchedSituation,
|
|
126
|
+
summary: entry.summary,
|
|
127
|
+
symbol: entry.symbol
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
function capabilityDetail(symbol, manifestFile = defaultManifestPath()) {
|
|
131
|
+
if (typeof symbol !== "string" || symbol.trim().length === 0)
|
|
132
|
+
throw new Error("engine_capability_detail requires a non-empty symbol string.");
|
|
133
|
+
const manifest = loadCapabilityManifest(manifestFile);
|
|
134
|
+
const entry = manifest.entries.find((candidate) => candidate.symbol === symbol);
|
|
135
|
+
if (entry === void 0) throw new Error(`Unknown engine capability '${symbol}'.`);
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
var TOOL_DEFINITIONS = [
|
|
139
|
+
{
|
|
140
|
+
annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
|
|
141
|
+
name: "engine_search_capabilities",
|
|
142
|
+
description: 'Search the installed engine by concrete gameplay mechanic. Decompose genres first. Use scope "request" for the mechanically explicit full request and "mechanic" for each focused search; matchedSituation explains every result.',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
properties: {
|
|
146
|
+
scope: {
|
|
147
|
+
description: "Use request for the complete cross-system game request; use mechanic or omit it for one mechanic.",
|
|
148
|
+
enum: ["request", "mechanic"],
|
|
149
|
+
type: "string"
|
|
150
|
+
},
|
|
151
|
+
situation: { type: "string" }
|
|
152
|
+
},
|
|
153
|
+
required: ["situation"],
|
|
154
|
+
type: "object"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
annotations: { destructiveHint: false, openWorldHint: false, readOnlyHint: true },
|
|
159
|
+
name: "engine_capability_detail",
|
|
160
|
+
description: "Inspect one engine capability's import, signature, example, constraints, and overrides.",
|
|
161
|
+
inputSchema: {
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: { symbol: { type: "string" } },
|
|
164
|
+
required: ["symbol"],
|
|
165
|
+
type: "object"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
function toolDefinitions() {
|
|
170
|
+
return TOOL_DEFINITIONS;
|
|
171
|
+
}
|
|
172
|
+
function jsonRpcError(id, code, message) {
|
|
173
|
+
return JSON.stringify({ id, jsonrpc: "2.0", error: { code, message } });
|
|
174
|
+
}
|
|
175
|
+
function jsonRpcResult(id, result) {
|
|
176
|
+
return JSON.stringify({ id, jsonrpc: "2.0", result });
|
|
177
|
+
}
|
|
178
|
+
function writeLine(value) {
|
|
179
|
+
process.stdout.write(`${value}
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
182
|
+
function toolText(value) {
|
|
183
|
+
return { content: [{ text: JSON.stringify(value, null, 2), type: "text" }] };
|
|
184
|
+
}
|
|
185
|
+
function handleToolCall(params, manifestFile) {
|
|
186
|
+
const name = params.name;
|
|
187
|
+
const argumentsValue = isRecord(params.arguments) ? params.arguments : {};
|
|
188
|
+
if (name === "engine_search_capabilities") {
|
|
189
|
+
if (typeof argumentsValue.situation !== "string")
|
|
190
|
+
throw new Error("engine_search_capabilities requires a string 'situation' argument.");
|
|
191
|
+
const scope = argumentsValue.scope ?? "mechanic";
|
|
192
|
+
if (scope !== "request" && scope !== "mechanic")
|
|
193
|
+
throw new Error("engine_search_capabilities scope must be 'request' or 'mechanic'.");
|
|
194
|
+
return toolText(searchCapabilities(argumentsValue.situation, manifestFile, scope));
|
|
195
|
+
}
|
|
196
|
+
if (name === "engine_capability_detail") {
|
|
197
|
+
if (typeof argumentsValue.symbol !== "string")
|
|
198
|
+
throw new Error("engine_capability_detail requires a string 'symbol' argument.");
|
|
199
|
+
return toolText(capabilityDetail(argumentsValue.symbol, manifestFile));
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unknown engine MCP tool '${String(name)}'.`);
|
|
202
|
+
}
|
|
203
|
+
function handleLine(line, manifestFile) {
|
|
204
|
+
if (line.trim().length === 0) return void 0;
|
|
205
|
+
let request;
|
|
206
|
+
try {
|
|
207
|
+
request = JSON.parse(line);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return jsonRpcError(null, -32700, `Invalid JSON: ${String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
if (!isRecord(request) || request.id === void 0 || typeof request.method !== "string")
|
|
212
|
+
return void 0;
|
|
213
|
+
try {
|
|
214
|
+
if (request.method === "initialize") {
|
|
215
|
+
return jsonRpcResult(request.id, {
|
|
216
|
+
capabilities: { tools: { listChanged: false } },
|
|
217
|
+
instructions: AUTHORING_INSTRUCTIONS,
|
|
218
|
+
protocolVersion: "2025-06-18",
|
|
219
|
+
serverInfo: { name: "threenative-engine-mcp", version: "0.2.0" }
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (request.method === "tools/list") {
|
|
223
|
+
return jsonRpcResult(request.id, { tools: TOOL_DEFINITIONS });
|
|
224
|
+
}
|
|
225
|
+
if (request.method === "tools/call") {
|
|
226
|
+
if (!isRecord(request.params)) throw new Error("tools/call requires an object params value.");
|
|
227
|
+
return jsonRpcResult(request.id, handleToolCall(request.params, manifestFile));
|
|
228
|
+
}
|
|
229
|
+
return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
return jsonRpcError(request.id, -32e3, error instanceof Error ? error.message : String(error));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function runServer(manifestFile = defaultManifestPath()) {
|
|
235
|
+
loadCapabilityManifest(manifestFile);
|
|
236
|
+
const input = createInterface({ input: process.stdin });
|
|
237
|
+
input.on("line", (line) => {
|
|
238
|
+
const response = handleLine(line, manifestFile);
|
|
239
|
+
if (response !== void 0) writeLine(response);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
var entryPath = process.argv[1];
|
|
243
|
+
if (entryPath !== void 0 && realpathSync(path.resolve(entryPath)) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
244
|
+
try {
|
|
245
|
+
runServer();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
248
|
+
`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export { ENGINE_MCP_TOOL_NAMES, capabilityDetail, defaultManifestPath, handleLine, loadCapabilityManifest, runServer, searchCapabilities, toolDefinitions };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "threenative-engine-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Offline MCP capability discovery for ThreeNative authoring agents.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"threenative-engine-mcp": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"publint": "0.3.22",
|
|
30
|
+
"tsup": "8.5.1",
|
|
31
|
+
"typescript": "5.9.3"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup --config tsup.config.ts && publint",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "publint --strict"
|
|
37
|
+
}
|
|
38
|
+
}
|