fix(tooling): 🔥 remove claude-player adapter

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 18:08:46 -07:00
parent ace555a5ae
commit b3bada0712
7 changed files with 0 additions and 856 deletions

View file

@ -1,57 +0,0 @@
# @magic-civ/claude-player
Claude Agent SDK adapter for the Magic Civilization Claude Player API
(p2-67 Phase 4). Drives one player slot in a headless game while the
production AI controls the others.
## Run
```bash
cd tooling/claude-player
npm install
export ANTHROPIC_API_KEY=sk-ant-...
npm run dev # tsx — no build needed
# or
npm run build && npm start # compiled JS
```
Each run writes append-only logs to `.local/runs/<stamp>/`:
- `log.jsonl` — agent decisions + notifications + run metadata.
- `wire.jsonl` — raw JSON-Lines between adapter and harness.
- `result.json` — final result (`turns_played`, `reason`, `final_view`).
## Env vars
| Var | Default | Purpose |
|---|---|---|
| `ANTHROPIC_API_KEY` | *(required)* | Claude API credentials |
| `CP_MODEL` | `claude-opus-4-7` | Model id |
| `CP_MAX_TURNS` | `100` | Hard turn cap |
| `CP_MAX_IDLE_ENDS` | `3` | Consecutive idle end_turns before bail |
| `CP_SERVER` | `<repo>/scripts/claude-player-server.sh` | Harness launcher |
| `CP_SEED` / `CP_PLAYERS` / `CP_CLAUDE_SLOT` / `CP_OMNISCIENT` / `CP_TIMEOUT_SEC` | see CLAUDE_PLAYER_API.md | Forwarded to harness |
## Architecture
```
@magic-civ/claude-player (Node)
├── HarnessClient — spawn() + JSON-Lines pump
└── runAgent — Anthropic Messages API tool-use loop
├── tool: view()
├── tool: act(action)
└── tool: end_turn()
▼ stdin/stdout
scripts/claude-player-server.sh
flatpak Godot --headless res://engine/scenes/headless/claude_player_main.tscn
GdPlayerApi → mc-player-api → mc-turn::action_handlers
```
Wire-protocol contract: `src/game/engine/docs/CLAUDE_PLAYER_API.md`.
Owning objective: `.project/objectives/p2-67-claude-player-api.md`.

View file

@ -1,25 +0,0 @@
{
"name": "@magic-civ/claude-player",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Claude Agent SDK adapter for the Magic Civilization Claude Player API (p2-67 Phase 4).",
"main": "dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node --enable-source-maps dist/index.js",
"dev": "tsx src/index.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.40.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.6.0"
},
"engines": {
"node": ">=20"
}
}

View file

