@openclaw-cloud/agent-controller 0.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.
Files changed (53) hide show
  1. package/Dockerfile +9 -0
  2. package/__tests__/backup.test.ts +145 -0
  3. package/__tests__/connection.test.ts +111 -0
  4. package/__tests__/handlers.test.ts +150 -0
  5. package/__tests__/heartbeat.test.ts +80 -0
  6. package/bin/agent-controller.js +5 -0
  7. package/dist/connection.d.ts +11 -0
  8. package/dist/connection.js +117 -0
  9. package/dist/connection.js.map +1 -0
  10. package/dist/handlers/backup.d.ts +2 -0
  11. package/dist/handlers/backup.js +81 -0
  12. package/dist/handlers/backup.js.map +1 -0
  13. package/dist/handlers/config.d.ts +2 -0
  14. package/dist/handlers/config.js +43 -0
  15. package/dist/handlers/config.js.map +1 -0
  16. package/dist/handlers/deploy.d.ts +2 -0
  17. package/dist/handlers/deploy.js +30 -0
  18. package/dist/handlers/deploy.js.map +1 -0
  19. package/dist/handlers/exec.d.ts +2 -0
  20. package/dist/handlers/exec.js +29 -0
  21. package/dist/handlers/exec.js.map +1 -0
  22. package/dist/handlers/pair.d.ts +2 -0
  23. package/dist/handlers/pair.js +23 -0
  24. package/dist/handlers/pair.js.map +1 -0
  25. package/dist/handlers/restart.d.ts +2 -0
  26. package/dist/handlers/restart.js +18 -0
  27. package/dist/handlers/restart.js.map +1 -0
  28. package/dist/handlers/stop.d.ts +2 -0
  29. package/dist/handlers/stop.js +14 -0
  30. package/dist/handlers/stop.js.map +1 -0
  31. package/dist/heartbeat.d.ts +9 -0
  32. package/dist/heartbeat.js +60 -0
  33. package/dist/heartbeat.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +43 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/types.d.ts +28 -0
  38. package/dist/types.js +7 -0
  39. package/dist/types.js.map +1 -0
  40. package/jest.config.ts +16 -0
  41. package/package.json +26 -0
  42. package/src/connection.ts +135 -0
  43. package/src/handlers/backup.ts +97 -0
  44. package/src/handlers/config.ts +48 -0
  45. package/src/handlers/deploy.ts +32 -0
  46. package/src/handlers/exec.ts +32 -0
  47. package/src/handlers/pair.ts +26 -0
  48. package/src/handlers/restart.ts +19 -0
  49. package/src/handlers/stop.ts +17 -0
  50. package/src/heartbeat.ts +73 -0
  51. package/src/index.ts +47 -0
  52. package/src/types.ts +32 -0
  53. package/tsconfig.json +18 -0
