@ai-development-environment/control-agent 0.0.76 → 0.0.78

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,69 @@ 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 toolDirectories(options = {}) {
12140
+ const home = options.home ?? homedir4();
12141
+ const platform2 = options.platform ?? process.platform;
12142
+ return [
12143
+ join6(home, ".local", "bin"),
12144
+ join6(home, ".bun", "bin"),
12145
+ join6(home, ".npm-global", "bin"),
12146
+ // Where the opencode.ai install script puts its binary.
12147
+ join6(home, ".opencode", "bin"),
12148
+ // Global npm installs land next to the node binary running this agent.
12149
+ options.nodeDirectory ?? dirname3(process.execPath),
12150
+ ...platform2 === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin"] : []
12151
+ ];
12152
+ }
12153
+ function findExecutable(name, options = {}) {
12154
+ const env = options.env ?? process.env;
12155
+ const platform2 = options.platform ?? process.platform;
12156
+ const executable = platform2 === "win32" ? `${name}.exe` : name;
12157
+ const directories = [
12158
+ ...(env.PATH ?? "").split(delimiter).filter(Boolean),
12159
+ ...toolDirectories(options)
12160
+ ];
12161
+ const override = options.overrideVariable ? env[options.overrideVariable]?.trim() : void 0;
12162
+ const candidates = [
12163
+ ...override ? [override] : [],
12164
+ ...directories.map((directory) => join6(directory, executable))
12165
+ ];
12166
+ const canExecute = options.isExecutable ?? isExecutable;
12167
+ return [...new Set(candidates)].find(canExecute);
12168
+ }
12169
+ function extendedPath(options = {}) {
12170
+ const env = options.env ?? process.env;
12171
+ const existing = (env.PATH ?? "").split(delimiter).filter(Boolean);
12172
+ const additions = toolDirectories(options).filter(
12173
+ (directory) => !existing.includes(directory)
12174
+ );
12175
+ return [...existing, ...additions].join(delimiter);
12176
+ }
12177
+ function repairToolPath(options = {}) {
12178
+ const env = options.env ?? process.env;
12179
+ env.PATH = extendedPath(options);
12180
+ }
12181
+ function prependPathDirectory(directory, env = process.env) {
12182
+ const existing = (env.PATH ?? "").split(delimiter).filter(Boolean);
12183
+ if (existing[0] === directory) return;
12184
+ env.PATH = [
12185
+ directory,
12186
+ ...existing.filter((entry) => entry !== directory)
12187
+ ].join(delimiter);
12188
+ }
12189
+
12065
12190
  // src/process-runner.ts
12066
12191
  import { spawn as spawn5 } from "node:child_process";