@ -1,296 +0,0 @@
// Claude Agent SDK loop driving the harness.
//
// Three tools are exposed to Claude:
// view() → returns the current PlayerView JSON
// act(action) → applies one PlayerAction, returns events + new view
// end_turn() → sugar for act({type:"end_turn"})
//
// The loop runs until a `game_over` notification fires, a `turn` field in
// the view exceeds `maxTurns`, or the model emits `end_turn` `maxIdleEndTurns`
// times in a row without any other action (defensive cap).
import Anthropic from "@anthropic-ai/sdk";
import type { HarnessClient } from "./harness.js";
import type { PlayerAction, PlayerView, Response } from "./types.js";
export interface AgentOptions {
/** Live HarnessClient. */
harness: HarnessClient;
/** Anthropic API client. */
anthropic: Anthropic;
/** Model id (claude-opus-4-7, claude-sonnet-4-6, ...). */
model: string;
/** Hard cap on turns played. */
maxTurns: number;
/** Defensive cap on consecutive `end_turn` calls without state change. */
maxIdleEndTurns: number;
/** Append-only log sink (one JSON entry per agent step). */
onLog?: (entry: Record<string, unknown>) => void;
}
const SYSTEM_PROMPT = `You are playing one player slot in Magic Civilization,
a turn-based 4X strategy game (the "Age of Dwarves" variant no magic, pure
mundane tech). You are playing against the production AI which controls the
other player slot(s).
Your goal: maximise your score. Score is dominated by city count, population,
researched tech count, and military strength. Domination victory (controlling
every opponent's original capital) ends the game in your favour immediately.
You drive the game by calling tools:
- view() read the current fog-aware game state.
- act(action) apply one player action. The action JSON shape matches the
PlayerAction enum documented in the game's CLAUDE_PLAYER_API.md.
- end_turn() end your turn so the AI can take theirs. The harness handles
AI turns automatically before returning control to you.
Always start your turn with view() to refresh your model of the state. Then
take whichever actions advance your plan. End your turn when you've done all
useful work don't end early, but don't waste turns on cosmetic actions.
Per turn, prioritise (in order):
1. Found a city if you have a Founder unit on a viable tile.
2. Train a military unit if you have no army and an enemy is within 5 hexes.
3. Build the next strategic building in your capital.
4. Move units toward their assigned objectives (scouting / attacking / defending).
5. Research the next tech on your path.
When you finish your turn, call end_turn(). Then read the new state and
take the next turn. The game ends when you call end_turn() and receive a
game_over notification, or when 100 turns have been played.`;
const TOOLS: Anthropic.Messages.Tool[] = [
{
name: "view",
description: "Read the current fog-aware game state.",
input_schema: { type: "object", properties: {}, required: [] },
},
{
name: "act",
description:
"Take one player action. Returns events and the post-action view.",
input_schema: {
type: "object",
properties: {
action: {
type: "object",
description:
"PlayerAction JSON (e.g. {type:'move',unit_id:'42',to:[5,7]}).",
},
},
required: ["action"],
},
},
{
name: "end_turn",
description: "End your turn. Sugar for act({type:'end_turn'}).",
input_schema: { type: "object", properties: {}, required: [] },
},
];
export interface AgentRunResult {
turns_played: number;
reason: "game_over" | "max_turns" | "idle_cap" | "blocker";
final_view: PlayerView | null;
}
export async function runAgent(opts: AgentOptions): Promise<AgentRunResult> {
try {
return await runAgentInner(opts);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
opts.onLog?.({ kind: "agent_fatal", error: message });
return {
turns_played: 0,
reason: "blocker",
final_view: null,
};
}
}
async function runAgentInner(opts: AgentOptions): Promise<AgentRunResult> {
const messages: Anthropic.Messages.MessageParam[] = [];
let consecutiveIdleEnds = 0;
let lastTurnNumber = -1;
let finalView: PlayerView | null = null;
// Seed the conversation with the initial view.
const initialView = await opts.harness.view();
if (!initialView.ok) {
return {
turns_played: 0,
reason: "blocker",
final_view: null,
};
}
finalView = initialView.view;
messages.push({
role: "user",
content: [
{
type: "text",
text:
"Initial game state (read this before deciding any actions):\n\n" +
JSON.stringify(initialView.view, null, 2),
},
],
});
for (let step = 0; step < opts.maxTurns * 50; step++) {
if (finalView !== null && finalView.turn > opts.maxTurns) {
return { turns_played: finalView.turn, reason: "max_turns", final_view: finalView };
}
if (consecutiveIdleEnds >= opts.maxIdleEndTurns) {
return {
turns_played: finalView?.turn ?? 0,
reason: "idle_cap",
final_view: finalView,
};
}
opts.onLog?.({ step, kind: "agent_request", messages: messages.length });
const response = await opts.anthropic.messages.create({
model: opts.model,
max_tokens: 4096,
system: SYSTEM_PROMPT,
tools: TOOLS,
messages,
});
opts.onLog?.({ step, kind: "agent_response", stop_reason: response.stop_reason });
messages.push({ role: "assistant", content: response.content });
const toolUses = response.content.filter(
(c): c is Anthropic.Messages.ToolUseBlock => c.type === "tool_use",
);
if (toolUses.length === 0) {
opts.onLog?.({ step, kind: "agent_finished_without_tool" });
break;
}
const toolResults: Anthropic.Messages.ToolResultBlockParam[] = [];
let endTurnCalledThisStep = false;
for (const tu of toolUses) {
const result = await runOneTool(opts, tu);
toolResults.push({
type: "tool_result",
tool_use_id: tu.id,
content: JSON.stringify(result.payload),
});
if (result.endedTurn) endTurnCalledThisStep = true;
if (result.view !== null) finalView = result.view;
if (result.gameOver) {
opts.onLog?.({ step, kind: "game_over_observed" });
return {
turns_played: finalView?.turn ?? 0,
reason: "game_over",
final_view: finalView,
};
}
}
messages.push({ role: "user", content: toolResults });
if (endTurnCalledThisStep) {
const newTurn = finalView?.turn ?? -1;
if (newTurn === lastTurnNumber) {
consecutiveIdleEnds++;
} else {
consecutiveIdleEnds = 0;
lastTurnNumber = newTurn;
}
}
}
return {
turns_played: finalView?.turn ?? 0,
reason: "max_turns",
final_view: finalView,
};
}
interface ToolOutcome {
payload: Record<string, unknown>;
view: PlayerView | null;
endedTurn: boolean;
gameOver: boolean;
}
async function runOneTool(
opts: AgentOptions,
tu: Anthropic.Messages.ToolUseBlock,
): Promise<ToolOutcome> {
try {
return await runOneToolInner(opts, tu);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
payload: { ok: false, error: { code: "internal", message } },
view: null,
endedTurn: false,
gameOver: false,
};
}
}
async function runOneToolInner(
opts: AgentOptions,
tu: Anthropic.Messages.ToolUseBlock,
): Promise<ToolOutcome> {
switch (tu.name) {
case "view": {
const r = await opts.harness.view();
return outcomeFromResponse(r, false);
}
case "end_turn": {
const r = await opts.harness.endTurn();
return outcomeFromResponse(r, true);
}
case "act": {
const input = tu.input as { action?: PlayerAction };
if (!input || typeof input !== "object" || !input.action) {
return {
payload: {
ok: false,
error: { code: "parse_error", message: "act tool requires {action: PlayerAction}" },
},
view: null,
endedTurn: false,
gameOver: false,
};
}
const r = await opts.harness.act(input.action);
const ended = input.action.type === "end_turn";
return outcomeFromResponse(r, ended);
}
default:
return {
payload: {
ok: false,
error: { code: "parse_error", message: `unknown tool ${tu.name}` },
},
view: null,
endedTurn: false,
gameOver: false,
};
}
}
function outcomeFromResponse(r: Response, endedTurn: boolean): ToolOutcome {
if (r.ok) {
const events = r.events ?? [];
const gameOver = events.some(
(e) => typeof e === "object" && e !== null && (e as { type?: string }).type === "game_over",
);
return {
payload: { ok: true, events, view: r.view },
view: r.view,
endedTurn,
gameOver,
};
}
return {
payload: { ok: false, error: r.error },
view: null,
endedTurn: false,
gameOver: false,
};
}

