@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 +41 -0
- package/bin/caura.js +6 -0
- package/lib/launcher.js +58 -0
- package/package.json +28 -0
- package/scripts/install.js +249 -0
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
package/lib/launcher.js
ADDED
|
@@ -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
|
+
};
|