herdrchat 0.0.0-stage → 1.0.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 +21 -0
- package/README.md +54 -2
- package/README.zh.md +53 -0
- package/bin/cli.mjs +140 -0
- package/bin/herdrchat.mjs +1260 -0
- package/config.env.example +17 -0
- package/docs/guide.md +198 -0
- package/lib/avatar.mjs +34 -0
- package/lib/config.mjs +93 -0
- package/lib/cursor.mjs +57 -0
- package/lib/events.mjs +95 -0
- package/lib/feishu.mjs +273 -0
- package/lib/hc.mjs +84 -0
- package/lib/home.mjs +26 -0
- package/lib/log.mjs +59 -0
- package/lib/menu.mjs +61 -0
- package/lib/overview.mjs +42 -0
- package/lib/readers/claude.mjs +119 -0
- package/lib/readers/index.mjs +24 -0
- package/lib/userauth.mjs +119 -0
- package/package.json +40 -4
- package/skills/README.md +9 -0
- package/skills/herdrchat/SKILL.md +45 -0
- package/skills/herdrchat/references/feishu.md +150 -0
- package/skills/herdrchat/references/ops.md +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zhangxiangliang
|
|
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
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# herdrchat
|
|
4
|
+
|
|
5
|
+
Every terminal hc runs, in the chat app you already use. Feishu first.
|
|
6
|
+
|
|
7
|
+
[English](README.md) · [简体中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/herdrchat)
|
|
10
|
+
[](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
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.
|
|
18
|
+
|
|
19
|
+
Feishu is already on your phone. herdrchat connects out to Feishu by itself, so there is
|
|
20
|
+
nothing to set up on your network: no public IP, no VPN. Each terminal becomes a Feishu
|
|
21
|
+
group. You read what the agent says while it works, tap buttons to answer its questions,
|
|
22
|
+
send and receive images, and see all terminals on one overview card. Your phone rings
|
|
23
|
+
only when an agent needs you or is done. It works with any agent hc runs.
|
|
24
|
+
|
|
25
|
+
Feishu comes first. When we have time, Slack, Discord and Telegram may follow.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx skills add zhangxiangliang/herdrchat # the skill, for your agent (pick Symlink)
|
|
31
|
+
npm install -g herdrchat # the herdrchat command it runs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then ask your agent:
|
|
35
|
+
|
|
36
|
+
| Command | What it does |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `/herdrchat setup` | Sets everything up with you: the Feishu app, the settings, the first start. |
|
|
39
|
+
| `/herdrchat status` | Says whether it runs, and what is wrong if anything is. |
|
|
40
|
+
| `/herdrchat fix` | Finds the cause of a problem and fixes it. |
|
|
41
|
+
| `/herdrchat config` | Changes a setting: which terminals, which groups stay quiet, tags. |
|
|
42
|
+
| `/herdrchat feishu <task>` | Does a task in the Feishu developer console for you. |
|
|
43
|
+
|
|
44
|
+
On the command line, without an agent: `herdrchat init`, `start`, `stop`, `status`, `logs`.
|
|
45
|
+
|
|
46
|
+
## Where things are
|
|
47
|
+
|
|
48
|
+
Settings in `~/.config/herdrchat/default/config.env`, everything the bot writes in
|
|
49
|
+
`~/.local/state/herdrchat/default/`. Updating or reinstalling herdrchat never touches them.
|
|
50
|
+
|
|
51
|
+
How it works and every setting: [docs/guide.md](docs/guide.md).
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT © [zhangxiangliang](https://github.com/zhangxiangliang)
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# herdrchat
|
|
4
|
+
|
|
5
|
+
hc 跑的每个终端,都能在你常用的聊天软件里看到和回复。先支持飞书。
|
|
6
|
+
|
|
7
|
+
[English](README.md) · [简体中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/herdrchat)
|
|
10
|
+
[](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
herdr-app 官方有手机端和电脑端。但想在外面用,手机得能连到跑 hc 的机器,
|
|
16
|
+
所以要先折腾 VPN 或内网环境;而且在手机的小屏幕上和 agent 交互,体验并不好。
|
|
17
|
+
|
|
18
|
+
飞书本来就在你手机上。herdrchat 自己主动连到飞书,网络上什么都不用配:不要公网 IP,也不要 VPN。
|
|
19
|
+
每个终端就是一个飞书群:agent 干活时说的话实时可见,它问你的问题点按钮就能回答,
|
|
20
|
+
图片双向都能传,所有终端汇总在一张总览卡片上。只有 agent 需要你、或者做完的时候手机才会响。
|
|
21
|
+
hc 能跑的 agent 都能用,我们自己用下来非常顺畅。
|
|
22
|
+
|
|
23
|
+
先支持飞书。后面有精力的话,也会考虑接入 Slack、Discord、Telegram 等其他聊天软件。
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx skills add zhangxiangliang/herdrchat # 给 agent 用的 skill(选 Symlink)
|
|
29
|
+
npm install -g herdrchat # skill 要用到的 herdrchat 命令
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
然后对你的 agent 说:
|
|
33
|
+
|
|
34
|
+
| 命令 | 做什么 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `/herdrchat setup` | 和你一起从零配好:飞书应用、配置、第一次启动。 |
|
|
37
|
+
| `/herdrchat status` | 告诉你它在不在跑,有问题的话问题在哪。 |
|
|
38
|
+
| `/herdrchat fix` | 找出问题的原因并修好。 |
|
|
39
|
+
| `/herdrchat config` | 改配置:接哪些终端、哪些群保持安静、标签。 |
|
|
40
|
+
| `/herdrchat feishu <任务>` | 替你在飞书开发者后台完成操作。 |
|
|
41
|
+
|
|
42
|
+
不用 agent 也可以直接在命令行里用:`herdrchat init`、`start`、`stop`、`status`、`logs`。
|
|
43
|
+
|
|
44
|
+
## 文件在哪
|
|
45
|
+
|
|
46
|
+
配置在 `~/.config/herdrchat/default/config.env`,bot 写的东西都在 `~/.local/state/herdrchat/default/`。
|
|
47
|
+
更新或重装 herdrchat 都不会动它们。
|
|
48
|
+
|
|
49
|
+
工作原理和全部配置:[docs/guide.md](docs/guide.md)(英文)。
|
|
50
|
+
|
|
51
|
+
## 许可证
|
|
52
|
+
|
|
53
|
+
MIT © [zhangxiangliang](https://github.com/zhangxiangliang)
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// herdrchat's command line: set up, start, stop and look at one profile.
|
|
3
|
+
//
|
|
4
|
+
// herdrchat <command> [--profile <name>]
|
|
5
|
+
// init make the profile's config.env from the example when it is missing
|
|
6
|
+
// start start (or restart) it in its own hc terminal, kept running
|
|
7
|
+
// stop stop it
|
|
8
|
+
// status running or not, its folders, sessions, /auth, errors today
|
|
9
|
+
// logs today's log (-n <lines>, --errors for ERROR and WARN only)
|
|
10
|
+
// run run in the foreground (what start keeps running)
|
|
11
|
+
//
|
|
12
|
+
// The profile is --profile or HERDRCHAT_PROFILE (default "default"); where its files are
|
|
13
|
+
// is in lib/config.mjs.
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const USAGE = `herdrchat <command> [--profile <name>]
|
|
20
|
+
|
|
21
|
+
init make the profile's config.env from the example when it is missing
|
|
22
|
+
start start (or restart) it in its own hc terminal, kept running
|
|
23
|
+
stop stop it
|
|
24
|
+
status running or not, its folders, sessions, /auth, errors today
|
|
25
|
+
logs today's log (-n <lines>, --errors for ERROR and WARN only)
|
|
26
|
+
run run in the foreground (what start keeps running)`;
|
|
27
|
+
|
|
28
|
+
const argv = process.argv.slice(2);
|
|
29
|
+
const flag = (name) => {
|
|
30
|
+
const i = argv.indexOf(name);
|
|
31
|
+
if (i < 0) return undefined;
|
|
32
|
+
const value = argv[i + 1];
|
|
33
|
+
argv.splice(i, 2);
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const profile = flag('--profile');
|
|
37
|
+
if (profile) process.env.HERDRCHAT_PROFILE = profile;
|
|
38
|
+
const lines = Number(flag('-n') || 40);
|
|
39
|
+
const errorsOnly = argv.includes('--errors');
|
|
40
|
+
const command = argv.find((a) => !a.startsWith('-'));
|
|
41
|
+
|
|
42
|
+
// after the profile is set: lib/config.mjs reads it when it loads
|
|
43
|
+
const { CONFIG_FILE, STATE_DIR, loadConfig, makeDirs, placesOf } = await import('../lib/config.mjs');
|
|
44
|
+
const { hc } = await import('../lib/hc.mjs');
|
|
45
|
+
const places = placesOf();
|
|
46
|
+
|
|
47
|
+
const shellQuote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
48
|
+
const firstSession = () => loadConfig().sessions[0];
|
|
49
|
+
|
|
50
|
+
async function terminalState(session) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(await hc(session, ['get', places.terminal, '--json'])).state;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const commands = {
|
|
59
|
+
init() {
|
|
60
|
+
makeDirs();
|
|
61
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
62
|
+
console.log(`${CONFIG_FILE} is there already; edit it.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fs.copyFileSync(path.join(ROOT, 'config.env.example'), CONFIG_FILE);
|
|
66
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
67
|
+
console.log(`Made ${CONFIG_FILE}: fill in the app id, the secret and HERDR_SESSIONS.`);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async start() {
|
|
71
|
+
const session = firstSession();
|
|
72
|
+
// hc starts a program with its own environment: the profile and folders go along
|
|
73
|
+
const env = ['HERDRCHAT_PROFILE', 'HERDRCHAT_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME']
|
|
74
|
+
.filter((k) => process.env[k])
|
|
75
|
+
.map((k) => `${k}=${shellQuote(process.env[k])}`)
|
|
76
|
+
.join(' ');
|
|
77
|
+
const run = `env ${env} ${shellQuote(process.execPath)} ${shellQuote(path.join(ROOT, 'bin', 'herdrchat.mjs'))}`;
|
|
78
|
+
const loop = `while true; do ${run}; echo 'herdrchat exited, restarting in 3 s'; sleep 3; done`;
|
|
79
|
+
await hc(session, ['kill', places.terminal]).catch(() => {});
|
|
80
|
+
await hc(session, ['rm', places.terminal]).catch(() => {});
|
|
81
|
+
await hc(session, ['run', '--bg', '--no-parent', '--name', places.terminal, '--cwd', ROOT, '--', 'sh', '-c', loop]);
|
|
82
|
+
console.log(`Started ${places.terminal} (profile ${places.profile}) in hc session ${session}.`);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
async stop() {
|
|
86
|
+
const session = firstSession();
|
|
87
|
+
await hc(session, ['kill', places.terminal]).catch(() => {});
|
|
88
|
+
await hc(session, ['rm', places.terminal]).catch(() => {});
|
|
89
|
+
console.log(`Stopped ${places.terminal}.`);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async status() {
|
|
93
|
+
console.log(`profile ${places.profile} (hc terminal ${places.terminal})`);
|
|
94
|
+
console.log(`settings ${CONFIG_FILE}${fs.existsSync(CONFIG_FILE) ? '' : ' (missing: run `herdrchat init`)'}`);
|
|
95
|
+
console.log(`state ${STATE_DIR}`);
|
|
96
|
+
if (!fs.existsSync(CONFIG_FILE)) return;
|
|
97
|
+
const cfg = loadConfig();
|
|
98
|
+
console.log(`sessions ${cfg.sessions.join(', ')}`);
|
|
99
|
+
console.log(`running ${(await terminalState(cfg.sessions[0])) === 'live' ? 'yes' : 'no (`herdrchat start`)'}`);
|
|
100
|
+
let token = null;
|
|
101
|
+
try {
|
|
102
|
+
token = JSON.parse(fs.readFileSync(path.join(STATE_DIR, 'user-token.json'), 'utf8'));
|
|
103
|
+
} catch {
|
|
104
|
+
// no /auth yet
|
|
105
|
+
}
|
|
106
|
+
console.log(`/auth ${!token ? 'not done' : (token.refreshUntil || 0) > Date.now() ? 'done' : 'expired: send /auth again'}`);
|
|
107
|
+
const log = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.log`);
|
|
108
|
+
if (fs.existsSync(log)) {
|
|
109
|
+
const text = fs.readFileSync(log, 'utf8');
|
|
110
|
+
const count = (level) => (text.match(new RegExp(` ${level} `, 'g')) || []).length;
|
|
111
|
+
console.log(`today ${count('ERROR')} errors, ${count('WARN')} warnings (herdrchat logs --errors)`);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
logs() {
|
|
116
|
+
const log = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.log`);
|
|
117
|
+
if (!fs.existsSync(log)) {
|
|
118
|
+
console.log(`No log today (${log}).`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let out = fs.readFileSync(log, 'utf8').trimEnd().split('\n');
|
|
122
|
+
if (errorsOnly) out = out.filter((l) => / (ERROR|WARN) /.test(l));
|
|
123
|
+
console.log(out.slice(-lines).join('\n'));
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async run() {
|
|
127
|
+
await import('./herdrchat.mjs');
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (!commands[command]) {
|
|
132
|
+
console.log(USAGE);
|
|
133
|
+
process.exit(command ? 2 : 0);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await commands[command]();
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(err.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|