View file

@ -1,198 +0,0 @@
// JSON-Lines pump over a child process running `scripts/claude-player-server.sh`.
//
// Each request/response/notification is one JSON value per line. Responses are
// correlated by an optional `id` field; the adapter assigns monotonically
// increasing ids so it can route concurrent in-flight requests if it ever
// adds parallelism (single-flight is enough for v1).
import { spawn, type ChildProcessByStdio } from "node:child_process";
import { createInterface } from "node:readline";
import type { Readable, Writable } from "node:stream";
import type {
ErrResponse,
Notification,
OkResponse,
PlayerAction,
Response,
} from "./types.js";
export interface HarnessOptions {
/** Absolute path to claude-player-server.sh. */
serverScript: string;
/** Env-var overrides for the harness (CP_SEED, CP_PLAYERS, ...). */
env?: Record<string, string>;
/** Hook invoked for every async `Notification` line. */
onNotification?: (note: Notification) => void;
/** Hook invoked for every line read off stdout, before parsing. */
onRawLine?: (direction: "<-" | "->", line: string) => void;
/** Default per-request timeout in ms. */
defaultTimeoutMs?: number;
}
interface PendingRequest {
id: number;
resolve: (resp: Response) => void;
reject: (err: Error) => void;
timer: NodeJS.Timeout;
}
/**
* Wraps the harness child process. Exposes `view()`, `act()`, `endTurn()`,
* `shutdown()` each returns the matching `Response` once it lands.
*/
export class HarnessClient {
private readonly child: ChildProcessByStdio<Writable, Readable, Readable>;
private readonly pending = new Map<number, PendingRequest>();
private readonly opts: Required<HarnessOptions>;
private nextId = 1;
private closed = false;
constructor(opts: HarnessOptions) {
this.opts = {
env: {},
onNotification: () => {},
onRawLine: () => {},
defaultTimeoutMs: 60_000,
...opts,
};
this.child = spawn(this.opts.serverScript, [], {
env: { ...process.env, ...this.opts.env },
stdio: ["pipe", "pipe", "pipe"],
}) as ChildProcessByStdio<Writable, Readable, Readable>;
const out = createInterface({ input: this.child.stdout });
out.on("line", (line) => {
this.opts.onRawLine("<-", line);
this.handleLine(line);
});
this.child.stderr.on("data", (chunk: Buffer) => {
// Surface harness stderr to our own stderr so users see crashes /
// engine warnings without them mingling into the protocol channel.
process.stderr.write(chunk);
});
this.child.on("exit", (code) => {
this.closed = true;
const err = new Error(`harness exited with code ${code ?? "null"}`);
for (const req of this.pending.values()) {
clearTimeout(req.timer);
req.reject(err);
}
this.pending.clear();
});
}
/** Read the current fog-aware view. */
async view(): Promise<Response> {
return this.send({ type: "view" });
}
/** Apply one action. Returns the response (ok or err). */
async act(action: PlayerAction): Promise<Response> {
return this.send({ type: "act", action });
}
/** Sugar — `act({type:"end_turn"})`. */
async endTurn(): Promise<Response> {
return this.act({ type: "end_turn" });
}
/** Tell the harness to exit cleanly. Resolves once the child exits. */
async shutdown(): Promise<void> {
if (this.closed) return;
try {
await this.send({ type: "shutdown" });
} catch {
// Child may have exited mid-shutdown — that's the desired terminal
// state, so consume the error and fall through to the exit wait.
}
await new Promise<void>((resolve) => {
if (this.closed) {
resolve();
return;
}
this.child.once("exit", () => resolve());
this.child.stdin.end();
});
}
private send(body: Record<string, unknown>): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
if (this.closed) {
reject(new Error("harness already closed"));
return;
}
const id = this.nextId++;
const payload = JSON.stringify({ ...body, id });
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error(`request ${id} (${String(body["type"])}) timed out`));
}, this.opts.defaultTimeoutMs);
this.pending.set(id, { id, resolve, reject, timer });
this.opts.onRawLine("->", payload);
this.child.stdin.write(payload + "\n");
});
}
private handleLine(line: string): void {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
// Non-JSON line on stdout (e.g. engine warnings); ignore.
return;
}
if (typeof parsed !== "object" || parsed === null) return;
const obj = parsed as Record<string, unknown>;
if ("id" in obj && (obj["id"] === null || typeof obj["id"] === "number")) {
const id = obj["id"];
if (typeof id === "number") {
const pend = this.pending.get(id);
if (pend !== undefined) {
this.pending.delete(id);
clearTimeout(pend.timer);
const response = toResponse(obj);
if (response !== null) {
pend.resolve(response);
} else {
pend.reject(new Error(`malformed response shape: ${line}`));
}
return;
}
}
}
// No id → notification.
if (typeof obj["type"] === "string") {
this.opts.onNotification(obj as Notification);
}
}
}
function toResponse(obj: Record<string, unknown>): Response | null {
if (obj["ok"] === true && typeof obj["view"] === "object" && obj["view"] !== null) {
const ok: OkResponse = {
id: (obj["id"] as number | null) ?? null,
ok: true,
view: obj["view"] as OkResponse["view"],
events: Array.isArray(obj["events"])
? (obj["events"] as OkResponse["events"])
: [],
};
return ok;
}
if (obj["ok"] === false && typeof obj["error"] === "object" && obj["error"] !== null) {
const errObj = obj["error"] as Record<string, unknown>;
const err: ErrResponse = {
id: (obj["id"] as number | null) ?? null,
ok: false,
error: {
code: typeof errObj["code"] === "string" ? errObj["code"] : "internal",
message: typeof errObj["message"] === "string" ? errObj["message"] : "",
},
};
return err;
}
return null;
}

