corkcli 0.0.0-stage → 2.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -2
  3. package/bin/cork.js +249 -0
  4. package/package.json +34 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lithometric
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,32 @@
1
- # Temporary Holding Version
1
+ # corkcli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Your coding agents, working as a team. One command installs Cork for every repo on your machine: Claude Code, Codex and Cursor agents in the same repo see each other's changes as they happen, get briefed on the code they are about to touch, and can message each other.
4
+
5
+ ```sh
6
+ npx corkcli
7
+ ```
8
+
9
+ Free, no account, and nothing leaves your machine. Works on macOS, Linux and Windows (PowerShell, cmd or Git Bash).
10
+
11
+ It downloads the `cork` program for your platform, checks it against its SHA-256 and Cork's release signature before running it, installs it into `~/.cork/bin`, connects your agent tools, and adds `cork` to your PATH.
12
+
13
+ ## Commands
14
+
15
+ ```sh
16
+ npx corkcli # install or update
17
+ npx corkcli status # what Cork did on this machine
18
+ npx corkcli login # link this machine to a Cork account
19
+ npx corkcli upgrade # bring in your team
20
+ ```
21
+
22
+ After install, `cork <command>` works directly in a new terminal.
23
+
24
+ ## Installed before the rename
25
+
26
+ Cork was called Collide, and this package was `collide-cli` (and `collidemcp` before that). It still answers as `collide`, `collide-cli` and `collidemcp`, so old instructions keep working. A machine set up then keeps its `~/.collide` folder, its account and its history: nothing is moved, and a `cork` is placed beside its `~/.collide/bin/collide`, so `cork <command>` works there too. Settings read `CORK_*` first, then the `COLLIDE_*` names from before.
27
+
28
+ No Node? `curl -fsSL https://corkcli.com/install.sh | sh`, or with Python, `pipx run cork-sdk install`.
29
+
30
+ Is the service up? On a machine signed in to an account, `cork status` adds one line from [corkcli.com/status](https://corkcli.com/status) (`Cork service: operational`, or what is not); agents can read [status.json](https://corkcli.com/status.json).
31
+
32
+ More at [corkcli.com](https://corkcli.com).
package/bin/cork.js ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ // npx corkcli: Cork for every repo on this machine, in one command.
3
+ // (It also answers as `cork`, and as `collide`, `collide-cli` and
4
+ // `collidemcp`, its names before the rename, so old instructions work.)
5
+ //
6
+ // Downloads the cork program for this platform from Cork's releases,
7
+ // checks it against its SHA-256 and CI's Ed25519 signature before it runs,
8
+ // puts it in ~/.cork/bin, sets it up for Claude Code, Codex and Cursor,
9
+ // and adds it to PATH. The same program the install script installs.
10
+ //
11
+ // A machine set up before the rename keeps its ~/.collide folder and its
12
+ // ~/.collide/bin/collide for as long as no ~/.cork exists beside it (the
13
+ // program picks the same folder, and puts a `cork` beside that `collide`).
14
+ //
15
+ // npx corkcli install (or update) Cork on this machine
16
+ // npx corkcli <command> run a cork command: login, status, upgrade, ...
17
+ // npx corkcli --help what it does, without installing anything
18
+
19
+ "use strict";
20
+
21
+ const crypto = require("node:crypto");
22
+ const fs = require("node:fs");
23
+ const os = require("node:os");
24
+ const path = require("node:path");
25
+ const { spawnSync } = require("node:child_process");
26
+
27
+ /** A setting: CORK_X, else the COLLIDE_X it was called before the rename. */
28
+ const setting = (name) => process.env[`CORK_${name}`] || process.env[`COLLIDE_${name}`] || "";
29
+
30
+ const SERVER = (setting("INSTALL_SERVER") || "https://mcp.corkcli.com").replace(/\/+$/, "");
31
+ const EXE = process.platform === "win32" ? ".exe" : "";
32
+
33
+ /** Where Cork lives on this machine: ~/.cork with `cork`, or, on a machine
34
+ * set up before the rename, ~/.collide with its `collide` (kept, never
35
+ * moved, for as long as no ~/.cork exists beside it). */
36
+ function layout() {
37
+ const home = setting("HOME") || os.homedir();
38
+ const cork = path.join(home, ".cork");
39
+ const collide = path.join(home, ".collide");
40
+ const legacy = !fs.existsSync(cork) && fs.existsSync(collide);
41
+ const dir = legacy ? collide : cork;
42
+ const name = legacy ? "collide" : "cork";
43
+ return { home, dir, legacy, bin: path.join(dir, "bin"), program: path.join(dir, "bin", name + EXE) };
44
+ }
45
+ // CI's release-signing key; the program checks its own updates against the same one
46
+ const SIGNING_KEY = "17092e6d5632783f1d9b08fc134b0343c7e67b4a4b42feb52df6ca295f325ef8";
47
+
48
+ const color = process.stdout.isTTY && !process.env.NO_COLOR;
49
+ const paint = (code) => (text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
50
+ const mint = paint("38;2;52;211;153");
51
+ const dim = paint("2");
52
+ const bold = paint("1");
53
+
54
+ function fail(message) {
55
+ console.error(`Cork: ${message}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ function target() {
60
+ const arch = { arm64: "aarch64", x64: "x86_64" }[process.arch];
61
+ const system = { darwin: "apple-darwin", linux: "unknown-linux-gnu", win32: "pc-windows-msvc" }[process.platform];
62
+ if (!arch || !system) fail(`there is no build for ${process.platform}-${process.arch} yet.`);
63
+ return `${arch}-${system}`;
64
+ }
65
+
66
+ function verifySignature(statement, signature) {
67
+ const der = Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(SIGNING_KEY, "hex")]);
68
+ const key = crypto.createPublicKey({ key: der, format: "der", type: "spki" });
69
+ return crypto.verify(null, Buffer.from(statement), key, Buffer.from(signature, "base64"));
70
+ }
71
+
72
+ function banner(version) {
73
+ if (!process.stdout.isTTY) return;
74
+ const art = [
75
+ " ██████╗ ██████╗ ██████╗ ██╗ ██╗",
76
+ "██╔════╝██╔═══██╗██╔══██╗██║ ██╔╝",
77
+ "██║ ██║ ██║██████╔╝█████╔╝ ",
78
+ "██║ ██║ ██║██╔══██╗██╔═██╗ ",
79
+ "╚██████╗╚██████╔╝██║ ██║██║ ██╗",
80
+ " ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝",
81
+ ];
82
+ console.log("\n" + art.map(mint).join("\n"));
83
+ console.log(` ${dim(`free · v${version} · your agents, working as a team`)}\n`);
84
+ }
85
+
86
+ /** `cork` by name in every new terminal: ~/.cork/bin (or an old install's
87
+ * ~/.collide/bin, where a `cork` sits beside its `collide`) joins PATH once. */
88
+ function addToPath(binDir) {
89
+ if (setting("NO_MODIFY_PATH") === "1") return "";
90
+ const home = os.homedir();
91
+ const rel = path.relative(home, binDir).split(path.sep).join("/");
92
+ // under the home folder as $HOME/..., else (a CORK_HOME elsewhere) as is
93
+ const entry = rel.startsWith("..") || path.isAbsolute(rel) ? binDir : `$HOME/${rel}`;
94
+ if (process.platform === "win32") {
95
+ // the user's own PATH, read and written through PowerShell (setx truncates long values)
96
+ const script =
97
+ `$p=[Environment]::GetEnvironmentVariable('Path','User'); if(-not $p){$p=''}; ` +
98
+ `if(($p -split ';') -notcontains '${binDir}'){[Environment]::SetEnvironmentVariable('Path',($p.TrimEnd(';')+';${binDir}').TrimStart(';'),'User'); 'added'}`;
99
+ const out = spawnSync("powershell", ["-NoProfile", "-Command", script], { encoding: "utf8" });
100
+ return out.stdout && out.stdout.includes("added") ? "your user PATH" : "";
101
+ }
102
+ const shell = process.env.SHELL || "";
103
+ const files = shell.endsWith("/zsh")
104
+ ? [".zshrc"]
105
+ : shell.endsWith("/bash")
106
+ ? process.platform === "darwin" ? [".bashrc", ".bash_profile"] : [".bashrc"]
107
+ : shell.endsWith("/fish")
108
+ ? [".config/fish/conf.d/cork.fish"]
109
+ : [".profile"];
110
+ const added = [];
111
+ for (const name of files) {
112
+ const file = path.join(home, name);
113
+ let current = "";
114
+ try {
115
+ current = fs.readFileSync(file, "utf8");
116
+ } catch {}
117
+ if (current.includes(entry.replace("$HOME/", ""))) continue;
118
+ fs.mkdirSync(path.dirname(file), { recursive: true });
119
+ fs.appendFileSync(
120
+ file,
121
+ name.endsWith(".fish") ? `fish_add_path "${entry}"\n` : `\n# Cork\nexport PATH="${entry}:$PATH"\n`,
122
+ );
123
+ added.push(`~/${name}`);
124
+ }
125
+ return added.join(", ");
126
+ }
127
+
128
+ async function install(args) {
129
+ const triple = target();
130
+ let index;
131
+ try {
132
+ const response = await fetch(`${SERVER}/artifacts/hooks`, { headers: { "user-agent": "corkcli-npm" } });
133
+ if (!response.ok) throw new Error(String(response.status));
134
+ index = await response.json();
135
+ } catch (err) {
136
+ fail(`could not reach Cork's releases (${err.message}). Check your connection and run it again.`);
137
+ }
138
+ // a release whose builds are still on their way: the version before,
139
+ // which every installed copy then updates from by itself
140
+ let version = index.version;
141
+ let build = (index.free_builds || []).find((b) => b.target === triple);
142
+ if (!build && index.previous) {
143
+ build = (index.previous.free_builds || []).find((b) => b.target === triple);
144
+ if (build) version = index.previous.version;
145
+ }
146
+ if (!build) fail(`there is no build for ${triple} in release v${index.version} yet; try again in a few minutes.`);
147
+ banner(version);
148
+
149
+ console.log(` ${dim(`Downloading cork for ${triple}...`)}`);
150
+ const response = await fetch(build.url, { headers: { "user-agent": "corkcli-npm" } });
151
+ if (!response.ok) fail(`the download failed (${response.status}). Run it again.`);
152
+ const bytes = Buffer.from(await response.arrayBuffer());
153
+ const digest = crypto.createHash("sha256").update(bytes).digest("hex");
154
+ if (digest !== build.sha256) fail("the download did not match its checksum, so nothing was installed.");
155
+ // CI signs the free program as `collide`, whatever it is called on the machine
156
+ if (!build.sig || !verifySignature(`collide ${triple} v${version} sha256:${digest}`, build.sig)) {
157
+ fail("the download is not signed by Cork's release key, so nothing was installed.");
158
+ }
159
+ console.log(` ${mint("✓")} Downloaded and verified ${dim("(SHA-256 and signature)")}`);
160
+
161
+ const where = layout();
162
+ const binDir = where.bin;
163
+ fs.mkdirSync(binDir, { recursive: true });
164
+ const program = where.program;
165
+ const staged = `${program}.part`;
166
+ fs.writeFileSync(staged, bytes, { mode: 0o755 });
167
+ // a running copy on Windows cannot be replaced, only renamed aside
168
+ if (fs.existsSync(program)) {
169
+ try {
170
+ fs.renameSync(program, `${program}.old`);
171
+ } catch {}
172
+ }
173
+ fs.renameSync(staged, program);
174
+
175
+ const noLogin = args.includes("--no-login") || setting("NO_LOGIN") === "1";
176
+ const passed = args.filter((a) => a !== "--no-login");
177
+ // the program's own install also places a `cork` beside an old `collide`
178
+ const setup = spawnSync(program, ["install", ...passed], { stdio: "inherit", env: { ...process.env, CORK_STYLE: "1", COLLIDE_STYLE: "1" } });
179
+ if (setup.status !== 0) process.exit(setup.status || 1);
180
+ const added = addToPath(binDir);
181
+ if (added) console.log(` ${mint("✓")} Added to your PATH ${dim(`(${added})`)}`);
182
+
183
+ // one sign-in, in the browser, as part of the install: the dashboard opens
184
+ // there, and this terminal comes back to the front when it is done
185
+ if (!noLogin && process.stdin.isTTY && process.stdout.isTTY && !linked()) {
186
+ console.log(`\n ${bold("Sign in")} ${dim("(opens your browser · free · links this machine to your dashboard)")}`);
187
+ const login = spawnSync(program, ["login"], { stdio: "inherit" });
188
+ focusTerminal();
189
+ if (login.status !== 0) console.log(` ${dim("Not signed in. Any time:")} ${mint("cork login")}`);
190
+ }
191
+
192
+ console.log(`\n ${bold("Next")}`);
193
+ console.log(` ${mint("1")} Open a new terminal, so the ${bold("cork")} command works`);
194
+ console.log(` ${mint("2")} Start or restart your agent sessions: agents in the same repo now see`);
195
+ console.log(" each other's work and can message each other");
196
+ console.log(`\n ${dim("Bring in your team:")} ${mint("cork upgrade")} ${dim("(100 agent hours a month for $20)")}\n`);
197
+ }
198
+
199
+ /** This machine already signed in (`cork login` keeps the account in
200
+ * machine.json): the install leaves it as it is. */
201
+ function linked() {
202
+ try {
203
+ return Boolean(JSON.parse(fs.readFileSync(path.join(layout().dir, "machine.json"), "utf8")).account);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ /** Back to the app the install was run from, once the browser is done with
210
+ * the sign-in. macOS names that app in __CFBundleIdentifier (Terminal,
211
+ * iTerm, Ghostty, VS Code, Cursor...); elsewhere the person switches back. */
212
+ function focusTerminal() {
213
+ const app = process.env.__CFBundleIdentifier;
214
+ if (process.platform === "darwin" && app && /^[\w.-]+$/.test(app)) spawnSync("open", ["-b", app]);
215
+ }
216
+
217
+ function run(args) {
218
+ const where = layout();
219
+ // the `cork` beside an old install's `collide` when it is there yet
220
+ const cork = path.join(where.bin, "cork" + EXE);
221
+ const program = fs.existsSync(cork) ? cork : where.program;
222
+ if (!fs.existsSync(program)) return install([]).then(() => run(args));
223
+ const result = spawnSync(program, args, { stdio: "inherit" });
224
+ process.exit(result.status ?? 1);
225
+ }
226
+
227
+ function help() {
228
+ // the package's name as it was run (`npx collidemcp`, `npx collide-cli` still work)
229
+ const invoked = path.basename(process.argv[1] || "").replace(/\.js$/, "");
230
+ const name = ["collidemcp", "collide-cli"].includes(invoked) ? invoked : "corkcli";
231
+ console.log(`${bold("corkcli")} ${dim(`v${require("../package.json").version}`)}: Cork for every repo on this machine.
232
+
233
+ ${mint(`npx ${name}`)} install or update Cork (checks SHA-256 and signature first)
234
+ ${mint(`npx ${name} --no-login`)} install without the browser sign-in
235
+ ${mint(`npx ${name} <command>`)} run a cork command: status, login, upgrade, ...
236
+ ${mint(`npx ${name} --help`)} this text
237
+
238
+ After the install, ${bold("cork <command>")} works directly in a new terminal.
239
+ More at https://corkcli.com/docs/cli`);
240
+ }
241
+
242
+ const args = process.argv.slice(2);
243
+ if (args[0] === "--help" || args[0] === "-h") {
244
+ help();
245
+ } else if (args.length === 0 || args[0] === "install" || args[0].startsWith("--")) {
246
+ install(args[0] === "install" ? args.slice(1) : args).catch((err) => fail(err.message));
247
+ } else {
248
+ run(args);
249
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "corkcli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.0",
4
+ "description": "Cork: your coding agents (Claude Code, Codex, Cursor) working as a team. One command installs it for every repo on your machine.",
5
+ "bin": {
6
+ "corkcli": "bin/cork.js",
7
+ "cork": "bin/cork.js",
8
+ "collide": "bin/cork.js",
9
+ "collide-cli": "bin/cork.js",
10
+ "collidemcp": "bin/cork.js"
11
+ },
12
+ "files": [
13
+ "bin/cork.js",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": [
21
+ "claude-code",
22
+ "codex",
23
+ "cursor",
24
+ "mcp",
25
+ "ai-agents",
26
+ "multi-agent",
27
+ "collaboration"
28
+ ],
29
+ "homepage": "https://corkcli.com",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/lithometric/cork"
33
+ },
34
+ "license": "MIT",
35
+ "author": "Lithometric"
36
+ }