@ai-development-environment/control-agent 0.0.75 → 0.0.77

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 CHANGED
@@ -21,6 +21,42 @@ control-agent enroll \
21
21
  control-agent run
22
22
  ```
23
23
 
24
+ ### Local and remote endpoints
25
+
26
+ One control plane is often reachable at two addresses — a LAN address at the desk and a public or VPN address elsewhere. Configure both and the agent prefers the local one, moving to the remote one when the local address stops answering and the remote one is confirmed to be answering. A control plane that is down at _every_ configured address is not a failover: the agent keeps its session and the jobs running under it, and reconnects when the server comes back.
27
+
28
+ ```bash
29
+ control-agent enroll \
30
+ --server http://192.168.1.10:3090 \
31
+ --remote-server https://control.example.com \
32
+ --enrollment-token <one-time-token>
33
+ ```
34
+
35
+ Change the addresses on an enrolled agent, or print the current ones by passing no flags:
36
+
37
+ ```bash
38
+ control-agent endpoints
39
+ control-agent endpoints --remote-server https://control.example.com
40
+ control-agent endpoints --clear-remote
41
+ ```
42
+
43
+ The websocket address of each endpoint is derived from its server URL unless `--websocket-server` or `--remote-websocket-server` sets it explicitly.
44
+
45
+ Changing a server address re-derives its websocket address, because one stored against the old server no longer points anywhere useful. If the websocket is served from somewhere other than `/graphql` on the same host and port — behind a reverse proxy, say — pass it alongside the new server address:
46
+
47
+ ```bash
48
+ control-agent endpoints --server http://192.168.1.20:3090 --websocket-server ws://192.168.1.20:3092/graphql
49
+ ```
50
+
51
+ ### Enrollment
52
+
53
+ ```bash
54
+ control-agent enrollment
55
+ control-agent unenroll
56
+ ```
57
+
58
+ `enrollment` reports whether this machine is enrolled and whether each configured endpoint answers for it. `unenroll` removes the local credential; the control plane still lists the agent until it is deleted there.
59
+
24
60
  Diagnostics:
25
61
 
26
62
  ```bash
@@ -4204,6 +4204,9 @@ var require_cross_spawn = __commonJS({
4204
4204
  }
4205
4205
  });
4206
4206
 
4207
+ // src/index.ts
4208
+ import { rm as rm14 } from "node:fs/promises";
4209
+
4207
4210
  // src/config.ts
4208
4211
  import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
4209
4212
  import { homedir } from "node:os";
@@ -4226,6 +4229,32 @@ function defaultWebSocketServer(server) {
4226
4229
  url.hash = "";
4227
4230
  return url.toString();
4228
4231
  }
4232
+ function agentEndpoints(config) {
4233
+ const endpoints2 = [
4234
+ {
4235
+ kind: "local",
4236
+ server: config.server,
4237
+ websocketServer: config.websocketServer
4238
+ }
4239
+ ];
4240
+ if (config.remoteServer) {
4241
+ endpoints2.push({
4242
+ kind: "remote",
4243
+ server: config.remoteServer,
4244
+ websocketServer: config.remoteWebSocketServer ?? defaultWebSocketServer(config.remoteServer)
4245
+ });
4246
+ }
4247
+ return endpoints2;
4248
+ }
4249
+ function configForEndpoint(config, endpoint) {
4250
+ return {
4251
+ ...config,
4252
+ server: endpoint.server,
4253
+ websocketServer: endpoint.websocketServer,
4254
+ remoteServer: null,
4255
+ remoteWebSocketServer: null
4256
+ };
4257
+ }
4229
4258
  async function loadConfig(path = configPath()) {
4230
4259
  try {
4231
4260
  const value = JSON.parse(await readFile(path, "utf8"));
@@ -4249,107 +4278,6 @@ async function saveConfig(config, path = configPath()) {
4249
4278
  await rename(temporaryPath, path);
4250
4279
  }
4251
4280
 
4252
- // ../agent-contract/src/index.ts
4253
- var CCUSAGE_REPORT_JOB_KIND = "ccusage.report";
4254
- var DEFAULT_AGENT_HEARTBEAT_INTERVAL_SECONDS = 15;
4255
- var MIN_AGENT_HEARTBEAT_INTERVAL_SECONDS = 5;
4256
- var MAX_AGENT_HEARTBEAT_INTERVAL_SECONDS = 300;
4257
- var DEFAULT_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 15;
4258
- var MIN_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 5;
4259
- var MAX_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 3600;
4260
- function objectValue(value, name) {
4261
- if (!value || typeof value !== "object" || Array.isArray(value)) {
4262
- throw new Error(`${name} must be an object`);
4263
- }
4264
- return value;
4265
- }
4266
- function numericValue(value, name) {
4267
- if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
4268
- throw new Error(`${name} must be a non-negative finite number`);
4269
- }
4270
- return value;
4271
- }
4272
- function stringValue(value, name) {
4273
- if (typeof value !== "string" || value.length === 0) {
4274
- throw new Error(`${name} must be a non-empty string`);
4275
- }
4276
- return value;
4277
- }
4278
- function stringArray(value, name) {
4279
- if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
4280
- return value.map((item, index) => stringValue(item, `${name}[${index}]`));
4281
- }
4282
- function tokenTotals(value, name) {
4283
- return {
4284
- inputTokens: numericValue(value.inputTokens, `${name}.inputTokens`),
4285
- outputTokens: numericValue(value.outputTokens, `${name}.outputTokens`),
4286
- cacheCreationTokens: numericValue(
4287
- value.cacheCreationTokens,
4288
- `${name}.cacheCreationTokens`
4289
- ),
4290
- cacheReadTokens: numericValue(
4291
- value.cacheReadTokens,
4292
- `${name}.cacheReadTokens`
4293
- ),
4294
- totalTokens: numericValue(value.totalTokens, `${name}.totalTokens`),
4295
- totalCost: numericValue(value.totalCost, `${name}.totalCost`)
4296
- };
4297
- }
4298
- function modelBreakdown(value, name) {
4299
- const model2 = objectValue(value, name);
4300
- return {
4301
- modelName: stringValue(model2.modelName, `${name}.modelName`),
4302
- inputTokens: numericValue(model2.inputTokens, `${name}.inputTokens`),
4303
- outputTokens: numericValue(model2.outputTokens, `${name}.outputTokens`),
4304
- cacheCreationTokens: numericValue(
4305
- model2.cacheCreationTokens,
4306
- `${name}.cacheCreationTokens`
4307
- ),
4308
- cacheReadTokens: numericValue(
4309
- model2.cacheReadTokens,
4310
- `${name}.cacheReadTokens`
4311
- ),
4312
- cost: numericValue(model2.cost, `${name}.cost`)
4313
- };
4314
- }
4315
- function dailyEntry(value, index) {
4316
- const name = `ccusage.daily[${index}]`;
4317
- const entry = objectValue(value, name);
4318
- const metadata = objectValue(entry.metadata, `${name}.metadata`);
4319
- if (!Array.isArray(entry.modelBreakdowns)) {
4320
- throw new Error(`${name}.modelBreakdowns must be an array`);
4321
- }
4322
- const period = stringValue(entry.period, `${name}.period`);
4323
- if (!/^\d{4}-\d{2}-\d{2}$/.test(period)) {
4324
- throw new Error(`${name}.period must use YYYY-MM-DD`);
4325
- }
4326
- return {
4327
- ...tokenTotals(entry, name),
4328
- agent: stringValue(entry.agent, `${name}.agent`),
4329
- period,
4330
- metadata: {
4331
- agents: stringArray(metadata.agents, `${name}.metadata.agents`)
4332
- },
4333
- modelBreakdowns: entry.modelBreakdowns.map(
4334
- (model2, modelIndex) => modelBreakdown(model2, `${name}.modelBreakdowns[${modelIndex}]`)
4335
- ),
4336
- modelsUsed: stringArray(entry.modelsUsed, `${name}.modelsUsed`)
4337
- };
4338
- }
4339
- function parseCcusageReport(value) {
4340
- const report = objectValue(value, "ccusage report");
4341
- if (!Array.isArray(report.daily)) {
4342
- throw new Error("ccusage.daily must be an array");
4343
- }
4344
- return {
4345
- daily: report.daily.map((entry, index) => dailyEntry(entry, index)),
4346
- totals: tokenTotals(
4347
- objectValue(report.totals, "ccusage.totals"),
4348
- "ccusage.totals"
4349
- )
4350
- };
4351
- }
4352
-
4353
4281
  // src/graphql-client.ts
4354
4282
  import { createReadStream } from "node:fs";
4355
4283
  import { rm, stat } from "node:fs/promises";
@@ -5588,6 +5516,140 @@ function subscribeToAgentEvents(client2, agentId, onEvent) {
5588
5516
  };
5589
5517
  }
