herdrchat 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/bin/cli.mjs +64 -15
- package/bin/herdrchat.mjs +63 -1246
- package/config.env.example +14 -0
- package/docs/architecture.md +76 -0
- package/docs/guide.md +18 -6
- package/lib/chats/contract.mjs +58 -0
- package/lib/{feishu.mjs → chats/feishu/client.mjs} +23 -2
- package/lib/chats/feishu/index.mjs +499 -0
- package/lib/{overview.mjs → chats/feishu/overview.mjs} +2 -2
- package/lib/chats/index.mjs +20 -0
- package/lib/chats/telegram/client.mjs +83 -0
- package/lib/chats/telegram/format.mjs +83 -0
- package/lib/chats/telegram/index.mjs +411 -0
- package/lib/{config.mjs → core/config.mjs} +39 -3
- package/lib/core/engine.mjs +1041 -0
- package/lib/{hc.mjs → core/hc.mjs} +50 -2
- package/lib/{log.mjs → core/log.mjs} +6 -3
- package/lib/{readers → core/readers}/claude.mjs +1 -0
- package/lib/core/readers/index.mjs +49 -0
- package/lib/core/views.mjs +58 -0
- package/package.json +6 -1
- package/skills/herdrchat/SKILL.md +1 -1
- package/skills/herdrchat/references/ops.md +15 -2
- package/skills/herdrchat/references/telegram.md +23 -0
- package/lib/readers/index.mjs +0 -24
- /package/lib/{avatar.mjs → chats/feishu/avatar.mjs} +0 -0
- /package/lib/{userauth.mjs → chats/feishu/userauth.mjs} +0 -0
- /package/lib/{cursor.mjs → core/cursor.mjs} +0 -0
- /package/lib/{events.mjs → core/events.mjs} +0 -0
- /package/lib/{home.mjs → core/home.mjs} +0 -0
- /package/lib/{menu.mjs → core/menu.mjs} +0 -0
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Every terminal hc runs, in the chat app you already use. Feishu first.
|
|
|
7
7
|
[English](README.md) · [简体中文](README.zh.md)
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/herdrchat)
|
|
10
|
+
[](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
|
|
10
11
|
[](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
|
|
11
12
|
|
|
12
13
|
</div>
|
package/README.zh.md
CHANGED
|
@@ -7,6 +7,7 @@ hc 跑的每个终端,都能在你常用的聊天软件里看到和回复。
|
|
|
7
7
|
[English](README.md) · [简体中文](README.zh.md)
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/herdrchat)
|
|
10
|
+
[](https://github.com/zhangxiangliang/herdrchat/actions/workflows/ci.yml)
|
|
10
11
|
[](https://github.com/zhangxiangliang/herdrchat/blob/main/LICENSE)
|
|
11
12
|
|
|
12
13
|
</div>
|
package/bin/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// run run in the foreground (what start keeps running)
|
|
11
11
|
//
|
|
12
12
|
// The profile is --profile or HERDRCHAT_PROFILE (default "default"); where its files are
|
|
13
|
-
// is in lib/config.mjs.
|
|
13
|
+
// is in lib/core/config.mjs.
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
@@ -39,13 +39,52 @@ const lines = Number(flag('-n') || 40);
|
|
|
39
39
|
const errorsOnly = argv.includes('--errors');
|
|
40
40
|
const command = argv.find((a) => !a.startsWith('-'));
|
|
41
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');
|
|
42
|
+
// after the profile is set: lib/core/config.mjs reads it when it loads
|
|
43
|
+
const { CONFIG_FILE, STATE_DIR, loadConfig, makeDirs, placesOf } = await import('../lib/core/config.mjs');
|
|
44
|
+
const { hc } = await import('../lib/core/hc.mjs');
|
|
45
45
|
const places = placesOf();
|
|
46
46
|
|
|
47
47
|
const shellQuote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
48
48
|
const firstSession = () => loadConfig().sessions[0];
|
|
49
|
+
const CONNECT_WAIT_MS = 30000;
|
|
50
|
+
|
|
51
|
+
// hc's full path, from HERDRCHAT_HC or PATH
|
|
52
|
+
function findHc() {
|
|
53
|
+
const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
54
|
+
for (const file of [process.env.HERDRCHAT_HC, ...dirs.map((d) => path.join(d, 'hc'))].filter(Boolean)) {
|
|
55
|
+
try {
|
|
56
|
+
fs.accessSync(file, fs.constants.X_OK);
|
|
57
|
+
return file;
|
|
58
|
+
} catch {
|
|
59
|
+
// not there
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The bot runs in the first session listed; after that list changes, an older one may
|
|
66
|
+
// still run in another, and two bots answer every message: it is stopped everywhere.
|
|
67
|
+
async function stopEverywhere() {
|
|
68
|
+
for (const session of loadConfig().sessions) {
|
|
69
|
+
await hc(session, ['kill', places.terminal]).catch(() => {});
|
|
70
|
+
await hc(session, ['rm', places.terminal]).catch(() => {});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Started is not running: wait until today's log says the bot reached Feishu after
|
|
75
|
+
// `since`, and hand back the errors it wrote meanwhile when it does not.
|
|
76
|
+
async function waitConnected(since) {
|
|
77
|
+
const log = path.join(STATE_DIR, 'logs', `${new Date().toISOString().slice(0, 10)}.log`);
|
|
78
|
+
const until = Date.now() + CONNECT_WAIT_MS;
|
|
79
|
+
let errors = [];
|
|
80
|
+
while (Date.now() < until) {
|
|
81
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
82
|
+
const lines = fs.existsSync(log) ? fs.readFileSync(log, 'utf8').split('\n').filter((l) => l.slice(0, 24) >= since) : [];
|
|
83
|
+
if (lines.some((l) => /\[\w+\] connected\b/.test(l))) return { ok: true }; // the chat app says it is in
|
|
84
|
+
errors = lines.filter((l) => / ERROR /.test(l));
|
|
85
|
+
}
|
|
86
|
+
return { ok: false, errors: [...new Set(errors.map((l) => l.slice(25)))].slice(-5) };
|
|
87
|
+
}
|
|
49
88
|
|
|
50
89
|
async function terminalState(session) {
|
|
51
90
|
try {
|
|
@@ -69,24 +108,34 @@ const commands = {
|
|
|
69
108
|
|
|
70
109
|
async start() {
|
|
71
110
|
const session = firstSession();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
const hcPath = findHc();
|
|
112
|
+
if (!hcPath) throw new Error('hc is not on PATH: install herdr-core first (https://herdr.app)');
|
|
113
|
+
// hc starts a program with the core's environment, not ours, and a core started by a
|
|
114
|
+
// service may not have hc on its PATH: hc's full path, our PATH, the profile and the
|
|
115
|
+
// folders go along
|
|
116
|
+
const env = [['HERDRCHAT_HC', hcPath], ...['PATH', 'HERDRCHAT_PROFILE', 'HERDRCHAT_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME'].map((k) => [k, process.env[k]])]
|
|
117
|
+
.filter(([, v]) => v)
|
|
118
|
+
.map(([k, v]) => `${k}=${shellQuote(v)}`)
|
|
76
119
|
.join(' ');
|
|
77
120
|
const run = `env ${env} ${shellQuote(process.execPath)} ${shellQuote(path.join(ROOT, 'bin', 'herdrchat.mjs'))}`;
|
|
78
121
|
const loop = `while true; do ${run}; echo 'herdrchat exited, restarting in 3 s'; sleep 3; done`;
|
|
79
|
-
await
|
|
80
|
-
|
|
122
|
+
await stopEverywhere();
|
|
123
|
+
const since = new Date().toISOString();
|
|
81
124
|
await hc(session, ['run', '--bg', '--no-parent', '--name', places.terminal, '--cwd', ROOT, '--', 'sh', '-c', loop]);
|
|
82
|
-
|
|
125
|
+
const result = await waitConnected(since);
|
|
126
|
+
if (!result.ok) {
|
|
127
|
+
console.error(`${places.terminal} started in hc session ${session} but did not reach its chat app within ${CONNECT_WAIT_MS / 1000} s.`);
|
|
128
|
+
if (result.errors.length) console.error(`Its errors:\n ${result.errors.join('\n ')}`);
|
|
129
|
+
console.error('It keeps retrying; `herdrchat logs --errors` shows more.');
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log(`Started ${places.terminal} (profile ${places.profile}) in hc session ${session}; connected to its chat app.`);
|
|
83
134
|
},
|
|
84
135
|
|
|
85
136
|
async stop() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
await hc(session, ['rm', places.terminal]).catch(() => {});
|
|
89
|
-
console.log(`Stopped ${places.terminal}.`);
|
|
137
|
+
await stopEverywhere();
|
|
138
|
+
console.log(`Stopped ${places.terminal}. Until it starts again, nothing reaches Feishu.`);
|
|
90
139
|
},
|
|
91
140
|
|
|
92
141
|
async status() {
|