clisbot 0.1.7 → 0.1.8
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/README.md +61 -16
- package/config/clisbot.json.template +27 -0
- package/dist/main.js +25 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,61 @@
|
|
|
1
|
-
# clisbot -
|
|
2
|
-
The cheapest path to
|
|
1
|
+
# clisbot - Agentic Coding CLI & chat bot
|
|
2
|
+
The cheapest, simplest path to frontier LLMs and agentic CLI workflows for teams and individuals.
|
|
3
3
|
|
|
4
|
-
`clisbot` exposes native agentic AI tool CLIs like Claude Code / Codex through chat surfaces, with each agent running inside its own durable tmux session and ready to behave like a real bot, a real assistant - with SOUL &
|
|
4
|
+
`clisbot` exposes native agentic AI tool CLIs like Claude Code / Codex through multi-channel chat surfaces, with each agent running inside its own durable tmux session and ready to behave like a real bot, a real assistant - with SOUL, IDENTITY & MEMORY, not just a coding tool.
|
|
5
|
+
|
|
6
|
+
It is not just another tmux bridge or Telegram bridge. `clisbot` is meant to grow into a reusable agent runtime layer that can support many CLI tools, many channels, and many workflow shapes on top of the same durable agent session.
|
|
5
7
|
|
|
6
8
|
Agentic AI is powerful, but only with frontier models. OpenClaw took off because people found many ways to access strong frontier models cheaply through subscription-based OAuth. Recent Anthropic enforcement around third-party and proxy-style usage made that risk harder to ignore.
|
|
7
9
|
|
|
8
10
|
Meanwhile, the strongest agentic coding tools already come from serious enterprise teams with real investment in model quality, security, safety, and operator controls, especially Claude Code, Codex, and Gemini CLI. That naturally leads to a simple question: why not reuse those agents as they already are, keep them alive in tmux, and add communication channels, team workflows, and more toys around them?
|
|
9
11
|
|
|
10
|
-
The idea has been stuck in my head since Claude Code introduced agent skills back in October 2025, which opened more possibilities for non-developer workers, and the OpenClaw trend recently only made it harder to ignore. Having played around with Anthropic's Agent SDK to address the agentic AI adoption inside my company but somehow it was not working, not stable, not flexible, not fast enough, and the CLI path now looks like the better choice until now. Now feels like the right time to push that idea.
|
|
11
|
-
|
|
12
12
|
Every company will likely need an OpenClaw-style strategy over time: a personal agentic assistant for each employee, plus shared agents for each team. `clisbot` starts from a team-first angle, with Slack and shared agent workflows as the default center of gravity instead of treating team collaboration as a later add-on.
|
|
13
13
|
|
|
14
|
+
## Important caution
|
|
15
|
+
|
|
16
|
+
Strong vendor investment in security and safety does not make frontier agentic CLI tools inherently safe. `clisbot` exposes those tools more broadly through chat and workflow surfaces, so you should treat the whole system as high-trust software and use it at your own risk.
|
|
17
|
+
|
|
18
|
+
## Acknowledgements
|
|
19
|
+
|
|
20
|
+
`clisbot` would not exist without the ideas, momentum, and practical inspiration created by OpenClaw. Many configuration, routing, and workspace concepts here were learned from studying OpenClaw, then adapted to `clisbot`'s own direction. Respect and thanks to the OpenClaw project and community.
|
|
21
|
+
|
|
14
22
|
## Why clisbot
|
|
15
23
|
|
|
16
|
-
-
|
|
24
|
+
- Reuses the native CLI tools you already know and subscribe to, such as Claude Code, Codex, and Gemini CLI, then extends them across coding, chatbot, and non-dev workflows without forcing you to switch tools.
|
|
17
25
|
- Optimized for cheap subscription-backed usage with tools like Codex CLI and Claude CLI... A practical response to the reality that high-quality frontier models are expensive and vendor policies can tighten around third-party usage.
|
|
18
26
|
- Compatible with OpenClaw-style configuration, commands and some concepts, agent personality for bot usecases, and workspace bootstrap templates, help Openclaw users to quickly get started.
|
|
19
27
|
- Team-first by design, with agent bootstrap templates that fit shared team agents as well as personal ones.
|
|
20
|
-
- Reuses mature agentic tools such as Claude Code, Codex, and Gemini CLI
|
|
21
28
|
- Fits the emerging pattern of one personal assistant per employee and shared assistants per team.
|
|
22
29
|
- Useful as a bot for coding, operations, teamwork, and general work in team environment, or on the go
|
|
23
30
|
- Strong team support in Slack, with Telegram already supported as another first-class channel.
|
|
24
31
|
- Configurable follow-up policy instead of a fixed TTL model, with a 5-minute default window and route-level controls so teams can tune behavior to how they actually work. Smart follow-up controls help avoid unwanted bot interruption in active threads: keep natural continuation when useful, or pause it when you want the bot to stay quiet until explicitly called again.
|
|
25
|
-
- Fast operator shortcuts for shell execution: `!<command>` or `/bash <command
|
|
32
|
+
- Fast operator shortcuts for shell execution: `!<command>` or `/bash <command>`, plus slash-prefix mappings such as `\bash` or `::bash` when Slack slash-command handling is incompatible. Turns Slack / Telegram into a terminal interface on the go.
|
|
26
33
|
- The proof of concept already shows high potential beyond internal coding workflows, including customer chatbot use cases once messaging MCP or CLI-based skills let the agent send messages proactively in a cleaner way.
|
|
27
34
|
|
|
28
35
|
## Current Focus
|
|
29
36
|
|
|
30
|
-
`clisbot` is
|
|
37
|
+
`clisbot` is growing toward a broader agent runtime layer:
|
|
38
|
+
|
|
39
|
+
- more CLI tool support beyond Claude Code and Codex, including Gemini CLI, OpenCode, Qwen, Kilo, and other agentic CLIs
|
|
40
|
+
- more communication channels beyond Slack and Telegram, including Zalo, WhatsApp, Facebook, Discord, and future API-compatible surfaces
|
|
41
|
+
- simple workflow building blocks such as cronjobs, heartbeat jobs, lightweight Ralph-style loops, and prompt combinations that just work
|
|
42
|
+
- durable agent sessions, workspaces, follow-up policy, commands, attachments, and operator controls that stay reusable across all those surfaces
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
- `agent-os`: agents, sessions, workspaces, commands, attachments, and follow-up policy
|
|
34
|
-
- `runners`: tmux today, with ACP, SDK, and other execution backends later
|
|
35
|
-
- `control`: operator-facing inspection, lifecycle, and debugging flows
|
|
36
|
-
- `configuration`: the local control plane that wires the system together
|
|
44
|
+
tmux is still the current stability boundary. One agent maps to one durable runner session in one workspace, and every CLI, channel, or workflow layer should route onto that durable runtime instead of recreating the agent from scratch.
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
## Showcase
|
|
47
|
+
|
|
48
|
+
Slack
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
Telegram
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+

|
|
39
59
|
|
|
40
60
|
## Quick Start
|
|
41
61
|
|
|
@@ -75,6 +95,19 @@ clisbot start --cli codex --bootstrap personal-assistant
|
|
|
75
95
|
clis start --cli codex --bootstrap personal-assistant
|
|
76
96
|
```
|
|
77
97
|
|
|
98
|
+
4. Fastest first conversation path: send a direct message to the bot in Slack or Telegram.
|
|
99
|
+
|
|
100
|
+
`clisbot` defaults direct messages to pairing mode. The bot will reply with a pairing code and approval command.
|
|
101
|
+
|
|
102
|
+
Approve it with:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
clisbot pairing approve slack <CODE>
|
|
106
|
+
clisbot pairing approve telegram <CODE>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After approval, keep chatting with the bot in that DM or add shared channel or topic routes later.
|
|
110
|
+
|
|
78
111
|
If you do not want to install globally, you can also run it directly with `npx`:
|
|
79
112
|
|
|
80
113
|
```bash
|
|
@@ -111,6 +144,19 @@ source ~/.zshrc
|
|
|
111
144
|
bun run start --cli codex --bootstrap personal-assistant
|
|
112
145
|
```
|
|
113
146
|
|
|
147
|
+
4. Fastest first conversation path: send a direct message to the bot in Slack or Telegram.
|
|
148
|
+
|
|
149
|
+
`clisbot` defaults direct messages to pairing mode. The bot will reply with a pairing code and approval command.
|
|
150
|
+
|
|
151
|
+
Approve it with:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
bun run pairing -- approve slack <CODE>
|
|
155
|
+
bun run pairing -- approve telegram <CODE>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
After approval, keep chatting with the bot in that DM or add shared channel or topic routes later.
|
|
159
|
+
|
|
114
160
|
Fresh config now starts with no configured agents, and first-run `clisbot start` requires both `--cli` and `--bootstrap` before it creates the first `default` agent.
|
|
115
161
|
Fresh config also starts with no preconfigured Slack channels or Telegram groups/topics. Add those routes manually in `~/.clisbot/clisbot.json`.
|
|
116
162
|
`clisbot start` now also requires Slack or Telegram token references before it bootstraps anything. By default it looks for `SLACK_APP_TOKEN`, `SLACK_BOT_TOKEN`, and `TELEGRAM_BOT_TOKEN`, but you can pass custom placeholders such as `--slack-app-token '${CUSTOM_SLACK_APP_TOKEN}'`.
|
|
@@ -155,7 +201,6 @@ clisbot agents list --bindings
|
|
|
155
201
|
Agent setup rules:
|
|
156
202
|
|
|
157
203
|
- `agents add` requires `--cli` and currently supports `codex` and `claude`.
|
|
158
|
-
- `--startup-option` is optional; if omitted, clisbot uses the built-in startup options for the selected CLI.
|
|
159
204
|
- `--bootstrap` accepts `personal-assistant` or `team-assistant` and seeds the workspace from `templates/openclaw` plus the selected customized template.
|
|
160
205
|
- `personal-assistant` fits one assistant for one human.
|
|
161
206
|
- `team-assistant` fits one shared assistant for a team, channel, or group workflow.
|
|
@@ -80,6 +80,18 @@
|
|
|
80
80
|
"mode": "socket",
|
|
81
81
|
"appToken": "${SLACK_APP_TOKEN}",
|
|
82
82
|
"botToken": "${SLACK_BOT_TOKEN}",
|
|
83
|
+
"defaultAccount": "default",
|
|
84
|
+
"accounts": {
|
|
85
|
+
"default": {
|
|
86
|
+
"appToken": "${SLACK_APP_TOKEN}",
|
|
87
|
+
"botToken": "${SLACK_BOT_TOKEN}"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"agentPrompt": {
|
|
91
|
+
"enabled": true,
|
|
92
|
+
"maxProgressMessages": 3,
|
|
93
|
+
"requireFinalResponse": true
|
|
94
|
+
},
|
|
83
95
|
"ackReaction": "",
|
|
84
96
|
"typingReaction": "",
|
|
85
97
|
"processingStatus": {
|
|
@@ -107,6 +119,8 @@
|
|
|
107
119
|
},
|
|
108
120
|
"streaming": "all",
|
|
109
121
|
"response": "final",
|
|
122
|
+
"responseMode": "message-tool",
|
|
123
|
+
"additionalMessageMode": "steer",
|
|
110
124
|
"followUp": {
|
|
111
125
|
"mode": "auto",
|
|
112
126
|
"participationTtlMin": 5
|
|
@@ -129,6 +143,17 @@
|
|
|
129
143
|
"enabled": true,
|
|
130
144
|
"mode": "polling",
|
|
131
145
|
"botToken": "${TELEGRAM_BOT_TOKEN}",
|
|
146
|
+
"defaultAccount": "default",
|
|
147
|
+
"accounts": {
|
|
148
|
+
"default": {
|
|
149
|
+
"botToken": "${TELEGRAM_BOT_TOKEN}"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"agentPrompt": {
|
|
153
|
+
"enabled": true,
|
|
154
|
+
"maxProgressMessages": 3,
|
|
155
|
+
"requireFinalResponse": true
|
|
156
|
+
},
|
|
132
157
|
"allowBots": false,
|
|
133
158
|
"groupPolicy": "allowlist",
|
|
134
159
|
"defaultAgentId": "default",
|
|
@@ -147,6 +172,8 @@
|
|
|
147
172
|
},
|
|
148
173
|
"streaming": "all",
|
|
149
174
|
"response": "final",
|
|
175
|
+
"responseMode": "message-tool",
|
|
176
|
+
"additionalMessageMode": "steer",
|
|
150
177
|
"followUp": {
|
|
151
178
|
"mode": "auto",
|
|
152
179
|
"participationTtlMin": 5
|
package/dist/main.js
CHANGED
|
@@ -51156,7 +51156,7 @@ var require_send = __commonJS((exports, module) => {
|
|
|
51156
51156
|
var join6 = path2.join;
|
|
51157
51157
|
var normalize = path2.normalize;
|
|
51158
51158
|
var resolve = path2.resolve;
|
|
51159
|
-
var
|
|
51159
|
+
var sep2 = path2.sep;
|
|
51160
51160
|
var BYTES_RANGE_REGEXP = /^ *bytes=/;
|
|
51161
51161
|
var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
51162
51162
|
var UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
@@ -51317,14 +51317,14 @@ var require_send = __commonJS((exports, module) => {
|
|
|
51317
51317
|
var parts;
|
|
51318
51318
|
if (root !== null) {
|
|
51319
51319
|
if (path3) {
|
|
51320
|
-
path3 = normalize("." +
|
|
51320
|
+
path3 = normalize("." + sep2 + path3);
|
|
51321
51321
|
}
|
|
51322
51322
|
if (UP_PATH_REGEXP.test(path3)) {
|
|
51323
51323
|
debug('malicious path "%s"', path3);
|
|
51324
51324
|
this.error(403);
|
|
51325
51325
|
return res;
|
|
51326
51326
|
}
|
|
51327
|
-
parts = path3.split(
|
|
51327
|
+
parts = path3.split(sep2);
|
|
51328
51328
|
path3 = normalize(join6(root, path3));
|
|
51329
51329
|
} else {
|
|
51330
51330
|
if (UP_PATH_REGEXP.test(path3)) {
|
|
@@ -51332,7 +51332,7 @@ var require_send = __commonJS((exports, module) => {
|
|
|
51332
51332
|
this.error(403);
|
|
51333
51333
|
return res;
|
|
51334
51334
|
}
|
|
51335
|
-
parts = normalize(path3).split(
|
|
51335
|
+
parts = normalize(path3).split(sep2);
|
|
51336
51336
|
path3 = resolve(path3);
|
|
51337
51337
|
}
|
|
51338
51338
|
if (containsDotFile(parts)) {
|
|
@@ -51427,7 +51427,7 @@ var require_send = __commonJS((exports, module) => {
|
|
|
51427
51427
|
var self2 = this;
|
|
51428
51428
|
debug('stat "%s"', path3);
|
|
51429
51429
|
fs2.stat(path3, function onstat(err, stat) {
|
|
51430
|
-
var pathEndsWithSep = path3[path3.length - 1] ===
|
|
51430
|
+
var pathEndsWithSep = path3[path3.length - 1] === sep2;
|
|
51431
51431
|
if (err && err.code === "ENOENT" && !extname(path3) && !pathEndsWithSep) {
|
|
51432
51432
|
return next(err);
|
|
51433
51433
|
}
|
|
@@ -62697,7 +62697,7 @@ function escapeRegExp(raw) {
|
|
|
62697
62697
|
// src/control/clisbot-wrapper.ts
|
|
62698
62698
|
import { chmod } from "node:fs/promises";
|
|
62699
62699
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
62700
|
-
import { dirname as dirname7, join as join5 } from "node:path";
|
|
62700
|
+
import { dirname as dirname7, join as join5, sep } from "node:path";
|
|
62701
62701
|
var DEFAULT_CLISBOT_BIN_DIR = join5(APP_HOME_DIR, "bin");
|
|
62702
62702
|
var DEFAULT_CLISBOT_WRAPPER_PATH = join5(DEFAULT_CLISBOT_BIN_DIR, "clisbot");
|
|
62703
62703
|
function shellQuote(value) {
|
|
@@ -62709,9 +62709,19 @@ function shellQuote(value) {
|
|
|
62709
62709
|
function getClisbotMainScriptPath() {
|
|
62710
62710
|
return fileURLToPath3(new URL("../main.ts", import.meta.url));
|
|
62711
62711
|
}
|
|
62712
|
+
function isPackagedRuntime() {
|
|
62713
|
+
const currentModulePath = fileURLToPath3(import.meta.url);
|
|
62714
|
+
return currentModulePath.includes(`${sep}dist${sep}`);
|
|
62715
|
+
}
|
|
62712
62716
|
function getClisbotWrapperPath() {
|
|
62713
62717
|
return expandHomePath(process.env.CLISBOT_WRAPPER_PATH || DEFAULT_CLISBOT_WRAPPER_PATH);
|
|
62714
62718
|
}
|
|
62719
|
+
function getClisbotPromptCommand() {
|
|
62720
|
+
if (process.env.CLISBOT_PROMPT_COMMAND?.trim()) {
|
|
62721
|
+
return process.env.CLISBOT_PROMPT_COMMAND.trim();
|
|
62722
|
+
}
|
|
62723
|
+
return isPackagedRuntime() ? "clis" : getClisbotWrapperPath();
|
|
62724
|
+
}
|
|
62715
62725
|
function getClisbotWrapperDir() {
|
|
62716
62726
|
return dirname7(getClisbotWrapperPath());
|
|
62717
62727
|
}
|
|
@@ -62731,7 +62741,7 @@ async function ensureClisbotWrapper() {
|
|
|
62731
62741
|
const wrapperDir = dirname7(wrapperPath);
|
|
62732
62742
|
await ensureDir2(wrapperDir);
|
|
62733
62743
|
const nextScript = renderClisbotWrapperScript();
|
|
62734
|
-
const existing = await fileExists(wrapperPath) ? await
|
|
62744
|
+
const existing = await fileExists(wrapperPath) ? await readTextFile(wrapperPath) : null;
|
|
62735
62745
|
if (existing !== nextScript) {
|
|
62736
62746
|
await writeTextFile(wrapperPath, nextScript);
|
|
62737
62747
|
}
|
|
@@ -64768,9 +64778,8 @@ function renderAgentPromptInstruction(params) {
|
|
|
64768
64778
|
messageToolMode ? "channel auto-delivery is disabled for this conversation; send user-facing progress updates and the final response yourself with the reply command" : "channel auto-delivery remains enabled for this conversation; do not send user-facing progress updates or the final response with clisbot message send"
|
|
64769
64779
|
];
|
|
64770
64780
|
if (messageToolMode) {
|
|
64771
|
-
const wrapperPath = getClisbotWrapperPath();
|
|
64772
64781
|
const replyCommand = buildReplyCommand({
|
|
64773
|
-
|
|
64782
|
+
command: getClisbotPromptCommand(),
|
|
64774
64783
|
identity: params.identity
|
|
64775
64784
|
});
|
|
64776
64785
|
lines.push("Use the exact command below when you need to send progress updates, media attachments, or the final response back to the user.", "reply command:", replyCommand, `progress updates: at most ${params.config.maxProgressMessages}`, params.config.requireFinalResponse ? "final response: send exactly 1 final user-facing response" : "final response: optional", "keep progress updates short and meaningful", "do not send progress updates for trivial internal steps");
|
|
@@ -64843,7 +64852,7 @@ function renderNamedValue(label, name, id) {
|
|
|
64843
64852
|
return value ? `${label} ${value}` : "";
|
|
64844
64853
|
}
|
|
64845
64854
|
function buildReplyCommand(params) {
|
|
64846
|
-
const lines = [`${params.
|
|
64855
|
+
const lines = [`${params.command} message send \\`];
|
|
64847
64856
|
if (params.identity.platform === "slack") {
|
|
64848
64857
|
lines.push(" --channel slack \\");
|
|
64849
64858
|
lines.push(` --target channel:${params.identity.channelId ?? ""} \\`);
|
|
@@ -66151,6 +66160,7 @@ class SlackSocketService {
|
|
|
66151
66160
|
// src/channels/slack/message-actions.ts
|
|
66152
66161
|
var import_bolt = __toESM(require_dist7(), 1);
|
|
66153
66162
|
import { basename } from "node:path";
|
|
66163
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
66154
66164
|
var { WebClient } = import_bolt.webApi;
|
|
66155
66165
|
function createSlackClient(botToken) {
|
|
66156
66166
|
return new WebClient(botToken);
|
|
@@ -66210,13 +66220,9 @@ async function loadSlackMedia(media) {
|
|
|
66210
66220
|
data: Buffer.from(arrayBuffer)
|
|
66211
66221
|
};
|
|
66212
66222
|
}
|
|
66213
|
-
const file = Bun.file(media);
|
|
66214
|
-
if (!await file.exists()) {
|
|
66215
|
-
throw new Error(`Media file not found: ${media}`);
|
|
66216
|
-
}
|
|
66217
66223
|
return {
|
|
66218
66224
|
filename: basename(media),
|
|
66219
|
-
data:
|
|
66225
|
+
data: await readFile2(media)
|
|
66220
66226
|
};
|
|
66221
66227
|
}
|
|
66222
66228
|
async function sendSlackMessage(params) {
|
|
@@ -67512,6 +67518,7 @@ function resolveRouteAndTarget(params) {
|
|
|
67512
67518
|
|
|
67513
67519
|
// src/channels/telegram/message-actions.ts
|
|
67514
67520
|
import { basename as basename3, extname as extname2 } from "node:path";
|
|
67521
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
67515
67522
|
function parseTelegramChatId(raw) {
|
|
67516
67523
|
const value = raw.trim();
|
|
67517
67524
|
if (value.startsWith("@")) {
|
|
@@ -67535,13 +67542,9 @@ async function loadTelegramMedia(media) {
|
|
|
67535
67542
|
remoteUrl: media
|
|
67536
67543
|
};
|
|
67537
67544
|
}
|
|
67538
|
-
const file = Bun.file(media);
|
|
67539
|
-
if (!await file.exists()) {
|
|
67540
|
-
throw new Error(`Media file not found: ${media}`);
|
|
67541
|
-
}
|
|
67542
67545
|
return {
|
|
67543
67546
|
filename: basename3(media),
|
|
67544
|
-
file
|
|
67547
|
+
file: new Blob([await readFile3(media)])
|
|
67545
67548
|
};
|
|
67546
67549
|
}
|
|
67547
67550
|
function inferTelegramMediaKind(filename, forceDocument) {
|
|
@@ -67973,7 +67976,7 @@ import { watch } from "node:fs";
|
|
|
67973
67976
|
import { basename as basename4, dirname as dirname11 } from "node:path";
|
|
67974
67977
|
|
|
67975
67978
|
// src/channels/processed-events-store.ts
|
|
67976
|
-
import { mkdir as mkdir2, readFile as
|
|
67979
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
67977
67980
|
var DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
67978
67981
|
var PROCESSING_STALE_MS = 30 * 60 * 1000;
|
|
67979
67982
|
|
|
@@ -67991,7 +67994,7 @@ class ProcessedEventsStore {
|
|
|
67991
67994
|
return;
|
|
67992
67995
|
}
|
|
67993
67996
|
try {
|
|
67994
|
-
const text = await
|
|
67997
|
+
const text = await readFile4(this.filePath, "utf8");
|
|
67995
67998
|
this.document = JSON.parse(text);
|
|
67996
67999
|
} catch {
|
|
67997
68000
|
this.document = { events: {} };
|