5590
5518
 
5519
+ // src/endpoint-selection.ts
5520
+ var PROBE_TIMEOUT_MS = 5e3;
5521
+ var probeEndpoint = async (endpoint, config) => {
5522
+ try {
5523
+ const response = await new AgentGraphQLClient(
5524
+ endpoint.server,
5525
+ config.credential,
5526
+ PROBE_TIMEOUT_MS,
5527
+ config.headers
5528
+ ).self();
5529
+ return response.agentSelf?.id === config.agentId;
5530
+ } catch {
5531
+ return false;
5532
+ }
5533
+ };
5534
+ async function selectAgentEndpoint(config, signal, probe = probeEndpoint) {
5535
+ const endpoints2 = agentEndpoints(config);
5536
+ if (endpoints2.length === 1) return endpoints2[0];
5537
+ for (const endpoint of endpoints2) {
5538
+ if (signal.aborted) break;
5539
+ if (await probe(endpoint, config)) return endpoint;
5540
+ }
5541
+ return endpoints2[0];
5542
+ }
5543
+ async function findHealthyAlternate(config, active, signal, probe = probeEndpoint) {
5544
+ for (const endpoint of agentEndpoints(config)) {
5545
+ if (signal.aborted) break;
5546
+ if (endpoint.server === active.server) continue;
5547
+ if (await probe(endpoint, config)) return endpoint;
5548
+ }
5549
+ return void 0;
5550
+ }
5551
+
5552
+ // ../agent-contract/src/index.ts
5553
+ var CCUSAGE_REPORT_JOB_KIND = "ccusage.report";
5554
+ var DEFAULT_AGENT_HEARTBEAT_INTERVAL_SECONDS = 15;
5555
+ var MIN_AGENT_HEARTBEAT_INTERVAL_SECONDS = 5;
5556
+ var MAX_AGENT_HEARTBEAT_INTERVAL_SECONDS = 300;
5557
+ var DEFAULT_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 15;
5558
+ var MIN_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 5;
5559
+ var MAX_AGENT_JOB_RECONCILIATION_INTERVAL_SECONDS = 3600;
5560
+ function objectValue(value, name) {
5561
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5562
+ throw new Error(`${name} must be an object`);
5563
+ }
5564
+ return value;
5565
+ }
5566
+ function numericValue(value, name) {
5567
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
5568
+ throw new Error(`${name} must be a non-negative finite number`);
5569
+ }
5570
+ return value;
5571
+ }
5572
+ function stringValue(value, name) {
5573
+ if (typeof value !== "string" || value.length === 0) {
5574
+ throw new Error(`${name} must be a non-empty string`);
5575
+ }
5576
+ return value;
5577
+ }
5578
+ function stringArray(value, name) {
5579
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
5580
+ return value.map((item, index) => stringValue(item, `${name}[${index}]`));
5581
+ }
5582
+ function tokenTotals(value, name) {
5583
+ return {
5584
+ inputTokens: numericValue(value.inputTokens, `${name}.inputTokens`),
5585
+ outputTokens: numericValue(value.outputTokens, `${name}.outputTokens`),
5586
+ cacheCreationTokens: numericValue(
5587
+ value.cacheCreationTokens,
5588
+ `${name}.cacheCreationTokens`
5589
+ ),
5590
+ cacheReadTokens: numericValue(
5591
+ value.cacheReadTokens,
5592
+ `${name}.cacheReadTokens`
5593
+ ),
5594
+ totalTokens: numericValue(value.totalTokens, `${name}.totalTokens`),
5595
+ totalCost: numericValue(value.totalCost, `${name}.totalCost`)
5596
+ };
5597
+ }
5598
+ function modelBreakdown(value, name) {
5599
+ const model2 = objectValue(value, name);
5600
+ return {
5601
+ modelName: stringValue(model2.modelName, `${name}.modelName`),
5602
+ inputTokens: numericValue(model2.inputTokens, `${name}.inputTokens`),
5603
+ outputTokens: numericValue(model2.outputTokens, `${name}.outputTokens`),
5604
+ cacheCreationTokens: numericValue(
5605
+ model2.cacheCreationTokens,
5606
+ `${name}.cacheCreationTokens`
5607
+ ),
5608
+ cacheReadTokens: numericValue(
5609
+ model2.cacheReadTokens,
5610
+ `${name}.cacheReadTokens`
5611
+ ),
5612
+ cost: numericValue(model2.cost, `${name}.cost`)
5613
+ };
5614
+ }
5615
+ function dailyEntry(value, index) {
5616
+ const name = `ccusage.daily[${index}]`;
5617
+ const entry = objectValue(value, name);
5618
+ const metadata = objectValue(entry.metadata, `${name}.metadata`);
5619
+ if (!Array.isArray(entry.modelBreakdowns)) {
5620
+ throw new Error(`${name}.modelBreakdowns must be an array`);
5621
+ }
5622
+ const period = stringValue(entry.period, `${name}.period`);
5623
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(period)) {
5624
+ throw new Error(`${name}.period must use YYYY-MM-DD`);
5625
+ }
5626
+ return {
5627
+ ...tokenTotals(entry, name),
5628
+ agent: stringValue(entry.agent, `${name}.agent`),
5629
+ period,
5630
+ metadata: {
5631
+ agents: stringArray(metadata.agents, `${name}.metadata.agents`)
5632
+ },
5633
+ modelBreakdowns: entry.modelBreakdowns.map(
5634
+ (model2, modelIndex) => modelBreakdown(model2, `${name}.modelBreakdowns[${modelIndex}]`)
5635
+ ),
5636
+ modelsUsed: stringArray(entry.modelsUsed, `${name}.modelsUsed`)
5637
+ };
5638
+ }
5639
+ function parseCcusageReport(value) {
5640
+ const report = objectValue(value, "ccusage report");
5641
+ if (!Array.isArray(report.daily)) {
5642
+ throw new Error("ccusage.daily must be an array");
5643
+ }
5644
+ return {
5645
+ daily: report.daily.map((entry, index) => dailyEntry(entry, index)),
5646
+ totals: tokenTotals(
5647
+ objectValue(report.totals, "ccusage.totals"),
5648
+ "ccusage.totals"
5649
+ )
5650
+ };
5651
+ }
5652
+
5591
5653
  // src/inventory.ts
5592
5654
  import { statfsSync } from "node:fs";
