port42-openclaw 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.
@@ -0,0 +1,25 @@
1
+ name: Publish Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: '22'
21
+ registry-url: 'https://registry.npmjs.org'
22
+
23
+ - run: npm ci
24
+ - run: npm run build
25
+ - run: npm publish
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gordon J. Mattey
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,74 @@
1
+ # port42-openclaw
2
+
3
+ Port42 channel adapter for OpenClaw. Bring your OpenClaw agents into [Port42](https://port42.ai) companion computing channels.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install port42-openclaw
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Someone shares a Port42 channel invite link with you. Add it to OpenClaw:
14
+
15
+ ```bash
16
+ openclaw channel add port42 \
17
+ --invite "https://your-host.ngrok-free.dev/invite?id=CHANNEL-UUID&name=my-channel&key=BASE64KEY" \
18
+ --agent my-researcher \
19
+ --name "Researcher"
20
+ ```
21
+
22
+ Your agent appears in the Port42 channel. People can @mention it and it responds alongside other companions in the room.
23
+
24
+ ### Manual config
25
+
26
+ Or edit `openclaw.json` directly:
27
+
28
+ ```json
29
+ {
30
+ "channels": {
31
+ "port42-project": {
32
+ "type": "port42",
33
+ "invite": "https://your-host.ngrok-free.dev/invite?id=CHANNEL-UUID&name=my-channel&key=BASE64KEY",
34
+ "displayName": "Researcher",
35
+ "trigger": "mention"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### Config options
42
+
43
+ | Option | Required | Default | Description |
44
+ |--------|----------|---------|-------------|
45
+ | `invite` | yes* | — | Port42 HTTPS invite link |
46
+ | `gateway` | yes* | — | WebSocket URL (derived from invite if provided) |
47
+ | `channelId` | yes* | — | Channel UUID (parsed from invite if provided) |
48
+ | `encryptionKey` | no | — | AES-256 key (parsed from invite if provided) |
49
+ | `displayName` | yes | — | How the agent appears in Port42 |
50
+ | `trigger` | no | `mention` | `mention` (respond to @name) or `all` (respond to everything) |
51
+
52
+ *Provide either `invite` or both `gateway` + `channelId`.
53
+
54
+ ## How it works
55
+
56
+ The adapter connects to a Port42 gateway as a regular peer over WebSocket. From Port42's perspective, your OpenClaw agent is just another companion in the channel.
57
+
58
+ - Messages are end-to-end encrypted (AES-256-GCM) using the channel key from the invite link
59
+ - The agent shows up in the presence list when connected
60
+ - Typing indicators show when the agent is generating a response
61
+ - Auto-reconnects if the connection drops
62
+
63
+ ## Building from source
64
+
65
+ ```bash
66
+ git clone https://github.com/gordonmattey/port42-openclaw.git
67
+ cd port42-openclaw
68
+ npm install
69
+ npm run build
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,33 @@
1
+ /**
2
+ * WebSocket connection to a Port42 gateway with reconnection.
3
+ */
4
+ export interface ConnectionConfig {
5
+ gateway: string;
6
+ channelId: string;
7
+ senderId: string;
8
+ displayName: string;
9
+ encryptionKey: string | null;
10
+ trigger: 'mention' | 'all';
11
+ onMessage: (senderName: string, content: string, messageId: string) => void;
12
+ onPresence?: (onlineIds: string[]) => void;
13
+ onConnected?: () => void;
14
+ onDisconnected?: () => void;
15
+ }
16
+ export declare class Port42Connection {
17
+ private ws;
18
+ private config;
19
+ private reconnectDelay;
20
+ private maxReconnectDelay;
21
+ private shouldReconnect;
22
+ private identified;
23
+ constructor(config: ConnectionConfig);
24
+ connect(): void;
25
+ disconnect(): void;
26
+ sendResponse(content: string): void;
27
+ sendTyping(isTyping: boolean): void;
28
+ private openSocket;
29
+ private handleEnvelope;
30
+ private handleIncomingMessage;
31
+ private send;
32
+ private scheduleReconnect;
33
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * WebSocket connection to a Port42 gateway with reconnection.
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Port42Connection = void 0;
10
+ const ws_1 = __importDefault(require("ws"));
11
+ const protocol_1 = require("./protocol");
12
+ const crypto_1 = require("./crypto");
13
+ class Port42Connection {
14
+ ws = null;
15
+ config;
16
+ reconnectDelay = 3000;
17
+ maxReconnectDelay = 30000;
18
+ shouldReconnect = true;
19
+ identified = false;
20
+ constructor(config) {
21
+ this.config = config;
22
+ }
23
+ connect() {
24
+ this.shouldReconnect = true;
25
+ this.openSocket();
26
+ }
27
+ disconnect() {
28
+ this.shouldReconnect = false;
29
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
30
+ this.send((0, protocol_1.createLeave)(this.config.channelId));
31
+ this.ws.close();
32
+ }
33
+ this.ws = null;
34
+ this.config.onDisconnected?.();
35
+ }
36
+ sendResponse(content) {
37
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
38
+ return;
39
+ if (this.config.encryptionKey) {
40
+ const payload = {
41
+ content,
42
+ senderName: this.config.displayName,
43
+ senderOwner: null,
44
+ replyToId: null,
45
+ };
46
+ const blob = (0, crypto_1.encrypt)(payload, this.config.encryptionKey);
47
+ this.send((0, protocol_1.createMessage)(this.config.channelId, this.config.senderId, this.config.displayName, blob, true));
48
+ }
49
+ else {
50
+ this.send((0, protocol_1.createMessage)(this.config.channelId, this.config.senderId, this.config.displayName, content, false));
51
+ }
52
+ }
53
+ sendTyping(isTyping) {
54
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN)
55
+ return;
56
+ this.send((0, protocol_1.createTyping)(this.config.channelId, this.config.senderId, isTyping));
57
+ }
58
+ openSocket() {
59
+ try {
60
+ this.ws = new ws_1.default(this.config.gateway);
61
+ }
62
+ catch (err) {
63
+ console.error('[port42] Failed to create WebSocket:', err);
64
+ this.scheduleReconnect();
65
+ return;
66
+ }
67
+ this.ws.on('open', () => {
68
+ this.reconnectDelay = 3000;
69
+ this.identified = false;
70
+ this.send((0, protocol_1.createIdentify)(this.config.senderId, this.config.displayName));
71
+ });
72
+ this.ws.on('message', (data) => {
73
+ try {
74
+ const envelope = JSON.parse(data.toString());
75
+ this.handleEnvelope(envelope);
76
+ }
77
+ catch (err) {
78
+ console.error('[port42] Failed to parse message:', err);
79
+ }
80
+ });
81
+ this.ws.on('close', () => {
82
+ this.identified = false;
83
+ this.config.onDisconnected?.();
84
+ this.scheduleReconnect();
85
+ });
86
+ this.ws.on('error', (err) => {
87
+ console.error('[port42] WebSocket error:', err.message);
88
+ });
89
+ }
90
+ handleEnvelope(envelope) {
91
+ switch (envelope.type) {
92
+ case 'welcome':
93
+ this.identified = true;
94
+ this.send((0, protocol_1.createJoin)(this.config.channelId));
95
+ this.config.onConnected?.();
96
+ break;
97
+ case 'message':
98
+ this.handleIncomingMessage(envelope);
99
+ break;
100
+ case 'presence':
101
+ if (envelope.online_ids) {
102
+ this.config.onPresence?.(envelope.online_ids);
103
+ }
104
+ break;
105
+ case 'error':
106
+ console.error('[port42] Gateway error:', envelope.error);
107
+ break;
108
+ case 'ack':
109
+ // Message delivered
110
+ break;
111
+ }
112
+ }
113
+ handleIncomingMessage(envelope) {
114
+ if (!envelope.payload || !envelope.message_id)
115
+ return;
116
+ // Ignore own messages
117
+ if (envelope.sender_id === this.config.senderId)
118
+ return;
119
+ // ACK receipt
120
+ this.send((0, protocol_1.createAck)(envelope.message_id, this.config.channelId));
121
+ // Decrypt if needed
122
+ let content;
123
+ let senderName;
124
+ if (envelope.payload.encrypted && this.config.encryptionKey) {
125
+ const decrypted = (0, crypto_1.decrypt)(envelope.payload.content, this.config.encryptionKey);
126
+ if (!decrypted) {
127
+ console.error('[port42] Decryption failed for message:', envelope.message_id);
128
+ return;
129
+ }
130
+ content = decrypted.content;
131
+ senderName = decrypted.senderName || envelope.sender_name || 'Unknown';
132
+ }
133
+ else {
134
+ content = envelope.payload.content;
135
+ senderName = envelope.payload.senderName || envelope.sender_name || 'Unknown';
136
+ }
137
+ // Check trigger rules
138
+ if (this.config.trigger === 'mention') {
139
+ const mentionPattern = new RegExp(`@${this.config.displayName}\\b`, 'i');
140
+ if (!mentionPattern.test(content))
141
+ return;
142
+ }
143
+ this.config.onMessage(senderName, content, envelope.message_id);
144
+ }
145
+ send(envelope) {
146
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
147
+ this.ws.send(JSON.stringify(envelope));
148
+ }
149
+ }
150
+ scheduleReconnect() {
151
+ if (!this.shouldReconnect)
152
+ return;
153
+ console.log(`[port42] Reconnecting in ${this.reconnectDelay / 1000}s...`);
154
+ setTimeout(() => this.openSocket(), this.reconnectDelay);
155
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
156
+ }
157
+ }
158
+ exports.Port42Connection = Port42Connection;
159
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * AES-256-GCM encryption/decryption matching Port42's ChannelCrypto.
3
+ *
4
+ * Wire format: base64(nonce[12] + ciphertext + tag[16])
5
+ */
6
+ import type { Payload } from './protocol';
7
+ export declare function encrypt(payload: Payload, keyBase64: string): string;
8
+ export declare function decrypt(blob: string, keyBase64: string): Payload | null;
package/dist/crypto.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * AES-256-GCM encryption/decryption matching Port42's ChannelCrypto.
4
+ *
5
+ * Wire format: base64(nonce[12] + ciphertext + tag[16])
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.encrypt = encrypt;
9
+ exports.decrypt = decrypt;
10
+ const node_crypto_1 = require("node:crypto");
11
+ const ALGORITHM = 'aes-256-gcm';
12
+ const NONCE_LENGTH = 12;
13
+ const TAG_LENGTH = 16;
14
+ function encrypt(payload, keyBase64) {
15
+ const key = Buffer.from(keyBase64, 'base64');
16
+ const nonce = (0, node_crypto_1.randomBytes)(NONCE_LENGTH);
17
+ const cleartext = JSON.stringify(payload);
18
+ const cipher = (0, node_crypto_1.createCipheriv)(ALGORITHM, key, nonce);
19
+ const encrypted = Buffer.concat([cipher.update(cleartext, 'utf8'), cipher.final()]);
20
+ const tag = cipher.getAuthTag();
21
+ // nonce + ciphertext + tag
22
+ const blob = Buffer.concat([nonce, encrypted, tag]);
23
+ return blob.toString('base64');
24
+ }
25
+ function decrypt(blob, keyBase64) {
26
+ try {
27
+ const key = Buffer.from(keyBase64, 'base64');
28
+ const data = Buffer.from(blob, 'base64');
29
+ if (data.length < NONCE_LENGTH + TAG_LENGTH) {
30
+ return null;
31
+ }
32
+ const nonce = data.subarray(0, NONCE_LENGTH);
33
+ const tag = data.subarray(data.length - TAG_LENGTH);
34
+ const ciphertext = data.subarray(NONCE_LENGTH, data.length - TAG_LENGTH);
35
+ const decipher = (0, node_crypto_1.createDecipheriv)(ALGORITHM, key, nonce);
36
+ decipher.setAuthTag(tag);
37
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
38
+ return JSON.parse(decrypted.toString('utf8'));
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Port42 channel adapter for OpenClaw.
3
+ *
4
+ * Bridges OpenClaw agents into Port42 companion computing channels.
5
+ * Users install with: openclaw plugins install port42-openclaw
6
+ * Then add a channel with a Port42 invite link.
7
+ */
8
+ export interface Port42ChannelConfig {
9
+ invite?: string;
10
+ gateway?: string;
11
+ channelId?: string;
12
+ encryptionKey?: string;
13
+ displayName: string;
14
+ trigger?: 'mention' | 'all';
15
+ }
16
+ interface PluginAPI {
17
+ registerChannel(name: string, handler: ChannelHandler): void;
18
+ log: {
19
+ info(msg: string): void;
20
+ error(msg: string): void;
21
+ debug(msg: string): void;
22
+ };
23
+ }
24
+ interface ChannelHandler {
25
+ connect(config: Port42ChannelConfig): Promise<ChannelInstance>;
26
+ }
27
+ interface ChannelInstance {
28
+ send(content: string): Promise<void>;
29
+ disconnect(): Promise<void>;
30
+ }
31
+ /**
32
+ * OpenClaw plugin entry point.
33
+ */
34
+ export declare function register(api: PluginAPI): void;
35
+ export { parseInviteLink } from './invite';
36
+ export { Port42Connection } from './connection';
37
+ export type { ConnectionConfig } from './connection';
38
+ export type { InviteConfig } from './invite';
39
+ export { encrypt, decrypt } from './crypto';
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Port42 channel adapter for OpenClaw.
4
+ *
5
+ * Bridges OpenClaw agents into Port42 companion computing channels.
6
+ * Users install with: openclaw plugins install port42-openclaw
7
+ * Then add a channel with a Port42 invite link.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.decrypt = exports.encrypt = exports.Port42Connection = exports.parseInviteLink = void 0;
11
+ exports.register = register;
12
+ const invite_1 = require("./invite");
13
+ const connection_1 = require("./connection");
14
+ const node_crypto_1 = require("node:crypto");
15
+ /**
16
+ * Generate a stable sender ID from the display name.
17
+ * This ensures the same agent gets the same ID across restarts
18
+ * so the gateway recognizes it as the same peer.
19
+ */
20
+ function stableSenderId(displayName) {
21
+ const hash = (0, node_crypto_1.createHash)('sha256').update(`port42-openclaw:${displayName}`).digest('hex');
22
+ // Format as UUID-like string for compatibility
23
+ return [
24
+ hash.slice(0, 8),
25
+ hash.slice(8, 12),
26
+ hash.slice(12, 16),
27
+ hash.slice(16, 20),
28
+ hash.slice(20, 32),
29
+ ].join('-');
30
+ }
31
+ /**
32
+ * Resolve config from either an invite link or explicit fields.
33
+ */
34
+ function resolveConfig(config) {
35
+ if (config.invite) {
36
+ const parsed = (0, invite_1.parseInviteLink)(config.invite);
37
+ return {
38
+ gateway: config.gateway || parsed.gateway,
39
+ channelId: config.channelId || parsed.channelId,
40
+ encryptionKey: config.encryptionKey || parsed.encryptionKey,
41
+ };
42
+ }
43
+ if (!config.gateway || !config.channelId) {
44
+ throw new Error('Port42 channel requires either an invite link or explicit gateway + channelId');
45
+ }
46
+ return {
47
+ gateway: config.gateway,
48
+ channelId: config.channelId,
49
+ encryptionKey: config.encryptionKey || null,
50
+ };
51
+ }
52
+ /**
53
+ * OpenClaw plugin entry point.
54
+ */
55
+ function register(api) {
56
+ api.registerChannel('port42', {
57
+ async connect(config) {
58
+ const resolved = resolveConfig(config);
59
+ const senderId = stableSenderId(config.displayName);
60
+ const trigger = config.trigger || 'mention';
61
+ api.log.info(`Connecting to Port42 channel ${resolved.channelId} as "${config.displayName}"`);
62
+ return new Promise((resolve, reject) => {
63
+ let messageHandler = null;
64
+ const connection = new connection_1.Port42Connection({
65
+ gateway: resolved.gateway,
66
+ channelId: resolved.channelId,
67
+ senderId,
68
+ displayName: config.displayName,
69
+ encryptionKey: resolved.encryptionKey,
70
+ trigger,
71
+ onMessage(senderName, content, messageId) {
72
+ if (messageHandler) {
73
+ messageHandler(senderName, content, messageId);
74
+ }
75
+ },
76
+ onConnected() {
77
+ api.log.info(`Connected to Port42 as "${config.displayName}"`);
78
+ resolve({
79
+ async send(content) {
80
+ connection.sendTyping(true);
81
+ // Small delay so typing indicator shows before response
82
+ await new Promise((r) => setTimeout(r, 100));
83
+ connection.sendResponse(content);
84
+ connection.sendTyping(false);
85
+ },
86
+ async disconnect() {
87
+ connection.disconnect();
88
+ api.log.info(`Disconnected from Port42`);
89
+ },
90
+ });
91
+ },
92
+ onDisconnected() {
93
+ api.log.debug('Port42 connection lost');
94
+ },
95
+ onPresence(onlineIds) {
96
+ api.log.debug(`Port42 presence: ${onlineIds.length} online`);
97
+ },
98
+ });
99
+ // Wire up the message handler for OpenClaw to receive inbound messages
100
+ messageHandler = (senderName, content, _messageId) => {
101
+ api.log.debug(`[${senderName}]: ${content.slice(0, 100)}`);
102
+ };
103
+ connection.connect();
104
+ // Timeout if we can't connect within 15 seconds
105
+ setTimeout(() => {
106
+ reject(new Error('Port42 connection timeout (15s)'));
107
+ }, 15000);
108
+ });
109
+ },
110
+ });
111
+ }
112
+ // Re-export modules for standalone use
113
+ var invite_2 = require("./invite");
114
+ Object.defineProperty(exports, "parseInviteLink", { enumerable: true, get: function () { return invite_2.parseInviteLink; } });
115
+ var connection_2 = require("./connection");
116
+ Object.defineProperty(exports, "Port42Connection", { enumerable: true, get: function () { return connection_2.Port42Connection; } });
117
+ var crypto_1 = require("./crypto");
118
+ Object.defineProperty(exports, "encrypt", { enumerable: true, get: function () { return crypto_1.encrypt; } });
119
+ Object.defineProperty(exports, "decrypt", { enumerable: true, get: function () { return crypto_1.decrypt; } });
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parse Port42 HTTPS invite links into connection config.
3
+ *
4
+ * Invite format:
5
+ * https://<host>/invite?id=<channel-uuid>&name=<channel-name>&key=<url-encoded-base64-aes-key>
6
+ */
7
+ export interface InviteConfig {
8
+ gateway: string;
9
+ channelId: string;
10
+ channelName: string;
11
+ encryptionKey: string | null;
12
+ }
13
+ export declare function parseInviteLink(invite: string): InviteConfig;
package/dist/invite.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * Parse Port42 HTTPS invite links into connection config.
4
+ *
5
+ * Invite format:
6
+ * https://<host>/invite?id=<channel-uuid>&name=<channel-name>&key=<url-encoded-base64-aes-key>
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseInviteLink = parseInviteLink;
10
+ function parseInviteLink(invite) {
11
+ const url = new URL(invite);
12
+ const channelId = url.searchParams.get('id');
13
+ if (!channelId) {
14
+ throw new Error('Invite link missing channel id (id= parameter)');
15
+ }
16
+ const channelName = url.searchParams.get('name') || 'unknown';
17
+ const encryptionKey = url.searchParams.get('key') || null;
18
+ // Derive WebSocket URL from the HTTPS host
19
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
20
+ const gateway = `${protocol}//${url.host}/ws`;
21
+ return { gateway, channelId, channelName, encryptionKey };
22
+ }
23
+ //# sourceMappingURL=invite.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Port42 gateway WebSocket protocol types.
3
+ */
4
+ export type EnvelopeType = 'identify' | 'welcome' | 'join' | 'leave' | 'message' | 'ack' | 'presence' | 'typing' | 'error';
5
+ export interface Envelope {
6
+ type: EnvelopeType;
7
+ channel_id?: string;
8
+ sender_id?: string;
9
+ sender_name?: string;
10
+ message_id?: string;
11
+ payload?: Payload;
12
+ timestamp?: number;
13
+ error?: string;
14
+ online_ids?: string[];
15
+ status?: 'online' | 'offline';
16
+ companion_ids?: string[];
17
+ }
18
+ export interface Payload {
19
+ content: string;
20
+ senderName?: string;
21
+ senderOwner?: string | null;
22
+ replyToId?: string | null;
23
+ encrypted?: boolean;
24
+ }
25
+ export declare function createIdentify(senderId: string, senderName: string): Envelope;
26
+ export declare function createJoin(channelId: string, companionIds?: string[]): Envelope;
27
+ export declare function createLeave(channelId: string): Envelope;
28
+ export declare function createMessage(channelId: string, senderId: string, senderName: string, content: string, encrypted?: boolean): Envelope;
29
+ export declare function createAck(messageId: string, channelId: string): Envelope;
30
+ export declare function createTyping(channelId: string, senderId: string, isTyping: boolean): Envelope;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * Port42 gateway WebSocket protocol types.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createIdentify = createIdentify;
7
+ exports.createJoin = createJoin;
8
+ exports.createLeave = createLeave;
9
+ exports.createMessage = createMessage;
10
+ exports.createAck = createAck;
11
+ exports.createTyping = createTyping;
12
+ function createIdentify(senderId, senderName) {
13
+ return {
14
+ type: 'identify',
15
+ sender_id: senderId,
16
+ sender_name: senderName,
17
+ };
18
+ }
19
+ function createJoin(channelId, companionIds = []) {
20
+ return {
21
+ type: 'join',
22
+ channel_id: channelId,
23
+ companion_ids: companionIds,
24
+ };
25
+ }
26
+ function createLeave(channelId) {
27
+ return {
28
+ type: 'leave',
29
+ channel_id: channelId,
30
+ };
31
+ }
32
+ function createMessage(channelId, senderId, senderName, content, encrypted = false) {
33
+ return {
34
+ type: 'message',
35
+ channel_id: channelId,
36
+ sender_id: senderId,
37
+ sender_name: senderName,
38
+ message_id: crypto.randomUUID(),
39
+ payload: {
40
+ content,
41
+ senderName: encrypted ? '' : senderName,
42
+ senderOwner: null,
43
+ replyToId: null,
44
+ encrypted,
45
+ },
46
+ timestamp: Date.now(),
47
+ };
48
+ }
49
+ function createAck(messageId, channelId) {
50
+ return {
51
+ type: 'ack',
52
+ message_id: messageId,
53
+ channel_id: channelId,
54
+ };
55
+ }
56
+ function createTyping(channelId, senderId, isTyping) {
57
+ return {
58
+ type: 'typing',
59
+ channel_id: channelId,
60
+ sender_id: senderId,
61
+ payload: {
62
+ content: isTyping ? 'typing' : 'stopped',
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "port42-openclaw",
3
+ "version": "0.1.0",
4
+ "description": "Port42 channel adapter — bring your OpenClaw agents into Port42 companion computing channels",
5
+ "type": "channel",
6
+ "entry": "dist/index.js",
7
+ "author": "Port42",
8
+ "license": "MIT",
9
+ "homepage": "https://port42.ai",
10
+ "repository": "https://github.com/gordonmattey/port42-openclaw",
11
+ "config": {
12
+ "invite": {
13
+ "type": "string",
14
+ "description": "Port42 HTTPS invite link",
15
+ "required": false
16
+ },
17
+ "gateway": {
18
+ "type": "string",
19
+ "description": "WebSocket URL of Port42 gateway",
20
+ "required": false
21
+ },
22
+ "channelId": {
23
+ "type": "string",
24
+ "description": "Port42 channel UUID",
25
+ "required": false
26
+ },
27
+ "encryptionKey": {
28
+ "type": "string",
29
+ "description": "Base64 AES-256 encryption key",
30
+ "required": false
31
+ },
32
+ "displayName": {
33
+ "type": "string",
34
+ "description": "Agent display name in Port42",
35
+ "required": true
36
+ },
37
+ "trigger": {
38
+ "type": "string",
39
+ "enum": ["mention", "all"],
40
+ "default": "mention",
41
+ "description": "Respond to @mentions only or all messages"
42
+ }
43
+ }
44
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "port42-openclaw",
3
+ "version": "0.1.0",
4
+ "description": "Port42 channel adapter for OpenClaw — bring your agents into Port42 companion computing channels",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "test": "node --test dist/**/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "openclaw",
14
+ "port42",
15
+ "channel",
16
+ "plugin",
17
+ "ai",
18
+ "companion-computing"
19
+ ],
20
+ "author": "Port42",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/gordonmattey/port42-openclaw"
25
+ },
26
+ "homepage": "https://port42.ai",
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "dependencies": {
31
+ "ws": "^8.16.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/ws": "^8.5.10",
35
+ "typescript": "^5.4.0"
36
+ }
37
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * WebSocket connection to a Port42 gateway with reconnection.
3
+ */
4
+
5
+ import WebSocket from 'ws';
6
+ import {
7
+ type Envelope,
8
+ createIdentify,
9
+ createJoin,
10
+ createLeave,
11
+ createAck,
12
+ createTyping,
13
+ createMessage,
14
+ } from './protocol';
15
+ import { encrypt, decrypt } from './crypto';
16
+
17
+ export interface ConnectionConfig {
18
+ gateway: string;
19
+ channelId: string;
20
+ senderId: string;
21
+ displayName: string;
22
+ encryptionKey: string | null;
23
+ trigger: 'mention' | 'all';
24
+ onMessage: (senderName: string, content: string, messageId: string) => void;
25
+ onPresence?: (onlineIds: string[]) => void;
26
+ onConnected?: () => void;
27
+ onDisconnected?: () => void;
28
+ }
29
+
30
+ export class Port42Connection {
31
+ private ws: WebSocket | null = null;
32
+ private config: ConnectionConfig;
33
+ private reconnectDelay = 3000;
34
+ private maxReconnectDelay = 30000;
35
+ private shouldReconnect = true;
36
+ private identified = false;
37
+
38
+ constructor(config: ConnectionConfig) {
39
+ this.config = config;
40
+ }
41
+
42
+ connect(): void {
43
+ this.shouldReconnect = true;
44
+ this.openSocket();
45
+ }
46
+
47
+ disconnect(): void {
48
+ this.shouldReconnect = false;
49
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
50
+ this.send(createLeave(this.config.channelId));
51
+ this.ws.close();
52
+ }
53
+ this.ws = null;
54
+ this.config.onDisconnected?.();
55
+ }
56
+
57
+ sendResponse(content: string): void {
58
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
59
+
60
+ if (this.config.encryptionKey) {
61
+ const payload = {
62
+ content,
63
+ senderName: this.config.displayName,
64
+ senderOwner: null,
65
+ replyToId: null,
66
+ };
67
+ const blob = encrypt(payload, this.config.encryptionKey);
68
+ this.send(createMessage(
69
+ this.config.channelId,
70
+ this.config.senderId,
71
+ this.config.displayName,
72
+ blob,
73
+ true,
74
+ ));
75
+ } else {
76
+ this.send(createMessage(
77
+ this.config.channelId,
78
+ this.config.senderId,
79
+ this.config.displayName,
80
+ content,
81
+ false,
82
+ ));
83
+ }
84
+ }
85
+
86
+ sendTyping(isTyping: boolean): void {
87
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
88
+ this.send(createTyping(this.config.channelId, this.config.senderId, isTyping));
89
+ }
90
+
91
+ private openSocket(): void {
92
+ try {
93
+ this.ws = new WebSocket(this.config.gateway);
94
+ } catch (err) {
95
+ console.error('[port42] Failed to create WebSocket:', err);
96
+ this.scheduleReconnect();
97
+ return;
98
+ }
99
+
100
+ this.ws.on('open', () => {
101
+ this.reconnectDelay = 3000;
102
+ this.identified = false;
103
+ this.send(createIdentify(this.config.senderId, this.config.displayName));
104
+ });
105
+
106
+ this.ws.on('message', (data) => {
107
+ try {
108
+ const envelope: Envelope = JSON.parse(data.toString());
109
+ this.handleEnvelope(envelope);
110
+ } catch (err) {
111
+ console.error('[port42] Failed to parse message:', err);
112
+ }
113
+ });
114
+
115
+ this.ws.on('close', () => {
116
+ this.identified = false;
117
+ this.config.onDisconnected?.();
118
+ this.scheduleReconnect();
119
+ });
120
+
121
+ this.ws.on('error', (err) => {
122
+ console.error('[port42] WebSocket error:', err.message);
123
+ });
124
+ }
125
+
126
+ private handleEnvelope(envelope: Envelope): void {
127
+ switch (envelope.type) {
128
+ case 'welcome':
129
+ this.identified = true;
130
+ this.send(createJoin(this.config.channelId));
131
+ this.config.onConnected?.();
132
+ break;
133
+
134
+ case 'message':
135
+ this.handleIncomingMessage(envelope);
136
+ break;
137
+
138
+ case 'presence':
139
+ if (envelope.online_ids) {
140
+ this.config.onPresence?.(envelope.online_ids);
141
+ }
142
+ break;
143
+
144
+ case 'error':
145
+ console.error('[port42] Gateway error:', envelope.error);
146
+ break;
147
+
148
+ case 'ack':
149
+ // Message delivered
150
+ break;
151
+ }
152
+ }
153
+
154
+ private handleIncomingMessage(envelope: Envelope): void {
155
+ if (!envelope.payload || !envelope.message_id) return;
156
+
157
+ // Ignore own messages
158
+ if (envelope.sender_id === this.config.senderId) return;
159
+
160
+ // ACK receipt
161
+ this.send(createAck(envelope.message_id, this.config.channelId!));
162
+
163
+ // Decrypt if needed
164
+ let content: string;
165
+ let senderName: string;
166
+
167
+ if (envelope.payload.encrypted && this.config.encryptionKey) {
168
+ const decrypted = decrypt(envelope.payload.content, this.config.encryptionKey);
169
+ if (!decrypted) {
170
+ console.error('[port42] Decryption failed for message:', envelope.message_id);
171
+ return;
172
+ }
173
+ content = decrypted.content;
174
+ senderName = decrypted.senderName || envelope.sender_name || 'Unknown';
175
+ } else {
176
+ content = envelope.payload.content;
177
+ senderName = envelope.payload.senderName || envelope.sender_name || 'Unknown';
178
+ }
179
+
180
+ // Check trigger rules
181
+ if (this.config.trigger === 'mention') {
182
+ const mentionPattern = new RegExp(`@${this.config.displayName}\\b`, 'i');
183
+ if (!mentionPattern.test(content)) return;
184
+ }
185
+
186
+ this.config.onMessage(senderName, content, envelope.message_id);
187
+ }
188
+
189
+ private send(envelope: Envelope): void {
190
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
191
+ this.ws.send(JSON.stringify(envelope));
192
+ }
193
+ }
194
+
195
+ private scheduleReconnect(): void {
196
+ if (!this.shouldReconnect) return;
197
+ console.log(`[port42] Reconnecting in ${this.reconnectDelay / 1000}s...`);
198
+ setTimeout(() => this.openSocket(), this.reconnectDelay);
199
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
200
+ }
201
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * AES-256-GCM encryption/decryption matching Port42's ChannelCrypto.
3
+ *
4
+ * Wire format: base64(nonce[12] + ciphertext + tag[16])
5
+ */
6
+
7
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
8
+ import type { Payload } from './protocol';
9
+
10
+ const ALGORITHM = 'aes-256-gcm';
11
+ const NONCE_LENGTH = 12;
12
+ const TAG_LENGTH = 16;
13
+
14
+ export function encrypt(payload: Payload, keyBase64: string): string {
15
+ const key = Buffer.from(keyBase64, 'base64');
16
+ const nonce = randomBytes(NONCE_LENGTH);
17
+ const cleartext = JSON.stringify(payload);
18
+
19
+ const cipher = createCipheriv(ALGORITHM, key, nonce);
20
+ const encrypted = Buffer.concat([cipher.update(cleartext, 'utf8'), cipher.final()]);
21
+ const tag = cipher.getAuthTag();
22
+
23
+ // nonce + ciphertext + tag
24
+ const blob = Buffer.concat([nonce, encrypted, tag]);
25
+ return blob.toString('base64');
26
+ }
27
+
28
+ export function decrypt(blob: string, keyBase64: string): Payload | null {
29
+ try {
30
+ const key = Buffer.from(keyBase64, 'base64');
31
+ const data = Buffer.from(blob, 'base64');
32
+
33
+ if (data.length < NONCE_LENGTH + TAG_LENGTH) {
34
+ return null;
35
+ }
36
+
37
+ const nonce = data.subarray(0, NONCE_LENGTH);
38
+ const tag = data.subarray(data.length - TAG_LENGTH);
39
+ const ciphertext = data.subarray(NONCE_LENGTH, data.length - TAG_LENGTH);
40
+
41
+ const decipher = createDecipheriv(ALGORITHM, key, nonce);
42
+ decipher.setAuthTag(tag);
43
+
44
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
45
+ return JSON.parse(decrypted.toString('utf8'));
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Port42 channel adapter for OpenClaw.
3
+ *
4
+ * Bridges OpenClaw agents into Port42 companion computing channels.
5
+ * Users install with: openclaw plugins install port42-openclaw
6
+ * Then add a channel with a Port42 invite link.
7
+ */
8
+
9
+ import { parseInviteLink, type InviteConfig } from './invite';
10
+ import { Port42Connection, type ConnectionConfig } from './connection';
11
+ import { createHash } from 'node:crypto';
12
+
13
+ export interface Port42ChannelConfig {
14
+ invite?: string;
15
+ gateway?: string;
16
+ channelId?: string;
17
+ encryptionKey?: string;
18
+ displayName: string;
19
+ trigger?: 'mention' | 'all';
20
+ }
21
+
22
+ interface PluginAPI {
23
+ registerChannel(
24
+ name: string,
25
+ handler: ChannelHandler,
26
+ ): void;
27
+ log: {
28
+ info(msg: string): void;
29
+ error(msg: string): void;
30
+ debug(msg: string): void;
31
+ };
32
+ }
33
+
34
+ interface ChannelHandler {
35
+ connect(config: Port42ChannelConfig): Promise<ChannelInstance>;
36
+ }
37
+
38
+ interface ChannelInstance {
39
+ send(content: string): Promise<void>;
40
+ disconnect(): Promise<void>;
41
+ }
42
+
43
+ /**
44
+ * Generate a stable sender ID from the display name.
45
+ * This ensures the same agent gets the same ID across restarts
46
+ * so the gateway recognizes it as the same peer.
47
+ */
48
+ function stableSenderId(displayName: string): string {
49
+ const hash = createHash('sha256').update(`port42-openclaw:${displayName}`).digest('hex');
50
+ // Format as UUID-like string for compatibility
51
+ return [
52
+ hash.slice(0, 8),
53
+ hash.slice(8, 12),
54
+ hash.slice(12, 16),
55
+ hash.slice(16, 20),
56
+ hash.slice(20, 32),
57
+ ].join('-');
58
+ }
59
+
60
+ /**
61
+ * Resolve config from either an invite link or explicit fields.
62
+ */
63
+ function resolveConfig(config: Port42ChannelConfig): {
64
+ gateway: string;
65
+ channelId: string;
66
+ encryptionKey: string | null;
67
+ } {
68
+ if (config.invite) {
69
+ const parsed = parseInviteLink(config.invite);
70
+ return {
71
+ gateway: config.gateway || parsed.gateway,
72
+ channelId: config.channelId || parsed.channelId,
73
+ encryptionKey: config.encryptionKey || parsed.encryptionKey,
74
+ };
75
+ }
76
+
77
+ if (!config.gateway || !config.channelId) {
78
+ throw new Error('Port42 channel requires either an invite link or explicit gateway + channelId');
79
+ }
80
+
81
+ return {
82
+ gateway: config.gateway,
83
+ channelId: config.channelId,
84
+ encryptionKey: config.encryptionKey || null,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * OpenClaw plugin entry point.
90
+ */
91
+ export function register(api: PluginAPI): void {
92
+ api.registerChannel('port42', {
93
+ async connect(config: Port42ChannelConfig): Promise<ChannelInstance> {
94
+ const resolved = resolveConfig(config);
95
+ const senderId = stableSenderId(config.displayName);
96
+ const trigger = config.trigger || 'mention';
97
+
98
+ api.log.info(`Connecting to Port42 channel ${resolved.channelId} as "${config.displayName}"`);
99
+
100
+ return new Promise<ChannelInstance>((resolve, reject) => {
101
+ let messageHandler: ((senderName: string, content: string, messageId: string) => void) | null = null;
102
+
103
+ const connection = new Port42Connection({
104
+ gateway: resolved.gateway,
105
+ channelId: resolved.channelId,
106
+ senderId,
107
+ displayName: config.displayName,
108
+ encryptionKey: resolved.encryptionKey,
109
+ trigger,
110
+
111
+ onMessage(senderName, content, messageId) {
112
+ if (messageHandler) {
113
+ messageHandler(senderName, content, messageId);
114
+ }
115
+ },
116
+
117
+ onConnected() {
118
+ api.log.info(`Connected to Port42 as "${config.displayName}"`);
119
+ resolve({
120
+ async send(content: string) {
121
+ connection.sendTyping(true);
122
+ // Small delay so typing indicator shows before response
123
+ await new Promise((r) => setTimeout(r, 100));
124
+ connection.sendResponse(content);
125
+ connection.sendTyping(false);
126
+ },
127
+
128
+ async disconnect() {
129
+ connection.disconnect();
130
+ api.log.info(`Disconnected from Port42`);
131
+ },
132
+ });
133
+ },
134
+
135
+ onDisconnected() {
136
+ api.log.debug('Port42 connection lost');
137
+ },
138
+
139
+ onPresence(onlineIds) {
140
+ api.log.debug(`Port42 presence: ${onlineIds.length} online`);
141
+ },
142
+ });
143
+
144
+ // Wire up the message handler for OpenClaw to receive inbound messages
145
+ messageHandler = (senderName, content, _messageId) => {
146
+ api.log.debug(`[${senderName}]: ${content.slice(0, 100)}`);
147
+ };
148
+
149
+ connection.connect();
150
+
151
+ // Timeout if we can't connect within 15 seconds
152
+ setTimeout(() => {
153
+ reject(new Error('Port42 connection timeout (15s)'));
154
+ }, 15000);
155
+ });
156
+ },
157
+ });
158
+ }
159
+
160
+ // Re-export modules for standalone use
161
+ export { parseInviteLink } from './invite';
162
+ export { Port42Connection } from './connection';
163
+ export type { ConnectionConfig } from './connection';
164
+ export type { InviteConfig } from './invite';
165
+ export { encrypt, decrypt } from './crypto';
package/src/invite.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Parse Port42 HTTPS invite links into connection config.
3
+ *
4
+ * Invite format:
5
+ * https://<host>/invite?id=<channel-uuid>&name=<channel-name>&key=<url-encoded-base64-aes-key>
6
+ */
7
+
8
+ export interface InviteConfig {
9
+ gateway: string;
10
+ channelId: string;
11
+ channelName: string;
12
+ encryptionKey: string | null;
13
+ }
14
+
15
+ export function parseInviteLink(invite: string): InviteConfig {
16
+ const url = new URL(invite);
17
+
18
+ const channelId = url.searchParams.get('id');
19
+ if (!channelId) {
20
+ throw new Error('Invite link missing channel id (id= parameter)');
21
+ }
22
+
23
+ const channelName = url.searchParams.get('name') || 'unknown';
24
+ const encryptionKey = url.searchParams.get('key') || null;
25
+
26
+ // Derive WebSocket URL from the HTTPS host
27
+ const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
28
+ const gateway = `${protocol}//${url.host}/ws`;
29
+
30
+ return { gateway, channelId, channelName, encryptionKey };
31
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Port42 gateway WebSocket protocol types.
3
+ */
4
+
5
+ export type EnvelopeType =
6
+ | 'identify'
7
+ | 'welcome'
8
+ | 'join'
9
+ | 'leave'
10
+ | 'message'
11
+ | 'ack'
12
+ | 'presence'
13
+ | 'typing'
14
+ | 'error';
15
+
16
+ export interface Envelope {
17
+ type: EnvelopeType;
18
+ channel_id?: string;
19
+ sender_id?: string;
20
+ sender_name?: string;
21
+ message_id?: string;
22
+ payload?: Payload;
23
+ timestamp?: number;
24
+ error?: string;
25
+ online_ids?: string[];
26
+ status?: 'online' | 'offline';
27
+ companion_ids?: string[];
28
+ }
29
+
30
+ export interface Payload {
31
+ content: string;
32
+ senderName?: string;
33
+ senderOwner?: string | null;
34
+ replyToId?: string | null;
35
+ encrypted?: boolean;
36
+ }
37
+
38
+ export function createIdentify(senderId: string, senderName: string): Envelope {
39
+ return {
40
+ type: 'identify',
41
+ sender_id: senderId,
42
+ sender_name: senderName,
43
+ };
44
+ }
45
+
46
+ export function createJoin(channelId: string, companionIds: string[] = []): Envelope {
47
+ return {
48
+ type: 'join',
49
+ channel_id: channelId,
50
+ companion_ids: companionIds,
51
+ };
52
+ }
53
+
54
+ export function createLeave(channelId: string): Envelope {
55
+ return {
56
+ type: 'leave',
57
+ channel_id: channelId,
58
+ };
59
+ }
60
+
61
+ export function createMessage(
62
+ channelId: string,
63
+ senderId: string,
64
+ senderName: string,
65
+ content: string,
66
+ encrypted: boolean = false,
67
+ ): Envelope {
68
+ return {
69
+ type: 'message',
70
+ channel_id: channelId,
71
+ sender_id: senderId,
72
+ sender_name: senderName,
73
+ message_id: crypto.randomUUID(),
74
+ payload: {
75
+ content,
76
+ senderName: encrypted ? '' : senderName,
77
+ senderOwner: null,
78
+ replyToId: null,
79
+ encrypted,
80
+ },
81
+ timestamp: Date.now(),
82
+ };
83
+ }
84
+
85
+ export function createAck(messageId: string, channelId: string): Envelope {
86
+ return {
87
+ type: 'ack',
88
+ message_id: messageId,
89
+ channel_id: channelId,
90
+ };
91
+ }
92
+
93
+ export function createTyping(
94
+ channelId: string,
95
+ senderId: string,
96
+ isTyping: boolean,
97
+ ): Envelope {
98
+ return {
99
+ type: 'typing',
100
+ channel_id: channelId,
101
+ sender_id: senderId,
102
+ payload: {
103
+ content: isTyping ? 'typing' : 'stopped',
104
+ },
105
+ };
106
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }