@openagentsinc/pylon 0.1.12 → 0.1.14
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 +15 -6
- package/package.json +1 -1
- package/src/cli.js +14 -4
- package/src/index.js +172 -6
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@ npx @openagentsinc/pylon
|
|
|
13
13
|
bunx @openagentsinc/pylon
|
|
14
14
|
npm install -g @openagentsinc/pylon && pylon
|
|
15
15
|
bun install -g @openagentsinc/pylon && pylon
|
|
16
|
-
npx @openagentsinc/pylon --version 0.1.
|
|
16
|
+
npx @openagentsinc/pylon --version 0.1.13
|
|
17
17
|
npx @openagentsinc/pylon --no-launch
|
|
18
|
+
npx @openagentsinc/pylon --no-updates
|
|
18
19
|
npx @openagentsinc/pylon --download-curated-cache --model gemma-4-e2b --run-diagnostics
|
|
19
20
|
npx @openagentsinc/pylon --verbose
|
|
20
21
|
```
|
|
@@ -25,6 +26,7 @@ The launcher:
|
|
|
25
26
|
`bun install -g` installs with the same `pylon` command
|
|
26
27
|
- checks GitHub for the latest tagged `pylon-v...` release on each default run,
|
|
27
28
|
or resolves a specific tagged `Pylon` version when `--version` is provided
|
|
29
|
+
- only installs releases initiated by `AtlantisPleb` in GitHub Releases
|
|
28
30
|
- resolves the correct `pylon-v<version>-<os>-<arch>.tar.gz` asset for the
|
|
29
31
|
current machine
|
|
30
32
|
- falls back to the exact tagged source checkout and builds `pylon` plus
|
|
@@ -57,12 +59,19 @@ The launcher:
|
|
|
57
59
|
- starts the installed `pylon-tui` by default after the smoke path; that TUI
|
|
58
60
|
starts and supervises the earning worker
|
|
59
61
|
unless `--no-launch` is set
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
- while the TUI is running on the default release track, checks GitHub Releases
|
|
63
|
+
every 30 seconds and restarts the TUI from a newer trusted cached release
|
|
64
|
+
without replacing the global npm/bun command
|
|
65
|
+
- use `--no-updates` to keep the current installed release running without
|
|
66
|
+
background GitHub release checks; `--version` remains a pinned release run
|
|
67
|
+
- for hosted homework/training work, use launcher `0.1.14` or newer so the
|
|
68
|
+
cached standalone binary auto-updates while the dashboard is open. The latest
|
|
69
|
+
trusted standalone binary still carries the `0.1.13` and `0.1.12` runtime
|
|
70
|
+
fixes: no legacy runtime wording, explicit runtime management, current
|
|
71
|
+
`target/release/psionic-train` preference, and `cargo run --release`
|
|
72
|
+
fallback instead of debug `cargo run`
|
|
64
73
|
- does not try to install or register a local runtime automatically; the
|
|
65
|
-
bootstrap stays honest about the separate
|
|
74
|
+
bootstrap stays honest about the separate local Gemma runtime
|
|
66
75
|
prerequisite instead of mutating the host behind the user's back
|
|
67
76
|
|
|
68
77
|
Set `OPENAGENTS_DISABLE_TELEMETRY=1` to disable installer telemetry, or
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
bootstrapInstalledPylon,
|
|
8
8
|
ensureReleaseInstall,
|
|
9
9
|
launchInstalledPylon,
|
|
10
|
+
launchInstalledPylonWithUpdates,
|
|
10
11
|
resolveBootstrapOutcome,
|
|
11
12
|
resolvePlatformTarget,
|
|
12
13
|
renderBootstrapSummary,
|
|
@@ -85,9 +86,10 @@ Description:
|
|
|
85
86
|
and build it locally instead. Cache the binaries, run the first-run smoke
|
|
86
87
|
path, and then start the Pylon terminal UI by default. The terminal UI manages
|
|
87
88
|
the earning worker and keeps live status visible. The launcher checks GitHub
|
|
88
|
-
for newer tagged pylon-v... releases on
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
for newer tagged pylon-v... releases on each default run and every 30 seconds
|
|
90
|
+
while the dashboard is open. Only releases initiated by AtlantisPleb are
|
|
91
|
+
accepted. New standalone binaries are cached under the local bootstrap root;
|
|
92
|
+
the global npm or bun pylon command is not replaced.
|
|
91
93
|
|
|
92
94
|
Options:
|
|
93
95
|
--version <x.y.z> Resolve a specific Pylon release.
|
|
@@ -107,6 +109,8 @@ Options:
|
|
|
107
109
|
--skip-model-download Keep the curated GGUF cache skipped.
|
|
108
110
|
--skip-diagnostics Keep optional pylon gemma diagnose skipped.
|
|
109
111
|
--no-launch Do not start pylon-tui after bootstrap.
|
|
112
|
+
--no-updates Disable 30-second GitHub release polling
|
|
113
|
+
and dashboard restart while pylon runs.
|
|
110
114
|
--verbose Print extra network and recovery detail.
|
|
111
115
|
--debug-network Alias for --verbose.
|
|
112
116
|
--json Emit a machine-readable JSON summary.
|
|
@@ -132,6 +136,7 @@ export function parseArgs(argv) {
|
|
|
132
136
|
skipModelDownload: true,
|
|
133
137
|
skipDiagnostics: true,
|
|
134
138
|
noLaunch: false,
|
|
139
|
+
noUpdates: false,
|
|
135
140
|
verbose: false,
|
|
136
141
|
json: false,
|
|
137
142
|
help: false,
|
|
@@ -197,6 +202,9 @@ export function parseArgs(argv) {
|
|
|
197
202
|
case "--no-launch":
|
|
198
203
|
options.noLaunch = true;
|
|
199
204
|
break;
|
|
205
|
+
case "--no-updates":
|
|
206
|
+
options.noUpdates = true;
|
|
207
|
+
break;
|
|
200
208
|
case "--verbose":
|
|
201
209
|
case "--debug-network":
|
|
202
210
|
options.verbose = true;
|
|
@@ -232,7 +240,7 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
232
240
|
const {
|
|
233
241
|
ensureReleaseInstallImpl = ensureReleaseInstall,
|
|
234
242
|
bootstrapInstalledPylonImpl = bootstrapInstalledPylon,
|
|
235
|
-
launchInstalledPylonImpl =
|
|
243
|
+
launchInstalledPylonImpl = launchInstalledPylonWithUpdates,
|
|
236
244
|
createTelemetryClientImpl = createTelemetryClient,
|
|
237
245
|
} = dependencies;
|
|
238
246
|
const options = parseArgs(argv);
|
|
@@ -318,10 +326,12 @@ export async function main(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
318
326
|
...options,
|
|
319
327
|
...install,
|
|
320
328
|
version: install.version,
|
|
329
|
+
pinnedVersion: Boolean(options.version),
|
|
321
330
|
},
|
|
322
331
|
{
|
|
323
332
|
...dependencies,
|
|
324
333
|
onStatus: reporter?.status,
|
|
334
|
+
telemetryClient,
|
|
325
335
|
},
|
|
326
336
|
);
|
|
327
337
|
} else {
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export const DEFAULT_MODEL_ID = "gemma-4-e4b";
|
|
|
20
20
|
export const DEFAULT_DIAGNOSTIC_REPEATS = 3;
|
|
21
21
|
export const DEFAULT_DIAGNOSTIC_MAX_OUTPUT_TOKENS = 96;
|
|
22
22
|
export const DEFAULT_FETCH_TIMEOUT_MS = 15_000;
|
|
23
|
+
export const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 30_000;
|
|
24
|
+
export const DEFAULT_TRUSTED_RELEASE_AUTHOR = "AtlantisPleb";
|
|
23
25
|
const PYLON_RELEASE_TAG_PREFIX = "pylon-v";
|
|
24
26
|
const RELEASE_ASSET_INSTALL_METHOD = "release_asset";
|
|
25
27
|
const SOURCE_BUILD_INSTALL_METHOD = "source_build";
|
|
@@ -116,6 +118,36 @@ function comparePylonReleaseTags(leftTagName, rightTagName) {
|
|
|
116
118
|
return left.normalized.localeCompare(right.normalized);
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
function trustedReleaseAuthor(release, trustedAuthor = DEFAULT_TRUSTED_RELEASE_AUTHOR) {
|
|
122
|
+
if (!trustedAuthor) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return release?.author?.login === trustedAuthor;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function assertTrustedReleaseAuthor(
|
|
129
|
+
release,
|
|
130
|
+
trustedAuthor = DEFAULT_TRUSTED_RELEASE_AUTHOR,
|
|
131
|
+
) {
|
|
132
|
+
if (trustedReleaseAuthor(release, trustedAuthor)) {
|
|
133
|
+
return release;
|
|
134
|
+
}
|
|
135
|
+
const tagName = release?.tag_name ?? "unknown";
|
|
136
|
+
const author = release?.author?.login ?? "unknown";
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Refusing ${tagName}: GitHub release author ${author} is not trusted. Expected ${trustedAuthor}.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isNewerPylonVersion(candidateVersion, currentVersion) {
|
|
143
|
+
return (
|
|
144
|
+
comparePylonReleaseTags(
|
|
145
|
+
`${PYLON_RELEASE_TAG_PREFIX}${normalizeVersion(candidateVersion)}`,
|
|
146
|
+
`${PYLON_RELEASE_TAG_PREFIX}${normalizeVersion(currentVersion)}`,
|
|
147
|
+
) > 0
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
119
151
|
function createBootstrapError(message, context = {}) {
|
|
120
152
|
const error = new Error(message);
|
|
121
153
|
Object.assign(error, context);
|
|
@@ -619,14 +651,19 @@ export function selectLatestPylonRelease(releases, target = null) {
|
|
|
619
651
|
}
|
|
620
652
|
|
|
621
653
|
const candidates = releases
|
|
622
|
-
.filter(
|
|
654
|
+
.filter(
|
|
655
|
+
(candidate) =>
|
|
656
|
+
!candidate?.draft &&
|
|
657
|
+
isPylonReleaseTag(candidate?.tag_name) &&
|
|
658
|
+
trustedReleaseAuthor(candidate),
|
|
659
|
+
)
|
|
623
660
|
.sort((left, right) => comparePylonReleaseTags(right.tag_name, left.tag_name));
|
|
624
661
|
const release =
|
|
625
662
|
candidates.find((candidate) => releaseHasTargetAssets(candidate, target)) ??
|
|
626
663
|
candidates[0];
|
|
627
664
|
if (!release) {
|
|
628
665
|
throw new Error(
|
|
629
|
-
`GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases.`,
|
|
666
|
+
`GitHub release lookup did not find any published ${PYLON_RELEASE_TAG_PREFIX} releases initiated by ${DEFAULT_TRUSTED_RELEASE_AUTHOR}.`,
|
|
630
667
|
);
|
|
631
668
|
}
|
|
632
669
|
|
|
@@ -658,7 +695,10 @@ export async function fetchReleaseMetadata({
|
|
|
658
695
|
? "GitHub tagged release lookup"
|
|
659
696
|
: "GitHub release list lookup",
|
|
660
697
|
});
|
|
661
|
-
|
|
698
|
+
const release = normalizedVersion
|
|
699
|
+
? payload
|
|
700
|
+
: selectLatestPylonRelease(payload, target);
|
|
701
|
+
return assertTrustedReleaseAuthor(release);
|
|
662
702
|
}
|
|
663
703
|
|
|
664
704
|
export function selectReleaseAssets(release, target) {
|
|
@@ -1367,6 +1407,45 @@ export async function runProcess(
|
|
|
1367
1407
|
});
|
|
1368
1408
|
}
|
|
1369
1409
|
|
|
1410
|
+
function delay(ms) {
|
|
1411
|
+
return new Promise((resolve) => {
|
|
1412
|
+
setTimeout(resolve, ms);
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function waitForChildExit(child) {
|
|
1417
|
+
return new Promise((resolve, reject) => {
|
|
1418
|
+
child.once?.("error", reject);
|
|
1419
|
+
child.once?.("close", (code, signal) => {
|
|
1420
|
+
resolve({ code, signal });
|
|
1421
|
+
});
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
async function stopChild(child, timeoutMs = 5_000) {
|
|
1426
|
+
if (!child || child.killed || child.exitCode != null) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
const exited = waitForChildExit(child).catch(() => null);
|
|
1431
|
+
child.kill?.("SIGTERM");
|
|
1432
|
+
const result = await Promise.race([
|
|
1433
|
+
exited,
|
|
1434
|
+
delay(timeoutMs).then(() => "timeout"),
|
|
1435
|
+
]);
|
|
1436
|
+
if (result === "timeout" && child.exitCode == null) {
|
|
1437
|
+
child.kill?.("SIGKILL");
|
|
1438
|
+
await exited;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function spawnPylonTui(pylonTuiPath, options, spawnProcessImpl) {
|
|
1443
|
+
return spawnProcessImpl(pylonTuiPath, [], {
|
|
1444
|
+
env: buildPylonEnv(options),
|
|
1445
|
+
stdio: "inherit",
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1370
1449
|
async function extractArchive(archivePath, destinationDir, runProcessImpl) {
|
|
1371
1450
|
await fs.mkdir(destinationDir, { recursive: true });
|
|
1372
1451
|
await runProcessImpl("tar", ["-xzf", archivePath, "-C", destinationDir]);
|
|
@@ -1937,6 +2016,93 @@ export async function launchInstalledPylon(
|
|
|
1937
2016
|
|
|
1938
2017
|
export const launchInstalledPylonTui = launchInstalledPylon;
|
|
1939
2018
|
|
|
2019
|
+
export async function launchInstalledPylonWithUpdates(
|
|
2020
|
+
options,
|
|
2021
|
+
{
|
|
2022
|
+
ensureReleaseInstallImpl = ensureReleaseInstall,
|
|
2023
|
+
spawnProcessImpl = spawn,
|
|
2024
|
+
updateCheckIntervalMs = DEFAULT_UPDATE_CHECK_INTERVAL_MS,
|
|
2025
|
+
onStatus = null,
|
|
2026
|
+
...dependencies
|
|
2027
|
+
} = {},
|
|
2028
|
+
) {
|
|
2029
|
+
if (options.noUpdates || options.pinnedVersion) {
|
|
2030
|
+
return launchInstalledPylon(options, { ...dependencies, onStatus });
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
let current = {
|
|
2034
|
+
...options,
|
|
2035
|
+
version: normalizeVersion(options.version),
|
|
2036
|
+
};
|
|
2037
|
+
let lastUpdateError = null;
|
|
2038
|
+
|
|
2039
|
+
while (true) {
|
|
2040
|
+
const pylonTuiPath = path.resolve(current.pylonTuiPath);
|
|
2041
|
+
emitStatus(
|
|
2042
|
+
onStatus,
|
|
2043
|
+
"Starting Pylon terminal UI",
|
|
2044
|
+
`${path.basename(pylonTuiPath)} manages the earning worker`,
|
|
2045
|
+
);
|
|
2046
|
+
const child = spawnPylonTui(pylonTuiPath, current, spawnProcessImpl);
|
|
2047
|
+
const childExit = waitForChildExit(child);
|
|
2048
|
+
let restartForUpdate = false;
|
|
2049
|
+
|
|
2050
|
+
while (!restartForUpdate) {
|
|
2051
|
+
const result = await Promise.race([
|
|
2052
|
+
childExit.then((exit) => ({ type: "exit", exit })),
|
|
2053
|
+
delay(updateCheckIntervalMs).then(() => ({ type: "tick" })),
|
|
2054
|
+
]);
|
|
2055
|
+
|
|
2056
|
+
if (result.type === "exit") {
|
|
2057
|
+
const { code, signal } = result.exit;
|
|
2058
|
+
if (code === 0 || signal === "SIGTERM" || signal === "SIGINT") {
|
|
2059
|
+
return result.exit;
|
|
2060
|
+
}
|
|
2061
|
+
throw new Error(
|
|
2062
|
+
`${path.basename(pylonTuiPath)} exited with code ${
|
|
2063
|
+
code ?? "null"
|
|
2064
|
+
}${signal ? ` signal ${signal}` : ""}`,
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
try {
|
|
2069
|
+
const install = await ensureReleaseInstallImpl(
|
|
2070
|
+
{
|
|
2071
|
+
...options,
|
|
2072
|
+
version: null,
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
...dependencies,
|
|
2076
|
+
onStatus: null,
|
|
2077
|
+
},
|
|
2078
|
+
);
|
|
2079
|
+
lastUpdateError = null;
|
|
2080
|
+
if (!isNewerPylonVersion(install.version, current.version)) {
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
emitStatus(
|
|
2084
|
+
onStatus,
|
|
2085
|
+
"Installed newer Pylon release",
|
|
2086
|
+
`${install.tagName}; restarting dashboard`,
|
|
2087
|
+
);
|
|
2088
|
+
await stopChild(child);
|
|
2089
|
+
current = {
|
|
2090
|
+
...options,
|
|
2091
|
+
...install,
|
|
2092
|
+
version: install.version,
|
|
2093
|
+
};
|
|
2094
|
+
restartForUpdate = true;
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2097
|
+
if (message !== lastUpdateError) {
|
|
2098
|
+
emitStatus(onStatus, "Pylon update check failed", message);
|
|
2099
|
+
lastUpdateError = message;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
1940
2106
|
export function resolveBootstrapOutcome(summary) {
|
|
1941
2107
|
const runtimeState =
|
|
1942
2108
|
summary.status?.snapshot?.runtime?.authoritative_status ?? "unknown";
|
|
@@ -1969,7 +2135,7 @@ export function resolveBootstrapOutcome(summary) {
|
|
|
1969
2135
|
level: "warning",
|
|
1970
2136
|
verdict: "installed but runtime missing",
|
|
1971
2137
|
detail:
|
|
1972
|
-
"no
|
|
2138
|
+
"no local Gemma runtime is answering /api/tags yet",
|
|
1973
2139
|
};
|
|
1974
2140
|
}
|
|
1975
2141
|
|
|
@@ -2005,11 +2171,11 @@ function renderBootstrapNextSteps(summary, outcome) {
|
|
|
2005
2171
|
|
|
2006
2172
|
if (summary.target?.os === "darwin") {
|
|
2007
2173
|
lines.push(
|
|
2008
|
-
"Runtime setup
|
|
2174
|
+
"Runtime setup: start a local Gemma runtime at `local_gemma_base_url` and load `gemma4:e4b`.",
|
|
2009
2175
|
);
|
|
2010
2176
|
} else {
|
|
2011
2177
|
lines.push(
|
|
2012
|
-
"Runtime setup: start
|
|
2178
|
+
"Runtime setup: start a local Gemma runtime at `local_gemma_base_url` and load `gemma4:e4b`.",
|
|
2013
2179
|
);
|
|
2014
2180
|
}
|
|
2015
2181
|
lines.push(
|