@spuentesp/zoanthrope 0.0.0-stage → 0.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 CHANGED
@@ -1,3 +1,26 @@
1
- # Temporary Holding Version
1
+ # @spuentesp/zoanthrope (npm shim)
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
+ Thin Node launcher for the `zoanthrope` binary. On install it
4
+ downloads the platform binary from the matching GitHub release asset
5
+ (`zoanthrope-{target}.tar.gz`), verifies it against the release's
6
+ `SHA256SUMS`, and caches it per version. Node builtins only — no
7
+ runtime dependencies.
8
+
9
+ ```
10
+ npx @spuentesp/zoanthrope mcp
11
+ zoanthrope claim --files a.rs --agent alpha
12
+ ```
13
+
14
+ Environment knobs:
15
+
16
+ - `ZOANTHROPE_BIN` — use this binary path instead of downloading.
17
+ - `ZOANTHROPE_VERSION` — override the release version to download
18
+ (defaults to this package's version).
19
+ - `ZOANTHROPE_CACHE_DIR` — override the binary cache directory.
20
+
21
+ Prebuilt targets: Linux and macOS, x86_64 and aarch64. Anything else
22
+ (or a failed download) gets a build-from-source message:
23
+ `cargo install --path .` from a clone of the repository.
24
+
25
+ What zoanthrope is, the tool surface, and the quickstart live in the
26
+ repository's top-level `README.md`.
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // Launcher: ensures the platform binary is present (downloaded from
3
+ // the GitHub release with SHA256 verification, or via ZOANTHROPE_BIN)
4
+ // and forwards argv to it.
5
+ 'use strict';
6
+
7
+ const { spawn } = require('child_process');
8
+ const { ensureBinary } = require('../scripts/runtime');
9
+
10
+ ensureBinary()
11
+ .then((binary) => {
12
+ const child = spawn(binary, process.argv.slice(2), {
13
+ cwd: process.cwd(),
14
+ stdio: 'inherit',
15
+ });
16
+ child.on('error', (error) => {
17
+ process.stderr.write(`zoanthrope: failed to start ${binary}: ${error.message}\n`);
18
+ process.exitCode = 1;
19
+ });
20
+ child.on('exit', (code, signal) => {
21
+ if (signal) {
22
+ process.kill(process.pid, signal);
23
+ return;
24
+ }
25
+ process.exitCode = code == null ? 1 : code;
26
+ });
27
+ })
28
+ .catch((error) => {
29
+ process.stderr.write(`zoanthrope: ${error.message}\n`);
30
+ process.exitCode = 1;
31
+ });
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@spuentesp/zoanthrope",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.spuentesp/zoanthrope",
5
+ "description": "Swarm coordination: file claims, agent presence, postboard intentions, filesystem leases, audit. MCP server + hook CLI.",
6
+ "keywords": [
7
+ "mcp",
8
+ "multi-agent",
9
+ "coordination",
10
+ "claims",
11
+ "presence",
12
+ "swarm"
13
+ ],
14
+ "license": "MIT",
15
+ "homepage": "https://github.com/spuentesp/zoanthrope#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/spuentesp/zoanthrope"
19
+ },
20
+ "bin": {
21
+ "zoanthrope": "./bin/zoanthrope.js"
22
+ },
23
+ "files": [
24
+ "bin/",
25
+ "scripts/",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "postinstall": "node scripts/install.js"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // postinstall: fetch and verify the platform binary so the `zoanthrope`
3
+ // bin works immediately after `npm install`.
4
+ 'use strict';
5
+
6
+ const { ensureBinary } = require('./runtime');
7
+
8
+ if (process.env.CI && !process.env.ZOANTHROPE_FORCE_INSTALL) {
9
+ process.stderr.write('Skipping zoanthrope download in CI; set ZOANTHROPE_FORCE_INSTALL=1 to test installation.\n');
10
+ } else if (process.env.npm_config_ignore_scripts === 'true') {
11
+ process.stderr.write('Skipping zoanthrope download because npm scripts are disabled.\n');
12
+ } else {
13
+ ensureBinary().catch((error) => {
14
+ process.stderr.write(`zoanthrope installation failed: ${error.message}\n`);
15
+ process.exitCode = 1;
16
+ });
17
+ }
@@ -0,0 +1,196 @@
1
+ 'use strict';
2
+
3
+ // Download-and-verify installer for the zoanthrope binary.
4
+ // Node builtins only (https, crypto, fs, child_process) — no runtime
5
+ // dependencies. Release assets are `zoanthrope-{target}.tar.gz` with
6
+ // the bare `zoanthrope` binary at the archive root; `SHA256SUMS` in the
7
+ // same release carries one checksum line per tarball.
8
+
9
+ const crypto = require('crypto');
10
+ const fs = require('fs');
11
+ const https = require('https');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const { execFileSync, spawnSync } = require('child_process');
15
+
16
+ const REPOSITORY = 'spuentesp/zoanthrope';
17
+
18
+ const BUILD_FROM_SOURCE =
19
+ 'Build from source instead: git clone https://github.com/spuentesp/zoanthrope.git && ' +
20
+ 'cargo install --path zoanthrope (or set ZOANTHROPE_BIN to an existing binary).';
21
+
22
+ function targetFor(platform = process.platform, arch = process.arch) {
23
+ const targets = {
24
+ 'darwin-arm64': 'aarch64-apple-darwin',
25
+ 'darwin-x64': 'x86_64-apple-darwin',
26
+ 'linux-arm64': 'aarch64-unknown-linux-gnu',
27
+ 'linux-x64': 'x86_64-unknown-linux-gnu',
28
+ };
29
+ const target = targets[`${platform}-${arch}`];
30
+ if (!target) throw new Error(`No prebuilt zoanthrope binary for ${platform}-${arch}. ${BUILD_FROM_SOURCE}`);
31
+ return target;
32
+ }
33
+
34
+ function normalizeVersion(value) {
35
+ const version = String(value || '').replace(/^v/, '');
36
+ if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
37
+ throw new Error(`Invalid ZOANTHROPE_VERSION: ${value}`);
38
+ }
39
+ return version;
40
+ }
41
+
42
+ function selectedVersion(env = process.env) {
43
+ return normalizeVersion(env.ZOANTHROPE_VERSION || require('../package.json').version);
44
+ }
45
+
46
+ function cacheRoot(env = process.env, platform = process.platform) {
47
+ if (env.ZOANTHROPE_CACHE_DIR) return path.resolve(env.ZOANTHROPE_CACHE_DIR);
48
+ if (platform === 'darwin') return path.join(os.homedir(), 'Library', 'Caches', 'zoanthrope');
49
+ return path.join(env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'zoanthrope');
50
+ }
51
+
52
+ function assetName(target) {
53
+ return `zoanthrope-${target}.tar.gz`;
54
+ }
55
+
56
+ function binaryPath(version, target, env = process.env, platform = process.platform) {
57
+ return path.join(cacheRoot(env, platform), normalizeVersion(version), target, platform === 'win32' ? 'zoanthrope.exe' : 'zoanthrope');
58
+ }
59
+
60
+ function releaseUrl(version, name) {
61
+ return `https://github.com/${REPOSITORY}/releases/download/v${normalizeVersion(version)}/${name}`;
62
+ }
63
+
64
+ function parseChecksum(text, expectedAsset) {
65
+ for (const line of text.split(/\r?\n/)) {
66
+ const match = line.trim().match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
67
+ if (match && match[2] === expectedAsset) return match[1].toLowerCase();
68
+ }
69
+ throw new Error(`SHA256SUMS has no checksum for ${expectedAsset}`);
70
+ }
71
+
72
+ function sha256(file) {
73
+ const hash = crypto.createHash('sha256');
74
+ hash.update(fs.readFileSync(file));
75
+ return hash.digest('hex');
76
+ }
77
+
78
+ function verifyArchive(file, expected) {
79
+ const actual = sha256(file);
80
+ if (actual !== expected.toLowerCase()) {
81
+ throw new Error(`Checksum mismatch for ${path.basename(file)}: expected ${expected}, got ${actual}`);
82
+ }
83
+ }
84
+
85
+ function verifyBinary(file, version) {
86
+ const result = spawnSync(file, ['--version'], { encoding: 'utf8', timeout: 10_000 });
87
+ if (result.error) throw new Error(`Cannot run zoanthrope binary: ${result.error.message}`);
88
+ if (result.status !== 0) throw new Error(`zoanthrope binary exited ${result.status} during version check`);
89
+ const reported = String(result.stdout).trim();
90
+ const expected = `zoanthrope ${normalizeVersion(version)}`;
91
+ if (reported !== expected) {
92
+ throw new Error(`Downloaded binary reports ${JSON.stringify(reported)}, expected ${JSON.stringify(expected)}`);
93
+ }
94
+ }
95
+
96
+ function download(url, destination, redirects = 5) {
97
+ return new Promise((resolve, reject) => {
98
+ const request = https.get(url, { headers: { 'User-Agent': '@spuentesp/zoanthrope' } }, (response) => {
99
+ if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
100
+ response.resume();
101
+ if (redirects === 0) return reject(new Error(`Too many redirects downloading ${url}`));
102
+ return download(response.headers.location, destination, redirects - 1).then(resolve, reject);
103
+ }
104
+ if (response.statusCode !== 200) {
105
+ response.resume();
106
+ return reject(new Error(`HTTP ${response.statusCode} downloading ${url}`));
107
+ }
108
+ const output = fs.createWriteStream(destination, { flags: 'wx' });
109
+ let received = 0;
110
+ const total = Number(response.headers['content-length']) || 0;
111
+ response.on('data', (chunk) => {
112
+ received += chunk.length;
113
+ if (process.stderr.isTTY && total) {
114
+ process.stderr.write(`\rDownloading ${path.basename(destination)} ${Math.floor(received * 100 / total)}%`);
115
+ }
116
+ });
117
+ response.pipe(output);
118
+ output.on('finish', () => output.close(() => {
119
+ if (process.stderr.isTTY && total) process.stderr.write('\n');
120
+ resolve();
121
+ }));
122
+ output.on('error', reject);
123
+ response.on('error', reject);
124
+ });
125
+ request.setTimeout(30_000, () => request.destroy(new Error(`Download timed out: ${url}`)));
126
+ request.on('error', reject);
127
+ });
128
+ }
129
+
130
+ async function ensureBinary(options = {}) {
131
+ const env = options.env || process.env;
132
+ const platform = options.platform || process.platform;
133
+ const arch = options.arch || process.arch;
134
+ // Explicit override wins: no download, no version gate (the caller
135
+ // owns that binary). Keeps local development and CI smoke tests easy.
136
+ if (env.ZOANTHROPE_BIN) return path.resolve(env.ZOANTHROPE_BIN);
137
+ const version = normalizeVersion(options.version || selectedVersion(env));
138
+ const target = targetFor(platform, arch);
139
+ const finalBinary = binaryPath(version, target, env, platform);
140
+ if (fs.existsSync(finalBinary)) {
141
+ verifyBinary(finalBinary, version);
142
+ return finalBinary;
143
+ }
144
+ const directory = path.dirname(finalBinary);
145
+ fs.mkdirSync(directory, { recursive: true });
146
+ const temporary = path.join(directory, `.install-${process.pid}-${crypto.randomBytes(6).toString('hex')}`);
147
+ fs.mkdirSync(temporary);
148
+ const asset = assetName(target);
149
+ const archive = path.join(temporary, asset);
150
+ const sums = path.join(temporary, 'SHA256SUMS');
151
+ try {
152
+ const fetch = options.download || download;
153
+ await fetch(releaseUrl(version, 'SHA256SUMS'), sums);
154
+ await fetch(releaseUrl(version, asset), archive);
155
+ verifyArchive(archive, parseChecksum(fs.readFileSync(sums, 'utf8'), asset));
156
+ execFileSync('tar', ['xzf', archive, '-C', temporary], { stdio: ['ignore', 'ignore', 'inherit'] });
157
+ const extracted = path.join(temporary, 'zoanthrope');
158
+ if (!fs.existsSync(extracted)) throw new Error(`${asset} does not contain the zoanthrope executable at its root`);
159
+ if (platform !== 'win32') fs.chmodSync(extracted, 0o755);
160
+ verifyBinary(extracted, version);
161
+ try {
162
+ fs.renameSync(extracted, finalBinary);
163
+ } catch (error) {
164
+ // A concurrent npm install of the same version may have won the
165
+ // race; the versioned cache directory keeps releases apart.
166
+ if (!fs.existsSync(finalBinary)) throw error;
167
+ verifyBinary(finalBinary, version);
168
+ }
169
+ return finalBinary;
170
+ } catch (error) {
171
+ if (error && /^HTTP 404/.test(error.message || '')) {
172
+ throw new Error(
173
+ `No release asset for zoanthrope v${version} (${target}). ${BUILD_FROM_SOURCE}`
174
+ );
175
+ }
176
+ throw error;
177
+ } finally {
178
+ fs.rmSync(temporary, { recursive: true, force: true });
179
+ }
180
+ }
181
+
182
+ module.exports = {
183
+ BUILD_FROM_SOURCE,
184
+ assetName,
185
+ binaryPath,
186
+ cacheRoot,
187
+ ensureBinary,
188
+ normalizeVersion,
189
+ parseChecksum,
190
+ releaseUrl,
191
+ selectedVersion,
192
+ sha256,
193
+ targetFor,
194
+ verifyArchive,
195
+ verifyBinary,
196
+ };