@qinghuangniao/heron-connect 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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # heron-connect
2
+
3
+ A bridge service that connects local AI coding agents to messaging platforms, so you can talk to your AI coding assistant directly from Feishu, Telegram, Discord, Slack, and more.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @qinghuangniao/heron-connect
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Check version
15
+ heron-connect --version
16
+
17
+ # Create config
18
+ heron-connect
19
+
20
+ # Edit config.toml, then run
21
+ heron-connect --config /path/to/config.toml
22
+ ```
23
+
24
+ ## Documentation
25
+
26
+ Repository: https://github.com/janostudio/heron-connect
27
+
28
+ Full documentation: see the repository README and `docs/` directory.
29
+
30
+ ## Publish Flow
31
+
32
+ Running `npm publish` in this directory triggers `prepublishOnly`, which:
33
+
34
+ 1. builds missing release archives into `../dist`
35
+ 2. uses `gh` to create or update the GitHub release `v<package-version>`
36
+ 3. uploads required archives and `checksums.txt` before publishing to npm
37
+
38
+ Prerequisites:
39
+
40
+ - `gh auth login`
41
+ - Go build environment available locally
42
+ - release repository push permission on `janostudio/heron-connect`
package/install.js ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { execSync } = require("child_process");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const https = require("https");
9
+ const http = require("http");
10
+ const zlib = require("zlib");
11
+
12
+ const PACKAGE = require("./package.json");
13
+ const VERSION = `v${PACKAGE.version}`;
14
+ const NAME = "heron-connect";
15
+
16
+ const GITHUB_REPO = "janostudio/heron-connect";
17
+
18
+ const PLATFORM_MAP = {
19
+ darwin: "darwin",
20
+ linux: "linux",
21
+ win32: "windows",
22
+ };
23
+
24
+ const ARCH_MAP = {
25
+ x64: "amd64",
26
+ arm64: "arm64",
27
+ };
28
+
29
+ function getPlatformInfo() {
30
+ const platform = PLATFORM_MAP[process.platform];
31
+ const arch = ARCH_MAP[process.arch];
32
+ if (!platform || !arch) {
33
+ throw new Error(
34
+ `Unsupported platform: ${process.platform}/${process.arch}. ` +
35
+ `Supported: linux/darwin/windows x64/arm64`
36
+ );
37
+ }
38
+ const ext = platform === "windows" ? ".zip" : ".tar.gz";
39
+ const filename = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
40
+ return { platform, arch, ext, filename };
41
+ }
42
+
43
+ function getDownloadURLs(filename) {
44
+ return [
45
+ `https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/${filename}`,
46
+ ];
47
+ }
48
+
49
+ function fetch(url, redirects = 5) {
50
+ return new Promise((resolve, reject) => {
51
+ if (redirects <= 0) return reject(new Error("Too many redirects"));
52
+ const mod = url.startsWith("https") ? https : http;
53
+ mod
54
+ .get(url, { headers: { "User-Agent": "heron-connect-npm" } }, (res) => {
55
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
56
+ return resolve(fetch(res.headers.location, redirects - 1));
57
+ }
58
+ if (res.statusCode !== 200) {
59
+ res.resume();
60
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
61
+ }
62
+ const chunks = [];
63
+ res.on("data", (c) => chunks.push(c));
64
+ res.on("end", () => resolve(Buffer.concat(chunks)));
65
+ res.on("error", reject);
66
+ })
67
+ .on("error", reject);
68
+ });
69
+ }
70
+
71
+ async function download(urls) {
72
+ for (const url of urls) {
73
+ try {
74
+ console.log(`[heron-connect] Downloading from ${url}`);
75
+ const data = await fetch(url);
76
+ console.log(`[heron-connect] Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`);
77
+ return data;
78
+ } catch (err) {
79
+ console.warn(`[heron-connect] Failed: ${err.message}, trying next source...`);
80
+ }
81
+ }
82
+ throw new Error(
83
+ `[heron-connect] Could not download binary from any source.\n` +
84
+ ` Tried: ${urls.join(", ")}\n` +
85
+ ` You can download manually from https://github.com/${GITHUB_REPO}/releases`
86
+ );
87
+ }
88
+
89
+ function extractTarGz(buffer, destDir, binaryName) {
90
+ const tmpFile = path.join(destDir, "_tmp.tar.gz");
91
+ fs.writeFileSync(tmpFile, buffer);
92
+ try {
93
+ execSync(`tar xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" });
94
+ } finally {
95
+ fs.unlinkSync(tmpFile);
96
+ }
97
+ const extracted = fs.readdirSync(destDir).find((f) => f.startsWith(NAME) && !f.endsWith(".tar.gz"));
98
+ if (extracted && extracted !== binaryName) {
99
+ fs.renameSync(path.join(destDir, extracted), path.join(destDir, binaryName));
100
+ }
101
+ }
102
+
103
+ function extractZip(buffer, destDir, binaryName) {
104
+ const tmpFile = path.join(destDir, "_tmp.zip");
105
+ fs.writeFileSync(tmpFile, buffer);
106
+ try {
107
+ try {
108
+ execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" });
109
+ } catch {
110
+ execSync(`powershell -Command "Expand-Archive -Force '${tmpFile}' '${destDir}'"`, {
111
+ stdio: "pipe",
112
+ });
113
+ }
114
+ } finally {
115
+ try { fs.unlinkSync(tmpFile); } catch {}
116
+ }
117
+ const extracted = fs.readdirSync(destDir).find((f) => f.startsWith(NAME) && f.endsWith(".exe"));
118
+ if (extracted && extracted !== binaryName) {
119
+ fs.renameSync(path.join(destDir, extracted), path.join(destDir, binaryName));
120
+ }
121
+ }
122
+
123
+ // parseVersion splits "1.2.3-beta.1" into { nums: [1,2,3], preTag: "beta", preNum: 1 }
124
+ function parseVersion(v) {
125
+ v = v.replace(/^v/, "").trim();
126
+ const [base, ...rest] = v.split("-");
127
+ const nums = base.split(".").map(Number);
128
+ const pre = rest.join("-");
129
+ const m = pre.match(/^([a-zA-Z]+)\.?(\d+)?$/);
130
+ return { nums, preTag: m ? m[1] : pre, preNum: m && m[2] ? parseInt(m[2], 10) : 0, hasPre: pre !== "" };
131
+ }
132
+
133
+ // isNewerOrEqual returns true if installed >= expected
134
+ function isNewerOrEqual(installed, expected) {
135
+ const a = parseVersion(installed);
136
+ const b = parseVersion(expected);
137
+ const len = Math.max(a.nums.length, b.nums.length);
138
+ for (let i = 0; i < len; i++) {
139
+ const av = a.nums[i] || 0;
140
+ const bv = b.nums[i] || 0;
141
+ if (av > bv) return true;
142
+ if (av < bv) return false;
143
+ }
144
+ if (!a.hasPre && b.hasPre) return true;
145
+ if (a.hasPre && !b.hasPre) return false;
146
+ if (!a.hasPre && !b.hasPre) return true;
147
+ // Both pre-release: compare tag then number (rc > beta, beta.10 > beta.9)
148
+ if (a.preTag !== b.preTag) return a.preTag > b.preTag;
149
+ return a.preNum >= b.preNum;
150
+ }
151
+
152
+ async function main() {
153
+ const { platform, arch, ext, filename } = getPlatformInfo();
154
+ console.log(`[heron-connect] Platform: ${platform}/${arch}`);
155
+
156
+ const binDir = path.join(__dirname, "bin");
157
+ fs.mkdirSync(binDir, { recursive: true });
158
+
159
+ const binaryName = platform === "windows" ? `${NAME}.exe` : NAME;
160
+ const binaryPath = path.join(binDir, binaryName);
161
+
162
+ if (fs.existsSync(binaryPath)) {
163
+ try {
164
+ const out = execSync(`"${binaryPath}" --version`, { encoding: "utf8", timeout: 5000 });
165
+ const expectedVer = VERSION.slice(1); // remove leading "v"
166
+ if (out.includes(expectedVer)) {
167
+ console.log(`[heron-connect] Binary ${VERSION} already installed, skipping.`);
168
+ return;
169
+ }
170
+ // Don't downgrade: if existing binary is newer, keep it
171
+ const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/);
172
+ if (match && isNewerOrEqual(match[1], expectedVer)) {
173
+ console.log(`[heron-connect] Binary ${match[1]} is newer than ${VERSION}, skipping.`);
174
+ return;
175
+ }
176
+ console.log(`[heron-connect] Existing binary is outdated, upgrading to ${VERSION}...`);
177
+ fs.unlinkSync(binaryPath);
178
+ } catch {
179
+ console.log(`[heron-connect] Replacing existing binary with ${VERSION}...`);
180
+ fs.unlinkSync(binaryPath);
181
+ }
182
+ }
183
+
184
+ const urls = getDownloadURLs(filename);
185
+ const data = await download(urls);
186
+
187
+ if (ext === ".tar.gz") {
188
+ extractTarGz(data, binDir, binaryName);
189
+ } else {
190
+ extractZip(data, binDir, binaryName);
191
+ }
192
+
193
+ if (platform !== "windows") {
194
+ fs.chmodSync(binaryPath, 0o755);
195
+ }
196
+
197
+ if (platform === "darwin") {
198
+ try {
199
+ execSync(`xattr -d com.apple.quarantine "${binaryPath}"`, { stdio: "pipe" });
200
+ console.log(`[heron-connect] Removed macOS quarantine attribute`);
201
+ } catch {
202
+ // xattr fails if the attribute doesn't exist, which is fine
203
+ }
204
+ }
205
+
206
+ console.log(`[heron-connect] Installed to ${binaryPath}`);
207
+ }
208
+
209
+ main().catch((err) => {
210
+ console.error(err.message);
211
+ console.error(
212
+ "[heron-connect] Installation failed. You can install manually:\n" +
213
+ ` https://github.com/${GITHUB_REPO}/releases/tag/${VERSION}`
214
+ );
215
+ process.exit(1);
216
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@qinghuangniao/heron-connect",
3
+ "version": "1.0.0",
4
+ "description": "Multi-IM agent bridge connecting AI coding agents to messaging platforms",
5
+ "keywords": [
6
+ "claude-code",
7
+ "ai-coding",
8
+ "feishu",
9
+ "dingtalk",
10
+ "slack",
11
+ "telegram",
12
+ "discord",
13
+ "line",
14
+ "wechat-work",
15
+ "chatbot",
16
+ "bridge",
17
+ "agent"
18
+ ],
19
+ "homepage": "https://github.com/janostudio/heron-connect",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/janostudio/heron-connect.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "janostudio",
26
+ "bin": {
27
+ "heron-connect": "run.js"
28
+ },
29
+ "scripts": {
30
+ "prepare-release": "node release-assets.js build",
31
+ "sync-release": "node release-assets.js ensure",
32
+ "prepublishOnly": "node release-assets.js ensure",
33
+ "postinstall": "node install.js"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "install.js",
40
+ "run.js",
41
+ "release-assets.js",
42
+ "README.md"
43
+ ]
44
+ }
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { execFileSync } = require("child_process");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const PACKAGE = require("./package.json");
10
+
11
+ const REPO = "janostudio/heron-connect";
12
+ const APP = "heron-connect";
13
+ const VERSION = `v${PACKAGE.version}`;
14
+ const ROOT_DIR = path.resolve(__dirname, "..");
15
+ const DIST_DIR = path.join(ROOT_DIR, "dist");
16
+
17
+ const PLATFORMS = [
18
+ ["linux", "amd64"],
19
+ ["linux", "arm64"],
20
+ ["darwin", "amd64"],
21
+ ["darwin", "arm64"],
22
+ ["windows", "amd64"],
23
+ ["windows", "arm64"],
24
+ ];
25
+
26
+ function run(bin, args, opts = {}) {
27
+ return execFileSync(bin, args, {
28
+ cwd: ROOT_DIR,
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ encoding: "utf8",
31
+ ...opts,
32
+ }).trim();
33
+ }
34
+
35
+ function archiveName(goos, goarch) {
36
+ const base = `${APP}-${VERSION}-${goos}-${goarch}`;
37
+ return goos === "windows" ? `${base}.zip` : `${base}.tar.gz`;
38
+ }
39
+
40
+ function expectedArchives() {
41
+ return PLATFORMS.map(([goos, goarch]) => archiveName(goos, goarch));
42
+ }
43
+
44
+ function expectedDistFiles() {
45
+ return [...expectedArchives(), "checksums.txt"];
46
+ }
47
+
48
+ function missingFiles(dir, names) {
49
+ return names.filter((name) => !fs.existsSync(path.join(dir, name)));
50
+ }
51
+
52
+ function buildReleaseAssets() {
53
+ console.log(`[release-assets] Building release archives for ${VERSION}`);
54
+ execFileSync("make", ["release-all", `VERSION=${VERSION}`], {
55
+ cwd: ROOT_DIR,
56
+ stdio: "inherit",
57
+ });
58
+ }
59
+
60
+ function ensureLocalAssets() {
61
+ const missing = missingFiles(DIST_DIR, expectedDistFiles());
62
+ if (missing.length === 0) {
63
+ console.log(`[release-assets] Local release assets already exist in ${DIST_DIR}`);
64
+ return;
65
+ }
66
+
67
+ console.log(`[release-assets] Missing local release assets:`);
68
+ for (const name of missing) console.log(` - ${name}`);
69
+ buildReleaseAssets();
70
+
71
+ const stillMissing = missingFiles(DIST_DIR, expectedDistFiles());
72
+ if (stillMissing.length > 0) {
73
+ throw new Error(
74
+ `Local release assets are still missing after build: ${stillMissing.join(", ")}`
75
+ );
76
+ }
77
+ }
78
+
79
+ function hasGh() {
80
+ try {
81
+ run("gh", ["--version"]);
82
+ return true;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function hasGhAuth() {
89
+ try {
90
+ execFileSync("gh", ["auth", "status"], {
91
+ cwd: ROOT_DIR,
92
+ stdio: "ignore",
93
+ });
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ function releaseInfo() {
101
+ try {
102
+ return JSON.parse(run("gh", ["release", "view", VERSION, "--repo", REPO, "--json", "tagName,assets"]));
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ function ensureReleaseExists() {
109
+ if (releaseInfo()) {
110
+ console.log(`[release-assets] GitHub release ${VERSION} already exists`);
111
+ return;
112
+ }
113
+
114
+ console.log(`[release-assets] Creating GitHub release ${VERSION}`);
115
+ const notes = [
116
+ `Release assets for npm package ${PACKAGE.name}@${PACKAGE.version}.`,
117
+ "",
118
+ "This is an unofficial personal fork build.",
119
+ ].join("\n");
120
+
121
+ execFileSync(
122
+ "gh",
123
+ ["release", "create", VERSION, "--repo", REPO, "--title", VERSION, "--notes", notes],
124
+ { cwd: ROOT_DIR, stdio: "inherit" }
125
+ );
126
+ }
127
+
128
+ function uploadReleaseAssets() {
129
+ const files = expectedDistFiles().map((name) => path.join(DIST_DIR, name));
130
+ console.log(`[release-assets] Uploading release assets to ${REPO}@${VERSION}`);
131
+ execFileSync("gh", ["release", "upload", VERSION, "--repo", REPO, "--clobber", ...files], {
132
+ cwd: ROOT_DIR,
133
+ stdio: "inherit",
134
+ });
135
+ }
136
+
137
+ function ensureRemoteAssets() {
138
+ if (!hasGh()) {
139
+ throw new Error(
140
+ "GitHub CLI `gh` is not installed. Install it or manually upload dist/ archives before npm publish."
141
+ );
142
+ }
143
+ if (!hasGhAuth()) {
144
+ throw new Error(
145
+ "GitHub CLI is not authenticated. Run `gh auth login` before npm publish so release assets can be synced."
146
+ );
147
+ }
148
+
149
+ ensureReleaseExists();
150
+ uploadReleaseAssets();
151
+
152
+ const info = releaseInfo();
153
+ const assetNames = new Set((info && info.assets ? info.assets : []).map((asset) => asset.name));
154
+ const missingRemote = expectedDistFiles().filter((name) => !assetNames.has(name));
155
+ if (missingRemote.length > 0) {
156
+ throw new Error(`Remote release is missing assets after upload: ${missingRemote.join(", ")}`);
157
+ }
158
+
159
+ console.log(`[release-assets] GitHub release ${VERSION} is ready for npm publish`);
160
+ }
161
+
162
+ function printExpectedArchives() {
163
+ for (const name of expectedArchives()) {
164
+ console.log(name);
165
+ }
166
+ }
167
+
168
+ function main() {
169
+ const command = process.argv[2] || "ensure";
170
+
171
+ if (command === "list") {
172
+ printExpectedArchives();
173
+ return;
174
+ }
175
+
176
+ if (command === "build") {
177
+ ensureLocalAssets();
178
+ return;
179
+ }
180
+
181
+ if (command === "ensure") {
182
+ ensureLocalAssets();
183
+ ensureRemoteAssets();
184
+ return;
185
+ }
186
+
187
+ console.error("Usage: node release-assets.js [list|build|ensure]");
188
+ process.exit(1);
189
+ }
190
+
191
+ main();
package/run.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { execFileSync, execSync } = require("child_process");
6
+ const path = require("path");
7
+ const fs = require("fs");
8
+
9
+ const PACKAGE = require("./package.json");
10
+ const EXPECTED_VER = PACKAGE.version; // e.g. "1.1.0-beta.4"
11
+ const NAME = "heron-connect";
12
+ const binDir = path.join(__dirname, "bin");
13
+ const ext = process.platform === "win32" ? ".exe" : "";
14
+ const binaryPath = path.join(binDir, NAME + ext);
15
+
16
+ // parseVersion splits "1.2.3-beta.1" into { nums: [1,2,3], preTag: "beta", preNum: 1 }
17
+ function parseVersion(v) {
18
+ v = v.replace(/^v/, "").trim();
19
+ const [base, ...rest] = v.split("-");
20
+ const nums = base.split(".").map(Number);
21
+ const pre = rest.join("-");
22
+ const m = pre.match(/^([a-zA-Z]+)\.?(\d+)?$/);
23
+ return { nums, preTag: m ? m[1] : pre, preNum: m && m[2] ? parseInt(m[2], 10) : 0, hasPre: pre !== "" };
24
+ }
25
+
26
+ // isNewerOrEqual returns true if installed >= expected
27
+ function isNewerOrEqual(installed, expected) {
28
+ const a = parseVersion(installed);
29
+ const b = parseVersion(expected);
30
+ const len = Math.max(a.nums.length, b.nums.length);
31
+ for (let i = 0; i < len; i++) {
32
+ const av = a.nums[i] || 0;
33
+ const bv = b.nums[i] || 0;
34
+ if (av > bv) return true;
35
+ if (av < bv) return false;
36
+ }
37
+ // Same base: no pre-release >= any pre-release (1.2.3 >= 1.2.3-beta.1)
38
+ if (!a.hasPre && b.hasPre) return true;
39
+ if (a.hasPre && !b.hasPre) return false;
40
+ if (!a.hasPre && !b.hasPre) return true;
41
+ // Both pre-release: compare tag then number (rc > beta, beta.10 > beta.9)
42
+ if (a.preTag !== b.preTag) return a.preTag > b.preTag;
43
+ return a.preNum >= b.preNum;
44
+ }
45
+
46
+ function needsReinstall() {
47
+ if (!fs.existsSync(binaryPath)) return true;
48
+ try {
49
+ const out = execFileSync(binaryPath, ["--version"], { encoding: "utf8", timeout: 5000 });
50
+ if (out.includes(EXPECTED_VER)) return false;
51
+ // Extract version from output (e.g. "heron-connect 1.2.2-beta.1" or "1.2.2-beta.1")
52
+ const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/);
53
+ if (match && isNewerOrEqual(match[1], EXPECTED_VER)) return false;
54
+ return true;
55
+ } catch {
56
+ return true;
57
+ }
58
+ }
59
+
60
+ if (needsReinstall()) {
61
+ console.log(`[heron-connect] Binary missing or outdated, installing v${EXPECTED_VER}...`);
62
+ try {
63
+ execSync("node " + JSON.stringify(path.join(__dirname, "install.js")), {
64
+ stdio: "inherit",
65
+ cwd: __dirname,
66
+ });
67
+ } catch {
68
+ console.error("[heron-connect] Auto-install failed. Run manually: npm uninstall -g @qinghuangniao/heron-connect && npm install -g @qinghuangniao/heron-connect");
69
+ process.exit(1);
70
+ }
71
+ }
72
+
73
+ try {
74
+ execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
75
+ } catch (err) {
76
+ process.exit(err.status || 1);
77
+ }