heyio 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Jolley
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,212 @@
1
+ # 🤖 IO
2
+
3
+ A personal AI assistant daemon built on the GitHub Copilot SDK. IO runs 24/7 on your machine, reachable via Telegram and a terminal TUI.
4
+
5
+ [![CI](https://github.com/michaeljolley/io/actions/workflows/ci.yml/badge.svg)](https://github.com/michaeljolley/io/actions/workflows/ci.yml)
6
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
7
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)
8
+
9
+ ## ✨ Features
10
+
11
+ - **Copilot SDK Integration** — powered by GitHub's Copilot SDK for LLM conversations with tool calling
12
+ - **Multi-Interface** — Telegram bot + terminal TUI + HTTP API (future web UI)
13
+ - **Persistent Memory** — wiki-based knowledge base stored at `~/.io/wiki/`
14
+ - **Squad System** — persistent project teams that remember decisions, context, and history
15
+ - **Skills** — modular skill system; install from git repos or the [skills.sh](https://skills.sh) registry
16
+ - **Adaptive Sessions** — infinite sessions with automatic context compaction
17
+ - **Worker Agents** — delegated task execution through specialized agent sessions
18
+ - **Self-Updating** — checks for updates and can apply them automatically
19
+
20
+ ## đź“‹ Prerequisites
21
+
22
+ - **Node.js** >= 18
23
+ - **GitHub Copilot subscription** — IO uses the Copilot SDK, which requires an active Copilot license
24
+ - **GitHub CLI** (`gh`) — authenticated via `gh auth login`
25
+
26
+ ## 🚀 Quick Start
27
+
28
+ ### Install
29
+
30
+ ```bash
31
+ npm install -g heyio
32
+ ```
33
+
34
+ ### Setup
35
+
36
+ Run the setup wizard to configure your Telegram bot token and user ID:
37
+
38
+ ```bash
39
+ io setup
40
+ ```
41
+
42
+ This creates a config file at `~/.io/config.json`.
43
+
44
+ ### Run
45
+
46
+ ```bash
47
+ # Interactive TUI mode
48
+ io
49
+
50
+ # Background daemon (Telegram + HTTP API)
51
+ io --daemon
52
+
53
+ # Allow IO to modify its own source code
54
+ io --self-edit
55
+ ```
56
+
57
+ ## đź’¬ CLI Usage
58
+
59
+ | Command | Description |
60
+ | --- | --- |
61
+ | `io` | Start interactive TUI mode |
62
+ | `io --daemon` | Run as background daemon (Telegram + API) |
63
+ | `io --self-edit` | Allow IO to modify its own source |
64
+ | `io setup` | Configure Telegram bot token and user ID |
65
+ | `io skill list` | List installed skills |
66
+ | `io skill add <repo-url>` | Install a skill from a git repository |
67
+ | `io skill remove <slug>` | Remove an installed skill |
68
+ | `io skill search <query>` | Search the skills.sh registry |
69
+
70
+ ## ⚙️ Configuration
71
+
72
+ IO stores its configuration at `~/.io/config.json`. The setup wizard (`io setup`) handles initial configuration, but you can also edit the file directly.
73
+
74
+ ```jsonc
75
+ {
76
+ // Telegram bot token from @BotFather
77
+ "telegramBotToken": "123456:ABC-DEF...",
78
+
79
+ // Your Telegram user ID (for authentication)
80
+ "telegramUserId": 123456789,
81
+
82
+ // Enable self-edit mode by default
83
+ "selfEdit": false
84
+ }
85
+ ```
86
+
87
+ All persistent data is stored under `~/.io/`:
88
+
89
+ | Path | Purpose |
90
+ | --- | --- |
91
+ | `~/.io/config.json` | User configuration |
92
+ | `~/.io/wiki/` | Knowledge base (markdown files) |
93
+ | `~/.io/io.db` | SQLite database (squads, tasks) |
94
+ | `~/.io/skills/` | Installed skills |
95
+
96
+ ## đź§© Skills System
97
+
98
+ Skills are modular extensions that add new tools and capabilities to IO. Each skill is a directory containing a `SKILL.md` manifest and tool definitions.
99
+
100
+ ### Managing Skills
101
+
102
+ ```bash
103
+ # Search the skills.sh registry
104
+ io skill search "github"
105
+
106
+ # Install from a git repo
107
+ io skill add https://github.com/user/my-skill.git
108
+
109
+ # List installed skills
110
+ io skill list
111
+
112
+ # Remove a skill
113
+ io skill remove my-skill
114
+ ```
115
+
116
+ ### Creating a Skill
117
+
118
+ A skill is a directory with a `SKILL.md` file that describes the skill and its tools. See the [Contributing Guide](CONTRIBUTING.md) for details on the skill format.
119
+
120
+ ## 👥 Squad System
121
+
122
+ Squads are persistent project teams that IO manages. Each squad:
123
+
124
+ - Is associated with a specific project or domain
125
+ - Remembers decisions, context, and conversation history
126
+ - Can have multiple specialized agents working together
127
+ - Persists across sessions in the SQLite database
128
+
129
+ IO's orchestrator automatically creates and manages squads based on your conversations.
130
+
131
+ ## 🏗️ Architecture
132
+
133
+ ```
134
+ User → [TUI / Telegram / HTTP API]
135
+ ↓
136
+ Orchestrator (Copilot SDK)
137
+ ↕ ↕
138
+ Squad Manager Wiki/Memory
139
+ ↓
140
+ Worker Agents
141
+ ```
142
+
143
+ IO is built around the **Copilot SDK** which handles all LLM interactions, including tool calling and context management. The **Orchestrator** manages the primary conversation session with automatic context compaction for infinite-length sessions.
144
+
145
+ For complex tasks, the orchestrator delegates work to **Worker Agents** — short-lived agent sessions that execute specific tasks and report back.
146
+
147
+ The **Squad System** provides persistent project context, while the **Wiki** serves as a long-term knowledge base that spans all conversations.
148
+
149
+ ## 🏗️ Project Structure
150
+
151
+ ```
152
+ src/
153
+ ├── index.ts # CLI entry (commander)
154
+ ├── daemon.ts # Daemon startup/shutdown
155
+ ├── config.ts # Config loading
156
+ ├── paths.ts # Path constants
157
+ ├── update.ts # Self-update checker
158
+ ├── copilot/
159
+ │ ├── client.ts # CopilotClient singleton
160
+ │ ├── orchestrator.ts # Main session management
161
+ │ ├── agents.ts # Worker agent sessions
162
+ │ ├── tools.ts # Tool definitions
163
+ │ ├── skills.ts # Skills loader
164
+ │ └── system-message.ts # System prompt builder
165
+ ├── store/
166
+ │ ├── db.ts # SQLite database
167
+ │ ├── squads.ts # Squad CRUD
168
+ │ └── tasks.ts # Agent task tracking
169
+ ├── wiki/
170
+ │ ├── fs.ts # Wiki filesystem
171
+ │ └── search.ts # Wiki search
172
+ ├── telegram/
173
+ │ ├── bot.ts # Grammy Telegram bot
174
+ │ └── handlers.ts # Command handlers
175
+ ├── tui/
176
+ │ └── index.ts # Terminal UI
177
+ └── api/
178
+ └── server.ts # Express HTTP + SSE
179
+ ```
180
+
181
+ ## 🛠️ Development
182
+
183
+ ```bash
184
+ # Clone the repository
185
+ git clone https://github.com/michaeljolley/io.git
186
+ cd io
187
+
188
+ # Install dependencies
189
+ npm install
190
+
191
+ # Run in development mode (watch)
192
+ npm run dev
193
+
194
+ # Build for production
195
+ npm run build
196
+
197
+ # Run the TUI directly
198
+ npm run tui
199
+
200
+ # Run the daemon directly
201
+ npm run daemon
202
+ ```
203
+
204
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed development guidelines.
205
+
206
+ ## đź“„ License
207
+
208
+ This project is licensed under the [MIT License](LICENSE).
209
+
210
+ ## 🤝 Contributing
211
+
212
+ Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) before submitting a pull request.
@@ -0,0 +1,70 @@
1
+ import express from "express";
2
+ import { config } from "../config.js";
3
+ let messageHandler;
4
+ const sseConnections = new Set();
5
+ export function setMessageHandler(handler) {
6
+ messageHandler = handler;
7
+ }
8
+ export function broadcastToSSE(text) {
9
+ const payload = JSON.stringify({ type: "delta", text });
10
+ for (const res of sseConnections) {
11
+ res.write(`data: ${payload}\n\n`);
12
+ }
13
+ }
14
+ export async function startApiServer() {
15
+ const app = express();
16
+ app.use(express.json());
17
+ app.use((_req, res, next) => {
18
+ res.setHeader("Access-Control-Allow-Origin", "*");
19
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
20
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
21
+ next();
22
+ });
23
+ app.get("/health", (_req, res) => {
24
+ res.json({ status: "ok" });
25
+ });
26
+ app.get("/status", (_req, res) => {
27
+ res.json({ version: "1.0.0", uptime: process.uptime() });
28
+ });
29
+ app.post("/message", async (req, res) => {
30
+ const { text } = req.body;
31
+ if (!text) {
32
+ res.status(400).json({ error: "Missing 'text' in request body" });
33
+ return;
34
+ }
35
+ if (!messageHandler) {
36
+ res.status(503).json({ error: "No message handler registered" });
37
+ return;
38
+ }
39
+ const connectionId = crypto.randomUUID();
40
+ let fullResponse = "";
41
+ await messageHandler(text, connectionId, (chunk, done) => {
42
+ fullResponse += chunk;
43
+ const ssePayload = JSON.stringify({
44
+ type: done ? "done" : "delta",
45
+ text: chunk,
46
+ });
47
+ for (const conn of sseConnections) {
48
+ conn.write(`data: ${ssePayload}\n\n`);
49
+ }
50
+ });
51
+ res.json({ response: fullResponse });
52
+ });
53
+ app.get("/events", (req, res) => {
54
+ res.setHeader("Content-Type", "text/event-stream");
55
+ res.setHeader("Cache-Control", "no-cache");
56
+ res.setHeader("Connection", "keep-alive");
57
+ res.flushHeaders();
58
+ sseConnections.add(res);
59
+ req.on("close", () => {
60
+ sseConnections.delete(res);
61
+ });
62
+ });
63
+ return new Promise((resolve) => {
64
+ app.listen(config.apiPort, () => {
65
+ console.log(`[io] API server listening on port ${config.apiPort}`);
66
+ resolve();
67
+ });
68
+ });
69
+ }
70
+ //# sourceMappingURL=server.js.map
package/dist/config.js ADDED
@@ -0,0 +1,28 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { CONFIG_PATH, IO_HOME } from "./paths.js";
3
+ const DEFAULT_CONFIG = {
4
+ telegramEnabled: false,
5
+ selfEditEnabled: false,
6
+ apiPort: 3170,
7
+ };
8
+ function loadConfig() {
9
+ mkdirSync(IO_HOME, { recursive: true });
10
+ if (!existsSync(CONFIG_PATH)) {
11
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
12
+ return { ...DEFAULT_CONFIG };
13
+ }
14
+ try {
15
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
16
+ const parsed = JSON.parse(raw);
17
+ return { ...DEFAULT_CONFIG, ...parsed };
18
+ }
19
+ catch {
20
+ return { ...DEFAULT_CONFIG };
21
+ }
22
+ }
23
+ export const config = loadConfig();
24
+ export function saveConfig(updates) {
25
+ Object.assign(config, updates);
26
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
27
+ }
28
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,267 @@
1
+ import { randomUUID } from "crypto";
2
+ import { execSync } from "child_process";
3
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, } from "fs";
4
+ import { join, dirname, resolve } from "path";
5
+ import { defineTool, approveAll } from "@github/copilot-sdk";
6
+ import { z } from "zod";
7
+ import { getClient } from "./client.js";
8
+ import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, } from "../store/squads.js";
9
+ import { createTask, completeTask, failTask, getActiveTasks, } from "../store/tasks.js";
10
+ import { SESSIONS_DIR } from "../paths.js";
11
+ const agentSessions = new Map();
12
+ export function getAgentInfo() {
13
+ const activeTasks = getActiveTasks();
14
+ const tasksByAgent = new Map();
15
+ for (const task of activeTasks) {
16
+ tasksByAgent.set(task.agent_slug, task.description);
17
+ }
18
+ const agents = [];
19
+ for (const [slug, _session] of agentSessions) {
20
+ const squad = getSquad(slug);
21
+ const currentTask = tasksByAgent.get(slug);
22
+ let status = "idle";
23
+ if (currentTask) {
24
+ status = "working";
25
+ }
26
+ if (squad?.status === "error") {
27
+ status = "error";
28
+ }
29
+ agents.push({
30
+ slug,
31
+ name: squad?.name ?? slug,
32
+ status,
33
+ currentTask,
34
+ });
35
+ }
36
+ return agents;
37
+ }
38
+ export async function delegateToAgent(squadSlug, task, onComplete) {
39
+ const squad = getSquad(squadSlug);
40
+ if (!squad) {
41
+ throw new Error(`Squad not found: ${squadSlug}`);
42
+ }
43
+ const session = await getOrCreateSession(squadSlug);
44
+ const taskId = randomUUID();
45
+ createTask(taskId, squadSlug, task);
46
+ updateSquadStatus(squadSlug, "working");
47
+ // Run the task in the background — return taskId immediately
48
+ void (async () => {
49
+ try {
50
+ const response = await session.sendAndWait({ prompt: task });
51
+ const result = response?.data?.content ?? "Task completed (no output)";
52
+ completeTask(taskId, result);
53
+ updateSquadStatus(squadSlug, "idle");
54
+ onComplete(taskId, result);
55
+ }
56
+ catch (err) {
57
+ const message = err instanceof Error ? err.message : String(err);
58
+ failTask(taskId, message);
59
+ updateSquadStatus(squadSlug, "error");
60
+ }
61
+ })();
62
+ return taskId;
63
+ }
64
+ export async function shutdownAgents() {
65
+ for (const [slug, session] of agentSessions) {
66
+ try {
67
+ await session.destroy();
68
+ }
69
+ catch {
70
+ // best-effort cleanup
71
+ }
72
+ agentSessions.delete(slug);
73
+ }
74
+ }
75
+ export function getActiveAgentTasks() {
76
+ return getActiveTasks().map((t) => ({
77
+ taskId: t.task_id,
78
+ agentSlug: t.agent_slug,
79
+ description: t.description,
80
+ status: t.status,
81
+ }));
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Internal helpers
85
+ // ---------------------------------------------------------------------------
86
+ async function getOrCreateSession(squadSlug) {
87
+ const existing = agentSessions.get(squadSlug);
88
+ if (existing)
89
+ return existing;
90
+ const squad = getSquad(squadSlug);
91
+ const client = await getClient();
92
+ const decisions = getDecisionsSummary(squadSlug);
93
+ const agentTools = buildAgentTools(squadSlug);
94
+ const commonConfig = {
95
+ model: "gpt-4.1",
96
+ configDir: SESSIONS_DIR,
97
+ streaming: false,
98
+ systemMessage: {
99
+ content: `You are a specialist agent working on the "${squad.name}" project at ${squad.project_path}.
100
+
101
+ ## Past Decisions
102
+ ${decisions}
103
+
104
+ ## Your Role
105
+ You are a coding agent. Use the shell tool to run commands and file_ops to read/write files.
106
+ Log important decisions with squad_log_decision so they persist.`,
107
+ },
108
+ tools: agentTools,
109
+ onPermissionRequest: approveAll,
110
+ infiniteSessions: {
111
+ enabled: true,
112
+ backgroundCompactionThreshold: 0.8,
113
+ bufferExhaustionThreshold: 0.95,
114
+ },
115
+ };
116
+ let session;
117
+ // Try to resume an existing session if we have a saved session ID
118
+ if (squad.copilot_session_id) {
119
+ try {
120
+ session = await client.resumeSession(squad.copilot_session_id, commonConfig);
121
+ }
122
+ catch {
123
+ session = await client.createSession(commonConfig);
124
+ }
125
+ }
126
+ else {
127
+ session = await client.createSession(commonConfig);
128
+ }
129
+ updateSquadSession(squadSlug, session.sessionId);
130
+ agentSessions.set(squadSlug, session);
131
+ return session;
132
+ }
133
+ function buildAgentTools(squadSlug) {
134
+ const shell = defineTool("shell", {
135
+ description: "Run a shell command. Use for git, build tools, file operations, etc.",
136
+ parameters: z.object({
137
+ command: z.string().describe("The command to run"),
138
+ timeout_secs: z
139
+ .number()
140
+ .optional()
141
+ .describe("Timeout in seconds (default: 60)"),
142
+ working_dir: z
143
+ .string()
144
+ .optional()
145
+ .describe("Working directory for the command"),
146
+ }),
147
+ handler: async ({ command, timeout_secs, working_dir }) => {
148
+ try {
149
+ const result = execSync(command, {
150
+ encoding: "utf-8",
151
+ timeout: (timeout_secs ?? 60) * 1000,
152
+ maxBuffer: 1024 * 1024,
153
+ cwd: working_dir,
154
+ });
155
+ const output = result.trim();
156
+ if (output.length > 8000) {
157
+ return output.slice(0, 8000) + "\n\n[…truncated]";
158
+ }
159
+ return output || "(no output)";
160
+ }
161
+ catch (err) {
162
+ const execErr = err;
163
+ const stderr = execErr.stderr?.trim() ?? "";
164
+ const stdout = execErr.stdout?.trim() ?? "";
165
+ const msg = stderr || stdout || execErr.message || "Command failed";
166
+ if (msg.length > 4000) {
167
+ return `Error:\n${msg.slice(0, 4000)}\n[…truncated]`;
168
+ }
169
+ return `Error:\n${msg}`;
170
+ }
171
+ },
172
+ });
173
+ const fileOps = defineTool("file_ops", {
174
+ description: "Read, write, or list files on the local filesystem.",
175
+ parameters: z.object({
176
+ operation: z
177
+ .enum(["read", "write", "list"])
178
+ .describe("Operation to perform"),
179
+ path: z.string().describe("File or directory path"),
180
+ content: z
181
+ .string()
182
+ .optional()
183
+ .describe("Content to write (for write operation)"),
184
+ recursive: z
185
+ .boolean()
186
+ .optional()
187
+ .describe("Recurse into subdirectories (for list)"),
188
+ }),
189
+ handler: async ({ operation, path: filePath, content, recursive }) => {
190
+ try {
191
+ const resolved = resolve(filePath);
192
+ if (operation === "read") {
193
+ if (!existsSync(resolved))
194
+ return `File not found: ${filePath}`;
195
+ const text = readFileSync(resolved, "utf-8");
196
+ if (text.length > 8000) {
197
+ return text.slice(0, 8000) + "\n\n[…truncated]";
198
+ }
199
+ return text;
200
+ }
201
+ if (operation === "write") {
202
+ if (!content)
203
+ return "Error: content is required for write operation";
204
+ mkdirSync(dirname(resolved), { recursive: true });
205
+ writeFileSync(resolved, content, "utf-8");
206
+ return `Written: ${filePath}`;
207
+ }
208
+ if (operation === "list") {
209
+ if (!existsSync(resolved))
210
+ return `Directory not found: ${filePath}`;
211
+ if (recursive) {
212
+ const files = walkDirectory(resolved);
213
+ return files.join("\n") || "(empty directory)";
214
+ }
215
+ const entries = readdirSync(resolved);
216
+ return (entries
217
+ .map((e) => {
218
+ const full = join(resolved, e);
219
+ const isDir = statSync(full).isDirectory();
220
+ return isDir ? `${e}/` : e;
221
+ })
222
+ .join("\n") || "(empty directory)");
223
+ }
224
+ return `Unknown operation: ${operation}`;
225
+ }
226
+ catch (err) {
227
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
228
+ }
229
+ },
230
+ });
231
+ const squadLogDecision = defineTool("squad_log_decision", {
232
+ description: "Log an important decision for this squad so it persists across sessions.",
233
+ parameters: z.object({
234
+ decision: z.string().describe("The decision made"),
235
+ context: z.string().optional().describe("Context or reasoning"),
236
+ }),
237
+ handler: async ({ decision, context }) => {
238
+ try {
239
+ logDecision(squadSlug, decision, context);
240
+ return `Decision logged for squad ${squadSlug}`;
241
+ }
242
+ catch (err) {
243
+ return `Error: ${err instanceof Error ? err.message : String(err)}`;
244
+ }
245
+ },
246
+ });
247
+ return [shell, fileOps, squadLogDecision];
248
+ }
249
+ function walkDirectory(dir, maxDepth = 3, depth = 0) {
250
+ if (depth >= maxDepth)
251
+ return [];
252
+ const results = [];
253
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
254
+ if (entry.name.startsWith("."))
255
+ continue;
256
+ const full = join(dir, entry.name);
257
+ if (entry.isDirectory()) {
258
+ results.push(`${entry.name}/`);
259
+ results.push(...walkDirectory(full, maxDepth, depth + 1).map((f) => ` ${entry.name}/${f}`));
260
+ }
261
+ else {
262
+ results.push(entry.name);
263
+ }
264
+ }
265
+ return results;
266
+ }
267
+ //# sourceMappingURL=agents.js.map
@@ -0,0 +1,29 @@
1
+ import { CopilotClient } from "@github/copilot-sdk";
2
+ let client;
3
+ export async function getClient() {
4
+ if (!client) {
5
+ client = new CopilotClient({
6
+ autoStart: true,
7
+ autoRestart: true,
8
+ });
9
+ await client.start();
10
+ }
11
+ return client;
12
+ }
13
+ export async function resetClient() {
14
+ if (client) {
15
+ try {
16
+ await client.stop();
17
+ }
18
+ catch { /* best-effort */ }
19
+ client = undefined;
20
+ }
21
+ return getClient();
22
+ }
23
+ export async function stopClient() {
24
+ if (client) {
25
+ await client.stop();
26
+ client = undefined;
27
+ }
28
+ }
29
+ //# sourceMappingURL=client.js.map