View file

@ -1,99 +0,0 @@
// Entry point — spawns the harness, wires up the Claude Agent loop, and
// writes an append-only run log under `.local/runs/<stamp>/log.jsonl`.
// See `README.md` for usage and the full env-var contract.
import Anthropic from "@anthropic-ai/sdk";
import { mkdir, writeFile } from "node:fs/promises";
import { createWriteStream } from "node:fs";
import { dirname, resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
import { runAgent } from "./agent.js";
import { HarnessClient } from "./harness.js";
const HERE = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = resolvePath(HERE, "..", "..", "..");
function emit(line: string): void {
process.stderr.write(line + "\n");
}
async function main(): Promise<void> {
const apiKey = process.env["ANTHROPIC_API_KEY"];
if (!apiKey || apiKey.length === 0) {
emit("ANTHROPIC_API_KEY not set. Export it before running the adapter.");
process.exitCode = 2;
return;
}
const serverScript =
process.env["CP_SERVER"] ??
resolvePath(PROJECT_ROOT, "scripts", "claude-player-server.sh");
const model = process.env["CP_MODEL"] ?? "claude-opus-4-7";
const maxTurns = parseInt(process.env["CP_MAX_TURNS"] ?? "100", 10);
const maxIdleEnds = parseInt(process.env["CP_MAX_IDLE_ENDS"] ?? "3", 10);
const stamp = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.replace("T", "_")
.replace("Z", "");
const runDir = resolvePath(HERE, "..", ".local", "runs", stamp);
await mkdir(runDir, { recursive: true });
const logPath = resolvePath(runDir, "log.jsonl");
const wirePath = resolvePath(runDir, "wire.jsonl");
const logStream = createWriteStream(logPath, { flags: "a" });
const wireStream = createWriteStream(wirePath, { flags: "a" });
const log = (entry: Record<string, unknown>): void => {
logStream.write(JSON.stringify({ ts: Date.now(), ...entry }) + "\n");
};
emit(`claude-player run dir: ${runDir}`);
emit(`spawning harness: ${serverScript}`);
const harness = new HarnessClient({
serverScript,
env: {},
onNotification: (note) => {
log({ kind: "notification", ...note });
},
onRawLine: (direction, line) => {
wireStream.write(`${direction} ${line}\n`);
},
});
const anthropic = new Anthropic({ apiKey });
try {
const result = await runAgent({
harness,
anthropic,
model,
maxTurns,
maxIdleEndTurns: maxIdleEnds,
onLog: log,
});
log({ kind: "run_complete", ...result });
emit(
`run complete — reason=${result.reason}, turns=${result.turns_played}`,
);
await writeFile(
resolvePath(runDir, "result.json"),
JSON.stringify(result, null, 2),
"utf-8",
);
} catch (err) {
log({ kind: "fatal", error: err instanceof Error ? err.message : String(err) });
emit(`fatal: ${err instanceof Error ? err.message : String(err)}`);
process.exitCode = 1;
} finally {
await harness.shutdown();
logStream.end();
wireStream.end();
}
}
main().catch((err: unknown) => {
emit(`uncaught: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
});

View file

@ -1,155 +0,0 @@
// Wire-protocol types — mirror the Rust enums in `mc-player-api`.
// Source of truth: src/game/engine/docs/CLAUDE_PLAYER_API.md.
/** Hex coordinate carried over the wire — `[col, row]`. */
export type WireHex = [number, number];
/** Stable player slot index, `u8` everywhere in the simulator. */
export type PlayerId = number;
/** Discriminated union of every player action variant. */
export type PlayerAction =
| { type: "end_turn" }
| { type: "noop" }
| { type: "move"; unit_id: string; to: WireHex }
| { type: "attack"; unit_id: string; target: WireHex }
| { type: "ranged_attack"; unit_id: string; target: WireHex }
| { type: "fortify"; unit_id: string }
| { type: "unfortify"; unit_id: string }
| { type: "skip"; unit_id: string }
| { type: "found_city"; unit_id: string }
| { type: "build_improvement"; unit_id: string; improvement_id: string }
| { type: "sentry"; unit_id: string }
| { type: "unsentry"; unit_id: string }
| { type: "issue_patrol"; unit_id: string; waypoints: WireHex[] }
| { type: "cancel_patrol"; unit_id: string }
| { type: "edit_patrol"; unit_id: string; waypoints: WireHex[] }
| { type: "queue_production"; city_id: string; item: string; tile?: WireHex }
| { type: "remove_from_queue"; city_id: string; index: number }
| { type: "queue_reorder"; city_id: string; from: number; to: number }
| { type: "rush_buy"; city_id: string }
| { type: "buy_tile"; city_id: string; tile: WireHex }
| { type: "set_focus"; city_id: string; focus: string }
| {
type: "merge_buildings";
city_id: string;
building_a: string;
building_b: string;
into: string;
}
| { type: "research_tech"; tech_id: string }
| { type: "research_tradition"; tradition_id: string }
| { type: "declare_war"; on: PlayerId }
| { type: "offer_peace"; to: PlayerId };
/** One entry in a unit's or empire's `legal_actions` list. */
export interface LegalActionEntry {
action: PlayerAction;
enabled: boolean;
disabled_reason?: string;
}
export interface UnitView {
id: string;
type: string;
position: WireHex;
owner: PlayerId;
hp: number;
max_hp: number;
movement_left: number;
movement_max: number;
experience: number;
promotion_available: boolean;
fortified: boolean;
sentry: boolean;
legal_actions?: LegalActionEntry[];
}
export interface CityView {
id: string;
name: string;
position: WireHex;
owner: PlayerId;
is_capital: boolean;
population: number;
food_stored: number;
food_growth_threshold: number;
production_queue: Array<{
item: string;
kind: string;
progress: number;
cost: number;
tile?: WireHex;
}>;
buildings: string[];
owned_tiles: WireHex[];
yields: Record<string, number>;
hp: number;
max_hp: number;
focus: string;
buildable?: Array<{
item: string;
kind: string;
cost: number;
rush_gold: number;
enabled: boolean;
}>;
}
export interface PlayerView {
turn: number;
player: PlayerId;
current_player: PlayerId;
phase: string;
is_human_turn: boolean;
resources: {
gold: number;
gold_per_turn: number;
science_per_turn: number;
culture_per_turn: number;
happiness_pool: number;
stockpile: Record<string, number>;
};
research: {
current_tech: string | null;
tech_progress: number;
tech_cost: number;
researched: string[];
available: string[];
};
culture: {
current_tradition: string | null;
tradition_progress: number;
tradition_cost: number;
researched: string[];
};
cities: CityView[];
units: UnitView[];
legal_actions: LegalActionEntry[];
score: {
gold_total: number;
city_count: number;
unit_count: number;
score_estimate: number;
};
}
export interface OkResponse {
id: number | null;
ok: true;
events?: Array<Record<string, unknown>>;
view: PlayerView;
}
export interface ErrResponse {
id: number | null;
ok: false;
error: { code: string; message: string };
}
export type Response = OkResponse | ErrResponse;
export interface Notification {
type: string;
[k: string]: unknown;
}

View file

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"strict": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", ".local"]
}