5593
5655
  import {
@@ -12062,6 +12124,41 @@ var deleteSigningIdentity = async (payload, timeoutMs, signal) => {
12062
12124
  // src/handlers/ccusage.ts
12063
12125
  import { Buffer as Buffer2 } from "node:buffer";
12064
12126
 
12127
+ // src/executable-lookup.ts
12128
+ import { accessSync, constants } from "node:fs";
12129
+ import { homedir as homedir4 } from "node:os";
12130
+ import { delimiter, dirname as dirname3, join as join6 } from "node:path";
12131
+ function isExecutable(path) {
12132
+ try {
12133
+ accessSync(path, constants.X_OK);
12134
+ return true;
12135
+ } catch {
12136
+ return false;
12137
+ }
12138
+ }
12139
+ function findExecutable(name, options = {}) {
12140
+ const env = options.env ?? process.env;
12141
+ const home = options.home ?? homedir4();
12142
+ const platform2 = options.platform ?? process.platform;
12143
+ const executable = platform2 === "win32" ? `${name}.exe` : name;
12144
+ const directories = [
12145
+ ...(env.PATH ?? "").split(delimiter).filter(Boolean),
12146
+ join6(home, ".local", "bin"),
12147
+ join6(home, ".bun", "bin"),
12148
+ join6(home, ".npm-global", "bin"),
12149
+ // Global npm installs land next to the node binary running this agent.
12150
+ options.nodeDirectory ?? dirname3(process.execPath),
12151
+ ...platform2 === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin"] : []
12152
+ ];
12153
+ const override = options.overrideVariable ? env[options.overrideVariable]?.trim() : void 0;
12154
+ const candidates = [
12155
+ ...override ? [override] : [],
12156
+ ...directories.map((directory) => join6(directory, executable))
12157
+ ];
12158
+ const canExecute = options.isExecutable ?? isExecutable;
12159
+ return [...new Set(candidates)].find(canExecute);
12160
+ }
12161
+
12065
12162
  // src/process-runner.ts
12066
12163
  import { spawn as spawn5 } from "node:child_process";
12067
12164
  async function runProcess(options) {
@@ -12180,11 +12277,19 @@ function validateCcusagePayload(payload) {
12180
12277
  }
12181
12278
  async function runCcusage(payload, timeoutMs, signal, onLog) {
12182
12279
  validateCcusagePayload(payload);
12280
+ const command3 = findExecutable("ccusage", {
12281
+ overrideVariable: "CONTROL_AGENT_CCUSAGE_EXECUTABLE"
12282
+ });
12283
+ if (!command3) {
12284
+ throw new Error(
12285
+ "ccusage was not found. Install it (brew install ccusage, or npm install -g ccusage) or set CONTROL_AGENT_CCUSAGE_EXECUTABLE to its full path."
12286
+ );
12287
+ }
12183
12288
  const stdout = [];
12184
12289
  let stdoutBytes = 0;
12185
12290
  let exceededLimit = false;
12186
12291
  const result = await runProcess({
12187
- command: "ccusage",
12292
+ command: command3,
12188
12293
  args: ["--json"],
12189
12294
  timeoutMs,
12190
12295
  signal,
@@ -12229,8 +12334,8 @@ import {
12229
12334
  stat as stat5,
12230
12335
  statfs
12231
12336
  } from "node:fs/promises";
12232
- import { homedir as homedir4 } from "node:os";
12233
- import { basename as basename4, dirname as dirname3, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
12337
+ import { homedir as homedir5 } from "node:os";
12338
+ import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "node:path";
12234
12339
  var successfulProcess4 = {
12235
12340
  exitCode: 0,
12236
12341
  signal: null,
@@ -12247,7 +12352,7 @@ function safeRelativePath3(value) {
12247
12352
  }
12248
12353
  async function configuredRoots(payload) {
12249
12354
  if (payload.mode === "DEFAULT") {
12250
- return [join6(homedir4(), "Library", "Developer", "Xcode", "DerivedData")];
12355
+ return [join7(homedir5(), "Library", "Developer", "Xcode", "DerivedData")];
12251
12356
  }
12252
12357
  if (payload.mode === "ABSOLUTE") {
12253
12358
  if (!payload.path || !isAbsolute4(payload.path)) {
@@ -12309,10 +12414,10 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
12309
12414
  for (const child of children) {
12310
12415
  signal.throwIfAborted();
12311
12416
  if (!child.isDirectory()) continue;
12312
- const path = join6(rootPath2, child.name);
12417
+ const path = join7(rootPath2, child.name);
12313
12418
  const information = await lstat2(path);
12314
12419
  const projectPath = await workspacePath(
12315
- join6(path, "info.plist"),
12420
+ join7(path, "info.plist"),
12316
12421
  timeoutMs,
12317
12422
  signal
12318
12423
  );
@@ -12330,8 +12435,8 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
12330
12435
  return { entries, warning: null };
12331
12436
  }
12332
12437
  async function scanDeviceSupport(signal) {
12333
- const configuredRoot = join6(
12334
- process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ?? homedir4(),
12438
+ const configuredRoot = join7(
12439
+ process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ?? homedir5(),
12335
12440
  ...process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ? [] : ["Library", "Developer", "Xcode", "iOS DeviceSupport"]
12336
12441
  );
12337
12442
  signal.throwIfAborted();
@@ -12351,7 +12456,7 @@ async function scanDeviceSupport(signal) {
12351
12456
  const children = await readdir3(rootPath2, { withFileTypes: true });
12352
12457
  return {
12353
12458
  entries: children.filter((child) => child.isDirectory() && !child.isSymbolicLink()).map((child) => ({
12354
- path: join6(rootPath2, child.name),
12459
+ path: join7(rootPath2, child.name),
12355
12460
  rootPath: rootPath2,
12356
12461
  name: child.name,
12357
12462
  kind: "DEVICE_SUPPORT",
@@ -12398,7 +12503,7 @@ function assertDirectChild(rootPath2, path) {
12398
12503
  }
12399
12504
  const root = resolve4(rootPath2);
12400
12505
  const target2 = resolve4(path);
12401
- if (target2 === root || dirname3(target2) !== root || basename4(target2) !== basename4(path)) {
12506
+ if (target2 === root || dirname4(target2) !== root || basename4(target2) !== basename4(path)) {
12402
12507
  throw new Error("Build Data target must be a direct child of its root");
12403
12508
  }
12404
12509
  }
@@ -12412,7 +12517,7 @@ async function allocatedBytes(path, signal) {
12412
12517
  try {
12413
12518
  for await (const entry of directory) {
12414
12519
  signal.throwIfAborted();
12415
- total += await allocatedBytes(join6(path, entry.name), signal);
12520
+ total += await allocatedBytes(join7(path, entry.name), signal);
12416
12521
  }
12417
12522
  } finally {
12418
12523
  await directory.close().catch(() => void 0);
@@ -12427,7 +12532,7 @@ var sizeBuildData = async (rawPayload, _timeoutMs, signal) => {
12427
12532
  try {
12428
12533
  assertDirectChild(target2.rootPath, target2.path);
12429
12534
  const rootPath2 = await realpath4(target2.rootPath);
12430
- if (rootPath2 !== resolve4(target2.rootPath) || dirname3(resolve4(target2.path)) !== rootPath2) {
12535
+ if (rootPath2 !== resolve4(target2.rootPath) || dirname4(resolve4(target2.path)) !== rootPath2) {
12431
12536
  throw new Error("Build Data root changed since the scan");
12432
12537
  }
12433
12538
  const stats = await lstat2(target2.path);
@@ -12458,7 +12563,7 @@ var deleteBuildData = async (rawPayload, _timeoutMs, signal) => {
12458
12563
  try {
12459
12564
  assertDirectChild(target2.rootPath, target2.path);
12460
12565
  const rootPath2 = await realpath4(target2.rootPath);
12461
- if (rootPath2 !== resolve4(target2.rootPath) || dirname3(resolve4(target2.path)) !== rootPath2) {
12566
+ if (rootPath2 !== resolve4(target2.rootPath) || dirname4(resolve4(target2.path)) !== rootPath2) {
12462
12567
  throw new Error("Build Data root changed since the scan");
12463
12568
  }
12464
12569
  const stats = await lstat2(target2.path);
@@ -12512,13 +12617,13 @@ async function inspectVolume(configuredPath, role, signal, allowMissing) {
12512
12617
  let candidate = resolve4(configuredPath);
12513
12618
  for (; ; ) {
12514
12619
  signal.throwIfAborted();
12515
- const parent = dirname3(candidate);
12620
+ const parent = dirname4(candidate);
12516
12621
  if (parent === candidate) throw error;
12517
12622
  missingSegments.unshift(basename4(candidate));
12518
12623
  candidate = parent;
12519
12624
  try {
12520
12625
  inspectionPath = await realpath4(candidate);
12521
- canonicalPath = join6(inspectionPath, ...missingSegments);
12626
+ canonicalPath = join7(inspectionPath, ...missingSegments);
12522
12627
  break;
12523
12628
  } catch (parentError) {
12524
12629
  if (parentError.code !== "ENOENT") {
@@ -12625,8 +12730,8 @@ async function collectAgentDiskSpace(configuration, signal) {
12625
12730
 
12626
12731
  // src/handlers/codebases.ts
12627
12732
  import { readdir as readdir4, realpath as realpath5, stat as stat6 } from "node:fs/promises";
12628
- import { homedir as homedir5 } from "node:os";
12629
- import { dirname as dirname4, join as join7, resolve as resolve5 } from "node:path";
12733
+ import { homedir as homedir6 } from "node:os";
12734
+ import { dirname as dirname5, join as join8, resolve as resolve5 } from "node:path";
12630
12735
  var successfulProcess5 = {
12631
12736
  exitCode: 0,
12632
12737
  signal: null,
@@ -12703,7 +12808,7 @@ function baseSnapshot(folder) {
12703
12808
  }
12704
12809
  async function fetchedAt(commonDirectory) {
12705
12810
  try {
12706
- return (await stat6(join7(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
12811
+ return (await stat6(join8(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
12707
12812
  } catch {
12708
12813
  return null;
12709
12814
  }
@@ -12882,7 +12987,7 @@ async function inspectCodebase(selectedFolder, timeoutMs, signal, expectedOrigin
12882
12987
  }
12883
12988
  var browseCodebaseDirectories = async (payload) => {
12884
12989
  const input = codebaseBrowsePayload(payload);
12885
- const homePath = await realpath5(homedir5());
12990
+ const homePath = await realpath5(homedir6());
12886
12991
  const path = await realpath5(input.path ? resolve5(input.path) : homePath);
12887
12992
  if (!(await stat6(path)).isDirectory())
12888
12993
  throw new Error("Path is not a directory");
@@ -12892,12 +12997,12 @@ var browseCodebaseDirectories = async (payload) => {
12892
12997
  if (candidate.isDirectory()) {
12893
12998
  entries.push({
12894
12999
  name: candidate.name,
12895
- path: join7(path, candidate.name),
13000
+ path: join8(path, candidate.name),
12896
13001
  hidden: candidate.name.startsWith(".")
12897
13002
  });
12898
13003
  } else if (candidate.isSymbolicLink()) {
12899
13004
  try {
12900
- const target2 = join7(path, candidate.name);
13005
+ const target2 = join8(path, candidate.name);
12901
13006
  if ((await stat6(target2)).isDirectory()) {
12902
13007
  entries.push({
12903
13008
  name: candidate.name,
@@ -12912,7 +13017,7 @@ var browseCodebaseDirectories = async (payload) => {
12912
13017
  entries.sort((first, second) => first.name.localeCompare(second.name));
12913
13018
  const listing = {
12914
13019
  path,
12915
- parentPath: dirname4(path) === path ? null : dirname4(path),
13020
+ parentPath: dirname5(path) === path ? null : dirname5(path),
12916
13021
  homePath,
12917
13022
  entries: entries.slice(0, 1e3),
12918
13023
  truncated: entries.length > 1e3
@@ -13510,8 +13615,8 @@ import {
13510
13615
  stat as stat7,
13511
13616
  writeFile as writeFile5
13512
13617
  } from "node:fs/promises";
13513
- import { homedir as homedir6 } from "node:os";
13514
- import { dirname as dirname5, isAbsolute as isAbsolute5, join as join8, relative as relative6, resolve as resolve6, sep as sep6 } from "node:path";
13618
+ import { homedir as homedir7 } from "node:os";
13619
+ import { dirname as dirname6, isAbsolute as isAbsolute5, join as join9, relative as relative6, resolve as resolve6, sep as sep6 } from "node:path";
13515
13620
  import { promisify } from "node:util";
13516
13621
  var executeFile = promisify(execFile);
13517
13622
  var successfulProcess6 = {
@@ -13572,17 +13677,17 @@ async function directoryExists(path) {
13572
13677
  }
13573
13678
  }
13574
13679
  async function configuredTools(tools) {
13575
- const homePath = homedir6();
13680
+ const homePath = homedir7();
13576
13681
  return Promise.all(
13577
13682
  AI_TOOLS.filter((tool) => tools.includes(tool)).map(async (tool) => ({
13578
13683
  tool,
13579
- configured: await directoryExists(join8(homePath, ...CONFIG_PATHS[tool])),
13684
+ configured: await directoryExists(join9(homePath, ...CONFIG_PATHS[tool])),
13580
13685
  homePath
13581
13686
  }))
13582
13687
  );
13583
13688
  }
13584
13689
  function rootPath(location) {
13585
- const base = location.scope === "GLOBAL" ? homedir6() : location.folder;
13690
+ const base = location.scope === "GLOBAL" ? homedir7() : location.folder;
13586
13691
  const parts = location.scope === "GLOBAL" ? GLOBAL_ROOTS[location.rootKind] : PROJECT_ROOTS[location.rootKind];
13587
13692
  const path = resolve6(base, ...parts);
13588
13693
  const normalizedBase = resolve6(base);
@@ -13599,7 +13704,7 @@ async function listPackageFiles(skillDirectory) {
13599
13704
  let totalBytes = 0;
13600
13705
  const visit = async (directory) => {
13601
13706
  for (const entry of await readdir5(directory, { withFileTypes: true })) {
13602
- const path = join8(directory, entry.name);
13707
+ const path = join9(directory, entry.name);
13603
13708
  const metadata = await lstat3(path);
13604
13709
  if (metadata.isSymbolicLink()) {
13605
13710
  throw new Error(
@@ -13688,7 +13793,7 @@ async function scanRoot2(location, configured, targets, warnings) {
13688
13793
  const installations = [];
13689
13794
  for (const entry of await readdir5(root, { withFileTypes: true })) {
13690
13795
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
13691
- const directory = join8(root, entry.name);
13796
+ const directory = join9(root, entry.name);
13692
13797
  try {
13693
13798
  const skillPackage = await readPackage(directory);
13694
13799
  const relativeDirectory = location.folder ? relative6(location.folder, directory).split(sep6).join("/") : "";
@@ -13767,7 +13872,7 @@ var readSkills = async (payload) => {
13767
13872
  }
13768
13873
  packages.push({
13769
13874
  ...request,
13770
- package: await readPackage(join8(root, request.skillName))
13875
+ package: await readPackage(join9(root, request.skillName))
13771
13876
  });
13772
13877
  }
13773
13878
  return { ...successfulProcess6, packages };
@@ -13785,7 +13890,7 @@ async function gitExcludePath(folder) {
13785
13890
  }
13786
13891
  async function updateManagedExclude(folder, relativeSkillPath, present) {
13787
13892
  const excludePath = await gitExcludePath(folder);
13788
- await mkdir4(dirname5(excludePath), { recursive: true });
13893
+ await mkdir4(dirname6(excludePath), { recursive: true });
13789
13894
  let existing = "";
13790
13895
  try {
13791
13896
  existing = await readFile5(excludePath, "utf8");
@@ -13829,18 +13934,18 @@ async function writePackage(location, skillPackage) {
13829
13934
  throw new Error("New skill copies may only be written to shared roots");
13830
13935
  }
13831
13936
  const root = rootPath(location);
13832
- const destination = join8(root, skillPackage.name);
13937
+ const destination = join9(root, skillPackage.name);
13833
13938
  await assertUntracked(location, destination);
13834
13939
  await mkdir4(root, { recursive: true });
13835
- const temporary = join8(root, `.${skillPackage.name}.${randomUUID3()}.tmp`);
13836
- const backup = join8(root, `.${skillPackage.name}.${randomUUID3()}.bak`);
13940
+ const temporary = join9(root, `.${skillPackage.name}.${randomUUID3()}.tmp`);
13941
+ const backup = join9(root, `.${skillPackage.name}.${randomUUID3()}.bak`);
13837
13942
  await mkdir4(temporary, { recursive: true });
13838
13943
  try {
13839
13944
  for (const file of skillPackage.files) {
13840
13945
  const path = resolve6(temporary, ...file.path.split("/"));
13841
13946
  if (!path.startsWith(`${temporary}${sep6}`))
13842
13947
  throw new Error("Skill file escaped temporary directory");
13843
- await mkdir4(dirname5(path), { recursive: true });
13948
+ await mkdir4(dirname6(path), { recursive: true });
13844
13949
  await writeFile5(path, Buffer.from(file.contentsBase64, "base64"));
13845
13950
  await chmod3(path, file.executable ? 493 : 420);
13846
13951
  }
@@ -13871,7 +13976,7 @@ var applySkills = async (payload) => {
13871
13976
  for (const operation of operations) {
13872
13977
  const root = rootPath(operation);
13873
13978
  const skillName = operation.kind === "WRITE" ? operation.package.name : operation.skillName;
13874
- const destination = join8(root, skillName);
13979
+ const destination = join9(root, skillName);
13875
13980
  await assertUntracked(operation, destination);
13876
13981
  if (operation.kind === "WRITE") {
13877
13982
  await writePackage(operation, operation.package);
@@ -13901,8 +14006,8 @@ var applySkills = async (payload) => {
13901
14006
 
13902
14007
  // src/handlers/runs.ts
13903
14008
  import { readFile as readFile6, readdir as readdir6, stat as stat8 } from "node:fs/promises";
13904
- import { homedir as homedir7 } from "node:os";
13905
- import { join as join9 } from "node:path";
14009
+ import { homedir as homedir8 } from "node:os";
14010
+ import { join as join10 } from "node:path";
13906
14011
  var successfulProcess7 = {
13907
14012
  exitCode: 0,
13908
14013
  signal: null,
@@ -13917,7 +14022,7 @@ async function fileExists(path) {
13917
14022
  }
13918
14023
  }
13919
14024
  async function findClaudeSessionFile(nativeId, home) {
13920
- const projectsDir = join9(home, ".claude", "projects");
14025
+ const projectsDir = join10(home, ".claude", "projects");
13921
14026
  let entries;
13922
14027
  try {
13923
14028
  entries = await readdir6(projectsDir, { withFileTypes: true });
@@ -13927,13 +14032,13 @@ async function findClaudeSessionFile(nativeId, home) {
13927
14032
  const target2 = `${nativeId}.jsonl`;
13928
14033
  for (const entry of entries) {
13929
14034
  if (!entry.isDirectory()) continue;
13930
- const candidate = join9(projectsDir, entry.name, target2);
14035
+ const candidate = join10(projectsDir, entry.name, target2);
13931
14036
  if (await fileExists(candidate)) return candidate;
13932
14037
  }
13933
14038
  return null;
13934
14039
  }
13935
14040
  async function findCodexSessionFile(nativeId, home) {
13936
- const sessionsDir = join9(home, ".codex", "sessions");
14041
+ const sessionsDir = join10(home, ".codex", "sessions");
13937
14042
  const suffix = `-${nativeId}.jsonl`;
13938
14043
  const walk = async (dir) => {
13939
14044
  let entries;
@@ -13943,7 +14048,7 @@ async function findCodexSessionFile(nativeId, home) {
13943
14048
  return null;
13944
14049
  }
13945
14050
  for (const entry of entries) {
13946
- const path = join9(dir, entry.name);
14051
+ const path = join10(dir, entry.name);
13947
14052
  if (entry.isDirectory()) {
13948
14053
  const found = await walk(path);
13949
14054
  if (found) return found;
@@ -13960,7 +14065,7 @@ async function locateSessionFile(payload, home) {
13960
14065
  }
13961
14066
  var readRunSession = async (payload) => {
13962
14067
  const input = parseRunSessionReadPayload(payload);
13963
- const path = await locateSessionFile(input, homedir7());
14068
+ const path = await locateSessionFile(input, homedir8());
13964
14069
  if (!path) {
13965
14070
  throw new Error(
13966
14071
  `No ${input.provider === "CLAUDE" ? "Claude" : "Codex"} session file found for this run on this agent`
@@ -13990,13 +14095,13 @@ import {
13990
14095
  writeFile as writeFile6
13991
14096
  } from "node:fs/promises";
13992
14097
  import { tmpdir as tmpdir5 } from "node:os";
13993
- import { join as join11 } from "node:path";
14098
+ import { join as join12 } from "node:path";
13994
14099
 
13995
14100
  // src/runs/git-checkpoint.ts
13996
14101
  import { execFile as execFile2 } from "node:child_process";
13997
14102
  import { copyFile, mkdtemp as mkdtemp4, rm as rm7 } from "node:fs/promises";
13998
14103
  import { tmpdir as tmpdir4 } from "node:os";
13999
- import { join as join10 } from "node:path";
14104
+ import { join as join11 } from "node:path";
14000
14105
  import { promisify as promisify2 } from "node:util";
14001
14106
  var execFileAsync = promisify2(execFile2);
14002
14107
  async function git2(cwd, args, env) {
@@ -14111,12 +14216,12 @@ async function captureGitCheckpoint(cwd, runId, kind) {
14111
14216
  const manifest = await optionalGit(cwd, ["status", "--porcelain=v2", "-z"]);
14112
14217
  const staged = await optionalGit(cwd, ["diff", "--cached", "--stat", "HEAD"]);
14113
14218
  const unstaged = await optionalGit(cwd, ["diff", "--stat", "HEAD"]);
14114
- const directory = await mkdtemp4(join10(tmpdir4(), "aide-run-index-"));
14219
+ const directory = await mkdtemp4(join11(tmpdir4(), "aide-run-index-"));
14115
14220
  let worktreeTree = null;
14116
14221
  let refName = null;
14117
14222
  try {
14118
14223
  const actualIndex = await git2(cwd, ["rev-parse", "--git-path", "index"]);
14119
- const temporaryIndex = join10(directory, "index");
14224
+ const temporaryIndex = join11(directory, "index");
14120
14225
  try {
14121
14226
  await copyFile(actualIndex, temporaryIndex);
14122
14227
  } catch {
@@ -14186,10 +14291,10 @@ var runWorkflowTerminal = async (payloadValue, timeoutMs, signal, onLog, context
14186
14291
  const information = await stat9(cwd);
14187
14292
  if (!information.isDirectory())
14188
14293
  throw new Error("Workflow working directory is not a directory");
14189
- const directory = await mkdtemp5(join11(tmpdir5(), "aide-workflow-"));
14294
+ const directory = await mkdtemp5(join12(tmpdir5(), "aide-workflow-"));
14190
14295
  const extension2 = payload.interpreter === "NODE" ? "mjs" : "sh";
14191
- const scriptPath = join11(directory, `step.${extension2}`);
14192
- const sessionPath = join11(directory, "session.json");
14296
+ const scriptPath = join12(directory, `step.${extension2}`);
14297
+ const sessionPath = join12(directory, "session.json");
14193
14298
  try {
14194
14299
  await writeFile6(scriptPath, `${payload.script}
14195
14300
  `, { mode: 384 });
@@ -14282,7 +14387,7 @@ var runWorkflowGitCheckpoint = async (payloadValue, _timeoutMs, signal) => {
14282
14387
  import { createWriteStream as createWriteStream2, watch } from "node:fs";
14283
14388
  import { mkdtemp as mkdtemp6, open as open2, readFile as readFile8, realpath as realpath8, rm as rm9, stat as stat10 } from "node:fs/promises";
14284
14389
  import { tmpdir as tmpdir6 } from "node:os";
14285
- import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute6, join as join12, relative as relative7 } from "node:path";
14390
+ import { basename as basename5, dirname as dirname7, isAbsolute as isAbsolute6, join as join13, relative as relative7 } from "node:path";
14286
14391
  import { spawn as spawn6 } from "node:child_process";
14287
14392
  var successfulProcess8 = {
14288
14393
  exitCode: 0,
@@ -14337,7 +14442,7 @@ async function rebaseInProgress(folder, timeoutMs, signal) {
14337
14442
  const candidate = pathResult.stdout.trim();
14338
14443
  if (!candidate) continue;
14339
14444
  try {
14340
- await stat10(isAbsolute6(candidate) ? candidate : join12(folder, candidate));
14445
+ await stat10(isAbsolute6(candidate) ? candidate : join13(folder, candidate));
14341
14446
  return true;
14342
14447
  } catch {
14343
14448
  }
@@ -14357,7 +14462,7 @@ async function rebaseBranch(folder, timeoutMs, signal) {
14357
14462
  if (!candidate) continue;
14358
14463
  try {
14359
14464
  const headName = (await readFile8(
14360
- isAbsolute6(candidate) ? candidate : join12(folder, candidate),
14465
+ isAbsolute6(candidate) ? candidate : join13(folder, candidate),
14361
14466
  "utf8"
14362
14467
  )).trim();
14363
14468
  if (headName.startsWith("refs/heads/")) {
@@ -15003,7 +15108,7 @@ async function comparisonSides(input, folder, timeoutMs, signal) {
15003
15108
  if (!input.path) return { before: null, after: null };
15004
15109
  const path = input.path;
15005
15110
  const previousPath = input.previousPath ?? path;
15006
- const currentPath = join12(folder, path);
15111
+ const currentPath = join13(folder, path);
15007
15112
  if (input.scope === "UNTRACKED") {
15008
15113
  return { before: null, after: { kind: "FILE", path: currentPath } };
15009
15114
  }
@@ -15178,7 +15283,7 @@ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
15178
15283
  let patch;
15179
15284
  if (input.scope === "UNTRACKED") {
15180
15285
  const bounded = await readBoundedFile(
15181
- join12(folder, input.path),
15286
+ join13(folder, input.path),
15182
15287
  MAX_DIFF_BYTES
15183
15288
  );
15184
15289
  const contents = bounded.contents;
@@ -15360,8 +15465,8 @@ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, conte
15360
15465
  if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
15361
15466
  throw new Error("Diff image exceeds the 20 MiB limit");
15362
15467
  }
15363
- temporaryDirectory = await mkdtemp6(join12(tmpdir6(), "ade-diff-image-"));
15364
- uploadPath = join12(temporaryDirectory, basename5(input.path));
15468
+ temporaryDirectory = await mkdtemp6(join13(tmpdir6(), "ade-diff-image-"));
15469
+ uploadPath = join13(temporaryDirectory, basename5(input.path));
15365
15470
  await writeGitObject(
15366
15471
  folder,
15367
15472
  selected.specification,
@@ -15624,8 +15729,8 @@ var branchWorktree = async (payload, timeoutMs, signal) => {
15624
15729
  timeoutMs,
15625
15730
  signal
15626
15731
  ) : input.candidates[0];
15627
- const targetFolder = input.action === "CREATE" ? join12(
15628
- dirname6(rootFolder),
15732
+ const targetFolder = input.action === "CREATE" ? join13(
15733
+ dirname7(rootFolder),
15629
15734
  `${basename5(rootFolder)}-${branch.replaceAll("/", "-")}`
15630
15735
  ) : changeFolder;
15631
15736
  if (input.action === "CREATE") {
@@ -15864,8 +15969,8 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
15864
15969
  `Branch ${input.branch} is already checked out in ${occupiedFolder}`
15865
15970
  );
15866
15971
  }
15867
- targetFolder = join12(
15868
- dirname6(rootFolder),
15972
+ targetFolder = join13(
15973
+ dirname7(rootFolder),
15869
15974
  `${basename5(rootFolder)}-${input.branch.replaceAll("/", "-")}`
15870
15975
  );
15871
15976
  try {
@@ -16804,7 +16909,7 @@ var CodebaseMonitor = class {
16804
16909
  // src/runs/run-manager.ts
16805
16910
  import { execFile as execFile4 } from "node:child_process";
16806
16911
  import { mkdir as mkdir6, rm as rm13 } from "node:fs/promises";
16807
- import { dirname as dirname8, join as join17 } from "node:path";
16912
+ import { dirname as dirname9, join as join17 } from "node:path";
16808
16913
  import { promisify as promisify4 } from "node:util";
16809
16914
 
16810
16915
  // src/runs/journal.ts
@@ -16817,7 +16922,7 @@ import {
16817
16922
  rm as rm10,
16818
16923
  writeFile as writeFile7
16819
16924
  } from "node:fs/promises";
16820
- import { dirname as dirname7, join as join13 } from "node:path";
16925
+ import { dirname as dirname8, join as join14 } from "node:path";
16821
16926
  var RunJournal = class {
16822
16927
  constructor(runId, attemptId) {
16823
16928
  this.runId = runId;
@@ -16836,13 +16941,13 @@ var RunJournal = class {
16836
16941
  this.attemptId = attemptId;
16837
16942
  }
16838
16943
  get directory() {
16839
- return join13(dirname7(configPath()), "runs", this.runId);
16944
+ return join14(dirname8(configPath()), "runs", this.runId);
16840
16945
  }
16841
16946
  get eventsPath() {
16842
- return join13(this.directory, "events.jsonl");
16947
+ return join14(this.directory, "events.jsonl");
16843
16948
  }
16844
16949
  get statePath() {
16845
- return join13(this.directory, "state.json");
16950
+ return join14(this.directory, "state.json");
16846
16951
  }
16847
16952
  async load() {
16848
16953
  if (this.loaded) return;
@@ -16957,9 +17062,6 @@ var RunJournal = class {
16957
17062
 
16958
17063
  // src/runs/claude-adapter.ts
16959
17064
  import { randomUUID as randomUUID5 } from "node:crypto";
16960
- import { accessSync, constants } from "node:fs";
16961
- import { homedir as homedir8 } from "node:os";
16962
- import { delimiter, join as join15 } from "node:path";
16963
17065
 
16964
17066
  // ../../node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
16965
17067
  import { createRequire as RV } from "node:module";
@@ -42767,27 +42869,11 @@ function answerArrays(value) {
42767
42869
  }
42768
42870
 
42769
42871
  // src/runs/claude-adapter.ts
42770
- function isExecutable(path) {
42771
- try {
42772
- accessSync(path, constants.X_OK);
42773
- return true;
42774
- } catch {
42775
- return false;
42776
- }
42777
- }
42778
42872
  function findClaudeCodeExecutable(options = {}) {
42779
- const env = options.env ?? process.env;
42780
- const home = options.home ?? homedir8();
42781
- const platform2 = options.platform ?? process.platform;
42782
- const executable = platform2 === "win32" ? "claude.exe" : "claude";
42783
- const candidates = [
42784
- env.CONTROL_AGENT_CLAUDE_EXECUTABLE?.trim(),
42785
- ...(env.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => join15(directory, executable)),
42786
- join15(home, ".local", "bin", executable),
42787
- ...platform2 === "darwin" ? ["/opt/homebrew/bin/claude", "/usr/local/bin/claude"] : []
42788
- ].filter((candidate) => Boolean(candidate));
42789
- const canExecute = options.isExecutable ?? isExecutable;
42790
- return [...new Set(candidates)].find(canExecute);
42873
+ return findExecutable("claude", {
42874
+ ...options,
42875
+ overrideVariable: "CONTROL_AGENT_CLAUDE_EXECUTABLE"
42876
+ });
42791
42877
  }
42792
42878
  var MessageQueue = class {
42793
42879
  values = [];
@@ -50011,7 +50097,7 @@ var RunManager = class {
50011
50097
  5
50012
50098
  );
50013
50099
  await this.completeCommand(command3.id, "SUCCEEDED");
50014
- await rm13(join17(dirname8(configPath()), "runs", run.id, "attachments"), {
50100
+ await rm13(join17(dirname9(configPath()), "runs", run.id, "attachments"), {
50015
50101
  recursive: true,
50016
50102
  force: true
50017
50103
  }).catch(() => void 0);
@@ -50193,7 +50279,7 @@ var RunManager = class {
50193
50279
  this.active.delete(run.id);
50194
50280
  this.interruptionRequests.delete(run.id);
50195
50281
  this.executingCommands.delete(command3.id);
50196
- await rm13(join17(dirname8(configPath()), "runs", run.id, "attachments"), {
50282
+ await rm13(join17(dirname9(configPath()), "runs", run.id, "attachments"), {
50197
50283
  recursive: true,
50198
50284
  force: true
50199
50285
  }).catch(() => void 0);
@@ -50421,7 +50507,7 @@ var RunManager = class {
50421
50507
  }
50422
50508
  async stageAttachments(runId, attachments) {
50423
50509
  if (!attachments.length) return [];
50424
- const directory = join17(dirname8(configPath()), "runs", runId, "attachments");
50510
+ const directory = join17(dirname9(configPath()), "runs", runId, "attachments");
50425
50511
  await mkdir6(directory, { recursive: true, mode: 448 });
50426
50512
  return Promise.all(
50427
50513
  attachments.map(async (attachment) => {
@@ -50549,7 +50635,8 @@ var DISK_SPACE_STALE_AFTER_SECONDS = DISK_SPACE_POLL_INTERVAL_SECONDS * 2;
50549
50635
  function configuredIntervalMs(value, fallbackSeconds, minSeconds, maxSeconds) {
50550
50636
  return (Number.isInteger(value) && value >= minSeconds && value <= maxSeconds ? value : fallbackSeconds) * 1e3;
50551
50637
  }
50552
- async function runAgent(config, signal) {
50638
+ var UNHEALTHY_HEARTBEAT_FAILURES = 3;
50639
+ async function runAgentSession(config, signal, onUnhealthy) {
50553
50640
  const client2 = new AgentGraphQLClient(
50554
50641
  config.server,
50555
50642
  config.credential,
@@ -50571,6 +50658,7 @@ async function runAgent(config, signal) {
50571
50658
  let diskSpaceRunning = false;
50572
50659
  let diskSpaceIntervalMs = DISK_SPACE_POLL_INTERVAL_SECONDS * 1e3;
50573
50660
  let startupRecoveryPending = true;
50661
+ let heartbeatFailures = 0;
50574
50662
  const interruptedJobs = /* @__PURE__ */ new Set();
50575
50663
  const reconcileJobs = async (recoverInterrupted) => {
50576
50664
  let jobs;
@@ -50653,11 +50741,17 @@ async function runAgent(config, signal) {
50653
50741
  heartbeatTimer = void 0;
50654
50742
  try {
50655
50743
  await client2.heartbeat(collectInventory());
50744
+ heartbeatFailures = 0;
50656
50745
  } catch (error) {
50746
+ heartbeatFailures += 1;
50657
50747
  console.error(
50658
50748
  "Heartbeat failed:",
50659
50749
  error instanceof Error ? error.message : error
50660
50750
  );
50751
+ if (heartbeatFailures >= UNHEALTHY_HEARTBEAT_FAILURES && onUnhealthy) {
50752
+ heartbeatFailures = 0;
50753
+ onUnhealthy();
50754
+ }
50661
50755
  } finally {
50662
50756
  heartbeatRunning = false;
50663
50757
  scheduleHeartbeat();
@@ -50802,6 +50896,53 @@ async function runAgent(config, signal) {
50802
50896
  await executor.cancelAll();
50803
50897
  await runManager.close();
50804
50898
  }
50899
+ async function runAgent(config, signal) {
50900
+ const failoverEnabled = agentEndpoints(config).length > 1;
50901
+ let preferred;
50902
+ while (!signal.aborted) {
50903
+ const endpoint = preferred ?? await selectAgentEndpoint(config, signal);
50904
+ preferred = void 0;
50905
+ if (signal.aborted) break;
50906
+ if (failoverEnabled) {
50907
+ console.log(
50908
+ `Using the ${endpoint.kind} control plane at ${endpoint.server}`
50909
+ );
50910
+ }
50911
+ const session = new AbortController();
50912
+ const abortSession = () => session.abort();
50913
+ signal.addEventListener("abort", abortSession, { once: true });
50914
+ let probing = false;
50915
+ const onUnhealthy = () => {
50916
+ if (probing || session.signal.aborted) return;
50917
+ probing = true;
50918
+ void findHealthyAlternate(config, endpoint, session.signal).then((alternate) => {
50919
+ if (session.signal.aborted) return;
50920
+ if (!alternate) {
50921
+ console.log(
50922
+ `The ${endpoint.kind} control plane is not answering and no other endpoint is either; keeping the session and retrying`
50923
+ );
50924
+ return;
50925
+ }
50926
+ console.log(
50927
+ `The ${endpoint.kind} control plane stopped responding; switching to the ${alternate.kind} control plane at ${alternate.server}`
50928
+ );
50929
+ preferred = alternate;
50930
+ session.abort();
50931
+ }).finally(() => {
50932
+ probing = false;
50933
+ });
50934
+ };
50935
+ try {
50936
+ await runAgentSession(
50937
+ configForEndpoint(config, endpoint),
50938
+ session.signal,
50939
+ failoverEnabled ? onUnhealthy : void 0
50940
+ );
50941
+ } finally {
50942
+ signal.removeEventListener("abort", abortSession);
50943
+ }
50944
+ }
50945
+ }
50805
50946
 
50806
50947
  // src/dev-runtime.ts
50807
50948
  var DEFAULT_SERVER_WAIT_MS = 12e4;
@@ -50883,10 +51024,10 @@ async function prepareDevelopmentAgent(options, signal, dependencies = defaultDe
50883
51024
  }
50884
51025
  } catch {
50885
51026
  }
50886
- const enrollment = await anonymousClient.createEnrollmentToken();
51027
+ const enrollment2 = await anonymousClient.createEnrollmentToken();
50887
51028
  const response = await anonymousClient.enroll({
50888
51029
  ...inventory,
50889
- enrollmentToken: enrollment.createAgentEnrollmentToken.token,
51030
+ enrollmentToken: enrollment2.createAgentEnrollmentToken.token,
50890
51031
  name
50891
51032
  });
50892
51033
  const config = {
@@ -50936,12 +51077,17 @@ function redactedRequestHeaders(headers) {
50936
51077
  }
50937
51078
 
50938
51079
  // src/index.ts
51080
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["clear-remote"]);
50939
51081
  function flags(args) {
50940
51082
  const result = {};
50941
51083
  const headerValues = [];
50942
51084
  for (let index = 0; index < args.length; index += 1) {
50943
51085
  const argument = args[index];
50944
51086
  if (!argument.startsWith("--")) continue;
51087
+ if (BOOLEAN_FLAGS.has(argument.slice(2))) {
51088
+ result[argument.slice(2)] = "true";
51089
+ continue;
51090
+ }
50945
51091
  const value = args[index + 1];
50946
51092
  if (!value || value.startsWith("--"))
50947
51093
  throw new Error(`Missing value for ${argument}`);
@@ -50955,26 +51101,50 @@ function usage() {
50955
51101
  console.log(`control-agent <command>
50956
51102
 
50957
51103
  Commands:
50958
- enroll --enrollment-token <token> [--server http://127.0.0.1:3090] [--websocket-server ws://127.0.0.1:3090/graphql] [--name <name>] [--header "Name: value"]...
51104
+ enroll --enrollment-token <token> [--server http://127.0.0.1:3090] [--websocket-server ws://127.0.0.1:3090/graphql] [--remote-server https://control.example.com] [--remote-websocket-server wss://control.example.com/graphql] [--name <name>] [--header "Name: value"]...
51105
+ endpoints [--server <url>] [--websocket-server <url>] [--remote-server <url>] [--remote-websocket-server <url>] [--clear-remote]
51106
+ enrollment
51107
+ unenroll
50959
51108
  dev [--server http://127.0.0.1:3000] [--websocket-server ws://127.0.0.1:3092/graphql] [--name <name>]
50960
51109
  run
50961
51110
  status
50962
51111
  doctor`);
50963
51112
  }
51113
+ function endpointOptions(options, fallback) {
51114
+ const server = normalizeServer(
51115
+ options.server ?? fallback?.server ?? "http://127.0.0.1:3090"
51116
+ );
51117
+ const websocketServer = options["websocket-server"] ?? // A new local address invalidates a websocket address derived from the old
51118
+ // one, so only an explicitly stored value survives a server change.
51119
+ (options.server ? void 0 : fallback?.websocketServer) ?? defaultWebSocketServer(server);
51120
+ const clearRemote = options["clear-remote"] === "true";
51121
+ const remote = options["remote-server"] ?? (clearRemote ? null : void 0);
51122
+ const remoteServer = remote === void 0 ? fallback?.remoteServer ?? null : remote && normalizeServer(remote);
51123
+ const remoteWebSocketServer = !remoteServer ? null : options["remote-websocket-server"] ?? (options["remote-server"] ? void 0 : fallback?.remoteWebSocketServer) ?? defaultWebSocketServer(remoteServer);
51124
+ return {
51125
+ server,
51126
+ websocketServer,
51127
+ remoteServer,
51128
+ remoteWebSocketServer
51129
+ };
51130
+ }
50964
51131
  async function enroll(args) {
50965
51132
  const parsed = flags(args);
50966
51133
  const options = parsed.values;
50967
51134
  const enrollmentToken = options["enrollment-token"];
50968
51135
  if (!enrollmentToken) throw new Error("--enrollment-token is required");
50969
- const server = normalizeServer(options.server ?? "http://127.0.0.1:3090");
50970
- const websocketServer = options["websocket-server"] ?? defaultWebSocketServer(server);
51136
+ const addresses = endpointOptions(options);
50971
51137
  const inventory = collectInventory();
50972
51138
  const name = options.name ?? inventory.hostname;
50973
- const client2 = new AgentGraphQLClient(server, null, 1e4, parsed.headers);
51139
+ const client2 = new AgentGraphQLClient(
51140
+ addresses.server,
51141
+ null,
51142
+ 1e4,
51143
+ parsed.headers
51144
+ );
50974
51145
  const response = await client2.enroll({ ...inventory, enrollmentToken, name });
50975
51146
  await saveConfig({
50976
- server,
50977
- websocketServer,
51147
+ ...addresses,
50978
51148
  agentId: response.enrollAgent.agent.id,
50979
51149
  credential: response.enrollAgent.credential,
50980
51150
  name,
@@ -50982,6 +51152,63 @@ async function enroll(args) {
50982
51152
  });
50983
51153
  console.log(`Enrolled agent ${name} (${response.enrollAgent.agent.id})`);
50984
51154
  }
51155
+ async function endpoints(args) {
51156
+ const options = flags(args).values;
51157
+ const config = await loadConfig();
51158
+ const changing = [
51159
+ "server",
51160
+ "websocket-server",
51161
+ "remote-server",
51162
+ "remote-websocket-server",
51163
+ "clear-remote"
51164
+ ].some((flag) => flag in options);
51165
+ const updated = changing ? { ...config, ...endpointOptions(options, config) } : config;
51166
+ if (changing) await saveConfig(updated);
51167
+ for (const endpoint of agentEndpoints(updated)) {
51168
+ console.log(
51169
+ `${endpoint.kind}: ${endpoint.server} (${endpoint.websocketServer})`
51170
+ );
51171
+ }
51172
+ if (!updated.remoteServer) console.log("remote: not configured");
51173
+ }
51174
+ async function enrollment() {
51175
+ let config;
51176
+ try {
51177
+ config = await loadConfig();
51178
+ } catch (error) {
51179
+ console.log("Enrolled: no");
51180
+ console.log(error instanceof Error ? error.message : String(error));
51181
+ process.exitCode = 1;
51182
+ return;
51183
+ }
51184
+ console.log("Enrolled: yes");
51185
+ console.log(`Agent: ${config.name} (${config.agentId})`);
51186
+ console.log(`Configuration: ${configPath()}`);
51187
+ let reachable = false;
51188
+ for (const endpoint of agentEndpoints(config)) {
51189
+ const ok2 = await probeEndpoint(endpoint, config);
51190
+ reachable ||= ok2;
51191
+ console.log(
51192
+ `${endpoint.kind}: ${endpoint.server} \u2014 ${ok2 ? "reachable" : "unreachable"}`
51193
+ );
51194
+ }
51195
+ if (!reachable) process.exitCode = 1;
51196
+ }
51197
+ async function unenroll() {
51198
+ const path = configPath();
51199
+ let config = null;
51200
+ try {
51201
+ config = await loadConfig(path);
51202
+ } catch {
51203
+ }
51204
+ await rm14(path, { force: true });
51205
+ console.log(
51206
+ config ? `Removed the enrollment for ${config.name} (${config.agentId}) from ${path}` : `Removed ${path}`
51207
+ );
51208
+ console.log(
51209
+ "The control plane still lists this agent; delete it there to finish removing it."
51210
+ );
51211
+ }
50985
51212
  async function status() {
50986
51213
  const config = await loadConfig();
50987
51214
  const response = await new AgentGraphQLClient(
@@ -51018,26 +51245,39 @@ async function doctor() {
51018
51245
  detail: error instanceof Error ? error.message : String(error)
51019
51246
  });
51020
51247
  }
51021
- try {
51022
- const server = config?.server ?? "http://127.0.0.1:3090";
51023
- const health = await new AgentGraphQLClient(
51024
- server,
51025
- null,
51026
- 1e4,
51027
- config?.headers
51028
- ).health();
51029
- checks.push({
51030
- check: "control plane",
51031
- ok: health.health === "ok",
51032
- detail: health.health
51033
- });
51034
- } catch (error) {
51035
- checks.push({
51036
- check: "control plane",
51037
- ok: false,
51038
- detail: error instanceof Error ? error.message : String(error)
51039
- });
51040
- }
51248
+ const reachable = config ? agentEndpoints(config) : [
51249
+ {
51250
+ kind: "local",
51251
+ server: "http://127.0.0.1:3090",
51252
+ websocketServer: "ws://127.0.0.1:3090/graphql"
51253
+ }
51254
+ ];
51255
+ const results = await Promise.all(
51256
+ reachable.map(async (endpoint) => {
51257
+ try {
51258
+ const health = await new AgentGraphQLClient(
51259
+ endpoint.server,
51260
+ null,
51261
+ 1e4,
51262
+ config?.headers
51263
+ ).health();
51264
+ return {
51265
+ ok: health.health === "ok",
51266
+ detail: `${endpoint.kind} ${endpoint.server}: ${health.health}`
51267
+ };
51268
+ } catch (error) {
51269
+ return {
51270
+ ok: false,
51271
+ detail: `${endpoint.kind} ${endpoint.server}: ${error instanceof Error ? error.message : String(error)}`
51272
+ };
51273
+ }
51274
+ })
51275
+ );
51276
+ checks.push({
51277
+ check: "control plane",
51278
+ ok: results.some((result) => result.ok),
51279
+ detail: results.map((result) => result.detail).join("; ")
51280
+ });
51041
51281
  for (const check of checks)
51042
51282
  console.log(
51043
51283
  `${check.ok ? "PASS" : "FAIL"} ${check.check}: ${check.detail}`
@@ -51051,6 +51291,9 @@ async function main() {
51051
51291
  return;
51052
51292
  }
51053
51293
  if (command3 === "enroll") return enroll(args);
51294
+ if (command3 === "endpoints") return endpoints(args);
51295
+ if (command3 === "enrollment") return enrollment();
51296
+ if (command3 === "unenroll") return unenroll();
51054
51297
  if (command3 === "status") return status();
51055
51298
  if (command3 === "doctor") return doctor();
51056
51299
  if (command3 === "run" || command3 === "dev") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-development-environment/control-agent",
3
- "version": "0.0.75",
3
+ "version": "0.0.77",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {