@hydra-acp/cli 0.1.23 → 0.1.24
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/dist/cli.js +495 -70
- package/dist/index.d.ts +8 -1
- package/dist/index.js +134 -19
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1203,10 +1203,14 @@ declare const RegistryDocument: z.ZodObject<{
|
|
|
1203
1203
|
extensions?: unknown[] | undefined;
|
|
1204
1204
|
}>;
|
|
1205
1205
|
type RegistryDocument = z.infer<typeof RegistryDocument>;
|
|
1206
|
+
interface RegistryOptions {
|
|
1207
|
+
onFetched?: (doc: RegistryDocument) => void | Promise<void>;
|
|
1208
|
+
}
|
|
1206
1209
|
declare class Registry {
|
|
1207
1210
|
private config;
|
|
1211
|
+
private options;
|
|
1208
1212
|
private cache;
|
|
1209
|
-
constructor(config: HydraConfig);
|
|
1213
|
+
constructor(config: HydraConfig, options?: RegistryOptions);
|
|
1210
1214
|
load(): Promise<RegistryDocument>;
|
|
1211
1215
|
refresh(): Promise<RegistryDocument>;
|
|
1212
1216
|
getAgent(id: string): Promise<RegistryAgent | undefined>;
|
|
@@ -1219,6 +1223,7 @@ interface SpawnPlan {
|
|
|
1219
1223
|
command: string;
|
|
1220
1224
|
args: string[];
|
|
1221
1225
|
env: Record<string, string>;
|
|
1226
|
+
version: string;
|
|
1222
1227
|
}
|
|
1223
1228
|
declare function planSpawn(agent: RegistryAgent, callerArgs?: string[], options?: {
|
|
1224
1229
|
npmRegistry?: string;
|
|
@@ -1618,6 +1623,7 @@ interface AgentLogger {
|
|
|
1618
1623
|
}
|
|
1619
1624
|
declare class AgentInstance {
|
|
1620
1625
|
readonly agentId: string;
|
|
1626
|
+
readonly version: string;
|
|
1621
1627
|
readonly cwd: string;
|
|
1622
1628
|
readonly connection: JsonRpcConnection;
|
|
1623
1629
|
private child;
|
|
@@ -2263,6 +2269,7 @@ declare class SessionManager {
|
|
|
2263
2269
|
loadFromDisk(sessionId: string): Promise<ResurrectParams | undefined>;
|
|
2264
2270
|
private deriveTitleFromHistory;
|
|
2265
2271
|
get(sessionId: string): Session | undefined;
|
|
2272
|
+
activeAgentVersions(): Map<string, Set<string>>;
|
|
2266
2273
|
resolveCanonicalId(input: string): Promise<string | undefined>;
|
|
2267
2274
|
require(sessionId: string): Session;
|
|
2268
2275
|
list(filter?: {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/daemon/server.ts
|
|
2
2
|
import * as fs14 from "fs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as fsp5 from "fs/promises";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import websocketPlugin from "@fastify/websocket";
|
|
6
6
|
import pino from "pino";
|
|
@@ -693,10 +693,12 @@ var RegistryDocument = z2.object({
|
|
|
693
693
|
extensions: z2.array(z2.unknown()).optional()
|
|
694
694
|
});
|
|
695
695
|
var Registry = class {
|
|
696
|
-
constructor(config) {
|
|
696
|
+
constructor(config, options = {}) {
|
|
697
697
|
this.config = config;
|
|
698
|
+
this.options = options;
|
|
698
699
|
}
|
|
699
700
|
config;
|
|
701
|
+
options;
|
|
700
702
|
cache;
|
|
701
703
|
async load() {
|
|
702
704
|
if (this.cache && this.isFresh(this.cache.fetchedAt)) {
|
|
@@ -746,7 +748,12 @@ var Registry = class {
|
|
|
746
748
|
}
|
|
747
749
|
const raw = await response.json();
|
|
748
750
|
const data = RegistryDocument.parse(raw);
|
|
749
|
-
|
|
751
|
+
const cached = { fetchedAt: Date.now(), raw, data };
|
|
752
|
+
const hook = this.options.onFetched;
|
|
753
|
+
if (hook) {
|
|
754
|
+
void Promise.resolve().then(() => hook(data)).catch(() => void 0);
|
|
755
|
+
}
|
|
756
|
+
return cached;
|
|
750
757
|
}
|
|
751
758
|
async readDiskCache() {
|
|
752
759
|
let text;
|
|
@@ -808,6 +815,7 @@ function npxPackageBasename(agent) {
|
|
|
808
815
|
return atIdx <= 0 ? afterSlash : afterSlash.slice(0, atIdx);
|
|
809
816
|
}
|
|
810
817
|
async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
818
|
+
const version = agent.version ?? "current";
|
|
811
819
|
if (agent.distribution.npx) {
|
|
812
820
|
const npx = agent.distribution.npx;
|
|
813
821
|
const tail = callerArgs.length > 0 ? callerArgs : npx.args ?? [];
|
|
@@ -815,13 +823,14 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
815
823
|
return {
|
|
816
824
|
command: "npx",
|
|
817
825
|
args: ["-y", npx.package, ...tail],
|
|
818
|
-
env: npx.env ?? {}
|
|
826
|
+
env: npx.env ?? {},
|
|
827
|
+
version
|
|
819
828
|
};
|
|
820
829
|
}
|
|
821
830
|
const bin = npx.bin ?? npxPackageBasename(agent) ?? npx.package;
|
|
822
831
|
const binPath = await ensureNpmPackage({
|
|
823
832
|
agentId: agent.id,
|
|
824
|
-
version
|
|
833
|
+
version,
|
|
825
834
|
packageSpec: npx.package,
|
|
826
835
|
bin,
|
|
827
836
|
registry: options.npmRegistry
|
|
@@ -829,7 +838,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
829
838
|
return {
|
|
830
839
|
command: binPath,
|
|
831
840
|
args: tail,
|
|
832
|
-
env: npx.env ?? {}
|
|
841
|
+
env: npx.env ?? {},
|
|
842
|
+
version
|
|
833
843
|
};
|
|
834
844
|
}
|
|
835
845
|
if (agent.distribution.binary) {
|
|
@@ -841,14 +851,15 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
841
851
|
}
|
|
842
852
|
const cmdPath = await ensureBinary({
|
|
843
853
|
agentId: agent.id,
|
|
844
|
-
version
|
|
854
|
+
version,
|
|
845
855
|
target
|
|
846
856
|
});
|
|
847
857
|
const tail = callerArgs.length > 0 ? callerArgs : target.args ?? [];
|
|
848
858
|
return {
|
|
849
859
|
command: cmdPath,
|
|
850
860
|
args: tail,
|
|
851
|
-
env: target.env ?? {}
|
|
861
|
+
env: target.env ?? {},
|
|
862
|
+
version
|
|
852
863
|
};
|
|
853
864
|
}
|
|
854
865
|
if (agent.distribution.uvx) {
|
|
@@ -857,7 +868,8 @@ async function planSpawn(agent, callerArgs = [], options = {}) {
|
|
|
857
868
|
return {
|
|
858
869
|
command: "uvx",
|
|
859
870
|
args: [uvx.package, ...tail],
|
|
860
|
-
env: uvx.env ?? {}
|
|
871
|
+
env: uvx.env ?? {},
|
|
872
|
+
version
|
|
861
873
|
};
|
|
862
874
|
}
|
|
863
875
|
throw new Error(`Agent ${agent.id} has no usable distribution method.`);
|
|
@@ -1443,6 +1455,9 @@ var JsonRpcConnection = class _JsonRpcConnection {
|
|
|
1443
1455
|
var DEFAULT_STDERR_TAIL_BYTES = 4096;
|
|
1444
1456
|
var AgentInstance = class _AgentInstance {
|
|
1445
1457
|
agentId;
|
|
1458
|
+
// Version this process was spawned from — used by the registry-fetch
|
|
1459
|
+
// prune sweep to skip install dirs belonging to a live agent.
|
|
1460
|
+
version;
|
|
1446
1461
|
cwd;
|
|
1447
1462
|
connection;
|
|
1448
1463
|
child;
|
|
@@ -1454,6 +1469,7 @@ var AgentInstance = class _AgentInstance {
|
|
|
1454
1469
|
exitHandlers = [];
|
|
1455
1470
|
constructor(opts, child) {
|
|
1456
1471
|
this.agentId = opts.agentId;
|
|
1472
|
+
this.version = opts.plan.version;
|
|
1457
1473
|
this.cwd = opts.cwd;
|
|
1458
1474
|
this.child = child;
|
|
1459
1475
|
this.stderrTailBytes = opts.stderrTailBytes ?? DEFAULT_STDERR_TAIL_BYTES;
|
|
@@ -4257,6 +4273,23 @@ var SessionManager = class {
|
|
|
4257
4273
|
get(sessionId) {
|
|
4258
4274
|
return this.sessions.get(sessionId);
|
|
4259
4275
|
}
|
|
4276
|
+
// Snapshot of which agent versions are currently in use by live
|
|
4277
|
+
// sessions, keyed by agentId. Read by the registry-fetch prune sweep
|
|
4278
|
+
// so it can skip install dirs that still back a running process.
|
|
4279
|
+
activeAgentVersions() {
|
|
4280
|
+
const out = /* @__PURE__ */ new Map();
|
|
4281
|
+
for (const session of this.sessions.values()) {
|
|
4282
|
+
const id = session.agent.agentId;
|
|
4283
|
+
const version = session.agent.version;
|
|
4284
|
+
let set = out.get(id);
|
|
4285
|
+
if (!set) {
|
|
4286
|
+
set = /* @__PURE__ */ new Set();
|
|
4287
|
+
out.set(id, set);
|
|
4288
|
+
}
|
|
4289
|
+
set.add(version);
|
|
4290
|
+
}
|
|
4291
|
+
return out;
|
|
4292
|
+
}
|
|
4260
4293
|
// Resolve a user-typed session id (which may have the hydra_session_
|
|
4261
4294
|
// prefix stripped — that's what `sessions list` and the picker show) to
|
|
4262
4295
|
// the canonical form that actually exists. Tries the input as-given
|
|
@@ -5230,9 +5263,85 @@ function withCode2(err, code) {
|
|
|
5230
5263
|
return err;
|
|
5231
5264
|
}
|
|
5232
5265
|
|
|
5266
|
+
// src/core/agent-prune.ts
|
|
5267
|
+
import * as fsp4 from "fs/promises";
|
|
5268
|
+
import * as path8 from "path";
|
|
5269
|
+
var logSink3 = (msg) => {
|
|
5270
|
+
process.stderr.write(msg + "\n");
|
|
5271
|
+
};
|
|
5272
|
+
function setAgentPruneLogger(log) {
|
|
5273
|
+
logSink3 = log ?? ((msg) => process.stderr.write(msg + "\n"));
|
|
5274
|
+
}
|
|
5275
|
+
async function pruneStaleAgentVersions(registry, sessionManager) {
|
|
5276
|
+
const platformKey = currentPlatformKey();
|
|
5277
|
+
if (!platformKey) {
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
const doc = await registry.load();
|
|
5281
|
+
const desiredByAgent = /* @__PURE__ */ new Map();
|
|
5282
|
+
for (const a of doc.agents) {
|
|
5283
|
+
desiredByAgent.set(a.id, a.version ?? "current");
|
|
5284
|
+
}
|
|
5285
|
+
const activeByAgent = sessionManager.activeAgentVersions();
|
|
5286
|
+
const platformDir = path8.join(paths.agentsDir(), platformKey);
|
|
5287
|
+
let agentEntries;
|
|
5288
|
+
try {
|
|
5289
|
+
agentEntries = await fsp4.readdir(platformDir, { withFileTypes: true });
|
|
5290
|
+
} catch (err) {
|
|
5291
|
+
const e = err;
|
|
5292
|
+
if (e.code === "ENOENT") {
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
logSink3(`hydra-acp: prune: failed to read ${platformDir}: ${e.message}`);
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
for (const agentEntry of agentEntries) {
|
|
5299
|
+
if (!agentEntry.isDirectory()) {
|
|
5300
|
+
continue;
|
|
5301
|
+
}
|
|
5302
|
+
const agentId = agentEntry.name;
|
|
5303
|
+
const desired = desiredByAgent.get(agentId);
|
|
5304
|
+
if (desired === void 0) {
|
|
5305
|
+
continue;
|
|
5306
|
+
}
|
|
5307
|
+
const activeVersions = activeByAgent.get(agentId) ?? /* @__PURE__ */ new Set();
|
|
5308
|
+
const agentDir = path8.join(platformDir, agentId);
|
|
5309
|
+
let versionEntries;
|
|
5310
|
+
try {
|
|
5311
|
+
versionEntries = await fsp4.readdir(agentDir, { withFileTypes: true });
|
|
5312
|
+
} catch (err) {
|
|
5313
|
+
logSink3(
|
|
5314
|
+
`hydra-acp: prune: failed to read ${agentDir}: ${err.message}`
|
|
5315
|
+
);
|
|
5316
|
+
continue;
|
|
5317
|
+
}
|
|
5318
|
+
for (const versionEntry of versionEntries) {
|
|
5319
|
+
if (!versionEntry.isDirectory()) {
|
|
5320
|
+
continue;
|
|
5321
|
+
}
|
|
5322
|
+
const version = versionEntry.name;
|
|
5323
|
+
if (version === desired) {
|
|
5324
|
+
continue;
|
|
5325
|
+
}
|
|
5326
|
+
if (activeVersions.has(version)) {
|
|
5327
|
+
continue;
|
|
5328
|
+
}
|
|
5329
|
+
const versionDir = path8.join(agentDir, version);
|
|
5330
|
+
try {
|
|
5331
|
+
await fsp4.rm(versionDir, { recursive: true, force: true });
|
|
5332
|
+
logSink3(`hydra-acp: pruned stale ${agentId} ${version} (${versionDir})`);
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
logSink3(
|
|
5335
|
+
`hydra-acp: prune: failed to remove ${versionDir}: ${err.message}`
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5233
5342
|
// src/core/session-tokens.ts
|
|
5234
5343
|
import * as fs12 from "fs/promises";
|
|
5235
|
-
import * as
|
|
5344
|
+
import * as path9 from "path";
|
|
5236
5345
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
5237
5346
|
var TOKEN_PREFIX = "hydra_session_";
|
|
5238
5347
|
var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
|
|
@@ -5240,7 +5349,7 @@ var ID_LENGTH = 12;
|
|
|
5240
5349
|
var TOKEN_BYTES = 32;
|
|
5241
5350
|
var WRITE_DEBOUNCE_MS = 50;
|
|
5242
5351
|
function tokensFilePath() {
|
|
5243
|
-
return
|
|
5352
|
+
return path9.join(paths.home(), "session-tokens.json");
|
|
5244
5353
|
}
|
|
5245
5354
|
function sha256Hex(input) {
|
|
5246
5355
|
return createHash("sha256").update(input).digest("hex");
|
|
@@ -6536,12 +6645,12 @@ import { z as z6 } from "zod";
|
|
|
6536
6645
|
|
|
6537
6646
|
// src/core/password.ts
|
|
6538
6647
|
import * as fs13 from "fs/promises";
|
|
6539
|
-
import * as
|
|
6648
|
+
import * as path10 from "path";
|
|
6540
6649
|
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
6541
6650
|
import { promisify } from "util";
|
|
6542
6651
|
var scryptAsync = promisify(scrypt);
|
|
6543
6652
|
function passwordHashPath() {
|
|
6544
|
-
return
|
|
6653
|
+
return path10.join(paths.home(), "password-hash");
|
|
6545
6654
|
}
|
|
6546
6655
|
var DEFAULT_N = 1 << 15;
|
|
6547
6656
|
var MAX_MEM = 128 * 1024 * 1024;
|
|
@@ -7174,10 +7283,10 @@ function bindClientToSession(connection, session, state, clientInfo, callerClien
|
|
|
7174
7283
|
async function startDaemon(config, serviceToken) {
|
|
7175
7284
|
ensureLoopbackOrTls(config);
|
|
7176
7285
|
const httpsOptions = config.daemon.tls ? {
|
|
7177
|
-
key: await
|
|
7178
|
-
cert: await
|
|
7286
|
+
key: await fsp5.readFile(config.daemon.tls.key),
|
|
7287
|
+
cert: await fsp5.readFile(config.daemon.tls.cert)
|
|
7179
7288
|
} : void 0;
|
|
7180
|
-
await
|
|
7289
|
+
await fsp5.mkdir(paths.home(), { recursive: true });
|
|
7181
7290
|
const { stream: logStream, fileStream } = await buildLogStream(
|
|
7182
7291
|
config.daemon.logLevel
|
|
7183
7292
|
);
|
|
@@ -7221,7 +7330,12 @@ async function startDaemon(config, serviceToken) {
|
|
|
7221
7330
|
5 * 60 * 1e3
|
|
7222
7331
|
);
|
|
7223
7332
|
sweepInterval.unref();
|
|
7224
|
-
const registry = new Registry(config
|
|
7333
|
+
const registry = new Registry(config, {
|
|
7334
|
+
onFetched: () => {
|
|
7335
|
+
void pruneStaleAgentVersions(registry, manager);
|
|
7336
|
+
}
|
|
7337
|
+
});
|
|
7338
|
+
setAgentPruneLogger((msg) => app.log.info(msg));
|
|
7225
7339
|
const agentLogger = {
|
|
7226
7340
|
info: (msg) => app.log.info(msg),
|
|
7227
7341
|
warn: (msg) => app.log.warn(msg)
|
|
@@ -7262,8 +7376,8 @@ async function startDaemon(config, serviceToken) {
|
|
|
7262
7376
|
await app.listen({ host: config.daemon.host, port: config.daemon.port });
|
|
7263
7377
|
const address = app.server.address();
|
|
7264
7378
|
const boundPort = address && typeof address === "object" ? address.port : config.daemon.port;
|
|
7265
|
-
await
|
|
7266
|
-
await
|
|
7379
|
+
await fsp5.mkdir(paths.home(), { recursive: true });
|
|
7380
|
+
await fsp5.writeFile(
|
|
7267
7381
|
paths.pidFile(),
|
|
7268
7382
|
JSON.stringify({
|
|
7269
7383
|
pid: process.pid,
|
|
@@ -7297,6 +7411,7 @@ async function startDaemon(config, serviceToken) {
|
|
|
7297
7411
|
await manager.flushMetaWrites();
|
|
7298
7412
|
setBinaryInstallLogger(null);
|
|
7299
7413
|
setNpmInstallLogger(null);
|
|
7414
|
+
setAgentPruneLogger(null);
|
|
7300
7415
|
await app.close();
|
|
7301
7416
|
try {
|
|
7302
7417
|
fs14.unlinkSync(paths.pidFile());
|