@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 +36 -0
- package/dist/control-agent.js +525 -227
- package/package.json +1 -1
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
|
package/dist/control-agent.js
CHANGED
|
@@ -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:
|
|
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
|
|
12233
|
-
import { basename as basename4, dirname as
|
|
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 [
|
|
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 =
|
|
12445
|
+
const path = join7(rootPath2, child.name);
|
|
12313
12446
|
const information = await lstat2(path);
|
|
12314
12447
|
const projectPath = await workspacePath(
|
|
12315
|
-
|
|
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 =
|
|
12334
|
-
process.env.ADE_IOS_DEVICE_SUPPORT_DIRECTORY ??
|
|
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:
|
|
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 ||
|
|
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(
|
|
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) ||
|
|
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) ||
|
|
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 =
|
|
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 =
|
|
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
|
|
12629
|
-
import { dirname as
|
|
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(
|
|
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(
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
13514
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
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" ?
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
13965
|
+
const destination = join9(root, skillPackage.name);
|
|
13833
13966
|
await assertUntracked(location, destination);
|
|
13834
13967
|
await mkdir4(root, { recursive: true });
|
|
13835
|
-
const temporary =
|
|
13836
|
-
const backup =
|
|
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(
|
|
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 =
|
|
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
|
|
13905
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
14322
|
+
const directory = await mkdtemp5(join12(tmpdir5(), "aide-workflow-"));
|
|
14190
14323
|
const extension2 = payload.interpreter === "NODE" ? "mjs" : "sh";
|
|
14191
|
-
const scriptPath =
|
|
14192
|
-
const sessionPath =
|
|
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
|
|
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 :
|
|
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 :
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
15364
|
-
uploadPath =
|
|
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" ?
|
|
15628
|
-
|
|
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 =
|
|
15868
|
-
|
|
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
|
|
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
|
|
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
|
|
16972
|
+
return join14(dirname8(configPath()), "runs", this.runId);
|
|
16840
16973
|
}
|
|
16841
16974
|
get eventsPath() {
|
|
16842
|
-
return
|
|
16975
|
+
return join14(this.directory, "events.jsonl");
|
|
16843
16976
|
}
|
|
16844
16977
|
get statePath() {
|
|
16845
|
-
return
|
|
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
|
-
|
|
42780
|
-
|
|
42781
|
-
|
|
42782
|
-
|
|
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
|
|
43336
|
+
const executable = this.executable();
|
|
43337
|
+
const modelCatalogPath = await this.createBundledModelCatalog(executable);
|
|
43210
43338
|
const modelCatalogDirectory = this.modelCatalogDirectory;
|
|
43211
|
-
const child = spawn7(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
51081
|
+
const enrollment2 = await anonymousClient.createEnrollmentToken();
|
|
50887
51082
|
const response = await anonymousClient.enroll({
|
|
50888
51083
|
...inventory,
|
|
50889
|
-
enrollmentToken:
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
51022
|
-
|
|
51023
|
-
|
|
51024
|
-
server,
|
|
51025
|
-
|
|
51026
|
-
|
|
51027
|
-
|
|
51028
|
-
|
|
51029
|
-
|
|
51030
|
-
|
|
51031
|
-
|
|
51032
|
-
|
|
51033
|
-
|
|
51034
|
-
|
|
51035
|
-
|
|
51036
|
-
|
|
51037
|
-
|
|
51038
|
-
|
|
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") {
|