agentworth 0.1.1 → 0.1.5

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 (3) hide show
  1. package/README.md +6 -10
  2. package/lib/resolver.js +274 -36
  3. package/package.json +10 -2
package/README.md CHANGED
@@ -89,16 +89,12 @@ The launcher searches for the native binary in the following priority order:
89
89
 
90
90
  You can also install the native AgentWorth binary directly:
91
91
 
92
- ```bash
93
- # Via Standalone Installer
94
- curl -fsSL https://agentworth.dev/install.sh | sh
95
-
96
- # Via Homebrew
97
- brew install unfoundbox/tap/agentworth
98
-
99
- # Via Cargo
100
- cargo install agentworth-cli
101
- ```
92
+ | Method | Command | Description |
93
+ | :--- | :--- | :--- |
94
+ | **Standalone Script** | `curl -fsSL https://agentworth.dev/install.sh | sh` | Installs the pre-built native binary directly to `~/.local/bin`. |
95
+ | **Homebrew** | `brew install unfoundbox-crew/tap/agentworth` | Installs via official Homebrew tap. |
96
+ | **Cargo (Native)** | `cargo install agentworth-cli` | Compiles and installs `agentworth` & `agwt` to `~/.cargo/bin`. |
97
+ | **NPX (Instant)** | `npx agentworth` | Zero-install runner that detects or downloads the native binary. |
102
98
 
103
99
  ---
104
100
 
package/lib/resolver.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { spawnSync } from 'node:child_process';
4
+ import https from 'node:https';
5
+ import zlib from 'node:zlib';
6
+ import { spawnSync, execFileSync } from 'node:child_process';
5
7
  import { fileURLToPath } from 'node:url';
6
8
 
7
9
  const __filename = fileURLToPath(import.meta.url);
@@ -18,6 +20,54 @@ export function getPlatformKey(platform = process.platform, arch = process.arch)
18
20
  return `${platform}-${arch}`;
19
21
  }
20
22
 