@@ -0,0 +1,135 @@
1
+ import { Centrifuge, Subscription } from 'centrifuge';
2
+ import WebSocket from 'ws';
3
+ import type { AgentCommand, AgentResponse } from './types.js';
4
+ import { handleExec } from './handlers/exec.js';
5
+ import { handleRestart } from './handlers/restart.js';
6
+ import { handleDeploy } from './handlers/deploy.js';
7
+ import { handleConfig } from './handlers/config.js';
8
+ import { handlePair } from './handlers/pair.js';
9
+ import { handleStop } from './handlers/stop.js';
10
+ import { handleBackup } from './handlers/backup.js';
11
+
12
+ export interface ConnectionOptions {
13
+ url: string;
14
+ token: string;
15
+ agentId: string;
16
+ backendUrl?: string;
17
+ }
18
+
19
+ const handlers: Record<string, (cmd: AgentCommand) => Promise<AgentResponse>> = {
20
+ exec: handleExec,
21
+ restart: handleRestart,
22
+ deploy: handleDeploy,
23
+ config: handleConfig,
24
+ pair: handlePair,
25
+ stop: handleStop,
26
+ backup: handleBackup,
27
+ };
28
+
29
+ async function fetchCentrifugoToken(backendUrl: string, agentToken: string, agentId: string): Promise<string> {
30
+ const controller = new AbortController();
31
+ const timeout = setTimeout(() => controller.abort(), 5000);
32
+ try {
33
+ const res = await fetch(`${backendUrl}/api/internal/centrifugo-token`, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ 'Authorization': `Bearer ${agentToken}`,
38
+ },
39
+ body: JSON.stringify({ agentId }),
40
+ signal: controller.signal,
41
+ });
42
+ if (!res.ok) {
43
+ throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
44
+ }
45
+ const data = await res.json() as { token: string };
46
+ return data.token;
47
+ } finally {
48
+ clearTimeout(timeout);
49
+ }
50
+ }
51
+
52
+ export function createConnection(opts: ConnectionOptions): {
53
+ client: Centrifuge;
54
+ commandSub: Subscription;
55
+ } {
56
+ const centrifugeOpts: Record<string, unknown> = {
57
+ websocket: WebSocket,
58
+ name: 'agent-controller',
59
+ minReconnectDelay: 1000,
60
+ maxReconnectDelay: 30000,
61
+ };
62
+
63
+ if (opts.backendUrl) {
64
+ // Dynamic JWT: fetch fresh token on every connect/reconnect
65
+ centrifugeOpts.getToken = async () => {
66
+ console.log('Fetching Centrifugo JWT from backend...');
67
+ try {
68
+ const token = await fetchCentrifugoToken(opts.backendUrl!, opts.token, opts.agentId);
69
+ console.log('Got Centrifugo JWT');
70
+ return token;
71
+ } catch (err) {
72
+ console.error('Failed to fetch Centrifugo JWT:', err instanceof Error ? err.message : err);
73
+ // Retry once
74
+ try {
75
+ const token = await fetchCentrifugoToken(opts.backendUrl!, opts.token, opts.agentId);
76
+ console.log('Got Centrifugo JWT (retry)');
77
+ return token;
78
+ } catch (retryErr) {
79
+ console.error('JWT fetch retry failed:', retryErr instanceof Error ? retryErr.message : retryErr);
80
+ throw retryErr;
81
+ }
82
+ }
83
+ };
84
+ } else {
85
+ // Fallback: use raw token (backward compat, won't work with Centrifugo JWT validation)
86
+ console.warn('BACKEND_INTERNAL_URL not set, using AGENT_TOKEN directly (may not work)');
87
+ centrifugeOpts.token = opts.token;
88
+ }
89
+
90
+ const client = new Centrifuge(opts.url, centrifugeOpts);
91
+
92
+ const commandChannel = `agent:${opts.agentId}`;
93
+ const commandSub = client.newSubscription(commandChannel);
94
+
95
+ commandSub.on('publication', async (ctx) => {
96
+ const command = ctx.data as AgentCommand;
97
+ if (!command || !command.type) return;
98
+
99
+ const handler = handlers[command.type];
100
+ if (!handler) {
101
+ console.error(`Unknown command type: ${command.type}`);
102
+ return;
103
+ }
104
+
105
+ try {
106
+ const response = await handler(command);
107
+ await client.publish(commandChannel, response);
108
+ } catch (err) {
109
+ const errorResponse: AgentResponse = {
110
+ id: command.id,
111
+ type: command.type,
112
+ success: false,
113
+ error: err instanceof Error ? err.message : String(err),
114
+ };
115
+ await client.publish(commandChannel, errorResponse);
116
+ }
117
+ });
118
+
119
+ client.on('connected', (ctx) => {
120
+ console.log(`Connected to Centrifugo via ${ctx.transport}`);
121
+ });
122
+
123
+ client.on('disconnected', (ctx) => {
124
+ console.log(`Disconnected: ${ctx.reason} (code ${ctx.code})`);
125
+ });
126
+
127
+ client.on('connecting', (ctx) => {
128
+ console.log(`Connecting: ${ctx.reason} (code ${ctx.code})`);
129
+ });
130
+
131
+ commandSub.subscribe();
132
+ client.connect();
133
+
134
+ return { client, commandSub };
135
+ }
@@ -0,0 +1,97 @@
1
+ import { exec } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import type { AgentCommand, AgentResponse } from '../types.js';
5
+
6
+ const WORKSPACE_DIR = '/home/node/.openclaw/workspace';
7
+ const BACKUP_DIR = '/tmp';
8
+ const TAR_TIMEOUT = 120_000;
9
+ const UPLOAD_TIMEOUT = 120_000;
10
+
11
+ export function handleBackup(command: AgentCommand): Promise<AgentResponse> {
12
+ const uploadUrl = command.payload.uploadUrl as string;
13
+ if (!uploadUrl) {
14
+ return Promise.resolve({
15
+ id: command.id,
16
+ type: 'backup',
17
+ success: false,
18
+ error: 'Missing "uploadUrl" in payload',
19
+ });
20
+ }
21
+
22
+ const timestamp = Date.now();
23
+ const filename = `backup-${timestamp}.tar.gz`;
24
+ const archivePath = path.join(BACKUP_DIR, filename);
25
+
26
+ return createArchive(archivePath)
27
+ .then(() => uploadArchive(archivePath, uploadUrl))
28
+ .then(async (size) => {
29
+ await cleanup(archivePath);
30
+ return {
31
+ id: command.id,
32
+ type: 'backup' as const,
33
+ success: true,
34
+ data: { size, filename },
35
+ };
36
+ })
37
+ .catch(async (err) => {
38
+ await cleanup(archivePath);
39
+ return {
40
+ id: command.id,
41
+ type: 'backup' as const,
42
+ success: false,
43
+ error: err instanceof Error ? err.message : String(err),
44
+ };
45
+ });
46
+ }
47
+
48
+ function createArchive(archivePath: string): Promise<void> {
49
+ return new Promise((resolve, reject) => {
50
+ exec(
51
+ `tar -czf ${archivePath} -C ${path.dirname(WORKSPACE_DIR)} ${path.basename(WORKSPACE_DIR)}`,
52
+ { timeout: TAR_TIMEOUT },
53
+ (error, _stdout, stderr) => {
54
+ if (error) {
55
+ reject(new Error(`tar failed: ${stderr.toString() || error.message}`));
56
+ } else {
57
+ resolve();
58
+ }
59
+ },
60
+ );
61
+ });
62
+ }
63
+
64
+ async function uploadArchive(archivePath: string, uploadUrl: string): Promise<number> {
65
+ const stat = await fs.stat(archivePath);
66
+ const fileBuffer = await fs.readFile(archivePath);
67
+
68
+ const boundary = `----backup-${Date.now()}`;
69
+ const filename = path.basename(archivePath);
70
+
71
+ const header = Buffer.from(
72
+ `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/gzip\r\n\r\n`,
73
+ );
74
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
75
+ const body = Buffer.concat([header, fileBuffer, footer]);
76
+
77
+ const response = await fetch(uploadUrl, {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
80
+ body,
81
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT),
82
+ });
83
+
84
+ if (!response.ok) {
85
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
86
+ }
87
+
88
+ return stat.size;
89
+ }
90
+
91
+ async function cleanup(archivePath: string): Promise<void> {
92
+ try {
93
+ await fs.unlink(archivePath);
94
+ } catch {
95
+ // ignore cleanup errors
96
+ }
97
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { exec } from 'node:child_process';
4
+ import type { AgentCommand, AgentResponse } from '../types.js';
5
+
6
+ const CONFIG_DIR = '/etc/openclaw';
7
+
8
+ export function handleConfig(command: AgentCommand): Promise<AgentResponse> {
9
+ const filename = command.payload.filename as string;
10
+ const content = command.payload.content as string;
11
+
12
+ if (!filename || content === undefined) {
13
+ return Promise.resolve({
14
+ id: command.id,
15
+ type: 'config',
16
+ success: false,
17
+ error: 'Missing "filename" or "content" in payload',
18
+ });
19
+ }
20
+
21
+ const configPath = path.join(CONFIG_DIR, path.basename(filename));
22
+
23
+ return fs.mkdir(CONFIG_DIR, { recursive: true })
24
+ .then(() => fs.writeFile(configPath, content, 'utf-8'))
25
+ .then(() => {
26
+ return new Promise<AgentResponse>((resolve) => {
27
+ exec('openclaw gateway restart', { timeout: 30_000 }, (error, stdout, stderr) => {
28
+ resolve({
29
+ id: command.id,
30
+ type: 'config',
31
+ success: !error,
32
+ data: {
33
+ path: configPath,
34
+ stdout: stdout.toString(),
35
+ stderr: stderr.toString(),
36
+ },
37
+ ...(error ? { error: error.message } : {}),
38
+ });
39
+ });
40
+ });
41
+ })
42
+ .catch((err) => ({
43
+ id: command.id,
44
+ type: 'config',
45
+ success: false,
46
+ error: err instanceof Error ? err.message : String(err),
47
+ }));
48
+ }
@@ -0,0 +1,32 @@
1
+ import { exec } from 'node:child_process';
2
+ import type { AgentCommand, AgentResponse } from '../types.js';
3
+
4
+ export function handleDeploy(command: AgentCommand): Promise<AgentResponse> {
5
+ return new Promise((resolve) => {
6
+ exec('npm i -g openclaw', { timeout: 120_000 }, (installErr, installOut, installStderr) => {
7
+ if (installErr) {
8
+ resolve({
9
+ id: command.id,
10
+ type: 'deploy',
11
+ success: false,
12
+ data: { stdout: installOut.toString(), stderr: installStderr.toString() },
13
+ error: `Install failed: ${installErr.message}`,
14
+ });
15
+ return;
16
+ }
17
+
18
+ exec('openclaw gateway restart', { timeout: 30_000 }, (restartErr, restartOut, restartStderr) => {
19
+ resolve({
20
+ id: command.id,
21
+ type: 'deploy',
22
+ success: !restartErr,
23
+ data: {
24
+ install: { stdout: installOut.toString(), stderr: installStderr.toString() },
25
+ restart: { stdout: restartOut.toString(), stderr: restartStderr.toString() },
26
+ },
27
+ ...(restartErr ? { error: `Restart failed: ${restartErr.message}` } : {}),
28
+ });
29
+ });
30
+ });
31
+ });
32
+ }
@@ -0,0 +1,32 @@
1
+ import { exec } from 'node:child_process';
2
+ import type { AgentCommand, AgentResponse } from '../types.js';
3
+
4
+ const EXEC_TIMEOUT = 60_000;
5
+
6
+ export function handleExec(command: AgentCommand): Promise<AgentResponse> {
7
+ const cmd = command.payload.command as string;
8
+ if (!cmd) {
9
+ return Promise.resolve({
10
+ id: command.id,
11
+ type: 'exec',
12
+ success: false,
13
+ error: 'Missing "command" in payload',
14
+ });
15
+ }
16
+
17
+ return new Promise((resolve) => {
18
+ exec(cmd, { timeout: EXEC_TIMEOUT }, (error, stdout, stderr) => {
19
+ resolve({
20
+ id: command.id,
21
+ type: 'exec',
22
+ success: !error,
23
+ data: {
24
+ exitCode: error ? (error as NodeJS.ErrnoException & { code?: number }).code ?? 1 : 0,
25
+ stdout: stdout.toString(),
26
+ stderr: stderr.toString(),
27
+ },
28
+ ...(error ? { error: error.message } : {}),
29
+ });
30
+ });
31
+ });
32
+ }
@@ -0,0 +1,26 @@
1
+ import type { AgentCommand, AgentResponse } from '../types.js';
2
+
3
+ export function handlePair(command: AgentCommand): Promise<AgentResponse> {
4
+ const pairToken = command.payload.token as string;
5
+ const targetId = command.payload.targetId as string;
6
+
7
+ if (!pairToken || !targetId) {
8
+ return Promise.resolve({
9
+ id: command.id,
10
+ type: 'pair',
11
+ success: false,
12
+ error: 'Missing "token" or "targetId" in payload',
13
+ });
14
+ }
15
+
16
+ return Promise.resolve({
17
+ id: command.id,
18
+ type: 'pair',
19
+ success: true,
20
+ data: {
21
+ paired: true,
22
+ targetId,
23
+ agentId: process.env.AGENT_ID,
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,19 @@
1
+ import { exec } from 'node:child_process';
2
+ import type { AgentCommand, AgentResponse } from '../types.js';
3
+
4
+ export function handleRestart(command: AgentCommand): Promise<AgentResponse> {
5
+ return new Promise((resolve) => {
6
+ exec('openclaw gateway restart', { timeout: 30_000 }, (error, stdout, stderr) => {
7
+ resolve({
8
+ id: command.id,
9
+ type: 'restart',
10
+ success: !error,
11
+ data: {
12
+ stdout: stdout.toString(),
13
+ stderr: stderr.toString(),
14
+ },
15
+ ...(error ? { error: error.message } : {}),
16
+ });
17
+ });
18
+ });
19
+ }
@@ -0,0 +1,17 @@
1
+ import type { AgentCommand, AgentResponse } from '../types.js';
2
+
3
+ export function handleStop(command: AgentCommand): Promise<AgentResponse> {
4
+ const response: AgentResponse = {
5
+ id: command.id,
6
+ type: 'stop',
7
+ success: true,
8
+ data: { message: 'Shutting down gracefully' },
9
+ };
10
+
11
+ setTimeout(() => {
12
+ console.log('Agent stopping gracefully...');
13
+ process.exit(0);
14
+ }, 500);
15
+
16
+ return Promise.resolve(response);
17
+ }
@@ -0,0 +1,73 @@
1
+ import os from 'node:os';
2
+ import type { HeartbeatPayload } from './types.js';
3
+
4
+ const HEARTBEAT_INTERVAL = 30_000;
5
+ const HEARTBEAT_CHANNEL = 'heartbeat:data';
6
+
7
+ export interface HeartbeatOptions {
8
+ centrifugoInternalUrl: string;
9
+ centrifugoApiKey: string;
10
+ agentId: string;
11
+ }
12
+
13
+ function getMetrics(): HeartbeatPayload['metrics'] {
14
+ const cpus = os.cpus();
15
+ const cpuUsage = cpus.reduce((acc, cpu) => {
16
+ const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
17
+ return acc + (1 - cpu.times.idle / total);
18
+ }, 0) / cpus.length;
19
+
20
+ const totalMem = os.totalmem();
21
+ const freeMem = os.freemem();
22
+
23
+ return {
24
+ cpu: Math.round(cpuUsage * 100) / 100,
25
+ mem: Math.round(((totalMem - freeMem) / totalMem) * 100) / 100,
26
+ uptime: os.uptime(),
27
+ };
28
+ }
29
+
30
+ async function publishHeartbeat(opts: HeartbeatOptions): Promise<void> {
31
+ const payload: HeartbeatPayload = {
32
+ type: 'heartbeat',
33
+ agentId: opts.agentId,
34
+ ts: Date.now(),
35
+ metrics: getMetrics(),
36
+ };
37
+
38
+ const controller = new AbortController();
39
+ const timeout = setTimeout(() => controller.abort(), 5000);
40
+
41
+ try {
42
+ const res = await fetch(`${opts.centrifugoInternalUrl}/api/publish`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Authorization': `apikey ${opts.centrifugoApiKey}`,
47
+ },
48
+ body: JSON.stringify({ channel: HEARTBEAT_CHANNEL, data: payload }),
49
+ signal: controller.signal,
50
+ });
51
+
52
+ if (!res.ok) {
53
+ const body = await res.text().catch(() => '');
54
+ console.error(`Heartbeat publish failed (HTTP ${res.status}): ${body}`);
55
+ }
56
+ } catch (err) {
57
+ if ((err as Error).name === 'AbortError') {
58
+ console.error('Heartbeat publish timed out');
59
+ } else {
60
+ console.error('Heartbeat publish failed:', err instanceof Error ? err.message : err);
61
+ }
62
+ } finally {
63
+ clearTimeout(timeout);
64
+ }
65
+ }
66
+
67
+ export function startHeartbeat(opts: HeartbeatOptions): NodeJS.Timeout {
68
+ const send = () => { publishHeartbeat(opts).catch(() => {}); };
69
+ send();
70
+ return setInterval(send, HEARTBEAT_INTERVAL);
71
+ }
72
+
73
+ export { getMetrics };
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { createConnection } from './connection.js';
2
+ import { startHeartbeat } from './heartbeat.js';
3
+
4
+ function requireEnv(name: string): string {
5
+ const value = process.env[name];
6
+ if (!value) {
7
+ console.error(`Missing required environment variable: ${name}`);
8
+ process.exit(1);
9
+ }
10
+ return value;
11
+ }
12
+
13
+ export function main(): void {
14
+ const rawUrl = requireEnv('CENTRIFUGO_URL');
15
+ // Centrifuge client requires ws:// scheme
16
+ const url = rawUrl.replace(/^http:\/\//, 'ws://').replace(/^https:\/\//, 'wss://') + '/connection/websocket';
17
+ const token = requireEnv('AGENT_TOKEN');
18
+ const agentId = requireEnv('AGENT_ID');
19
+ const backendUrl = process.env.BACKEND_INTERNAL_URL || '';
20
+ const centrifugoInternalUrl = process.env.CENTRIFUGO_INTERNAL_URL || '';
21
+ const centrifugoApiKey = process.env.CENTRIFUGO_API_KEY || '';
22
+
23
+ console.log(`Starting agent-controller for agent: ${agentId}`);
24
+ if (backendUrl) {
25
+ console.log(`Backend URL: ${backendUrl} (JWT mode)`);
26
+ }
27
+
28
+ const { client } = createConnection({ url, token, agentId, backendUrl: backendUrl || undefined });
29
+
30
+ let heartbeatTimer: NodeJS.Timeout | undefined;
31
+ if (centrifugoInternalUrl && centrifugoApiKey) {
32
+ console.log(`Heartbeat via Centrifugo internal API: ${centrifugoInternalUrl}`);
33
+ heartbeatTimer = startHeartbeat({ centrifugoInternalUrl, centrifugoApiKey, agentId });
34
+ } else {
35
+ console.warn('CENTRIFUGO_INTERNAL_URL or CENTRIFUGO_API_KEY not set, heartbeat disabled');
36
+ }
37
+
38
+ const shutdown = () => {
39
+ console.log('Shutting down...');
40
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
41
+ client.disconnect();
42
+ process.exit(0);
43
+ };
44
+
45
+ process.on('SIGINT', shutdown);
46
+ process.on('SIGTERM', shutdown);
47
+ }
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Centrifugo channel layout:
3
+ * agent:<agentId> — commands from backend, responses from agent
4
+ * heartbeat:<agentId> — heartbeat publications from agent
5
+ */
6
+
7
+ export type CommandType = 'exec' | 'restart' | 'deploy' | 'config' | 'pair' | 'stop' | 'backup';
8
+
9
+ export interface AgentCommand {
10
+ id: string;
11
+ type: CommandType;
12
+ payload: Record<string, unknown>;
13
+ }
14
+
15
+ export interface AgentResponse {
16
+ id: string;
17
+ type: CommandType;
18
+ success: boolean;
19
+ data?: Record<string, unknown>;
20
+ error?: string;
21
+ }
22
+
23
+ export interface HeartbeatPayload {
24
+ type: 'heartbeat';
25
+ agentId: string;
26
+ ts: number;
27
+ metrics: {
28
+ cpu: number;
29
+ mem: number;
30
+ uptime: number;
31
+ };
32
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "dist",
11
+ "rootDir": "src",
12
+ "declaration": true,
13
+ "resolveJsonModule": true,
14
+ "sourceMap": true
15
+ },
16
+ "include": ["src"],
17
+ "exclude": ["node_modules", "dist", "__tests__"]
18
+ }