herdrchat 1.2.1 → 1.3.1
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 +87 -23
- package/README.zh.md +70 -16
- package/bin/cli.mjs +126 -47
- package/bin/herdrchat.mjs +2 -2
- package/config.env.example +37 -20
- package/docs/architecture.md +13 -10
- package/docs/guide.md +134 -45
- package/lib/chats/contract.mjs +3 -2
- package/lib/chats/feishu/client.mjs +14 -5
- package/lib/chats/feishu/index.mjs +15 -2
- package/lib/chats/telegram/index.mjs +29 -7
- package/lib/core/config.mjs +28 -10
- package/lib/core/engine.mjs +17 -11
- package/lib/core/hc.mjs +57 -1
- package/lib/core/log.mjs +10 -4
- package/lib/core/readers/index.mjs +1 -1
- package/lib/core/service.mjs +1 -0
- package/package.json +1 -1
- package/skills/herdrchat/SKILL.md +9 -8
- package/skills/herdrchat/references/feishu.md +25 -3
- package/skills/herdrchat/references/ops.md +22 -52
- package/skills/herdrchat/references/telegram.md +31 -36
package/README.md
CHANGED
|
@@ -12,47 +12,111 @@ Every terminal hc runs, in the chat app you already use: Feishu or Telegram.
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
herdr
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
[herdr](https://herdr.dev) runs your coding agents (Claude Code, codex, pi, …) in
|
|
16
|
+
terminals that stay alive: **hc** (herdr-core) keeps them running, **herdr-app** shows
|
|
17
|
+
them in spaces and tabs. herdr-app has phone and desktop apps, but your phone has to
|
|
18
|
+
reach the machine that runs hc, so you first need a VPN or a private network, and working
|
|
19
|
+
with an agent on a small phone screen is clumsy.
|
|
20
|
+
|
|
21
|
+
herdrchat puts them in a chat app instead. It connects out to Feishu or Telegram by
|
|
22
|
+
itself, so there is nothing to set up on your network: no public IP, no VPN. Each
|
|
23
|
+
herdr-app tab gets its own Feishu group, or a topic in your private chat with the
|
|
24
|
+
Telegram bot. You read what the agent says while it works, tap buttons to answer its
|
|
25
|
+
questions, send and receive images, and see all terminals in one overview. A new group or
|
|
26
|
+
topic starts with the conversation so far, and your phone rings only when an agent needs
|
|
27
|
+
you or is done (on Feishu once you have sent `/auth` to the bot; on Telegram right away).
|
|
28
|
+
|
|
29
|
+
Claude Code is mirrored fully. Other agents show their screen when you type to them,
|
|
30
|
+
until herdrchat learns to read their conversations too.
|
|
31
|
+
|
|
32
|
+
## You need
|
|
33
|
+
|
|
34
|
+
- **hc** running on this machine, with at least one session ([herdr.dev](https://herdr.dev)).
|
|
35
|
+
- **Node.js 20** or newer, on **macOS or Linux**.
|
|
36
|
+
- **Feishu**: a Feishu team (not Lark) where you may create a custom app and publish it.
|
|
37
|
+
**Telegram**: a Telegram account. Use one app, or both at once.
|
|
38
|
+
|
|
39
|
+
A few words used below: a **session** is one hc core (its terminals); a **space** and a
|
|
40
|
+
**tab** are how herdr-app groups terminals; a **profile** is one herdrchat setup for one
|
|
41
|
+
chat app (`default` unless you name another).
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
is nothing to set up on your network: no public IP, no VPN. Each herdr-app tab becomes a
|
|
21
|
-
Feishu group, or a topic in your private chat with the Telegram bot. You read what the
|
|
22
|
-
agent says while it works, tap buttons to answer its questions, send and receive images,
|
|
23
|
-
and see all terminals on one overview. A new room starts with the conversation so far.
|
|
24
|
-
Your phone rings only when an agent needs you or is done. It works with any agent hc
|
|
25
|
-
runs.
|
|
43
|
+
## Install
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g herdrchat # the herdrchat command
|
|
47
|
+
npx skills add zhangxiangliang/herdrchat # optional: the skill, for your coding agent
|
|
48
|
+
```
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
The skill teaches your coding agent (Claude Code, Codex, …) to set herdrchat up and look
|
|
51
|
+
after it for you. When the `skills` tool asks Copy or Symlink, pick Symlink, so updates
|
|
52
|
+
reach the agent.
|
|
53
|
+
|
|
54
|
+
## Set up
|
|
55
|
+
|
|
56
|
+
**With your agent:** ask it `/herdrchat setup`. It asks which chat app, then walks you
|
|
57
|
+
through everything, clicking in the Feishu console for you when it can.
|
|
58
|
+
|
|
59
|
+
**By hand:**
|
|
30
60
|
|
|
31
61
|
```bash
|
|
32
|
-
|
|
33
|
-
|
|
62
|
+
herdrchat init --chat telegram # or --chat feishu; it prints where the settings file is
|
|
63
|
+
# fill in that file (each line says where its value comes from), then:
|
|
64
|
+
herdrchat start # starts it and waits until it is connected
|
|
65
|
+
herdrchat install # once it works: start at login, come back after a crash
|
|
34
66
|
```
|
|
35
67
|
|
|
36
|
-
|
|
68
|
+
The steps for each app (making the Feishu app, or the Telegram bot) are in the
|
|
69
|
+
[guide](docs/guide.md#set-up-telegram).
|
|
70
|
+
|
|
71
|
+
## Use
|
|
72
|
+
|
|
73
|
+
In your chat app: type in a terminal's group or topic to talk to it; tap a card's buttons
|
|
74
|
+
to answer a question. In the direct chat with the bot: `/ls` for the overview, `/new` to
|
|
75
|
+
start a terminal, `/kill` to end one.
|
|
37
76
|
|
|
38
|
-
|
|
|
77
|
+
| Ask your agent | What it does |
|
|
39
78
|
|---|---|
|
|
40
79
|
| `/herdrchat setup` | Sets everything up with you: the Feishu app or the Telegram bot, the settings, the first start. |
|
|
41
80
|
| `/herdrchat status` | Says whether it runs, and what is wrong if anything is. |
|
|
42
81
|
| `/herdrchat fix` | Finds the cause of a problem and fixes it. |
|
|
43
|
-
| `/herdrchat config` | Changes a setting: which terminals, which
|
|
44
|
-
| `/herdrchat
|
|
82
|
+
| `/herdrchat config` | Changes a setting: which terminals, which ones stay quiet, tags. |
|
|
83
|
+
| `/herdrchat start`, `stop`, `install`, `logs`, `auth` | The same as the commands below, with checks first. |
|
|
84
|
+
| `/herdrchat feishu <task>` | Does a task in the Feishu developer console (with the [Ego Lite](https://lite.ego.app/) browser, or step by step). |
|
|
45
85
|
| `/herdrchat telegram <task>` | The Telegram bot, from @BotFather to the first topic. |
|
|
46
86
|
|
|
47
|
-
On the command line
|
|
48
|
-
`
|
|
49
|
-
|
|
87
|
+
On the command line: `herdrchat init`, `start`, `stop`, `status`, `logs`, `install`,
|
|
88
|
+
`uninstall`, `version`; `herdrchat` alone lists them. Add `--profile <name>` to run a
|
|
89
|
+
second chat app beside the first.
|
|
90
|
+
|
|
91
|
+
## Update
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -g herdrchat@latest && npx skills add zhangxiangliang/herdrchat
|
|
95
|
+
herdrchat start # for each profile: it keeps the old code until then
|
|
96
|
+
```
|
|
50
97
|
|
|
51
98
|
## Where things are
|
|
52
99
|
|
|
53
100
|
Settings in `~/.config/herdrchat/<profile>/config.env`, everything the bot writes in
|
|
54
|
-
`~/.local/state/herdrchat/<profile
|
|
55
|
-
|
|
101
|
+
`~/.local/state/herdrchat/<profile>/`. Updating or reinstalling herdrchat never touches
|
|
102
|
+
them. `herdrchat status` prints both.
|
|
103
|
+
|
|
104
|
+
## Privacy and safety
|
|
105
|
+
|
|
106
|
+
- What your terminals show goes to Feishu's or Telegram's servers. Telegram bot chats are
|
|
107
|
+
not end-to-end encrypted.
|
|
108
|
+
- Only the owner set in `config.env` can type into your terminals; everyone else is
|
|
109
|
+
ignored.
|
|
110
|
+
- Keep the Feishu app secret or the Telegram bot token like a password: whoever has it
|
|
111
|
+
can drive your terminals.
|
|
112
|
+
- On Feishu, the bot's tags are visible to your whole team (`HERDR_BOT_TAGS=off` turns
|
|
113
|
+
them off).
|
|
114
|
+
|
|
115
|
+
## Remove
|
|
116
|
+
|
|
117
|
+
`herdrchat uninstall` (for each profile), `npm rm -g herdrchat`, delete
|
|
118
|
+
`~/.config/herdrchat` and `~/.local/state/herdrchat`, then delete the Feishu app or the
|
|
119
|
+
Telegram bot (@BotFather, `/deletebot`).
|
|
56
120
|
|
|
57
121
|
How it works and every setting: [docs/guide.md](docs/guide.md).
|
|
58
122
|
|
package/README.zh.md
CHANGED
|
@@ -12,42 +12,96 @@ hc 跑的每个终端,都能在你常用的聊天软件里看到和回复:
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
herdr
|
|
16
|
-
|
|
15
|
+
[herdr](https://herdr.dev) 让你的编程 agent(Claude Code、codex、pi……)跑在不会断的终端里:
|
|
16
|
+
**hc**(herdr-core)让它们一直跑着,**herdr-app** 按空间和标签页把它们展示出来。herdr-app 有手机端和电脑端,
|
|
17
|
+
但手机得能连到跑 hc 的机器,所以要先折腾 VPN 或内网;而且在手机的小屏幕上和 agent 交互,体验并不好。
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
herdrchat 把它们搬进聊天软件。它自己主动连到飞书或 Telegram,网络上什么都不用配:不要公网 IP,也不要 VPN。
|
|
19
20
|
herdr-app 的每个标签页,在飞书里是一个群,在 Telegram 里是和 bot 私聊中的一个话题:
|
|
20
21
|
agent 干活时说的话实时可见,它问你的问题点按钮就能回答,图片双向都能传,所有终端汇总在一张总览里。
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
新开的群或话题会先补上之前的对话;只有 agent 需要你、或者做完的时候手机才会响
|
|
23
|
+
(飞书要先给 bot 发一次 `/auth`;Telegram 直接就是这样)。
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
目前 Claude Code 是完整同步的。其他 agent 在你给它发消息时会回一次屏幕内容,
|
|
26
|
+
等 herdrchat 学会读它们的对话记录以后,也会完整同步。
|
|
27
|
+
|
|
28
|
+
## 需要什么
|
|
29
|
+
|
|
30
|
+
- 这台机器上在跑 **hc**,至少有一个 session([herdr.dev](https://herdr.dev))。
|
|
31
|
+
- **Node.js 20** 或更新版本,系统是 **macOS 或 Linux**。
|
|
32
|
+
- **飞书**:一个能创建并发布企业自建应用的飞书团队(暂不支持 Lark 国际版)。
|
|
33
|
+
**Telegram**:一个 Telegram 账号。可以只用一个,也可以两个同时用。
|
|
34
|
+
|
|
35
|
+
下面会用到几个词:**session** 是一个 hc 核心(以及它的终端);**空间**和**标签页**是 herdr-app 给终端分组的方式;
|
|
36
|
+
**profile** 是 herdrchat 对一个聊天软件的一套配置(不指定就叫 `default`)。
|
|
25
37
|
|
|
26
38
|
## 安装
|
|
27
39
|
|
|
28
40
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
npm install -g herdrchat # herdrchat 命令
|
|
42
|
+
npx skills add zhangxiangliang/herdrchat # 可选:给编程 agent 用的 skill
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
skill 会教你的编程 agent(Claude Code、Codex……)替你配置和照看 herdrchat。
|
|
46
|
+
`skills` 工具问你选 Copy 还是 Symlink 时,选 Symlink,以后更新才会同步给 agent。
|
|
47
|
+
|
|
48
|
+
## 配置
|
|
49
|
+
|
|
50
|
+
**让 agent 帮你:** 对它说 `/herdrchat setup`。它会先问你用哪个聊天软件,然后一步步带你配好,
|
|
51
|
+
能自动点的飞书后台操作它会替你点。
|
|
52
|
+
|
|
53
|
+
**自己动手:**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
herdrchat init --chat telegram # 或者 --chat feishu;它会告诉你配置文件在哪
|
|
57
|
+
# 填好那个文件(每一行上面都写了值从哪来),然后:
|
|
58
|
+
herdrchat start # 启动,并等到真的连上
|
|
59
|
+
herdrchat install # 跑通以后:装成系统服务,开机自动启动,崩溃自动重启
|
|
31
60
|
```
|
|
32
61
|
|
|
33
|
-
|
|
62
|
+
每个聊天软件的具体步骤(建飞书应用,或者建 Telegram bot)在[详细说明](docs/guide.md#set-up-telegram)里(英文)。
|
|
34
63
|
|
|
35
|
-
|
|
64
|
+
## 使用
|
|
65
|
+
|
|
66
|
+
在聊天软件里:在某个终端的群或话题里打字,就是在跟它说话;点卡片上的按钮回答它的问题。
|
|
67
|
+
在和 bot 的私聊里:`/ls` 看总览,`/new` 启动一个终端,`/kill` 结束一个终端。
|
|
68
|
+
|
|
69
|
+
| 对 agent 说 | 做什么 |
|
|
36
70
|
|---|---|
|
|
37
71
|
| `/herdrchat setup` | 和你一起从零配好:飞书应用或 Telegram bot、配置、第一次启动。 |
|
|
38
72
|
| `/herdrchat status` | 告诉你它在不在跑,有问题的话问题在哪。 |
|
|
39
73
|
| `/herdrchat fix` | 找出问题的原因并修好。 |
|
|
40
|
-
| `/herdrchat config` |
|
|
41
|
-
| `/herdrchat
|
|
74
|
+
| `/herdrchat config` | 改配置:接哪些终端、哪些保持安静、标签。 |
|
|
75
|
+
| `/herdrchat start`、`stop`、`install`、`logs`、`auth` | 和下面的命令一样,只是会先检查。 |
|
|
76
|
+
| `/herdrchat feishu <任务>` | 替你在飞书开发者后台完成操作(用 [Ego Lite](https://lite.ego.app/) 浏览器,或者一步步带你点)。 |
|
|
42
77
|
| `/herdrchat telegram <任务>` | Telegram bot:从 @BotFather 一直到第一个话题。 |
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
`herdrchat
|
|
79
|
+
命令行:`herdrchat init`、`start`、`stop`、`status`、`logs`、`install`、`uninstall`、`version`;
|
|
80
|
+
只输入 `herdrchat` 会列出全部。加上 `--profile <名字>`,可以再开一个聊天软件。
|
|
81
|
+
|
|
82
|
+
## 更新
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install -g herdrchat@latest && npx skills add zhangxiangliang/herdrchat
|
|
86
|
+
herdrchat start # 每个 profile 都要:在这之前它还跑着旧代码
|
|
87
|
+
```
|
|
46
88
|
|
|
47
89
|
## 文件在哪
|
|
48
90
|
|
|
49
|
-
配置在 `~/.config/herdrchat/<profile>/config.env`,bot 写的东西都在 `~/.local/state/herdrchat/<profile
|
|
50
|
-
更新或重装 herdrchat
|
|
91
|
+
配置在 `~/.config/herdrchat/<profile>/config.env`,bot 写的东西都在 `~/.local/state/herdrchat/<profile>/`。
|
|
92
|
+
更新或重装 herdrchat 都不会动它们。`herdrchat status` 会把这两个位置都打印出来。
|
|
93
|
+
|
|
94
|
+
## 隐私和安全
|
|
95
|
+
|
|
96
|
+
- 终端里显示的内容会发到飞书或 Telegram 的服务器上。Telegram 的 bot 聊天不是端到端加密的。
|
|
97
|
+
- 只有 `config.env` 里设置的 owner 能往你的终端里输入;其他人发的都会被忽略。
|
|
98
|
+
- 飞书的 App Secret、Telegram 的 bot token 要像密码一样保管:谁拿到了,谁就能操作你的终端。
|
|
99
|
+
- 飞书里 bot 打的标签,你整个团队都看得见(`HERDR_BOT_TAGS=off` 可以关掉)。
|
|
100
|
+
|
|
101
|
+
## 卸载
|
|
102
|
+
|
|
103
|
+
每个 profile 运行一次 `herdrchat uninstall`,然后 `npm rm -g herdrchat`,删掉 `~/.config/herdrchat`
|
|
104
|
+
和 `~/.local/state/herdrchat`,最后删掉飞书应用或 Telegram bot(在 @BotFather 里用 `/deletebot`)。
|
|
51
105
|
|
|
52
106
|
工作原理和全部配置:[docs/guide.md](docs/guide.md)(英文)。
|
|
53
107
|
|
package/bin/cli.mjs
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// start start (or restart) it: the service, or else its own hc terminal
|
|
10
10
|
// stop stop it
|
|
11
11
|
// status how it runs, its folders, sessions, its chat app, errors today
|
|
12
|
-
// logs today's log (-n <lines>, --errors for ERROR and WARN only)
|
|
12
|
+
// logs today's log (-n <lines>, --errors for ERROR and WARN only, --day <date>)
|
|
13
13
|
// run run in the foreground (what start keeps running)
|
|
14
|
+
// version which herdrchat this is
|
|
14
15
|
//
|
|
15
16
|
// The profile is --profile or HERDRCHAT_PROFILE (default "default"); where its files are
|
|
16
17
|
// is in lib/core/config.mjs.
|
|
@@ -19,40 +20,97 @@ import path from 'node:path';
|
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
21
|
|
|
21
22
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const GUIDE = 'https://github.com/zhangxiangliang/herdrchat/blob/main/docs/guide.md';
|
|
22
24
|
const USAGE = `herdrchat <command> [--profile <name>]
|
|
23
25
|
|
|
24
|
-
init make the profile's config.env
|
|
25
|
-
install run it as a service of the system: starts at login, back after a crash
|
|
26
|
-
uninstall no service any more (it is stopped)
|
|
26
|
+
init make the profile's config.env (--chat feishu|telegram picks the app)
|
|
27
27
|
start start (or restart) it: the service, or else its own hc terminal
|
|
28
28
|
stop stop it
|
|
29
29
|
status how it runs, its folders, sessions, its chat app, errors today
|
|
30
|
-
logs today's log (-n <lines>, --errors for ERROR and WARN only)
|
|
31
|
-
run
|
|
30
|
+
logs today's log (-n <lines>, --errors for ERROR and WARN only, --day <date>)
|
|
31
|
+
install run it as a service of the system: starts at login, back after a crash
|
|
32
|
+
uninstall no service any more (it is stopped)
|
|
33
|
+
run run in the foreground (what start keeps running)
|
|
34
|
+
version which herdrchat this is
|
|
35
|
+
|
|
36
|
+
First time: herdrchat init, fill in the file it names, then herdrchat start.
|
|
37
|
+
A second chat app: add --profile <name> to every command.
|
|
38
|
+
Environment: HERDRCHAT_PROFILE (same as --profile), HERDRCHAT_HOME (one folder for a
|
|
39
|
+
profile's settings and state).
|
|
40
|
+
Guide: ${GUIDE}`;
|
|
41
|
+
|
|
42
|
+
// "herdrchat foo | head" closing the pipe is not an error
|
|
43
|
+
process.stdout.on('error', (err) => { if (err.code === 'EPIPE') process.exit(0); throw err; });
|
|
44
|
+
|
|
45
|
+
function fail(message, code = 2) {
|
|
46
|
+
console.error(message);
|
|
47
|
+
process.exit(code);
|
|
48
|
+
}
|
|
32
49
|
|
|
33
50
|
const argv = process.argv.slice(2);
|
|
34
|
-
const flag = (name) => {
|
|
51
|
+
const flag = (name, what) => {
|
|
35
52
|
const i = argv.indexOf(name);
|
|
36
53
|
if (i < 0) return undefined;
|
|
37
54
|
const value = argv[i + 1];
|
|
55
|
+
if (value === undefined || value.startsWith('-')) fail(`${name} needs ${what}, for example ${name} ${name === '--profile' ? 'telegram' : name === '--chat' ? 'telegram' : name === '--day' ? '2026-01-31' : '100'}`);
|
|
38
56
|
argv.splice(i, 2);
|
|
39
57
|
return value;
|
|
40
58
|
};
|
|
41
|
-
const profile = flag('--profile');
|
|
42
|
-
if (profile)
|
|
43
|
-
|
|
59
|
+
const profile = flag('--profile', 'a name');
|
|
60
|
+
if (profile) {
|
|
61
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(profile)) fail(`Profile "${profile}": use letters, digits, - and _ only.`);
|
|
62
|
+
process.env.HERDRCHAT_PROFILE = profile;
|
|
63
|
+
}
|
|
64
|
+
const linesRaw = flag('-n', 'a number of lines');
|
|
65
|
+
if (linesRaw !== undefined && !/^\d+$/.test(linesRaw)) fail('-n needs a number of lines, for example -n 100');
|
|
66
|
+
const lines = Number(linesRaw || 40);
|
|
67
|
+
const day = flag('--day', 'a date');
|
|
68
|
+
if (day !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(day)) fail('--day needs a date as YYYY-MM-DD, for example --day 2026-01-31');
|
|
69
|
+
const chatFlag = flag('--chat', 'feishu or telegram');
|
|
44
70
|
const errorsOnly = argv.includes('--errors');
|
|
71
|
+
const helpAsked = argv.some((a) => ['-h', '--help', 'help'].includes(a));
|
|
72
|
+
const versionAsked = argv.some((a) => ['-v', '--version', 'version'].includes(a));
|
|
45
73
|
const command = argv.find((a) => !a.startsWith('-'));
|
|
74
|
+
if (versionAsked) {
|
|
75
|
+
console.log(JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
if (helpAsked || !command) {
|
|
79
|
+
console.log(USAGE);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
if (process.env.HERDRCHAT_PROFILE && !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(process.env.HERDRCHAT_PROFILE)) fail(`HERDRCHAT_PROFILE "${process.env.HERDRCHAT_PROFILE}": use letters, digits, - and _ only.`);
|
|
46
83
|
|
|
47
84
|
// after the profile is set: lib/core/config.mjs reads it when it loads
|
|
48
|
-
const { CONFIG_FILE, STATE_DIR, loadConfig, makeDirs, placesOf } = await import('../lib/core/config.mjs');
|
|
49
|
-
const {
|
|
85
|
+
const { CONFIG_FILE, STATE_DIR, chatOf, loadConfig, makeDirs, placesOf } = await import('../lib/core/config.mjs');
|
|
86
|
+
const { chatModule, chatNames } = await import('../lib/chats/index.mjs');
|
|
87
|
+
const { coreUp, hc, sessionsHere } = await import('../lib/core/hc.mjs');
|
|
50
88
|
const { serviceOf } = await import('../lib/core/service.mjs');
|
|
51
89
|
const places = placesOf();
|
|
52
90
|
|
|
53
91
|
const shellQuote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
54
|
-
const
|
|
55
|
-
|
|
92
|
+
const again = profile || (places.profile !== 'default' ? places.profile : null) ? ` --profile ${places.profile}` : '';
|
|
93
|
+
// the settings, or a plain word on what to do when there are none yet
|
|
94
|
+
function config() {
|
|
95
|
+
if (!fs.existsSync(CONFIG_FILE)) fail(`No settings yet at ${CONFIG_FILE}.\nRun \`herdrchat init${again}\`, fill it in, then \`herdrchat start${again}\`.`, 1);
|
|
96
|
+
return loadConfig();
|
|
97
|
+
}
|
|
98
|
+
const firstSession = () => config().sessions[0];
|
|
99
|
+
// the settings the bot needs to start, checked here so a missing key says so at once
|
|
100
|
+
// instead of a bot that waits and fails
|
|
101
|
+
function readyConfig() {
|
|
102
|
+
config();
|
|
103
|
+
const cfg = loadConfig({ requires: chatModule(chatOf()).requires || [] });
|
|
104
|
+
// a session not on this machine would be made by hc as the bot asks for it: a typo
|
|
105
|
+
const here = sessionsHere();
|
|
106
|
+
const unknown = cfg.sessions.filter((s) => !here.includes(s));
|
|
107
|
+
if (unknown.length) fail(`HERDR_SESSIONS in ${CONFIG_FILE} names ${unknown.join(', ')}, which ${unknown.length > 1 ? 'are' : 'is'} not an hc session on this machine.\nThe sessions here: ${here.join(', ') || 'none (start hc first)'}.`, 1);
|
|
108
|
+
return cfg;
|
|
109
|
+
}
|
|
110
|
+
// a day's log file: the local date, as the log names its files
|
|
111
|
+
const today = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; };
|
|
112
|
+
const logFile = (date = today()) => path.join(STATE_DIR, 'logs', `${date}.log`);
|
|
113
|
+
const CONNECT_WAIT_MS = 60000; // a first attempt that times out (15 s) is tried again
|
|
56
114
|
|
|
57
115
|
// hc's full path, from HERDRCHAT_HC or PATH
|
|
58
116
|
function findHc() {
|
|
@@ -71,7 +129,7 @@ function findHc() {
|
|
|
71
129
|
// The bot runs in the first session listed; after that list changes, an older one may
|
|
72
130
|
// still run in another, and two bots answer every message: it is stopped everywhere.
|
|
73
131
|
async function stopEverywhere() {
|
|
74
|
-
for (const session of
|
|
132
|
+
for (const session of config().sessions) {
|
|
75
133
|
await hc(session, ['kill', places.terminal]).catch(() => {});
|
|
76
134
|
await hc(session, ['rm', places.terminal]).catch(() => {});
|
|
77
135
|
}
|
|
@@ -80,7 +138,7 @@ async function stopEverywhere() {
|
|
|
80
138
|
// Started is not running: wait until today's log says the bot reached Feishu after
|
|
81
139
|
// `since`, and hand back the errors it wrote meanwhile when it does not.
|
|
82
140
|
async function waitConnected(since) {
|
|
83
|
-
const log =
|
|
141
|
+
const log = logFile();
|
|
84
142
|
const until = Date.now() + CONNECT_WAIT_MS;
|
|
85
143
|
let errors = [];
|
|
86
144
|
while (Date.now() < until) {
|
|
@@ -97,13 +155,13 @@ async function waitConnected(since) {
|
|
|
97
155
|
// Nothing else, so no agent's variables come along.
|
|
98
156
|
function botEnv() {
|
|
99
157
|
const hcPath = findHc();
|
|
100
|
-
if (!hcPath) throw new Error('hc is not on PATH: install herdr-core first (https://herdr.
|
|
158
|
+
if (!hcPath) throw new Error('hc is not on PATH: install herdr-core first (https://herdr.dev)');
|
|
101
159
|
const env = { HERDRCHAT_HC: hcPath, HERDRCHAT_PROFILE: places.profile };
|
|
102
160
|
for (const k of ['PATH', 'HERDRCHAT_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME']) if (process.env[k]) env[k] = process.env[k];
|
|
103
161
|
return env;
|
|
104
162
|
}
|
|
105
163
|
|
|
106
|
-
const service = () => serviceOf({ profile: places.
|
|
164
|
+
const service = () => serviceOf({ profile: places.id, root: ROOT, node: process.execPath, env: botEnv(), stateDir: STATE_DIR });
|
|
107
165
|
|
|
108
166
|
function failedToConnect(where, result) {
|
|
109
167
|
console.error(`${where} but did not reach its chat app within ${CONNECT_WAIT_MS / 1000} s.`);
|
|
@@ -122,14 +180,17 @@ async function terminalState(session) {
|
|
|
122
180
|
|
|
123
181
|
const commands = {
|
|
124
182
|
init() {
|
|
183
|
+
const chatName = chatFlag || 'feishu';
|
|
184
|
+
if (!chatNames().includes(chatName)) fail(`--chat ${chatName}: the chat apps are ${chatNames().join(', ')}`);
|
|
125
185
|
makeDirs();
|
|
126
186
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
127
|
-
console.log(`${CONFIG_FILE} is there already; edit it
|
|
187
|
+
console.log(`${CONFIG_FILE} is there already; open it and edit it, then run \`herdrchat start${again}\`.`);
|
|
128
188
|
return;
|
|
129
189
|
}
|
|
130
|
-
fs.
|
|
131
|
-
fs.
|
|
132
|
-
|
|
190
|
+
const example = fs.readFileSync(path.join(ROOT, 'config.env.example'), 'utf8').replace(/^HERDRCHAT_CHAT=.*$/m, `HERDRCHAT_CHAT=${chatName}`);
|
|
191
|
+
fs.writeFileSync(CONFIG_FILE, example, { mode: 0o600 });
|
|
192
|
+
const keys = (chatModule(chatName).requires || []).join(', ');
|
|
193
|
+
console.log(`Made ${CONFIG_FILE} (only you can read it) for ${chatName}.\nOpen it and fill in ${keys} and HERDR_SESSIONS (the hc sessions to mirror), then run \`herdrchat start${again}\`.\nThe guide walks through it: ${GUIDE}`);
|
|
133
194
|
},
|
|
134
195
|
|
|
135
196
|
// The service file is written again each time (a moved node or hc is picked up), and
|
|
@@ -137,7 +198,7 @@ const commands = {
|
|
|
137
198
|
async install() {
|
|
138
199
|
const svc = service();
|
|
139
200
|
if (!svc) throw new Error(`no launchd or systemd on ${process.platform}: run \`herdrchat start\` instead`);
|
|
140
|
-
|
|
201
|
+
readyConfig(); // a config that cannot start the bot fails here, not in the service
|
|
141
202
|
await stopEverywhere();
|
|
142
203
|
svc.unload();
|
|
143
204
|
svc.write(svc.content);
|
|
@@ -161,6 +222,7 @@ const commands = {
|
|
|
161
222
|
},
|
|
162
223
|
|
|
163
224
|
async start() {
|
|
225
|
+
readyConfig();
|
|
164
226
|
await stopEverywhere();
|
|
165
227
|
const svc = service();
|
|
166
228
|
const since = new Date().toISOString();
|
|
@@ -190,25 +252,39 @@ const commands = {
|
|
|
190
252
|
},
|
|
191
253
|
|
|
192
254
|
async status() {
|
|
193
|
-
console.log(`profile ${places.profile}`);
|
|
194
|
-
console.log(`settings ${CONFIG_FILE}${fs.existsSync(CONFIG_FILE) ? '' :
|
|
255
|
+
console.log(`profile ${places.profile}${places.id !== places.profile ? ` (named ${places.id} for its own folder)` : ''}`);
|
|
256
|
+
console.log(`settings ${CONFIG_FILE}${fs.existsSync(CONFIG_FILE) ? '' : ` (none yet: run \`herdrchat init${again}\`)`}`);
|
|
195
257
|
console.log(`state ${STATE_DIR}`);
|
|
196
258
|
if (!fs.existsSync(CONFIG_FILE)) return;
|
|
197
|
-
const cfg = loadConfig();
|
|
198
|
-
|
|
259
|
+
const cfg = loadConfig({ sessions: false }); // what is filled in so far, empty or not
|
|
260
|
+
const hcThere = Boolean(findHc());
|
|
261
|
+
// each session, and whether its hc core answers
|
|
262
|
+
const here = sessionsHere();
|
|
263
|
+
const sessionWords = [];
|
|
264
|
+
for (const session of cfg.sessions) {
|
|
265
|
+
if (!here.includes(session)) sessionWords.push(`${session} (no such session here; this machine has: ${here.join(', ') || 'none'})`);
|
|
266
|
+
else sessionWords.push(`${session} (${(await coreUp(session)) ? 'core up' : 'core not running'})`);
|
|
267
|
+
}
|
|
268
|
+
console.log(`sessions ${sessionWords.join(', ') || 'none yet: set HERDR_SESSIONS (`ls ~/.local/state/herdr-core` lists them)'}`);
|
|
199
269
|
// how it runs: the service, an hc terminal, or not at all
|
|
200
|
-
const svc = service();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const st = svc.state();
|
|
204
|
-
console.log(`running ${st.running ? `yes, as the ${svc.kind} service ${svc.name}${st.pid ? ` (pid ${st.pid})` : ''}` : `no: the ${svc.kind} service ${svc.name} is stopped (\`herdrchat start\`)`}`);
|
|
205
|
-
const gone = svc.nodeGone();
|
|
206
|
-
if (gone) console.log(` the service runs ${gone}, which is gone (node moved): \`herdrchat install\` again`);
|
|
207
|
-
if (inTerminal) console.log(` also in hc terminal ${places.terminal}: two bots; \`herdrchat start\` keeps one`);
|
|
270
|
+
const svc = hcThere ? service() : null;
|
|
271
|
+
if (!hcThere) {
|
|
272
|
+
console.log('running unknown: hc is not on PATH (install herdr-core: https://herdr.dev)');
|
|
208
273
|
} else {
|
|
209
|
-
|
|
274
|
+
const inTerminal = cfg.sessions.length ? (await terminalState(cfg.sessions[0])) === 'live' : false;
|
|
275
|
+
if (svc?.installed()) {
|
|
276
|
+
const st = svc.state();
|
|
277
|
+
console.log(`running ${st.running ? `yes, as the ${svc.kind} service ${svc.name}${st.pid ? ` (pid ${st.pid})` : ''}` : `no: the ${svc.kind} service ${svc.name} is stopped (\`herdrchat start${again}\`)`}`);
|
|
278
|
+
const gone = svc.nodeGone();
|
|
279
|
+
if (gone) console.log(` the service runs ${gone}, which is gone (node moved): \`herdrchat install${again}\` again`);
|
|
280
|
+
if (inTerminal) console.log(` also in hc terminal ${places.terminal}: two bots; \`herdrchat start${again}\` keeps one`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`running ${inTerminal ? `yes, in hc terminal ${places.terminal} (session ${cfg.sessions[0]}); \`herdrchat install${again}\` makes it a service` : `no (\`herdrchat start${again}\`, or \`herdrchat install${again}\` for a service)`}`);
|
|
283
|
+
}
|
|
210
284
|
}
|
|
211
285
|
// what its chat app needs, without printing a secret
|
|
286
|
+
const set = (value, missing) => (value ? 'set' : missing);
|
|
287
|
+
const owner = 'not set: message the bot, it says what to put in config.env';
|
|
212
288
|
console.log(`chat ${cfg.chat}`);
|
|
213
289
|
if (cfg.chat === 'feishu') {
|
|
214
290
|
let token = null;
|
|
@@ -217,24 +293,26 @@ const commands = {
|
|
|
217
293
|
} catch {
|
|
218
294
|
// no /auth yet
|
|
219
295
|
}
|
|
220
|
-
console.log(`
|
|
221
|
-
console.log(
|
|
296
|
+
console.log(`app id ${set(cfg.appId, 'missing: FEISHU_APP_ID, from the Feishu developer console')}`);
|
|
297
|
+
console.log(`secret ${set(cfg.appSecret, 'missing: FEISHU_APP_SECRET, from the Feishu developer console')}`);
|
|
298
|
+
console.log(`owner ${set(cfg.ownerOpenId, owner)}`);
|
|
299
|
+
console.log(`/auth ${!token ? 'not done (muting, labels and pins need it: send /auth to the bot)' : (token.refreshUntil || 0) > Date.now() ? 'done' : 'expired: send /auth to the bot again'}`);
|
|
222
300
|
} else if (cfg.chat === 'telegram') {
|
|
223
|
-
console.log(`token ${cfg.env.TELEGRAM_BOT_TOKEN
|
|
224
|
-
console.log(`owner ${cfg.env.TELEGRAM_OWNER_ID
|
|
301
|
+
console.log(`token ${set(cfg.env.TELEGRAM_BOT_TOKEN, 'missing: TELEGRAM_BOT_TOKEN, from @BotFather')}`);
|
|
302
|
+
console.log(`owner ${set(cfg.env.TELEGRAM_OWNER_ID, owner)}`);
|
|
225
303
|
}
|
|
226
|
-
const log =
|
|
304
|
+
const log = logFile();
|
|
227
305
|
if (fs.existsSync(log)) {
|
|
228
306
|
const text = fs.readFileSync(log, 'utf8');
|
|
229
|
-
const count = (level) => (text.match(new RegExp(` ${level} `, 'g')) || []).length;
|
|
230
|
-
console.log(`today ${count('ERROR')}
|
|
307
|
+
const count = (level, word) => { const n = (text.match(new RegExp(` ${level} `, 'g')) || []).length; return `${n} ${word}${n === 1 ? '' : 's'}`; };
|
|
308
|
+
console.log(`today ${count('ERROR', 'error')}, ${count('WARN', 'warning')} (herdrchat logs --errors${again})`);
|
|
231
309
|
}
|
|
232
310
|
},
|
|
233
311
|
|
|
234
312
|
logs() {
|
|
235
|
-
const log =
|
|
313
|
+
const log = logFile(day);
|
|
236
314
|
if (!fs.existsSync(log)) {
|
|
237
|
-
console.log(`No log today (${log})
|
|
315
|
+
console.log(`No log for ${day || 'today'} (${log}).${day ? '' : ' Older days: --day YYYY-MM-DD.'}`);
|
|
238
316
|
return;
|
|
239
317
|
}
|
|
240
318
|
let out = fs.readFileSync(log, 'utf8').trimEnd().split('\n');
|
|
@@ -248,8 +326,9 @@ const commands = {
|
|
|
248
326
|
};
|
|
249
327
|
|
|
250
328
|
if (!commands[command]) {
|
|
251
|
-
console.
|
|
252
|
-
|
|
329
|
+
console.error(`Unknown command: ${command}\n`);
|
|
330
|
+
console.error(USAGE);
|
|
331
|
+
process.exit(2);
|
|
253
332
|
}
|
|
254
333
|
try {
|
|
255
334
|
await commands[command]();
|
package/bin/herdrchat.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { STATE_DIR, chatOf, loadConfig, makeDirs, readState, writeState } from '../lib/core/config.mjs';
|
|
9
9
|
import { createEngine } from '../lib/core/engine.mjs';
|
|
10
10
|
import { followEvents } from '../lib/core/events.mjs';
|
|
11
|
-
import { cores, getTerminal, hc, keys, machines, put, run, screenTail, send } from '../lib/core/hc.mjs';
|
|
11
|
+
import { cores, getTerminal, hc, keys, machines, nameTab, put, run, screenTail, send } from '../lib/core/hc.mjs';
|
|
12
12
|
import { createLog } from '../lib/core/log.mjs';
|
|
13
13
|
import * as readers from '../lib/core/readers/index.mjs';
|
|
14
14
|
import { chatModule, createChat } from '../lib/chats/index.mjs';
|
|
@@ -67,7 +67,7 @@ logger.capture({ onCrash: sayCrash });
|
|
|
67
67
|
engine = createEngine({
|
|
68
68
|
cfg,
|
|
69
69
|
chat,
|
|
70
|
-
hc: { followEvents, cores, machines, getTerminal, screenTail, send, keys, put, run, kill: (session, address) => hc(session, ['kill', address]) },
|
|
70
|
+
hc: { followEvents, cores, machines, getTerminal, screenTail, send, keys, put, run, nameTab, kill: (session, address) => hc(session, ['kill', address]) },
|
|
71
71
|
readers,
|
|
72
72
|
store,
|
|
73
73
|
log,
|
package/config.env.example
CHANGED
|
@@ -1,32 +1,49 @@
|
|
|
1
|
-
# herdrchat settings
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# herdrchat settings. `herdrchat init` copies this file to your profile's folder (only
|
|
2
|
+
# you can read it there). Fill in what is empty above the line, then run
|
|
3
|
+
# `herdrchat start`. Every setting is explained in docs/guide.md, "Settings".
|
|
4
|
+
# A key set in the environment wins over this file.
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Which chat app: feishu or telegram. To use both, run one profile each
|
|
7
|
+
# (`herdrchat init --profile telegram --chat telegram`).
|
|
7
8
|
HERDRCHAT_CHAT=feishu
|
|
8
9
|
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# The hc sessions to mirror, comma-separated (machines they reach come along).
|
|
11
|
+
# `ls ~/.local/state/herdr-core` lists the sessions on this machine.
|
|
12
|
+
HERDR_SESSIONS=
|
|
13
|
+
|
|
14
|
+
# --- Feishu only ---
|
|
15
|
+
# From the Feishu developer console: your app's App ID and App Secret.
|
|
16
|
+
FEISHU_APP_ID=
|
|
17
|
+
FEISHU_APP_SECRET=
|
|
18
|
+
# You. Leave it empty: message the bot once and it tells you the line to put here.
|
|
12
19
|
FEISHU_OWNER_OPEN_ID=
|
|
13
20
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
21
|
+
# --- Telegram only ---
|
|
22
|
+
# The token @BotFather gave you (turn on Threaded Mode in BotFather's mini app too).
|
|
16
23
|
TELEGRAM_BOT_TOKEN=
|
|
24
|
+
# You. Leave it empty: message the bot once and it tells you the line to put here.
|
|
17
25
|
TELEGRAM_OWNER_ID=
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
#
|
|
27
|
+
# ------------------------------------------------------------------------------------
|
|
28
|
+
# The rest has good defaults; change it when you want to.
|
|
29
|
+
|
|
30
|
+
# Quiet spaces: only "needs you" and "done" make a sound there.
|
|
31
|
+
# Values: all (default), none (empty), or space names, comma-separated.
|
|
32
|
+
# Feishu does this once you send /auth to the bot; Telegram does it right away.
|
|
23
33
|
HERDR_MUTED_SPACES=all
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
# Fully silent spaces: nothing makes a sound there, not even "needs you".
|
|
36
|
+
# Values: none (empty, default), all, or space names, comma-separated.
|
|
25
37
|
HERDR_SILENT_SPACES=
|
|
26
|
-
|
|
38
|
+
|
|
39
|
+
# What a new group or topic gets of the conversation before it:
|
|
40
|
+
# file,folded (default: all of it as a file, its newest part in a folded post),
|
|
41
|
+
# file, folded, or empty for nothing. /history posts both whenever you ask.
|
|
42
|
+
HERDR_HISTORY=file,folded
|
|
43
|
+
|
|
44
|
+
# (Feishu only) The bot's tags next to group names: the session or machine, and "ended".
|
|
45
|
+
# They are visible to your whole Feishu team. on (default) or off.
|
|
27
46
|
HERDR_BOT_TAGS=on
|
|
28
|
-
|
|
47
|
+
|
|
48
|
+
# (Feishu only) Your own sidebar labels, once /auth is done. on (default) or off.
|
|
29
49
|
HERDR_USER_TAGS=on
|
|
30
|
-
# what a new room gets of the conversation before it: file,folded (default: all of it as a
|
|
31
|
-
# file, the newest part in a folded post), file, folded, or empty for nothing
|
|
32
|
-
HERDR_HISTORY=file,folded
|