@caura-ai/caura 0.10.2

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,41 @@
1
+ # `@caura-ai/caura` — npx / npm wrapper
2
+
3
+ Install and run the Caura broker from the Node ecosystem:
4
+
5
+ ```bash
6
+ npx @caura-ai/caura --help # run without a global install
7
+ npm install -g @caura-ai/caura # or install the `caura` command globally
8
+ ```
9
+
10
+ ## How it works
11
+
12
+ This package is a thin dispatcher — the broker itself is a single Go binary.
13
+
14
+ - On install (`postinstall`, [`scripts/install.js`](scripts/install.js)) it
15
+ detects your platform, downloads the matching release archive from the public
16
+ release mirror, verifies its SHA-256 against the published `checksums.txt`,
17
+ and copies the canonical root payload into `bin/`. Pre-bridge archives fall
18
+ back to their compatibility payload.
19
+ - [`bin/caura.js`](bin/caura.js) uses the shared launcher to execute that binary
20
+ with forwarded arguments and a canonical invocation name.
21
+
22
+ The binary is downloaded from the public release mirror
23
+ (`https://memclaw.net/memclaw/<tag>/`) — the same source the native installer <!-- legacy-name-floor: published mirror path -->
24
+ uses, not this repository's private GitHub Releases. SHA-256 verification is
25
+ the integrity gate.
26
+
27
+ ## Environment overrides
28
+
29
+ Both are also read under their historical `MEMCLAW_*` spellings<!-- legacy-name-ok: rule 3 dual-read alias -->, which keep
30
+ working indefinitely; the `CAURA_*` name wins when it holds a non-empty value.
31
+
32
+ - `CAURA_BINARY_BASE` — base URL of a release mirror (default
33
+ `https://memclaw.net/memclaw`). Point it at an on-prem gateway's <!-- legacy-name-floor: published mirror path -->
34
+ `<origin>/memclaw` to install from a private deployment. <!-- legacy-name-floor: served mirror path -->
35
+ - `CAURA_VERSION` — pin a specific release tag instead of the installed
36
+ package's version.
37
+
38
+ The published package version is set from the release tag by the canonical npm
39
+ workflow; the `0.0.0` in `package.json` is a local-checkout placeholder.
40
+
41
+ Licensed under Apache-2.0, matching the `LICENSE` at the repository root.
package/bin/caura.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { invocationCommand, launch } = require('../lib/launcher.js');
5
+
6
+ process.exit(launch(invocationCommand(process.argv[1])));
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const CANONICAL_COMMAND = 'caura';
8
+ const LEGACY_COMMAND = 'memclaw'; // legacy-name-ok: permanent npm compatibility command
9
+ const CANONICAL_PACKAGE = '@caura-ai/caura';
10
+ const LEGACY_PACKAGE = '@caura-ai/memclawd'; // legacy-name-ok: permanent npm compatibility alias
11
+
12
+ function invocationCommand(executable) {
13
+ let name = String(executable || '').split(/[\\/]/).pop().toLowerCase();
14
+ for (const suffix of ['.js', '.cmd', '.exe']) {
15
+ if (name.endsWith(suffix)) name = name.slice(0, -suffix.length);
16
+ }
17
+ return name === LEGACY_COMMAND ? LEGACY_COMMAND : CANONICAL_COMMAND;
18
+ }
19
+
20
+ function installPackage(command) {
21
+ return command === LEGACY_COMMAND ? LEGACY_PACKAGE : CANONICAL_PACKAGE;
22
+ }
23
+
24
+ function launch(command, args = process.argv.slice(2), runtime = {}) {
25
+ const platform = runtime.platform || process.platform;
26
+ const existsSync = runtime.existsSync || fs.existsSync;
27
+ const run = runtime.spawnSync || spawnSync;
28
+ const report = runtime.report || ((message) => console.error(message));
29
+ const suffix = platform === 'win32' ? '.exe' : '';
30
+ const binPath = path.join(__dirname, '..', 'bin', `${CANONICAL_COMMAND}${suffix}`);
31
+
32
+ if (!existsSync(binPath)) {
33
+ report(
34
+ `${command}: binary not found at ${binPath}. ` +
35
+ `The postinstall hook may have failed; reinstall via ` +
36
+ `'npm install -g ${installPackage(command)}' or download manually from ` +
37
+ `https://github.com/caura-ai/caura-daemon/releases.`,
38
+ );
39
+ return 1;
40
+ }
41
+
42
+ const result = run(binPath, args, {
43
+ stdio: 'inherit',
44
+ argv0: command,
45
+ });
46
+ if (result.error) {
47
+ report(`${command}: ${result.error.message || String(result.error)}`);
48
+ return 1;
49
+ }
50
+ return result.status === null ? 1 : result.status;
51
+ }
52
+
53
+ module.exports = {
54
+ CANONICAL_COMMAND,
55
+ LEGACY_COMMAND,
56
+ invocationCommand,
57
+ launch,
58
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@caura-ai/caura",
3
+ "version": "0.10.2",
4
+ "description": "npx/npm wrapper for the Caura broker: downloads the platform binary from the public release mirror, SHA-256-verifies it, and executes it.",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://caura.ai",
7
+ "bin": {
8
+ "caura": "bin/caura.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node scripts/install.js",
12
+ "test": "node --test test/*.test.js"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "files": [
18
+ "bin/caura.js",
19
+ "lib/launcher.js",
20
+ "scripts/install.js",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/caura-ai/caura-daemon.git",
26
+ "directory": "wrappers/npx"
27
+ }
28
+ }
@@ -0,0 +1,249 @@
1
+ // Caura broker — npx wrapper postinstall (T124).
2
+ //
3
+ // Downloads the platform-specific signed binary from the public
4
+ // release mirror and verifies SHA-256 against the published
5
+ // checksums.txt. Same source + security posture as the
6
+ // curl-pipe-shell installer at wrappers/curl/install.sh —
7
+ // SHA-256 verification is the integrity gate.
8
+ //
9
+ // Runs as `npm install` / `npx` postinstall. On Windows
10
+ // installs the .exe; on Unix-ish OSes installs the
11
+ // unsuffixed binary.
12
+ //
13
+ // What this script does NOT do:
14
+ // - Auto-start the daemon.
15
+ // - Modify shell rc files (the binary lives in
16
+ // node_modules/@caura-ai/caura/bin/; `npx @caura-ai/caura`
17
+ // execs it directly).
18
+ // - Verify GPG signatures. The release pipeline
19
+ // publishes signed binaries (T116/T117/T118 once cert
20
+ // procurement lands) — npm's own provenance can also
21
+ // gate the wrapper itself; per-binary signatures are
22
+ // additive.
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const https = require('https');
29
+ const crypto = require('crypto');
30
+ const { pipeline } = require('stream/promises');
31
+ const { spawnSync } = require('child_process');
32
+
33
+ const CANONICAL_COMMAND = 'caura';
34
+ const LEGACY_COMMAND = 'memclaw'; // legacy-name-ok: pre-bridge archive payload
35
+
36
+ // Binaries come from the public release mirror, NOT this repo's GitHub
37
+ // Releases: the repo is private, so unauthenticated downloads 404. The
38
+ // mirror layout matches wrappers/curl/install.sh — <base>/<tag>/<archive>
39
+ // plus <base>/latest.txt — and is owned by publish-mirror.yml. On-prem
40
+ // gateways serve their own mirror at <origin>/memclaw; point
41
+ // CAURA_BINARY_BASE there to install from a private deployment.
42
+ //
43
+ // Both spellings are read, CAURA_* first, and `||` picks the first
44
+ // NON-EMPTY one rather than the first defined one — an operator whose
45
+ // environment carries a blank CAURA_BINARY_BASE beside a working
46
+ // old-spelling one would otherwise silently fetch from the public
47
+ // mirror instead of their private deployment. Same contract as
48
+ // wrappers/curl/install.sh and the uvx wrapper, which is the point: an
49
+ // operator sets one name and every entry point honors it.
50
+ const BINARY_BASE = (process.env.CAURA_BINARY_BASE || process.env.MEMCLAW_BINARY_BASE || 'https://memclaw.net/memclaw').replace(/\/+$/, ''); // legacy-name-ok: rule 3 dual-read alias
51
+ const VERSION = (process.env.CAURA_VERSION || process.env.MEMCLAW_VERSION || readPackageVersion()).replace(/^v/, ''); // legacy-name-ok: rule 3 dual-read alias
52
+ const BIN_DIR = path.join(__dirname, '..', 'bin');
53
+
54
+ // Map node's `process.platform` / `process.arch` to the
55
+ // goreleaser archive naming.
56
+ function mapPlatform() {
57
+ const platform = process.platform;
58
+ const arch = process.arch;
59
+ switch (platform) {
60
+ case 'darwin':
61
+ // The npm wrapper installs per-arch binaries (not
62
+ // universal) so the postinstall payload is smaller.
63
+ return { os: 'darwin', arch: arch === 'arm64' ? 'arm64' : 'x86_64', exe: false };
64
+ case 'linux':
65
+ return { os: 'linux', arch: arch === 'arm64' ? 'arm64' : 'x86_64', exe: false };
66
+ case 'win32':
67
+ return { os: 'windows', arch: arch === 'arm64' ? 'arm64' : 'x86_64', exe: true };
68
+ default:
69
+ die(`unsupported platform: ${platform}`);
70
+ }
71
+ }
72
+
73
+ function die(msg) {
74
+ console.error(`${CANONICAL_COMMAND} postinstall: ${msg}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ function readPackageVersion() {
79
+ const pkg = require('../package.json');
80
+ // The wrapper's npm version is derived from the broker's
81
+ // git tag at publish time (see publish-npm.yml). Strip any
82
+ // leading "v" — npm versions don't carry it but the
83
+ // GitHub Release tag does.
84
+ return pkg.version.replace(/^v/, '');
85
+ }
86
+
87
+ function archiveName({ os, arch, exe }) {
88
+ const v = VERSION;
89
+ if (exe) {
90
+ return `memclaw_${v}_${os}_${arch}.zip`;
91
+ }
92
+ return `memclaw_${v}_${os}_${arch}.tar.gz`;
93
+ }
94
+
95
+ function rootMemberName(name) {
96
+ while (name.startsWith('./')) name = name.slice(2);
97
+ if (!name || name.includes('/') || name.includes('\\')) return null;
98
+ return name;
99
+ }
100
+
101
+ function selectRootMember(members, exe) {
102
+ const suffix = exe ? '.exe' : '';
103
+ const candidates = [`${CANONICAL_COMMAND}${suffix}`, `${LEGACY_COMMAND}${suffix}`];
104
+ const matches = new Map();
105
+
106
+ for (const member of members) {
107
+ const normalized = rootMemberName(member);
108
+ if (!candidates.includes(normalized)) continue;
109
+ if (matches.has(normalized)) {
110
+ throw new Error(`archive contains duplicate '${normalized}' entries`);
111
+ }
112
+ matches.set(normalized, member);
113
+ }
114
+
115
+ for (const candidate of candidates) {
116
+ if (matches.has(candidate)) return { archiveName: matches.get(candidate), candidate };
117
+ }
118
+ throw new Error(
119
+ `archive did not contain a canonical or pre-bridge root binary: ${candidates.join(', ')}`,
120
+ );
121
+ }
122
+
123
+ function runTar(args, capture = false) {
124
+ const options = capture
125
+ ? { encoding: 'utf8', maxBuffer: 1024 * 1024 }
126
+ : { stdio: 'inherit' };
127
+ const result = spawnSync('tar', args, options);
128
+ if (result.error) throw result.error;
129
+ if (result.status !== 0) {
130
+ const detail = capture && result.stderr ? `: ${result.stderr.trim()}` : '';
131
+ throw new Error(`tar ${args[0]} failed with exit code ${result.status}${detail}`);
132
+ }
133
+ return capture ? result.stdout : '';
134
+ }
135
+
136
+ function extractPayload(archivePath, binDir, exe) {
137
+ const members = runTar(['-tf', archivePath], true).split(/\r?\n/).filter(Boolean);
138
+ const selected = selectRootMember(members, exe);
139
+ const tempDir = fs.mkdtempSync(path.join(binDir, '.caura-extract-'));
140
+
141
+ try {
142
+ runTar(['-xf', archivePath, '-C', tempDir, selected.archiveName]);
143
+ const extracted = path.join(tempDir, selected.candidate);
144
+ const info = fs.lstatSync(extracted);
145
+ if (!info.isFile() || info.isSymbolicLink()) {
146
+ throw new Error(`archive member '${selected.archiveName}' is not a regular file`);
147
+ }
148
+
149
+ const destination = path.join(binDir, `${CANONICAL_COMMAND}${exe ? '.exe' : ''}`);
150
+ fs.copyFileSync(extracted, destination);
151
+ if (!exe) fs.chmodSync(destination, 0o755);
152
+ return destination;
153
+ } finally {
154
+ fs.rmSync(tempDir, { recursive: true, force: true });
155
+ }
156
+ }
157
+
158
+ function fetchToFile(url, dest) {
159
+ return new Promise((resolve, reject) => {
160
+ https
161
+ .get(url, { headers: { 'User-Agent': 'caura-npx-postinstall' } }, (res) => {
162
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
163
+ return fetchToFile(res.headers.location, dest).then(resolve, reject);
164
+ }
165
+ if (res.statusCode !== 200) {
166
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
167
+ }
168
+ const out = fs.createWriteStream(dest);
169
+ pipeline(res, out).then(resolve, reject);
170
+ })
171
+ .on('error', reject);
172
+ });
173
+ }
174
+
175
+ async function fetchText(url) {
176
+ return new Promise((resolve, reject) => {
177
+ https
178
+ .get(url, { headers: { 'User-Agent': 'caura-npx-postinstall' } }, (res) => {
179
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
180
+ return fetchText(res.headers.location).then(resolve, reject);
181
+ }
182
+ if (res.statusCode !== 200) {
183
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
184
+ }
185
+ let buf = '';
186
+ res.setEncoding('utf8');
187
+ res.on('data', (chunk) => (buf += chunk));
188
+ res.on('end', () => resolve(buf));
189
+ })
190
+ .on('error', reject);
191
+ });
192
+ }
193
+
194
+ function sha256(filePath) {
195
+ return new Promise((resolve, reject) => {
196
+ const h = crypto.createHash('sha256');
197
+ const s = fs.createReadStream(filePath);
198
+ s.on('error', reject);
199
+ s.on('end', () => resolve(h.digest('hex')));
200
+ s.pipe(h);
201
+ });
202
+ }
203
+
204
+ async function main() {
205
+ const plat = mapPlatform();
206
+ const archive = archiveName(plat);
207
+ // The tag directory carries a leading "v" (VERSION is stripped of
208
+ // it); archive names do not.
209
+ const base = `${BINARY_BASE}/v${VERSION}`;
210
+ const archiveURL = `${base}/${archive}`;
211
+ const checksumsURL = `${base}/checksums.txt`;
212
+
213
+ if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true });
214
+ const archivePath = path.join(BIN_DIR, archive);
215
+
216
+ console.log(`${CANONICAL_COMMAND} postinstall: downloading ${archiveURL}`);
217
+ await fetchToFile(archiveURL, archivePath);
218
+
219
+ console.log(`${CANONICAL_COMMAND} postinstall: downloading checksums`);
220
+ const checksums = await fetchText(checksumsURL);
221
+ const expectedLine = checksums.split('\n').find((l) => l.endsWith(` ${archive}`));
222
+ if (!expectedLine) die(`no checksum entry for ${archive}`);
223
+ const expected = expectedLine.split(/\s+/)[0];
224
+
225
+ console.log(`${CANONICAL_COMMAND} postinstall: verifying SHA-256`);
226
+ const actual = await sha256(archivePath);
227
+ if (actual !== expected) {
228
+ die(`checksum mismatch for ${archive}: expected=${expected} actual=${actual}`);
229
+ }
230
+
231
+ console.log(`${CANONICAL_COMMAND} postinstall: extracting`);
232
+ let binPath;
233
+ try {
234
+ binPath = extractPayload(archivePath, BIN_DIR, plat.exe);
235
+ } finally {
236
+ fs.rmSync(archivePath, { force: true });
237
+ }
238
+ console.log(`${CANONICAL_COMMAND} postinstall: installed ${binPath}`);
239
+ }
240
+
241
+ if (require.main === module) {
242
+ main().catch((err) => die(err.message || String(err)));
243
+ }
244
+
245
+ module.exports = {
246
+ extractPayload,
247
+ rootMemberName,
248
+ selectRootMember,
249
+ };