cc-connect-next 0.1.0-beta.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.
Files changed (4) hide show
  1. package/README.md +30 -0
  2. package/install.js +301 -0
  3. package/package.json +37 -0
  4. package/run.js +77 -0
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # cc-connect-next
2
+
3
+ Bridge local AI coding agents (Claude Code, Cursor, Gemini CLI, Codex) to messaging platforms (Feishu/Lark, DingTalk, Slack, Telegram, Discord, LINE, WeChat Work).
4
+
5
+ Chat with your AI dev assistant from anywhere.
6
+
7
+ ## Install
8
+
9
+ Install the public beta channel:
10
+
11
+ ```bash
12
+ npm install -g cc-connect-next@beta
13
+ ```
14
+
15
+ The installer verifies the matching GitHub Release archive against `checksums.txt` before extraction.
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Create config
21
+ cc-connect-next --version
22
+
23
+ # Edit config.toml, then run
24
+ cc-connect-next
25
+ cc-connect-next --config /path/to/config.toml
26
+ ```
27
+
28
+ ## Documentation
29
+
30
+ See full documentation at: https://github.com/timmyagentic/cc-connect-next
package/install.js ADDED
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { execFileSync } = require("child_process");
6
+ const crypto = require("crypto");
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const https = require("https");
10
+ const http = require("http");
11
+
12
+ const PACKAGE = require("./package.json");
13
+ const VERSION = `v${PACKAGE.version}`;
14
+ const NAME = "cc-connect-next";
15
+
16
+ const GITHUB_REPO = "timmyagentic/cc-connect-next";
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 getChecksumURLs() {
50
+ return [
51
+ `https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/checksums.txt`,
52
+ ];
53
+ }
54
+
55
+ function fetch(url, redirects = 5) {
56
+ return new Promise((resolve, reject) => {
57
+ if (redirects <= 0) return reject(new Error("Too many redirects"));
58
+ const mod = url.startsWith("https") ? https : http;
59
+ mod
60
+ .get(url, { headers: { "User-Agent": "cc-connect-next-npm" } }, (res) => {
61
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
62
+ const redirect = new URL(res.headers.location, url).toString();
63
+ return resolve(fetch(redirect, redirects - 1));
64
+ }
65
+ if (res.statusCode !== 200) {
66
+ res.resume();
67
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
68
+ }
69
+ const chunks = [];
70
+ res.on("data", (c) => chunks.push(c));
71
+ res.on("end", () => resolve(Buffer.concat(chunks)));
72
+ res.on("error", reject);
73
+ })
74
+ .on("error", reject);
75
+ });
76
+ }
77
+
78
+ function parseChecksumManifest(manifest, filename) {
79
+ const matches = [];
80
+ for (const line of String(manifest).split(/\r?\n/)) {
81
+ const match = line.match(/^([0-9a-fA-F]{64})[ \t]+(?:\*)?(.+)$/);
82
+ if (match && match[2] === filename) {
83
+ matches.push(match[1].toLowerCase());
84
+ }
85
+ }
86
+ if (matches.length === 0) {
87
+ throw new Error(`[cc-connect-next] checksums.txt does not contain ${filename}`);
88
+ }
89
+ if (matches.length > 1) {
90
+ throw new Error(`[cc-connect-next] checksums.txt contains more than one entry for ${filename}`);
91
+ }
92
+ return matches[0];
93
+ }
94
+
95
+ function verifyChecksum(data, expected, filename) {
96
+ if (!/^[0-9a-fA-F]{64}$/.test(expected)) {
97
+ throw new Error(`[cc-connect-next] invalid SHA-256 for ${filename}`);
98
+ }
99
+ const actual = crypto.createHash("sha256").update(data).digest("hex");
100
+ const matches = crypto.timingSafeEqual(
101
+ Buffer.from(actual, "hex"),
102
+ Buffer.from(expected, "hex")
103
+ );
104
+ if (!matches) {
105
+ throw new Error(
106
+ `[cc-connect-next] checksum mismatch for ${filename}; refusing to extract the archive`
107
+ );
108
+ }
109
+ return actual;
110
+ }
111
+
112
+ function replaceBinary(stagedPath, binaryPath) {
113
+ const backupPath = `${binaryPath}.previous-${process.pid}`;
114
+ const hadExisting = fs.existsSync(binaryPath);
115
+ if (hadExisting) {
116
+ fs.renameSync(binaryPath, backupPath);
117
+ }
118
+ try {
119
+ fs.renameSync(stagedPath, binaryPath);
120
+ if (hadExisting) {
121
+ fs.rmSync(backupPath, { force: true });
122
+ }
123
+ } catch (err) {
124
+ if (hadExisting && fs.existsSync(backupPath) && !fs.existsSync(binaryPath)) {
125
+ fs.renameSync(backupPath, binaryPath);
126
+ }
127
+ throw err;
128
+ }
129
+ }
130
+
131
+ function validateExtractedBinary(extractDir, archiveBinaryName) {
132
+ const extractedPath = path.join(extractDir, archiveBinaryName);
133
+ const info = fs.lstatSync(extractedPath);
134
+ if (!info.isFile() || info.isSymbolicLink()) {
135
+ throw new Error(`[cc-connect-next] archive entry is not a regular file: ${archiveBinaryName}`);
136
+ }
137
+ return extractedPath;
138
+ }
139
+
140
+ async function download(urls) {
141
+ for (const url of urls) {
142
+ try {
143
+ console.log(`[cc-connect-next] Downloading from ${url}`);
144
+ const data = await fetch(url);
145
+ console.log(`[cc-connect-next] Downloaded ${(data.length / 1024 / 1024).toFixed(1)} MB`);
146
+ return data;
147
+ } catch (err) {
148
+ console.warn(`[cc-connect-next] Failed: ${err.message}, trying next source...`);
149
+ }
150
+ }
151
+ throw new Error(
152
+ `[cc-connect-next] Could not download binary from any source.\n` +
153
+ ` Tried: ${urls.join(", ")}\n` +
154
+ ` You can download manually from https://github.com/${GITHUB_REPO}/releases`
155
+ );
156
+ }
157
+
158
+ function extractTarGz(buffer, destDir, binaryName, archiveBinaryName) {
159
+ const extractDir = fs.mkdtempSync(path.join(destDir, ".extract-"));
160
+ const tmpFile = path.join(extractDir, "archive.tar.gz");
161
+ try {
162
+ fs.writeFileSync(tmpFile, buffer);
163
+ execFileSync("tar", ["xzf", tmpFile, "-C", extractDir], { stdio: "pipe" });
164
+ const extractedPath = validateExtractedBinary(extractDir, archiveBinaryName);
165
+ const stagedPath = path.join(destDir, `.${binaryName}.install-${process.pid}-${crypto.randomBytes(6).toString("hex")}`);
166
+ fs.renameSync(extractedPath, stagedPath);
167
+ replaceBinary(stagedPath, path.join(destDir, binaryName));
168
+ } finally {
169
+ fs.rmSync(extractDir, { recursive: true, force: true });
170
+ }
171
+ }
172
+
173
+ function extractZip(buffer, destDir, binaryName, archiveBinaryName) {
174
+ const extractDir = fs.mkdtempSync(path.join(destDir, ".extract-"));
175
+ const tmpFile = path.join(extractDir, "archive.zip");
176
+ try {
177
+ fs.writeFileSync(tmpFile, buffer);
178
+ try {
179
+ execFileSync("unzip", ["-o", tmpFile, "-d", extractDir], { stdio: "pipe" });
180
+ } catch {
181
+ const quotePowerShell = (value) => value.replace(/'/g, "''");
182
+ execFileSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
183
+ `Expand-Archive -Force -LiteralPath '${quotePowerShell(tmpFile)}' -DestinationPath '${quotePowerShell(extractDir)}'`], {
184
+ stdio: "pipe",
185
+ });
186
+ }
187
+ const extractedPath = validateExtractedBinary(extractDir, archiveBinaryName);
188
+ const stagedPath = path.join(destDir, `.${binaryName}.install-${process.pid}-${crypto.randomBytes(6).toString("hex")}`);
189
+ fs.renameSync(extractedPath, stagedPath);
190
+ replaceBinary(stagedPath, path.join(destDir, binaryName));
191
+ } finally {
192
+ fs.rmSync(extractDir, { recursive: true, force: true });
193
+ }
194
+ }
195
+
196
+ // parseVersion splits "1.2.3-beta.1" into { nums: [1,2,3], preTag: "beta", preNum: 1 }
197
+ function parseVersion(v) {
198
+ v = v.replace(/^v/, "").trim();
199
+ const [base, ...rest] = v.split("-");
200
+ const nums = base.split(".").map(Number);
201
+ const pre = rest.join("-");
202
+ const m = pre.match(/^([a-zA-Z]+)\.?(\d+)?$/);
203
+ return { nums, preTag: m ? m[1] : pre, preNum: m && m[2] ? parseInt(m[2], 10) : 0, hasPre: pre !== "" };
204
+ }
205
+
206
+ // isNewerOrEqual returns true if installed >= expected
207
+ function isNewerOrEqual(installed, expected) {
208
+ const a = parseVersion(installed);
209
+ const b = parseVersion(expected);
210
+ const len = Math.max(a.nums.length, b.nums.length);
211
+ for (let i = 0; i < len; i++) {
212
+ const av = a.nums[i] || 0;
213
+ const bv = b.nums[i] || 0;
214
+ if (av > bv) return true;
215
+ if (av < bv) return false;
216
+ }
217
+ if (!a.hasPre && b.hasPre) return true;
218
+ if (a.hasPre && !b.hasPre) return false;
219
+ if (!a.hasPre && !b.hasPre) return true;
220
+ // Both pre-release: compare tag then number (rc > beta, beta.10 > beta.9)
221
+ if (a.preTag !== b.preTag) return a.preTag > b.preTag;
222
+ return a.preNum >= b.preNum;
223
+ }
224
+
225
+ async function main() {
226
+ const { platform, arch, ext, filename } = getPlatformInfo();
227
+ console.log(`[cc-connect-next] Platform: ${platform}/${arch}`);
228
+
229
+ const binDir = path.join(__dirname, "bin");
230
+ fs.mkdirSync(binDir, { recursive: true });
231
+
232
+ const binaryName = platform === "windows" ? `${NAME}.exe` : NAME;
233
+ const binaryPath = path.join(binDir, binaryName);
234
+
235
+ if (fs.existsSync(binaryPath)) {
236
+ try {
237
+ const out = execFileSync(binaryPath, ["--version"], { encoding: "utf8", timeout: 5000 });
238
+ const expectedVer = VERSION.slice(1); // remove leading "v"
239
+ if (out.includes(expectedVer)) {
240
+ console.log(`[cc-connect-next] Binary ${VERSION} already installed, skipping.`);
241
+ return;
242
+ }
243
+ // Don't downgrade: if existing binary is newer, keep it
244
+ const match = out.match(/(\d+\.\d+\.\d+[^\s]*)/);
245
+ if (match && isNewerOrEqual(match[1], expectedVer)) {
246
+ console.log(`[cc-connect-next] Binary ${match[1]} is newer than ${VERSION}, skipping.`);
247
+ return;
248
+ }
249
+ console.log(`[cc-connect-next] Existing binary is outdated, upgrading to ${VERSION}...`);
250
+ } catch {
251
+ console.log(`[cc-connect-next] Replacing existing binary with ${VERSION}...`);
252
+ }
253
+ }
254
+
255
+ const checksumData = await download(getChecksumURLs());
256
+ const expectedChecksum = parseChecksumManifest(checksumData.toString("utf8"), filename);
257
+ const data = await download(getDownloadURLs(filename));
258
+ verifyChecksum(data, expectedChecksum, filename);
259
+ console.log(`[cc-connect-next] Verified SHA-256 for ${filename}`);
260
+
261
+ const archiveBinaryName = `${NAME}-${VERSION}-${platform}-${arch}${platform === "windows" ? ".exe" : ""}`;
262
+
263
+ if (ext === ".tar.gz") {
264
+ extractTarGz(data, binDir, binaryName, archiveBinaryName);
265
+ } else {
266
+ extractZip(data, binDir, binaryName, archiveBinaryName);
267
+ }
268
+
269
+ if (platform !== "windows") {
270
+ fs.chmodSync(binaryPath, 0o755);
271
+ }
272
+
273
+ if (platform === "darwin") {
274
+ try {
275
+ execFileSync("xattr", ["-d", "com.apple.quarantine", binaryPath], { stdio: "pipe" });
276
+ console.log(`[cc-connect-next] Removed macOS quarantine attribute`);
277
+ } catch {
278
+ // xattr fails if the attribute doesn't exist, which is fine
279
+ }
280
+ }
281
+
282
+ console.log(`[cc-connect-next] Installed to ${binaryPath}`);
283
+ }
284
+
285
+ if (require.main === module) {
286
+ main().catch((err) => {
287
+ console.error(err.message);
288
+ console.error(
289
+ "[cc-connect-next] Installation failed. You can install manually:\n" +
290
+ ` https://github.com/${GITHUB_REPO}/releases/tag/${VERSION}`
291
+ );
292
+ process.exit(1);
293
+ });
294
+ }
295
+
296
+ module.exports = {
297
+ getPlatformInfo,
298
+ isNewerOrEqual,
299
+ parseChecksumManifest,
300
+ verifyChecksum,
301
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "cc-connect-next",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Privacy-first AI agent bridge with native Feishu Card 2.0 streaming",
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
+ ],
18
+ "homepage": "https://github.com/timmyagentic/cc-connect-next",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/timmyagentic/cc-connect-next.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "cc-connect-next contributors",
25
+ "bin": {
26
+ "cc-connect-next": "run.js"
27
+ },
28
+ "scripts": {
29
+ "postinstall": "node install.js",
30
+ "test": "node --test install.test.js"
31
+ },
32
+ "files": [
33
+ "install.js",
34
+ "run.js",
35
+ "README.md"
36
+ ]
37
+ }
package/run.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const { execFileSync } = 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 = "cc-connect-next";
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. "cc-connect-next 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(`[cc-connect-next] Binary missing or outdated, installing v${EXPECTED_VER}...`);
62
+ try {
63
+ execFileSync(process.execPath, [path.join(__dirname, "install.js")], {
64
+ stdio: "inherit",
65
+ cwd: __dirname,
66
+ });
67
+ } catch {
68
+ console.error("[cc-connect-next] Auto-install failed. Run manually: npm uninstall -g cc-connect-next && npm install -g cc-connect-next@beta");
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
+ }