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 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-app has official phone and desktop apps. But to use them away from your desk, your
16
- phone has to reach the machine that runs hc, so you first need a VPN or a private network.
17
- And on a small phone screen, working with an agent is clumsy.
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
- Your chat app is already on your phone. herdrchat connects out to it by itself, so there
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
- Use one app or both at once (a profile each). Slack and Discord may follow.
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
- ## Install
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
- npx skills add zhangxiangliang/herdrchat # the skill, for your agent (pick Symlink)
33
- npm install -g herdrchat # the herdrchat command it runs
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
- Then ask your agent:
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
- | Command | What it does |
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 groups stay quiet, tags. |
44
- | `/herdrchat feishu <task>` | Does a task in the Feishu developer console for you. |
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, without an agent: `herdrchat init`, `start`, `stop`, `status`, `logs`;
48
- `herdrchat install` runs it as a service that starts at login and comes back after a
49
- crash. Add `--profile <name>` for a second chat app.
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>/` (the profile is `default` unless you name one).
55
- Updating or reinstalling herdrchat never touches them.
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-app 官方有手机端和电脑端。但想在外面用,手机得能连到跑 hc 的机器,
16
- 所以要先折腾 VPN 或内网环境;而且在手机的小屏幕上和 agent 交互,体验并不好。
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
- 聊天软件本来就在你手机上。herdrchat 自己主动连过去,网络上什么都不用配:不要公网 IP,也不要 VPN。
19
+ herdrchat 把它们搬进聊天软件。它自己主动连到飞书或 Telegram,网络上什么都不用配:不要公网 IP,也不要 VPN。
19
20
  herdr-app 的每个标签页,在飞书里是一个群,在 Telegram 里是和 bot 私聊中的一个话题:
20
21
  agent 干活时说的话实时可见,它问你的问题点按钮就能回答,图片双向都能传,所有终端汇总在一张总览里。
21
- 新开的群或话题会先补上之前的对话。只有 agent 需要你、或者做完的时候手机才会响。
22
- hc 能跑的 agent 都能用,我们自己用下来非常顺畅。
22
+ 新开的群或话题会先补上之前的对话;只有 agent 需要你、或者做完的时候手机才会响
23
+ (飞书要先给 bot 发一次 `/auth`;Telegram 直接就是这样)。
23
24
 
24
- 可以只用一个,也可以两个同时用(各开一个 profile)。后面有精力的话,也会考虑接入 Slack、Discord。
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
- npx skills add zhangxiangliang/herdrchat # 给 agent 用的 skill(选 Symlink)
30
- npm install -g herdrchat # skill 要用到的 herdrchat 命令
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
- 然后对你的 agent 说:
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 feishu <任务>` | 替你在飞书开发者后台完成操作。 |
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
- 不用 agent 也可以直接在命令行里用:`herdrchat init`、`start`、`stop`、`status`、`logs`;
45
- `herdrchat install` 把它装成系统服务,开机自动启动,崩溃后自动重启。第二个聊天软件加 `--profile <名字>`。
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>/`(没指定 profile 时就是 `default`)。
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 from the example when it is missing
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 run in the foreground (what start keeps running)`;
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) process.env.HERDRCHAT_PROFILE = profile;
43
- const lines = Number(flag('-n') || 40);
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 { hc } = await import('../lib/core/hc.mjs');
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 firstSession = () => loadConfig().sessions[0];
55
- const CONNECT_WAIT_MS = 30000;
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 loadConfig().sessions) {
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 = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.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.app)');
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.profile, root: ROOT, node: process.execPath, env: botEnv(), stateDir: STATE_DIR });
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.copyFileSync(path.join(ROOT, 'config.env.example'), CONFIG_FILE);
131
- fs.chmodSync(CONFIG_FILE, 0o600);
132
- console.log(`Made ${CONFIG_FILE}: fill in the app id, the secret and HERDR_SESSIONS.`);
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
- loadConfig(); // a config that cannot start the bot fails here, not in the service
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) ? '' : ' (missing: run `herdrchat init`)'}`);
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
- console.log(`sessions ${cfg.sessions.join(', ')}`);
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
- const inTerminal = (await terminalState(cfg.sessions[0])) === 'live';
202
- if (svc?.installed()) {
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
- console.log(`running ${inTerminal ? `yes, in hc terminal ${places.terminal} (session ${cfg.sessions[0]}); \`herdrchat install\` makes it a service` : 'no (`herdrchat start`, or `herdrchat install` for a service)'}`);
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(`owner ${cfg.ownerOpenId ? 'set' : 'not set: message the bot, it says what to put in config.env'}`);
221
- console.log(`/auth ${!token ? 'not done (muting, labels and pins need it)' : (token.refreshUntil || 0) > Date.now() ? 'done' : 'expired: send /auth again'}`);
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 ? 'set' : 'missing: TELEGRAM_BOT_TOKEN from @BotFather'}`);
224
- console.log(`owner ${cfg.env.TELEGRAM_OWNER_ID ? 'set' : 'not set: message the bot, it says what to put in config.env'}`);
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 = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.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')} errors, ${count('WARN')} warnings (herdrchat logs --errors)`);
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 = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.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.log(USAGE);
252
- process.exit(command ? 2 : 0);
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,
@@ -1,32 +1,49 @@
1
- # herdrchat settings, the example of config.env. `herdrchat init` copies it to where the
2
- # profile reads it (~/.config/herdrchat/default/config.env, ~/.config/herdrchat/<profile>/
3
- # with --profile, or HERDRCHAT_HOME), private; then fill it in.
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
- # the chat app this profile mirrors into: feishu (default) or telegram. To use both, run
6
- # one profile each (herdrchat start --profile telegram).
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
- # feishu
10
- FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx
11
- FEISHU_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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
- # telegram: the token from @BotFather (turn topic mode on there); your user id, which the
15
- # bot tells you when you message it
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
- HERDR_SESSIONS=session-a,session-b
20
- # spaces whose rooms are quiet, so only "needs you" and "done" ring (Feishu: groups muted
21
- # once /auth is done; Telegram: silent posts): `all` (default), empty for none, or spaces,
22
- # comma-separated; elsewhere every post rings
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
- # spaces where nothing rings, not even "needs you": empty (default), `all`, or spaces
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
- # the bot's tags next to group names (tenant tags): on (default) or off
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
- # your sidebar labels, once /auth is done: on (default) or off
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