23
+ /**
24
+ * Maps Node platform and arch to Rust release target triple.
25
+ *
26
+ * @param {string} [platform=process.platform]
27
+ * @param {string} [arch=process.arch]
28
+ * @returns {string | null}
29
+ */
30
+ export function getTargetTriple(platform = process.platform, arch = process.arch) {
31
+ if (platform === 'darwin' && arch === 'arm64') return 'aarch64-apple-darwin';
32
+ if (platform === 'darwin' && arch === 'x64') return 'x86_64-apple-darwin';
33
+ if (platform === 'linux' && arch === 'x64') return 'x86_64-unknown-linux-gnu';
34
+ if (platform === 'linux' && arch === 'arm64') return 'aarch64-unknown-linux-gnu';
35
+ if (platform === 'win32' && arch === 'x64') return 'x86_64-pc-windows-msvc';
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * Reads the package version from package.json.
41
+ *
42
+ * @param {string} [baseDir=__dirname]
43
+ * @returns {string}
44
+ */
45
+ export function getPackageVersion(baseDir = __dirname) {
46
+ try {
47
+ const pkgPath = path.resolve(baseDir, '..', 'package.json');
48
+ if (fs.existsSync(pkgPath)) {
49
+ const data = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
50
+ return data.version || '0.1.3';
51
+ }
52
+ } catch {
53
+ // fallback
54
+ }
55
+ return '0.1.3';
56
+ }
57
+
58
+ /**
59
+ * Returns the local cache directory for AgentWorth binaries (~/.agentworth/bin/v{version}/).
60
+ *
61
+ * @param {string} [version]
62
+ * @param {string} [homeDir]
63
+ * @returns {string}
64
+ */
65
+ export function getCacheDir(version, homeDir) {
66
+ const v = version || getPackageVersion();
67
+ const home = homeDir || os.homedir();
68
+ return path.join(home, '.agentworth', 'bin', `v${v}`);
69
+ }
70
+
21
71
  /**
22
72
  * Returns the expected native binary name for the target platform.
23
73
  *
@@ -95,6 +145,117 @@ export function findCargoTargetBinary(startDir, binName = getBinaryName()) {
95
145
  return null;
96
146
  }
97
147
 
148
+ /**
149
+ * Downloads a file following HTTP redirects.
150
+ *
151
+ * @param {string} url
152
+ * @param {string} destPath
153
+ * @param {number} [redirects=5]
154
+ * @returns {Promise<void>}
155
+ */
156
+ export function downloadFile(url, destPath, redirects = 5) {
157
+ return new Promise((resolve, reject) => {
158
+ if (redirects < 0) {
159
+ return reject(new Error('Too many redirects while downloading binary.'));
160
+ }
161
+
162
+ const request = https.get(url, { headers: { 'User-Agent': 'agentworth-npm-resolver' } }, (res) => {
163
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
164
+ return downloadFile(res.headers.location, destPath, redirects - 1).then(resolve, reject);
165
+ }
166
+
167
+ if (res.statusCode !== 200) {
168
+ return reject(new Error(`Failed to download binary: HTTP ${res.statusCode} from ${url}`));
169
+ }
170
+
171
+ const fileStream = fs.createWriteStream(destPath);
172
+ res.pipe(fileStream);
173
+
174
+ fileStream.on('finish', () => {
175
+ fileStream.close(resolve);
176
+ });
177
+
178
+ fileStream.on('error', (err) => {
179
+ fs.unlink(destPath, () => reject(err));
180
+ });
181
+ });
182
+
183
+ request.on('error', (err) => {
184
+ reject(err);
185
+ });
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Downloads and extracts the precompiled native binary from GitHub Releases into the user's local cache.
191
+ *
192
+ * @param {Object} [options={}]
193
+ * @param {string} [options.platform=process.platform]
194
+ * @param {string} [options.arch=process.arch]
195
+ * @param {string} [options.version]
196
+ * @param {string} [options.homeDir]
197
+ * @param {boolean} [options.silent=false]
198
+ * @returns {Promise<string>} Path to extracted binary
199
+ */
200
+ export async function downloadAndExtractBinary(options = {}) {
201
+ const platform = options.platform || process.platform;
202
+ const arch = options.arch || process.arch;
203
+ const version = options.version || getPackageVersion();
204
+ const targetTriple = getTargetTriple(platform, arch);
205
+ const binName = getBinaryName(platform);
206
+
207
+ if (!targetTriple) {
208
+ throw new Error(`Unsupported platform/architecture: ${platform}-${arch}`);
209
+ }
210
+
211
+ const cacheDir = getCacheDir(version, options.homeDir);
212
+ const cachedBinary = path.join(cacheDir, binName);
213
+
214
+ if (isExecutable(cachedBinary)) {
215
+ return cachedBinary;
216
+ }
217
+
218
+ fs.mkdirSync(cacheDir, { recursive: true });
219
+
220
+ const archiveName = `agentworth-v${version}-${targetTriple}.tar.gz`;
221
+ const url = `https://github.com/unfoundbox-crew/agentworth/releases/download/v${version}/${archiveName}`;
222
+
223
+ if (!options.silent) {
224
+ console.error(`\x1b[36m⚡ AgentWorth native binary not found locally. Downloading v${version} for ${platform}-${arch}...\x1b[0m`);
225
+ }
226
+
227
+ const archivePath = path.join(cacheDir, archiveName);
228
+
229
+ await downloadFile(url, archivePath);
230
+
231
+ // Extract archive
232
+ try {
233
+ execFileSync('tar', ['-xzf', archivePath, '-C', cacheDir]);
234
+ } catch (err) {
235
+ throw new Error(`Failed to extract ${archiveName}: ${err.message}`);
236
+ } finally {
237
+ try {
238
+ if (fs.existsSync(archivePath)) {
239
+ fs.unlinkSync(archivePath);
240
+ }
241
+ } catch {}
242
+ }
243
+
244
+ if (process.platform !== 'win32') {
245
+ fs.chmodSync(cachedBinary, 0o755);
246
+ }
247
+
248
+ if (!isExecutable(cachedBinary)) {
249
+ throw new Error(`Downloaded binary is not executable: ${cachedBinary}`);
250
+ }
251
+
252
+ if (!options.silent) {
253
+ console.error(`\x1b[32m✔ Successfully installed AgentWorth native binary to ${cachedBinary}\x1b[0m\n`);
254
+ }
255
+
256
+ return cachedBinary;
257
+ }
258
+
98
259
  /**
99
260
  * Searches for the agentworth binary in the system PATH.
100
261
  *
@@ -103,6 +264,36 @@ export function findCargoTargetBinary(startDir, binName = getBinaryName()) {
103
264
  * @param {string} [currentScriptPath]
104
265
  * @returns {string | null}
105
266
  */
267
+ /**
268
+ * True when `candidate` and `selfPath` are the same file, following symlinks.
269
+ */
270
+ export function isSelf(candidate, selfPath) {
271
+ if (!selfPath) return false;
272
+ try {
273
+ return fs.realpathSync(candidate) === fs.realpathSync(selfPath);
274
+ } catch {
275
+ return path.resolve(candidate) === path.resolve(selfPath);
276
+ }
277
+ }
278
+
279
+ /**
280
+ * True when `candidate` is a Node script (a bin shim) rather than the native
281
+ * binary. Covers shims that are copies rather than symlinks.
282
+ */
283
+ export function looksLikeNodeScript(filePath) {
284
+ try {
285
+ if (/\.(js|cjs|mjs)$/.test(filePath)) return true;
286
+ const fd = fs.openSync(filePath, 'r');
287
+ const buf = Buffer.alloc(64);
288
+ const n = fs.readSync(fd, buf, 0, 64, 0);
289
+ fs.closeSync(fd);
290
+ const first = buf.subarray(0, n).toString('utf8').split('\n')[0];
291
+ return first.startsWith('#!') && /\bnode\b/.test(first);
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+
106
297
  export function findPathBinary(binName = getBinaryName(), pathEnv = process.env.PATH, currentScriptPath) {
107
298
  if (!pathEnv) {
108
299
  return null;
@@ -113,8 +304,12 @@ export function findPathBinary(binName = getBinaryName(), pathEnv = process.env.
113
304
  if (!entry) continue;
114
305
  const candidate = path.join(entry, binName);
115
306
  if (isExecutable(candidate)) {
116
- // Avoid circular invocation if PATH points to this JS wrapper script
117
- if (currentScriptPath && path.resolve(candidate) === path.resolve(currentScriptPath)) {
307
+ // Avoid circular invocation if PATH points back at this JS wrapper.
308
+ // npm/npx put node_modules/.bin on PATH and the entry there is a SYMLINK
309
+ // to this very file. path.resolve() does not follow symlinks, so this guard
310
+ // missed it and the launcher spawned itself until the OS refused to fork
311
+ // (EAGAIN on macOS, v0.1.4). Compare real paths, and reject Node scripts.
312
+ if (isSelf(candidate, currentScriptPath) || looksLikeNodeScript(candidate)) {
118
313
  continue;
119
314
  }
120
315
  return candidate;
@@ -130,11 +325,12 @@ export function findPathBinary(binName = getBinaryName(), pathEnv = process.env.
130
325
  * Search priority:
131
326
  * 1. AGENTWORTH_BIN environment variable
132
327
  * 2. Platform-specific optional package / vendor directory
133
- * 3. Cargo target in cwd directory hierarchy (release or debug)
134
- * 4. Cargo target in package directory hierarchy
135
- * 5. CARGO_TARGET_DIR if set
136
- * 6. User ~/.cargo/bin/
137
- * 7. System PATH
328
+ * 3. System PATH
329
+ * 4. Cargo target in cwd directory hierarchy (release or debug)
330
+ * 5. Cargo target in package directory hierarchy
331
+ * 6. CARGO_TARGET_DIR if set
332
+ * 7. User ~/.cargo/bin/
333
+ * 8. User local cache ~/.agentworth/bin/v{version}/
138
334
  *
139
335
  * @param {Object} [options={}]
140
336
  * @param {string} [options.cwd=process.cwd()]
@@ -142,6 +338,7 @@ export function findPathBinary(binName = getBinaryName(), pathEnv = process.env.
142
338
  * @param {string} [options.arch=process.arch]
143
339
  * @param {NodeJS.ProcessEnv} [options.env=process.env]
144
340
  * @param {string} [options.baseDir=__dirname]
341
+ * @param {string} [options.homeDir]
145
342
  * @returns {{ found: boolean, path?: string, source?: string, error?: string }}
146
343
  */
147
344
  export function resolveBinary(options = {}) {
@@ -150,6 +347,7 @@ export function resolveBinary(options = {}) {
150
347
  const arch = options.arch || process.arch;
151
348
  const env = options.env || process.env;
152
349
  const baseDir = options.baseDir || __dirname;
350
+ const homeDir = options.homeDir || (env.HOME ? env.HOME : os.homedir());
153
351
  const binName = getBinaryName(platform);
154
352
  const platformKey = getPlatformKey(platform, arch);
155
353
 
@@ -172,7 +370,21 @@ export function resolveBinary(options = {}) {
172
370
  };
173
371
  }
174
372
 
175
- // 3. Local Cargo target from working directory
373
+ // 3. System PATH (highest user priority for installed binaries)
374
+ // Skipped when already inside a launcher: the only thing PATH could offer is us.
375
+ const currentBin = path.resolve(baseDir, '..', 'bin', 'agentworth.js');
376
+ if (env.PATH !== undefined && !env.AGENTWORTH_LAUNCHER_ACTIVE) {
377
+ const pathBin = findPathBinary(binName, env.PATH, currentBin);
378
+ if (pathBin) {
379
+ return {
380
+ found: true,
381
+ path: pathBin,
382
+ source: 'path',
383
+ };
384
+ }
385
+ }
386
+
387
+ // 4. Local Cargo target from working directory
176
388
  const cwdCargoBin = findCargoTargetBinary(cwd, binName);
177
389
  if (cwdCargoBin) {
178
390
  return {
@@ -182,7 +394,7 @@ export function resolveBinary(options = {}) {
182
394
  };
183
395
  }
184
396
 
185
- // 4. Local Cargo target from package directory hierarchy
397
+ // 5. Local Cargo target from package directory hierarchy
186
398
  const pkgCargoBin = findCargoTargetBinary(baseDir, binName);
187
399
  if (pkgCargoBin) {
188
400
  return {
@@ -192,7 +404,7 @@ export function resolveBinary(options = {}) {
192
404
  };
193
405
  }
194
406
 
195
- // 5. CARGO_TARGET_DIR environment variable if specified
407
+ // 6. CARGO_TARGET_DIR environment variable if specified
196
408
  if (env.CARGO_TARGET_DIR) {
197
409
  const targetDirRelease = path.join(env.CARGO_TARGET_DIR, 'release', binName);
198
410
  if (isExecutable(targetDirRelease)) {
@@ -212,25 +424,27 @@ export function resolveBinary(options = {}) {
212
424
  }
213
425
  }
214
426
 
215
- // 6. User ~/.cargo/bin directory
216
- const cargoHomeBin = path.join(os.homedir(), '.cargo', 'bin', binName);
217
- if (isExecutable(cargoHomeBin)) {
218
- return {
219
- found: true,
220
- path: cargoHomeBin,
221
- source: 'cargo-home-bin',
222
- };
223
- }
427
+ // 7. User ~/.cargo/bin directory
428
+ if (homeDir) {
429
+ const cargoHomeBin = path.join(homeDir, '.cargo', 'bin', binName);
430
+ if (isExecutable(cargoHomeBin)) {
431
+ return {
432
+ found: true,
433
+ path: cargoHomeBin,
434
+ source: 'cargo-home-bin',
435
+ };
436
+ }
224
437
 
225
- // 7. System PATH
226
- const currentBin = path.resolve(baseDir, '..', 'bin', 'agentworth.js');
227
- const pathBin = findPathBinary(binName, env.PATH, currentBin);
228
- if (pathBin) {
229
- return {
230
- found: true,
231
- path: pathBin,
232
- source: 'path',
233
- };
438
+ // 8. User local cache (~/.agentworth/bin/v{version}/)
439
+ const cacheDir = getCacheDir(getPackageVersion(baseDir), homeDir);
440
+ const cachedBin = path.join(cacheDir, binName);
441
+ if (isExecutable(cachedBin)) {
442
+ return {
443
+ found: true,
444
+ path: cachedBin,
445
+ source: 'cache',
446
+ };
447
+ }
234
448
  }
235
449
 
236
450
  return {
@@ -257,11 +471,8 @@ export function formatMissingBinaryMessage(platformKey = getPlatformKey()) {
257
471
  ' \x1b[36m• Install via Cargo:\x1b[0m',
258
472
  ' cargo install --path apps/cli',
259
473
  '',
260
- ' \x1b[36m• Install via Homebrew:\x1b[0m',
261
- ' brew install agentworth',
262
- '',
263
- ' \x1b[36m• Install via Shell script:\x1b[0m',
264
- ' curl -fsSL https://agentworth.dev/install.sh | sh',
474
+ ' \x1b[36m• Download the release for your platform:\x1b[0m',
475
+ ` https://github.com/unfoundbox-crew/agentworth/releases/latest`,
265
476
  '',
266
477
  ' \x1b[36m• Set custom binary path:\x1b[0m',
267
478
  ' export AGENTWORTH_BIN=/path/to/agentworth',
@@ -278,7 +489,32 @@ export function formatMissingBinaryMessage(platformKey = getPlatformKey()) {
278
489
  */
279
490
  export function run(argv = process.argv.slice(2), options = {}) {
280
491
  const resolvedArgs = resolveArguments(argv);
281
- const binaryResult = resolveBinary(options);
492
+ let binaryResult = resolveBinary(options);
493
+
494
+ // If binary not found on clean machine, attempt on-demand download from GitHub Release
495
+ if ((!binaryResult.found || !binaryResult.path) && options.autoDownload !== false) {
496
+ try {
497
+ const resolverModuleUrl = new URL('./resolver.js', import.meta.url).href;
498
+ const syncDownloadScript = `
499
+ import { downloadAndExtractBinary } from '${resolverModuleUrl}';
500
+ await downloadAndExtractBinary({
501
+ platform: ${JSON.stringify(options.platform || process.platform)},
502
+ arch: ${JSON.stringify(options.arch || process.arch)},
503
+ homeDir: ${JSON.stringify(options.homeDir || (options.env && options.env.HOME) || '')}
504
+ });
505
+ `;
506
+ const dlResult = spawnSync(process.execPath, ['--input-type=module', '-e', syncDownloadScript], {
507
+ stdio: 'inherit',
508
+ env: options.env || process.env,
509
+ });
510
+
511
+ if (dlResult.status === 0) {
512
+ binaryResult = resolveBinary(options);
513
+ }
514
+ } catch {
515
+ // Fall through to error message below
516
+ }
517
+ }
282
518
 
283
519
  if (!binaryResult.found || !binaryResult.path) {
284
520
  const message = formatMissingBinaryMessage(getPlatformKey(options.platform, options.arch));
@@ -286,9 +522,11 @@ export function run(argv = process.argv.slice(2), options = {}) {
286
522
  return 1;
287
523
  }
288
524
 
525
+ // Belt and braces: a child that somehow re-enters this launcher cannot loop.
526
+ const childEnv = { ...(options.env || process.env), AGENTWORTH_LAUNCHER_ACTIVE: '1' };
289
527
  const result = spawnSync(binaryResult.path, resolvedArgs, {
290
528
  stdio: 'inherit',
291
- env: options.env || process.env,
529
+ env: childEnv,
292
530
  });
293
531
 
294
532
  if (result.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworth",
3
- "version": "0.1.1",
3
+ "version": "0.1.5",
4
4
  "description": "Discover, normalize, and understand AI-agent histories locally.",
5
5
  "type": "module",
6
6
  "main": "./lib/resolver.js",
@@ -23,8 +23,16 @@
23
23
  "analytics",
24
24
  "developer-tools"
25
25
  ],
26
- "author": "Unfoundbox",
26
+ "author": "Unfoundbox Crew",
27
27
  "license": "Apache-2.0",
28
+ "homepage": "https://agentworth.dev",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/unfoundbox-crew/agentworth.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/unfoundbox-crew/agentworth/issues"
35
+ },
28
36
  "engines": {
29
37
  "node": ">=18.0.0"
30
38
  },