12067
12192
  async function runProcess(options) {
@@ -12180,11 +12305,19 @@ function validateCcusagePayload(payload) {
12180
12305
  }
12181
12306
  async function runCcusage(payload, timeoutMs, signal, onLog) {
12182
12307
  validateCcusagePayload(payload);
12308
+ const command3 = findExecutable("ccusage", {
12309
+ overrideVariable: "CONTROL_AGENT_CCUSAGE_EXECUTABLE"
12310
+ });
12311
+ if (!command3) {
12312
+ throw new Error(
12313
+ "ccusage was not found. Install it (brew install ccusage, or npm install -g ccusage) or set CONTROL_AGENT_CCUSAGE_EXECUTABLE to its full path."
12314
+ );
12315
+ }
12183
12316
  const stdout = [];
12184
12317
  let stdoutBytes = 0;
12185
12318
  let exceededLimit = false;
12186
12319
  const result = await runProcess({
12187
- command: "ccusage",
12320
+ command: command3,
12188
12321
  args: ["--json"],
12189
12322
  timeoutMs,
12190
12323
  signal,
@@ -12229,8 +12362,8 @@ import {
12229
12362
  stat as stat5,
12230
12363
  statfs
12231
12364
  } 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";
12365
+ import { homedir as homedir5 } from "node:os";
12366
+ import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "node:path";
12234
12367
  var successfulProcess4 = {
12235
12368
  exitCode: 0,
12236
12369
  signal: null,
@@ -12247,7 +12380,7 @@ function safeRelativePath3(value) {
12247
12380
  }
12248
12381
  async function configuredRoots(payload) {
12249
12382
  if (payload.mode === "DEFAULT") {
12250
- return [join6(homedir4(), "Library", "Developer", "Xcode", "DerivedData")];
12383
+ return [join7(homedir5(), "Library", "Developer", "Xcode", "DerivedData")];
12251
12384
  }
12252
12385
  if (payload.mode === "ABSOLUTE") {
12253
12386
  if (!payload.path || !isAbsolute4(payload.path)) {
@@ -12309,10 +12442,10 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
12309
12442
  for (const child of children) {
12310
12443
  signal.throwIfAborted();
12311
12444
  if (!child.isDirectory()) continue;
12312
- const path = join6(rootPath2, child.name);
12445
+ const path = join7(rootPath2, child.name);
12313
12446
  const information = await lstat2(path);
12314
12447
  const projectPath = await workspacePath(
12315
- join6(path, "info.plist"),
12448
+ join7(path, "info.plist"),
12316
12449
  timeoutMs,
12317
12450
  signal
12318
12451
  );
@@ -12330,8 +12463,8 @@ async function scanRoot(configuredRoot, timeoutMs, signal) {
12330
12463
  return { entries, warning: null };
12331
12464
  }
12332
12465
  async function scanDeviceSupport(signal) {
12333
- const configuredRoot = join6(
12334
- process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ?? homedir4(),
12466
+ const configuredRoot = join7(
12467
+ process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ?? homedir5(),
12335
12468
  ...process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ? [] : ["Library", "Developer", "Xcode", "iOS DeviceSupport"]
12336
12469
  );
12337
12470
  signal.throwIfAborted();
@@ -12351,7 +12484,7 @@ async function scanDeviceSupport(signal) {
12351
12484
  const children = await readdir3(rootPath2, { withFileTypes: true });
12352
12485
  return {
12353
12486
  entries: children.filter((child) => child.isDirectory() && !child.isSymbolicLink()).map((child) => ({
12354
- path: join6(rootPath2, child.name),
12487
+ path: join7(rootPath2, child.name),
12355
12488
  rootPath: rootPath2,
12356
12489
  name: child.name,
12357
12490
  kind: "DEVICE_SUPPORT",
@@ -12398,7 +12531,7 @@ function assertDirectChild(rootPath2, path) {
12398
12531
  }
12399
12532
  const root = resolve4(rootPath2);
12400
12533
  const target2 = resolve4(path);
12401
- if (target2 === root || dirname3(target2) !== root || basename4(target2) !== basename4(path)) {
12534
+ if (target2 === root || dirname4(target2) !== root || basename4(target2) !== basename4(path)) {
12402
12535
  throw new Error("Build Data target must be a direct child of its root");
12403
12536
  }
12404
12537
  }
@@ -12412,7 +12545,7 @@ async function allocatedBytes(path, signal) {
12412
12545
  try {
12413
12546
  for await (const entry of directory) {
12414
12547
  signal.throwIfAborted();
12415
- total += await allocatedBytes(join6(path, entry.name), signal);
12548
+ total += await allocatedBytes(join7(path, entry.name), signal);
12416
12549
  }
12417
12550
  } finally {
12418
12551
  await directory.close().catch(() => void 0);
@@ -12427,7 +12560,7 @@ var sizeBuildData = async (rawPayload, _timeoutMs, signal) => {
12427
12560
  try {
12428
12561
  assertDirectChild(target2.rootPath, target2.path);
12429
12562
  const rootPath2 = await realpath4(target2.rootPath);
12430
- if (rootPath2 !== resolve4(target2.rootPath) || dirname3(resolve4(target2.path)) !== rootPath2) {
12563
+ if (rootPath2 !== resolve4(target2.rootPath) || dirname4(resolve4(target2.path)) !== rootPath2) {
12431
12564
  throw new Error("Build Data root changed since the scan");
12432
12565
  }
12433
12566
  const stats = await lstat2(target2.path);
@@ -12458,7 +12591,7 @@ var deleteBuildData = async (rawPayload, _timeoutMs, signal) => {
12458
12591
  try {
12459
12592
  assertDirectChild(target2.rootPath, target2.path);
12460
12593
  const rootPath2 = await realpath4(target2.rootPath);
12461
- if (rootPath2 !== resolve4(target2.rootPath) || dirname3(resolve4(target2.path)) !== rootPath2) {
12594
+ if (rootPath2 !== resolve4(target2.rootPath) || dirname4(resolve4(target2.path)) !== rootPath2) {
12462
12595
  throw new Error("Build Data root changed since the scan");
12463
12596
  }
12464
12597
  const stats = await lstat2(target2.path);
@@ -12512,13 +12645,13 @@ async function inspectVolume(configuredPath, role, signal, allowMissing) {
12512
12645
  let candidate = resolve4(configuredPath);
12513
12646
  for (; ; ) {
12514
12647
  signal.throwIfAborted();
12515
- const parent = dirname3(candidate);
12648
+ const parent = dirname4(candidate);
12516
12649
  if (parent === candidate) throw error;
12517
12650
  missingSegments.unshift(basename4(candidate));
12518
12651
  candidate = parent;
12519
12652
  try {
12520
12653
  inspectionPath = await realpath4(candidate);
12521
- canonicalPath = join6(inspectionPath, ...missingSegments);
12654
+ canonicalPath = join7(inspectionPath, ...missingSegments);
12522
12655
  break;
12523
12656
  } catch (parentError) {
12524
12657
  if (parentError.code !== "ENOENT") {
@@ -12625,8 +12758,8 @@ async function collectAgentDiskSpace(configuration, signal) {
12625
12758
 
12626
12759
  // src/handlers/codebases.ts
12627
12760
  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";
12761
+ import { homedir as homedir6 } from "node:os";
12762
+ import { dirname as dirname5, join as join8, resolve as resolve5 } from "node:path";
12630
12763
  var successfulProcess5 = {
12631
12764
  exitCode: 0,
12632
12765
  signal: null,
@@ -12703,7 +12836,7 @@ function baseSnapshot(folder) {
12703
12836
  }
12704
12837
  async function fetchedAt(commonDirectory) {
12705
12838
  try {
12706
- return (await stat6(join7(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
12839
+ return (await stat6(join8(commonDirectory, "FETCH_HEAD"))).mtime.toISOString();
12707
12840
  } catch {
12708
12841
  return null;
12709
12842
  }
@@ -12882,7 +13015,7 @@ async function inspectCodebase(selectedFolder, timeoutMs, signal, expectedOrigin
12882
13015
  }
12883
13016
  var browseCodebaseDirectories = async (payload) => {
12884
13017
  const input = codebaseBrowsePayload(payload);
12885
- const homePath = await realpath5(homedir5());
13018
+ const homePath = await realpath5(homedir6());
12886
13019
  const path = await realpath5(input.path ? resolve5(input.path) : homePath);
12887
13020
  if (!(await stat6(path)).isDirectory())
12888
13021
  throw new Error("Path is not a directory");
@@ -12892,12 +13025,12 @@ var browseCodebaseDirectories = async (payload) => {
12892
13025
  if (candidate.isDirectory()) {
12893
13026
  entries.push({
12894
13027
  name: candidate.name,
12895
- path: join7(path, candidate.name),
13028
+ path: join8(path, candidate.name),
12896
13029
  hidden: candidate.name.startsWith(".")
12897
13030
  });
12898
13031
  } else if (candidate.isSymbolicLink()) {
12899
13032
  try {
12900
- const target2 = join7(path, candidate.name);
13033
+ const target2 = join8(path, candidate.name);
12901
13034
  if ((await stat6(target2)).isDirectory()) {
12902
13035
  entries.push({
12903
13036
  name: candidate.name,
@@ -12912,7 +13045,7 @@ var browseCodebaseDirectories = async (payload) => {
12912
13045
  entries.sort((first, second) => first.name.localeCompare(second.name));
12913
13046
  const listing = {
12914
13047
  path,
12915
- parentPath: dirname4(path) === path ? null : dirname4(path),
13048
+ parentPath: dirname5(path) === path ? null : dirname5(path),
12916
13049
  homePath,
12917
13050
  entries: entries.slice(0, 1e3),
12918
13051
  truncated: entries.length > 1e3
@@ -13510,8 +13643,8 @@ import {
13510
13643
  stat as stat7,
13511
13644
  writeFile as writeFile5
13512
13645
  } 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";
13646
+ import { homedir as homedir7 } from "node:os";
13647
+ import { dirname as dirname6, isAbsolute as isAbsolute5, join as join9, relative as relative6, resolve as resolve6, sep as sep6 } from "node:path";
13515
13648
  import { promisify } from "node:util";
13516
13649
  var executeFile = promisify(execFile);
13517
13650
  var successfulProcess6 = {
@@ -13572,17 +13705,17 @@ async function directoryExists(path) {
13572
13705
  }
13573
13706
  }
13574
13707
  async function configuredTools(tools) {
13575
- const homePath = homedir6();
13708
+ const homePath = homedir7();
13576
13709
  return Promise.all(
13577
13710
  AI_TOOLS.filter((tool) => tools.includes(tool)).map(async (tool) => ({
13578
13711
  tool,
13579
- configured: await directoryExists(join8(homePath, ...CONFIG_PATHS[tool])),
13712
+ configured: await directoryExists(join9(homePath, ...CONFIG_PATHS[tool])),
13580
13713
  homePath
13581
13714
  }))
13582
13715
  );
13583
13716
  }
13584
13717
  function rootPath(location) {
13585
- const base = location.scope === "GLOBAL" ? homedir6() : location.folder;
13718
+ const base = location.scope === "GLOBAL" ? homedir7() : location.folder;
13586
13719
  const parts = location.scope === "GLOBAL" ? GLOBAL_ROOTS[location.rootKind] : PROJECT_ROOTS[location.rootKind];
13587
13720
  const path = resolve6(base, ...parts);
13588
13721
  const normalizedBase = resolve6(base);
@@ -13599,7 +13732,7 @@ async function listPackageFiles(skillDirectory) {
13599
13732
  let totalBytes = 0;
13600
13733
  const visit = async (directory) => {
13601
13734
  for (const entry of await readdir5(directory, { withFileTypes: true })) {
13602
- const path = join8(directory, entry.name);
13735
+ const path = join9(directory, entry.name);
13603
13736
  const metadata = await lstat3(path);
13604
13737
  if (metadata.isSymbolicLink()) {
13605
13738
  throw new Error(
@@ -13688,7 +13821,7 @@ async function scanRoot2(location, configured, targets, warnings) {
13688
13821
  const installations = [];
13689
13822
  for (const entry of await readdir5(root, { withFileTypes: true })) {
13690
13823
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
13691
- const directory = join8(root, entry.name);
13824
+ const directory = join9(root, entry.name);
13692
13825
  try {
13693
13826
  const skillPackage = await readPackage(directory);
13694
13827
  const relativeDirectory = location.folder ? relative6(location.folder, directory).split(sep6).join("/") : "";
@@ -13767,7 +13900,7 @@ var readSkills = async (payload) => {
13767
13900
  }
13768
13901
  packages.push({
13769
13902
  ...request,
13770
- package: await readPackage(join8(root, request.skillName))
13903
+ package: await readPackage(join9(root, request.skillName))
13771
13904
  });
13772
13905
  }
13773
13906
  return { ...successfulProcess6, packages };
@@ -13785,7 +13918,7 @@ async function gitExcludePath(folder) {
13785
13918
  }
13786
13919
  async function updateManagedExclude(folder, relativeSkillPath, present) {
13787
13920
  const excludePath = await gitExcludePath(folder);
13788
- await mkdir4(dirname5(excludePath), { recursive: true });
13921
+ await mkdir4(dirname6(excludePath), { recursive: true });
13789
13922
  let existing = "";
13790
13923
  try {
13791
13924
  existing = await readFile5(excludePath, "utf8");
@@ -13829,18 +13962,18 @@ async function writePackage(location, skillPackage) {
13829
13962
  throw new Error("New skill copies may only be written to shared roots");
13830
13963
  }
13831
13964
  const root = rootPath(location);
13832
- const destination = join8(root, skillPackage.name);
13965
+ const destination = join9(root, skillPackage.name);
13833
13966
  await assertUntracked(location, destination);
13834
13967
  await mkdir4(root, { recursive: true });
13835
- const temporary = join8(root, `.${skillPackage.name}.${randomUUID3()}.tmp`);
13836
- const backup = join8(root, `.${skillPackage.name}.${randomUUID3()}.bak`);
13968
+ const temporary = join9(root, `.${skillPackage.name}.${randomUUID3()}.tmp`);
13969
+ const backup = join9(root, `.${skillPackage.name}.${randomUUID3()}.bak`);
13837
13970
  await mkdir4(temporary, { recursive: true });
13838
13971
  try {
13839
13972
  for (const file of skillPackage.files) {
13840
13973
  const path = resolve6(temporary, ...file.path.split("/"));
13841
13974
  if (!path.startsWith(`${temporary}${sep6}`))
13842
13975
  throw new Error("Skill file escaped temporary directory");
13843
- await mkdir4(dirname5(path), { recursive: true });
13976
+ await mkdir4(dirname6(path), { recursive: true });
13844
13977
  await writeFile5(path, Buffer.from(file.contentsBase64, "base64"));
13845
13978
  await chmod3(path, file.executable ? 493 : 420);
13846
13979
  }
@@ -13871,7 +14004,7 @@ var applySkills = async (payload) => {
13871
14004
  for (const operation of operations) {
13872
14005
  const root = rootPath(operation);
13873
14006
  const skillName = operation.kind === "WRITE" ? operation.package.name : operation.skillName;
13874
- const destination = join8(root, skillName);
14007
+ const destination = join9(root, skillName);
13875
14008
  await assertUntracked(operation, destination);
13876
14009
  if (operation.kind === "WRITE") {
13877
14010
  await writePackage(operation, operation.package);
@@ -13901,8 +14034,8 @@ var applySkills = async (payload) => {
13901
14034
 
13902
14035
  // src/handlers/runs.ts
13903
14036
  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";
14037
+ import { homedir as homedir8 } from "node:os";
14038
+ import { join as join10 } from "node:path";
13906
14039
  var successfulProcess7 = {
13907
14040
  exitCode: 0,
13908
14041
  signal: null,
@@ -13917,7 +14050,7 @@ async function fileExists(path) {
13917
14050
  }
13918
14051
  }
13919
14052
  async function findClaudeSessionFile(nativeId, home) {
13920
- const projectsDir = join9(home, ".claude", "projects");
14053
+ const projectsDir = join10(home, ".claude", "projects");
13921
14054
  let entries;
13922
14055
  try {
13923
14056
  entries = await readdir6(projectsDir, { withFileTypes: true });
@@ -13927,13 +14060,13 @@ async function findClaudeSessionFile(nativeId, home) {
13927
14060
  const target2 = `${nativeId}.jsonl`;
13928
14061
  for (const entry of entries) {
13929
14062
  if (!entry.isDirectory()) continue;
13930
- const candidate = join9(projectsDir, entry.name, target2);
14063
+ const candidate = join10(projectsDir, entry.name, target2);
13931
14064
  if (await fileExists(candidate)) return candidate;
13932
14065
  }
13933
14066
  return null;
13934
14067
  }
13935
14068
  async function findCodexSessionFile(nativeId, home) {
13936
- const sessionsDir = join9(home, ".codex", "sessions");
14069
+ const sessionsDir = join10(home, ".codex", "sessions");
13937
14070
  const suffix = `-${nativeId}.jsonl`;
13938
14071
  const walk = async (dir) => {
13939
14072
  let entries;
@@ -13943,7 +14076,7 @@ async function findCodexSessionFile(nativeId, home) {
13943
14076
  return null;
13944
14077
  }
13945
14078
  for (const entry of entries) {
13946
- const path = join9(dir, entry.name);
14079
+ const path = join10(dir, entry.name);
13947
14080
  if (entry.isDirectory()) {
13948
14081
  const found = await walk(path);
13949
14082
  if (found) return found;
@@ -13960,7 +14093,7 @@ async function locateSessionFile(payload, home) {
13960
14093
  }
13961
14094
  var readRunSession = async (payload) => {
13962
14095
  const input = parseRunSessionReadPayload(payload);
13963
- const path = await locateSessionFile(input, homedir7());
14096
+ const path = await locateSessionFile(input, homedir8());
13964
14097
  if (!path) {
13965
14098
  throw new Error(
13966
14099
  `No ${input.provider === "CLAUDE" ? "Claude" : "Codex"} session file found for this run on this agent`
@@ -13990,13 +14123,13 @@ import {
13990
14123
  writeFile as writeFile6
13991
14124
  } from "node:fs/promises";
13992
14125
  import { tmpdir as tmpdir5 } from "node:os";
13993
- import { join as join11 } from "node:path";
14126
+ import { join as join12 } from "node:path";
13994
14127
 
13995
14128
  // src/runs/git-checkpoint.ts
13996
14129
  import { execFile as execFile2 } from "node:child_process";
13997
14130
  import { copyFile, mkdtemp as mkdtemp4, rm as rm7 } from "node:fs/promises";
13998
14131
  import { tmpdir as tmpdir4 } from "node:os";
13999
- import { join as join10 } from "node:path";
14132
+ import { join as join11 } from "node:path";
14000
14133
  import { promisify as promisify2 } from "node:util";
14001
14134
  var execFileAsync = promisify2(execFile2);
14002
14135
  async function git2(cwd, args, env) {
@@ -14111,12 +14244,12 @@ async function captureGitCheckpoint(cwd, runId, kind) {
14111
14244
  const manifest = await optionalGit(cwd, ["status", "--porcelain=v2", "-z"]);
14112
14245
  const staged = await optionalGit(cwd, ["diff", "--cached", "--stat", "HEAD"]);
14113
14246
  const unstaged = await optionalGit(cwd, ["diff", "--stat", "HEAD"]);
14114
- const directory = await mkdtemp4(join10(tmpdir4(), "aide-run-index-"));
14247
+ const directory = await mkdtemp4(join11(tmpdir4(), "aide-run-index-"));
14115
14248
  let worktreeTree = null;
14116
14249
  let refName = null;
14117
14250
  try {
14118
14251
  const actualIndex = await git2(cwd, ["rev-parse", "--git-path", "index"]);
14119
- const temporaryIndex = join10(directory, "index");
14252
+ const temporaryIndex = join11(directory, "index");
14120
14253
  try {
14121
14254
  await copyFile(actualIndex, temporaryIndex);
14122
14255
  } catch {
@@ -14186,10 +14319,10 @@ var runWorkflowTerminal = async (payloadValue, timeoutMs, signal, onLog, context
14186
14319
  const information = await stat9(cwd);
14187
14320
  if (!information.isDirectory())
14188
14321
  throw new Error("Workflow working directory is not a directory");
14189
- const directory = await mkdtemp5(join11(tmpdir5(), "aide-workflow-"));
14322
+ const directory = await mkdtemp5(join12(tmpdir5(), "aide-workflow-"));
14190
14323
  const extension2 = payload.interpreter === "NODE" ? "mjs" : "sh";
14191
- const scriptPath = join11(directory, `step.${extension2}`);
14192
- const sessionPath = join11(directory, "session.json");
14324
+ const scriptPath = join12(directory, `step.${extension2}`);
14325
+ const sessionPath = join12(directory, "session.json");
14193
14326
  try {
14194
14327
  await writeFile6(scriptPath, `${payload.script}
14195
14328
  `, { mode: 384 });
@@ -14282,7 +14415,7 @@ var runWorkflowGitCheckpoint = async (payloadValue, _timeoutMs, signal) => {
14282
14415
  import { createWriteStream as createWriteStream2, watch } from "node:fs";
14283
14416
  import { mkdtemp as mkdtemp6, open as open2, readFile as readFile8, realpath as realpath8, rm as rm9, stat as stat10 } from "node:fs/promises";
14284
14417
  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";
14418
+ import { basename as basename5, dirname as dirname7, isAbsolute as isAbsolute6, join as join13, relative as relative7 } from "node:path";
14286
14419
  import { spawn as spawn6 } from "node:child_process";
14287
14420
  var successfulProcess8 = {
14288
14421
  exitCode: 0,
@@ -14337,7 +14470,7 @@ async function rebaseInProgress(folder, timeoutMs, signal) {
14337
14470
  const candidate = pathResult.stdout.trim();
14338
14471
  if (!candidate) continue;
14339
14472
  try {
14340
- await stat10(isAbsolute6(candidate) ? candidate : join12(folder, candidate));
14473
+ await stat10(isAbsolute6(candidate) ? candidate : join13(folder, candidate));
14341
14474
  return true;
14342
14475
  } catch {
14343
14476
  }
@@ -14357,7 +14490,7 @@ async function rebaseBranch(folder, timeoutMs, signal) {
14357
14490
  if (!candidate) continue;
14358
14491
  try {
14359
14492
  const headName = (await readFile8(
14360
- isAbsolute6(candidate) ? candidate : join12(folder, candidate),
14493
+ isAbsolute6(candidate) ? candidate : join13(folder, candidate),
14361
14494
  "utf8"
14362
14495
  )).trim();
14363
14496
  if (headName.startsWith("refs/heads/")) {
@@ -15003,7 +15136,7 @@ async function comparisonSides(input, folder, timeoutMs, signal) {
15003
15136
  if (!input.path) return { before: null, after: null };
15004
15137
  const path = input.path;
15005
15138
  const previousPath = input.previousPath ?? path;
15006
- const currentPath = join12(folder, path);
15139
+ const currentPath = join13(folder, path);
15007
15140
  if (input.scope === "UNTRACKED") {
15008
15141
  return { before: null, after: { kind: "FILE", path: currentPath } };
15009
15142
  }
@@ -15178,7 +15311,7 @@ async function inspectRequestedDiff(input, folder, timeoutMs, signal) {
15178
15311
  let patch;
15179
15312
  if (input.scope === "UNTRACKED") {
15180
15313
  const bounded = await readBoundedFile(
15181
- join12(folder, input.path),
15314
+ join13(folder, input.path),
15182
15315
  MAX_DIFF_BYTES
15183
15316
  );
15184
15317
  const contents = bounded.contents;
@@ -15360,8 +15493,8 @@ var downloadWorktreeDiffAsset = async (payload, timeoutMs, signal, _onLog, conte
15360
15493
  if (!/^\d+$/.test(size) || Number(size) > 20 * 1024 * 1024) {
15361
15494
  throw new Error("Diff image exceeds the 20 MiB limit");
15362
15495
  }
15363
- temporaryDirectory = await mkdtemp6(join12(tmpdir6(), "ade-diff-image-"));
15364
- uploadPath = join12(temporaryDirectory, basename5(input.path));
15496
+ temporaryDirectory = await mkdtemp6(join13(tmpdir6(), "ade-diff-image-"));
15497
+ uploadPath = join13(temporaryDirectory, basename5(input.path));
15365
15498
  await writeGitObject(
15366
15499
  folder,
15367
15500
  selected.specification,
@@ -15624,8 +15757,8 @@ var branchWorktree = async (payload, timeoutMs, signal) => {
15624
15757
  timeoutMs,
15625
15758
  signal
15626
15759
  ) : input.candidates[0];
15627
- const targetFolder = input.action === "CREATE" ? join12(
15628
- dirname6(rootFolder),
15760
+ const targetFolder = input.action === "CREATE" ? join13(
15761
+ dirname7(rootFolder),
15629
15762
  `${basename5(rootFolder)}-${branch.replaceAll("/", "-")}`
15630
15763
  ) : changeFolder;
15631
15764
  if (input.action === "CREATE") {
@@ -15864,8 +15997,8 @@ var checkoutMovedWorktree = async (payload, timeoutMs, signal) => {
15864
15997
  `Branch ${input.branch} is already checked out in ${occupiedFolder}`
15865
15998
  );
15866
15999
  }
15867
- targetFolder = join12(
15868
- dirname6(rootFolder),
16000
+ targetFolder = join13(
16001
+ dirname7(rootFolder),
15869
16002
  `${basename5(rootFolder)}-${input.branch.replaceAll("/", "-")}`
15870
16003
  );
15871
16004
  try {
@@ -16804,7 +16937,7 @@ var CodebaseMonitor = class {
16804
16937
  // src/runs/run-manager.ts
16805
16938
  import { execFile as execFile4 } from "node:child_process";
16806
16939
  import { mkdir as mkdir6, rm as rm13 } from "node:fs/promises";
16807
- import { dirname as dirname8, join as join17 } from "node:path";
16940
+ import { dirname as dirname10, join as join17 } from "node:path";
16808
16941
  import { promisify as promisify4 } from "node:util";
16809
16942
 
16810
16943
  // src/runs/journal.ts
@@ -16817,7 +16950,7 @@ import {
16817
16950
  rm as rm10,
16818
16951
  writeFile as writeFile7
16819
16952
  } from "node:fs/promises";
16820
- import { dirname as dirname7, join as join13 } from "node:path";
16953
+ import { dirname as dirname8, join as join14 } from "node:path";
16821
16954
  var RunJournal = class {
16822
16955
  constructor(runId, attemptId) {
16823
16956
  this.runId = runId;
@@ -16836,13 +16969,13 @@ var RunJournal = class {
16836
16969
  this.attemptId = attemptId;
16837
16970
  }
16838
16971
  get directory() {
16839
- return join13(dirname7(configPath()), "runs", this.runId);
16972
+ return join14(dirname8(configPath()), "runs", this.runId);
16840
16973
  }
16841
16974
  get eventsPath() {
16842
- return join13(this.directory, "events.jsonl");
16975
+ return join14(this.directory, "events.jsonl");
16843
16976
  }
16844
16977
  get statePath() {
16845
- return join13(this.directory, "state.json");
16978
+ return join14(this.directory, "state.json");
16846
16979
  }
16847
16980
  async load() {
16848
16981
  if (this.loaded) return;
@@ -16957,9 +17090,6 @@ var RunJournal = class {
16957
17090
 
16958
17091
  // src/runs/claude-adapter.ts
16959
17092
  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
17093
 
16964
17094
  // ../../node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs
16965
17095
  import { createRequire as RV } from "node:module";
@@ -42767,27 +42897,11 @@ function answerArrays(value) {
42767
42897
  }
42768
42898
 
42769
42899
  // 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
42900
  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);
42901
+ return findExecutable("claude", {
42902
+ ...options,
42903
+ overrideVariable: "CONTROL_AGENT_CLAUDE_EXECUTABLE"
42904
+ });
42791
42905
  }
42792
42906
  var MessageQueue = class {
42793
42907
  values = [];
@@ -43205,10 +43319,24 @@ var CodexAppServer = class {
43205
43319
  this.initialized ??= this.start();
43206
43320
  return this.initialized;
43207
43321
  }
43322
+ // The service PATH omits Homebrew and npm, so resolve the install once and
43323
+ // use the absolute path for every codex invocation.
43324
+ executable() {
43325
+ const executable = findExecutable("codex", {
43326
+ overrideVariable: "CONTROL_AGENT_CODEX_EXECUTABLE"
43327
+ });
43328
+ if (!executable) {
43329
+ throw new Error(
43330
+ "codex was not found. Install it (brew install codex, or npm install -g @openai/codex) or set CONTROL_AGENT_CODEX_EXECUTABLE to its full path."
43331
+ );
43332
+ }
43333
+ return executable;
43334
+ }
43208
43335
  async start() {
43209
- const modelCatalogPath = await this.createBundledModelCatalog();
43336
+ const executable = this.executable();
43337
+ const modelCatalogPath = await this.createBundledModelCatalog(executable);
43210
43338
  const modelCatalogDirectory = this.modelCatalogDirectory;
43211
- const child = spawn7("codex", codexAppServerArgs(modelCatalogPath), {
43339
+ const child = spawn7(executable, codexAppServerArgs(modelCatalogPath), {
43212
43340
  stdio: ["pipe", "pipe", "pipe"],
43213
43341
  env: process.env
43214
43342
  });
@@ -43276,11 +43404,11 @@ var CodexAppServer = class {
43276
43404
  );
43277
43405
  }
43278
43406
  }
43279
- async createBundledModelCatalog() {
43407
+ async createBundledModelCatalog(executable) {
43280
43408
  const directory = await mkdtemp7(join16(tmpdir7(), "aide-codex-models-"));
43281
43409
  try {
43282
43410
  const { stdout } = await execFileAsync2(
43283
- "codex",
43411
+ executable,
43284
43412
  ["debug", "models", "--bundled"],
43285
43413
  {
43286
43414
  encoding: "utf8",
@@ -43718,6 +43846,7 @@ var CodexAdapter = class {
43718
43846
  };
43719
43847
 
43720
43848
  // src/runs/opencode-adapter.ts
43849
+ import { dirname as dirname9 } from "node:path";
43721
43850
  import { pathToFileURL } from "node:url";
43722
43851
 
43723
43852
  // ../../node_modules/@opencode-ai/sdk/dist/v2/gen/core/serverSentEvents.gen.js
@@ -49389,7 +49518,18 @@ var OpenCodeAdapter = class {
49389
49518
  };
49390
49519
  runtime;
49391
49520
  async client() {
49392
- this.runtime ??= await createOpencode();
49521
+ if (!this.runtime) {
49522
+ const executable = findExecutable("opencode", {
49523
+ overrideVariable: "CONTROL_AGENT_OPENCODE_EXECUTABLE"
49524
+ });
49525
+ if (!executable) {
49526
+ throw new Error(
49527
+ "opencode was not found. Install it (brew install sst/tap/opencode, or npm install -g opencode-ai) or set CONTROL_AGENT_OPENCODE_EXECUTABLE to its full path."
49528
+ );
49529
+ }
49530
+ prependPathDirectory(dirname9(executable));
49531
+ this.runtime = await createOpencode();
49532
+ }
49393
49533
  return this.runtime.client;
49394
49534
  }
49395
49535
  async catalog() {
@@ -50011,7 +50151,7 @@ var RunManager = class {
50011
50151
  5
50012
50152
  );
50013
50153
  await this.completeCommand(command3.id, "SUCCEEDED");
50014
- await rm13(join17(dirname8(configPath()), "runs", run.id, "attachments"), {
50154
+ await rm13(join17(dirname10(configPath()), "runs", run.id, "attachments"), {
50015
50155
  recursive: true,
50016
50156
  force: true
50017
50157
  }).catch(() => void 0);
@@ -50193,7 +50333,7 @@ var RunManager = class {
50193
50333
  this.active.delete(run.id);
50194
50334
  this.interruptionRequests.delete(run.id);
50195
50335
  this.executingCommands.delete(command3.id);
50196
- await rm13(join17(dirname8(configPath()), "runs", run.id, "attachments"), {
50336
+ await rm13(join17(dirname10(configPath()), "runs", run.id, "attachments"), {
50197
50337
  recursive: true,
50198
50338
  force: true
50199
50339
  }).catch(() => void 0);
@@ -50421,7 +50561,7 @@ var RunManager = class {
50421
50561
  }
50422
50562
  async stageAttachments(runId, attachments) {
50423
50563
  if (!attachments.length) return [];
50424
- const directory = join17(dirname8(configPath()), "runs", runId, "attachments");
50564
+ const directory = join17(dirname10(configPath()), "runs", runId, "attachments");
50425
50565
  await mkdir6(directory, { recursive: true, mode: 448 });
50426
50566
  return Promise.all(
50427
50567
  attachments.map(async (attachment) => {
@@ -50549,7 +50689,8 @@ var DISK_SPACE_STALE_AFTER_SECONDS = DISK_SPACE_POLL_INTERVAL_SECONDS * 2;
50549
50689
  function configuredIntervalMs(value, fallbackSeconds, minSeconds, maxSeconds) {
50550
50690
  return (Number.isInteger(value) && value >= minSeconds && value <= maxSeconds ? value : fallbackSeconds) * 1e3;
50551
50691
  }
50552
- async function runAgent(config, signal) {
50692
+ var UNHEALTHY_HEARTBEAT_FAILURES = 3;
50693
+ async function runAgentSession(config, signal, onUnhealthy) {
50553
50694
  const client2 = new AgentGraphQLClient(
50554
50695
  config.server,
50555
50696
  config.credential,
@@ -50571,6 +50712,7 @@ async function runAgent(config, signal) {
50571
50712
  let diskSpaceRunning = false;
50572
50713
  let diskSpaceIntervalMs = DISK_SPACE_POLL_INTERVAL_SECONDS * 1e3;
50573
50714
  let startupRecoveryPending = true;
50715
+ let heartbeatFailures = 0;
50574
50716
  const interruptedJobs = /* @__PURE__ */ new Set();
50575
50717
  const reconcileJobs = async (recoverInterrupted) => {
50576
50718
  let jobs;
@@ -50653,11 +50795,17 @@ async function runAgent(config, signal) {
50653
50795
  heartbeatTimer = void 0;
50654
50796
  try {
50655
50797
  await client2.heartbeat(collectInventory());
50798
+ heartbeatFailures = 0;
50656
50799
  } catch (error) {
50800
+ heartbeatFailures += 1;
50657
50801
  console.error(
50658
50802
  "Heartbeat failed:",
50659
50803
  error instanceof Error ? error.message : error
50660
50804
  );
50805
+ if (heartbeatFailures >= UNHEALTHY_HEARTBEAT_FAILURES && onUnhealthy) {
50806
+ heartbeatFailures = 0;
50807
+ onUnhealthy();
50808
+ }
50661
50809
  } finally {
50662
50810
  heartbeatRunning = false;
50663
50811
  scheduleHeartbeat();
@@ -50802,6 +50950,53 @@ async function runAgent(config, signal) {
50802
50950
  await executor.cancelAll();
50803
50951
  await runManager.close();
50804
50952
  }
50953
+ async function runAgent(config, signal) {
50954
+ const failoverEnabled = agentEndpoints(config).length > 1;
50955
+ let preferred;
50956
+ while (!signal.aborted) {
50957
+ const endpoint = preferred ?? await selectAgentEndpoint(config, signal);
50958
+ preferred = void 0;
50959
+ if (signal.aborted) break;
50960
+ if (failoverEnabled) {
50961
+ console.log(
50962
+ `Using the ${endpoint.kind} control plane at ${endpoint.server}`
50963
+ );
50964
+ }
50965
+ const session = new AbortController();
50966
+ const abortSession = () => session.abort();
50967
+ signal.addEventListener("abort", abortSession, { once: true });
50968
+ let probing = false;
50969
+ const onUnhealthy = () => {
50970
+ if (probing || session.signal.aborted) return;
50971
+ probing = true;
50972
+ void findHealthyAlternate(config, endpoint, session.signal).then((alternate) => {
50973
+ if (session.signal.aborted) return;
50974
+ if (!alternate) {
50975
+ console.log(
50976
+ `The ${endpoint.kind} control plane is not answering and no other endpoint is either; keeping the session and retrying`
50977
+ );
50978
+ return;
50979
+ }
50980
+ console.log(
50981
+ `The ${endpoint.kind} control plane stopped responding; switching to the ${alternate.kind} control plane at ${alternate.server}`
50982
+ );
50983
+ preferred = alternate;
50984
+ session.abort();
50985
+ }).finally(() => {
50986
+ probing = false;
50987
+ });
50988
+ };
50989
+ try {
50990
+ await runAgentSession(
50991
+ configForEndpoint(config, endpoint),
50992
+ session.signal,
50993
+ failoverEnabled ? onUnhealthy : void 0
50994
+ );
50995
+ } finally {
50996
+ signal.removeEventListener("abort", abortSession);
50997
+ }
50998
+ }
50999
+ }
50805
51000
 
50806
51001
  // src/dev-runtime.ts
50807
51002
  var DEFAULT_SERVER_WAIT_MS = 12e4;
@@ -50883,10 +51078,10 @@ async function prepareDevelopmentAgent(options, signal, dependencies = defaultDe
50883
51078
  }
50884
51079
  } catch {
50885
51080
  }
50886
- const enrollment = await anonymousClient.createEnrollmentToken();
51081
+ const enrollment2 = await anonymousClient.createEnrollmentToken();
50887
51082
  const response = await anonymousClient.enroll({
50888
51083
  ...inventory,
50889
- enrollmentToken: enrollment.createAgentEnrollmentToken.token,
51084
+ enrollmentToken: enrollment2.createAgentEnrollmentToken.token,
50890
51085
  name
50891
51086
  });
50892
51087
  const config = {
@@ -50936,12 +51131,17 @@ function redactedRequestHeaders(headers) {
50936
51131
  }
50937
51132
 
50938
51133
  // src/index.ts
51134
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["clear-remote"]);
50939
51135
  function flags(args) {
50940
51136
  const result = {};
50941
51137
  const headerValues = [];
50942
51138
  for (let index = 0; index < args.length; index += 1) {
50943
51139
  const argument = args[index];
50944
51140
  if (!argument.startsWith("--")) continue;
51141
+ if (BOOLEAN_FLAGS.has(argument.slice(2))) {
51142
+ result[argument.slice(2)] = "true";
51143
+ continue;
51144
+ }
50945
51145
  const value = args[index + 1];
50946
51146
  if (!value || value.startsWith("--"))
50947
51147
  throw new Error(`Missing value for ${argument}`);
@@ -50955,26 +51155,50 @@ function usage() {
50955
51155
  console.log(`control-agent <command>
50956
51156
 
50957
51157
  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"]...
51158
+ 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"]...
51159
+ endpoints [--server <url>] [--websocket-server <url>] [--remote-server <url>] [--remote-websocket-server <url>] [--clear-remote]
51160
+ enrollment
51161
+ unenroll
50959
51162
  dev [--server http://127.0.0.1:3000] [--websocket-server ws://127.0.0.1:3092/graphql] [--name <name>]
50960
51163
  run
50961
51164
  status
50962
51165
  doctor`);
50963
51166
  }
51167
+ function endpointOptions(options, fallback) {
51168
+ const server = normalizeServer(
51169
+ options.server ?? fallback?.server ?? "http://127.0.0.1:3090"
51170
+ );
51171
+ const websocketServer = options["websocket-server"] ?? // A new local address invalidates a websocket address derived from the old
51172
+ // one, so only an explicitly stored value survives a server change.
51173
+ (options.server ? void 0 : fallback?.websocketServer) ?? defaultWebSocketServer(server);
51174
+ const clearRemote = options["clear-remote"] === "true";
51175
+ const remote = options["remote-server"] ?? (clearRemote ? null : void 0);
51176
+ const remoteServer = remote === void 0 ? fallback?.remoteServer ?? null : remote && normalizeServer(remote);
51177
+ const remoteWebSocketServer = !remoteServer ? null : options["remote-websocket-server"] ?? (options["remote-server"] ? void 0 : fallback?.remoteWebSocketServer) ?? defaultWebSocketServer(remoteServer);
51178
+ return {
51179
+ server,
51180
+ websocketServer,
51181
+ remoteServer,
51182
+ remoteWebSocketServer
51183
+ };
51184
+ }
50964
51185
  async function enroll(args) {
50965
51186
  const parsed = flags(args);
50966
51187
  const options = parsed.values;
50967
51188
  const enrollmentToken = options["enrollment-token"];
50968
51189
  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);
51190
+ const addresses = endpointOptions(options);
50971
51191
  const inventory = collectInventory();
50972
51192
  const name = options.name ?? inventory.hostname;
50973
- const client2 = new AgentGraphQLClient(server, null, 1e4, parsed.headers);
51193
+ const client2 = new AgentGraphQLClient(
51194
+ addresses.server,
51195
+ null,
51196
+ 1e4,
51197
+ parsed.headers
51198
+ );
50974
51199
  const response = await client2.enroll({ ...inventory, enrollmentToken, name });
50975
51200
  await saveConfig({
50976
- server,
50977
- websocketServer,
51201
+ ...addresses,
50978
51202
  agentId: response.enrollAgent.agent.id,
50979
51203
  credential: response.enrollAgent.credential,
50980
51204
  name,
@@ -50982,6 +51206,63 @@ async function enroll(args) {
50982
51206
  });
50983
51207
  console.log(`Enrolled agent ${name} (${response.enrollAgent.agent.id})`);
50984
51208
  }
51209
+ async function endpoints(args) {
51210
+ const options = flags(args).values;
51211
+ const config = await loadConfig();
51212
+ const changing = [
51213
+ "server",
51214
+ "websocket-server",
51215
+ "remote-server",
51216
+ "remote-websocket-server",
51217
+ "clear-remote"
51218
+ ].some((flag) => flag in options);
51219
+ const updated = changing ? { ...config, ...endpointOptions(options, config) } : config;
51220
+ if (changing) await saveConfig(updated);
51221
+ for (const endpoint of agentEndpoints(updated)) {
51222
+ console.log(
51223
+ `${endpoint.kind}: ${endpoint.server} (${endpoint.websocketServer})`
51224
+ );
51225
+ }
51226
+ if (!updated.remoteServer) console.log("remote: not configured");
51227
+ }
51228
+ async function enrollment() {
51229
+ let config;
51230
+ try {
51231
+ config = await loadConfig();
51232
+ } catch (error) {
51233
+ console.log("Enrolled: no");
51234
+ console.log(error instanceof Error ? error.message : String(error));
51235
+ process.exitCode = 1;
51236
+ return;
51237
+ }
51238
+ console.log("Enrolled: yes");
51239
+ console.log(`Agent: ${config.name} (${config.agentId})`);
51240
+ console.log(`Configuration: ${configPath()}`);
51241
+ let reachable = false;
51242
+ for (const endpoint of agentEndpoints(config)) {
51243
+ const ok2 = await probeEndpoint(endpoint, config);
51244
+ reachable ||= ok2;
51245
+ console.log(
51246
+ `${endpoint.kind}: ${endpoint.server} \u2014 ${ok2 ? "reachable" : "unreachable"}`
51247
+ );
51248
+ }
51249
+ if (!reachable) process.exitCode = 1;
51250
+ }
51251
+ async function unenroll() {
51252
+ const path = configPath();
51253
+ let config = null;
51254
+ try {
51255
+ config = await loadConfig(path);
51256
+ } catch {
51257
+ }
51258
+ await rm14(path, { force: true });
51259
+ console.log(
51260
+ config ? `Removed the enrollment for ${config.name} (${config.agentId}) from ${path}` : `Removed ${path}`
51261
+ );
51262
+ console.log(
51263
+ "The control plane still lists this agent; delete it there to finish removing it."
51264
+ );
51265
+ }
50985
51266
  async function status() {
50986
51267
  const config = await loadConfig();
50987
51268
  const response = await new AgentGraphQLClient(
@@ -51018,26 +51299,39 @@ async function doctor() {
51018
51299
  detail: error instanceof Error ? error.message : String(error)
51019
51300
  });
51020
51301
  }
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
- }
51302
+ const reachable = config ? agentEndpoints(config) : [
51303
+ {
51304
+ kind: "local",
51305
+ server: "http://127.0.0.1:3090",
51306
+ websocketServer: "ws://127.0.0.1:3090/graphql"
51307
+ }
51308
+ ];
51309
+ const results = await Promise.all(
51310
+ reachable.map(async (endpoint) => {
51311
+ try {
51312
+ const health = await new AgentGraphQLClient(
51313
+ endpoint.server,
51314
+ null,
51315
+ 1e4,
51316
+ config?.headers
51317
+ ).health();
51318
+ return {
51319
+ ok: health.health === "ok",
51320
+ detail: `${endpoint.kind} ${endpoint.server}: ${health.health}`
51321
+ };
51322
+ } catch (error) {
51323
+ return {
51324
+ ok: false,
51325
+ detail: `${endpoint.kind} ${endpoint.server}: ${error instanceof Error ? error.message : String(error)}`
51326
+ };
51327
+ }
51328
+ })
51329
+ );
51330
+ checks.push({
51331
+ check: "control plane",
51332
+ ok: results.some((result) => result.ok),
51333
+ detail: results.map((result) => result.detail).join("; ")
51334
+ });
51041
51335
  for (const check of checks)
51042
51336
  console.log(
51043
51337
  `${check.ok ? "PASS" : "FAIL"} ${check.check}: ${check.detail}`
@@ -51045,12 +51339,16 @@ async function doctor() {
51045
51339
  if (checks.some((check) => !check.ok)) process.exitCode = 1;
51046
51340
  }
51047
51341
  async function main() {
51342
+ repairToolPath();
51048
51343
  const [command3, ...args] = process.argv.slice(2);
51049
51344
  if (!command3 || command3 === "help" || command3 === "--help") {
51050
51345
  usage();
51051
51346
  return;
51052
51347
  }
51053
51348
  if (command3 === "enroll") return enroll(args);
51349
+ if (command3 === "endpoints") return endpoints(args);
51350
+ if (command3 === "enrollment") return enrollment();
51351
+ if (command3 === "unenroll") return unenroll();
51054
51352
  if (command3 === "status") return status();
51055
51353
  if (command3 === "doctor") return doctor();
51056
51354
  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.76",
3
+ "version": "0.0.78",
4
4
  "description": "Control agent for the AI Development Environment",
5
5
  "license": "MIT",
6
6
  "repository": {