@arcadeai/hypotenuse 0.19.0 → 0.21.0

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.
@@ -343,7 +343,7 @@ import { Command } from "commander";
343
343
  // package.json
344
344
  var package_default = {
345
345
  name: "@arcadeai/hypotenuse",
346
- version: "0.19.0",
346
+ version: "0.21.0",
347
347
  description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
348
348
  type: "module",
349
349
  license: "Apache-2.0",
@@ -1097,172 +1097,55 @@ var RunnerLimits = z2.strictObject({
1097
1097
  agents: z2.record(identifier, cap).optional()
1098
1098
  });
1099
1099
  // ../protocol/src/complete.ts
1100
- import { z as z3 } from "zod";
1101
- var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
1102
- var CompleteRequest = z3.strictObject({
1103
- workItemId: z3.number().int().positive(),
1104
- outcome: z3.enum(COMPLETE_OUTCOMES),
1105
- error: z3.string().max(4096).nullable().optional(),
1106
- acpSessionId: z3.string().max(255).nullable().optional(),
1107
- branch: z3.string().max(255).nullable().optional(),
1108
- answersConsumed: z3.array(z3.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt")
1109
- });
1110
- var CompleteResponse = z3.looseObject({
1111
- alreadyTerminal: z3.boolean()
1112
- });
1113
- // ../protocol/src/connect.ts
1114
- import { z as z5 } from "zod";
1115
-
1116
- // ../protocol/src/heartbeat.ts
1117
1100
  import { z as z4 } from "zod";
1118
- var ACTIVE_STATES = ["starting", "running", "cancelling"];
1119
- var ActiveItem = z4.strictObject({
1120
- workItemId: z4.number().int().positive(),
1121
- agentSessionId: z4.number().int().positive(),
1122
- role: RunnerRole,
1123
- agentId: z4.string().min(1).max(64),
1124
- state: z4.enum(ACTIVE_STATES)
1125
- });
1126
- var MachineId = z4.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
1127
- var HeartbeatRequest = z4.strictObject({
1128
- protocolVersion: z4.number().int().positive(),
1129
- daemonVersion: z4.string().trim().min(1).max(64),
1130
- platform: z4.string().trim().min(1).max(64),
1131
- hostname: z4.string().trim().min(1).max(256),
1132
- draining: z4.boolean(),
1133
- machineId: MachineId,
1134
- limits: RunnerLimits,
1135
- active: z4.array(ActiveItem).max(1000),
1136
- capabilities: RunnerCapabilities,
1137
- dryRun: z4.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
1138
- });
1139
- var DryRunRunner = z4.looseObject({
1140
- id: z4.number().int().positive(),
1141
- name: z4.string(),
1142
- projectId: z4.number().int().positive(),
1143
- projectSlug: z4.string()
1144
- });
1145
- var OFFER_KINDS = [
1146
- "session_created",
1147
- "prompt",
1148
- "dispatch",
1149
- "review"
1150
- ];
1151
- var Offer = z4.looseObject({
1152
- workItemId: z4.number().int().positive(),
1153
- agentSessionId: z4.number().int().positive(),
1154
- kind: z4.enum(OFFER_KINDS),
1155
- role: RunnerRole,
1156
- offerExpiresAt: z4.number().int()
1157
- });
1158
- var CANCEL_REASONS = [
1159
- "stop",
1160
- "dashboard",
1161
- "delegate_removed",
1162
- "issue_closed",
1163
- "unassigned",
1164
- "integration_revoked",
1165
- "superseded"
1166
- ];
1167
- var Cancel = z4.looseObject({
1168
- workItemId: z4.number().int().positive(),
1169
- reason: z4.union([z4.enum(CANCEL_REASONS), z4.string().min(1).max(64)]),
1170
- by: z4.string().min(1).max(200).optional(),
1171
- said: z4.string().min(1).max(2000).optional()
1172
- });
1173
- var STOP_QUOTE_MAX = 500;
1174
- function stopQuote(said) {
1175
- const text = said?.trim();
1176
- if (!text || /^stop[.!]*$/i.test(text))
1177
- return "";
1178
- const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
1179
- return `
1180
1101
 
1181
- > You said: ${cut.split(`
1182
- `).join(`
1183
- > `)}`;
1184
- }
1185
- var Answer = z4.looseObject({
1186
- workItemId: z4.number().int().positive(),
1187
- activityId: z4.number().int().positive(),
1188
- body: z4.string()
1189
- });
1190
- var HeartbeatResponse = z4.looseObject({
1191
- serverTime: z4.number().int(),
1192
- heartbeatIntervalMs: z4.number().int().positive(),
1193
- leaseMs: z4.number().int().positive(),
1194
- offers: z4.array(Offer),
1195
- cancels: z4.array(Cancel),
1196
- unknownActive: z4.array(z4.number().int().positive()),
1197
- answers: z4.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
1198
- runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
1199
- unknownSubscriptions: z4.looseObject({ teams: z4.array(z4.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
1200
- });
1201
-
1202
- // ../protocol/src/connect.ts
1203
- var ConnectRequest = z5.strictObject({
1204
- machineId: MachineId
1205
- });
1206
- var ConnectResponse = z5.looseObject({
1207
- runnerId: z5.number().int().positive(),
1208
- channel: z5.string().regex(/^runner:\d+$/),
1209
- heartbeatIntervalMs: z5.number().int().positive(),
1210
- leaseMs: z5.number().int().positive(),
1211
- serverTime: z5.number().int()
1212
- });
1213
- var MACHINE_IN_USE = "MACHINE_IN_USE";
1214
- var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
1215
- var RunnerFrame = z5.object({ event: z5.enum(RUNNER_FRAME_EVENTS) });
1216
- function runnerChannel(runnerId) {
1217
- return `runner:${runnerId}`;
1218
- }
1219
1102
  // ../protocol/src/events.ts
1220
- import { z as z6 } from "zod";
1103
+ import { z as z3 } from "zod";
1221
1104
  var MAX_EVENTS_PER_BATCH = 100;
1222
1105
  var MAX_EVENTS_BATCH_BYTES = 256 * 1024;
1223
1106
  var NOT_YOUR_WORK = "NOT_YOUR_WORK";
1224
1107
  var SEQ_REGRESSION = "SEQ_REGRESSION";
1225
- var body = z6.string().trim().min(1).max(64 * 1024);
1108
+ var body = z3.string().trim().min(1).max(64 * 1024);
1226
1109
  var envelope = {
1227
- eventId: z6.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
1228
- seq: z6.number().int().positive().describe("Per work item, from 1, never going backwards"),
1229
- at: z6.number().int().describe("When the daemon saw it, epoch ms")
1110
+ eventId: z3.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
1111
+ seq: z3.number().int().positive().describe("Per work item, from 1, never going backwards"),
1112
+ at: z3.number().int().describe("When the daemon saw it, epoch ms")
1230
1113
  };
1231
- var SelectMetadata = z6.strictObject({
1232
- options: z6.array(z6.strictObject({
1233
- label: z6.string().trim().min(1).max(255).optional(),
1234
- value: z6.string().trim().min(1).max(1024)
1114
+ var SelectMetadata = z3.strictObject({
1115
+ options: z3.array(z3.strictObject({
1116
+ label: z3.string().trim().min(1).max(255).optional(),
1117
+ value: z3.string().trim().min(1).max(1024)
1235
1118
  })).min(1).max(25)
1236
1119
  });
1237
- var AuthProvider = z6.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
1238
- var AuthMetadata = z6.strictObject({
1239
- url: z6.url().optional(),
1120
+ var AuthProvider = z3.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
1121
+ var AuthMetadata = z3.strictObject({
1122
+ url: z3.url().optional(),
1240
1123
  provider: AuthProvider.optional(),
1241
- userId: z6.string().max(255).optional(),
1242
- providerName: z6.string().max(255).optional()
1124
+ userId: z3.string().max(255).optional(),
1125
+ providerName: z3.string().max(255).optional()
1243
1126
  }).refine((m) => m.url !== undefined || m.provider !== undefined, {
1244
1127
  message: "an auth elicitation needs a url or a provider"
1245
1128
  });
1246
- var ThoughtEvent = z6.strictObject({
1129
+ var ThoughtEvent = z3.strictObject({
1247
1130
  ...envelope,
1248
- type: z6.literal("thought"),
1131
+ type: z3.literal("thought"),
1249
1132
  body,
1250
- ephemeral: z6.boolean().default(false)
1133
+ ephemeral: z3.boolean().default(false)
1251
1134
  });
1252
- var ActionEvent = z6.strictObject({
1135
+ var ActionEvent = z3.strictObject({
1253
1136
  ...envelope,
1254
- type: z6.literal("action"),
1255
- action: z6.string().trim().min(1).max(255),
1256
- parameter: z6.string().trim().min(1).max(4096),
1257
- result: z6.string().max(64 * 1024).optional(),
1258
- ephemeral: z6.boolean().default(false)
1137
+ type: z3.literal("action"),
1138
+ action: z3.string().trim().min(1).max(255),
1139
+ parameter: z3.string().trim().min(1).max(4096),
1140
+ result: z3.string().max(64 * 1024).optional(),
1141
+ ephemeral: z3.boolean().default(false)
1259
1142
  });
1260
- var ElicitationEvent = z6.strictObject({
1143
+ var ElicitationEvent = z3.strictObject({
1261
1144
  ...envelope,
1262
- type: z6.literal("elicitation"),
1145
+ type: z3.literal("elicitation"),
1263
1146
  body,
1264
- signal: z6.enum(["select", "auth"]).optional(),
1265
- signalMetadata: z6.record(z6.string(), z6.unknown()).optional()
1147
+ signal: z3.enum(["select", "auth"]).optional(),
1148
+ signalMetadata: z3.record(z3.string(), z3.unknown()).optional()
1266
1149
  }).superRefine((event, ctx) => {
1267
1150
  if (!event.signal) {
1268
1151
  if (event.signalMetadata) {
@@ -1284,14 +1167,14 @@ var ElicitationEvent = z6.strictObject({
1284
1167
  });
1285
1168
  }
1286
1169
  });
1287
- var ResponseEvent = z6.strictObject({
1170
+ var ResponseEvent = z3.strictObject({
1288
1171
  ...envelope,
1289
- type: z6.literal("response"),
1172
+ type: z3.literal("response"),
1290
1173
  body
1291
1174
  });
1292
- var ErrorEvent = z6.strictObject({
1175
+ var ErrorEvent = z3.strictObject({
1293
1176
  ...envelope,
1294
- type: z6.literal("error"),
1177
+ type: z3.literal("error"),
1295
1178
  body
1296
1179
  });
1297
1180
  var PLAN_ENTRY_STATUSES = [
@@ -1300,33 +1183,36 @@ var PLAN_ENTRY_STATUSES = [
1300
1183
  "completed",
1301
1184
  "canceled"
1302
1185
  ];
1303
- var PlanEvent = z6.strictObject({
1186
+ var PlanEvent = z3.strictObject({
1304
1187
  ...envelope,
1305
- type: z6.literal("plan"),
1306
- entries: z6.array(z6.strictObject({
1307
- content: z6.string().trim().min(1).max(1024),
1308
- status: z6.enum(PLAN_ENTRY_STATUSES)
1188
+ type: z3.literal("plan"),
1189
+ entries: z3.array(z3.strictObject({
1190
+ content: z3.string().trim().min(1).max(1024),
1191
+ status: z3.enum(PLAN_ENTRY_STATUSES)
1309
1192
  })).max(100)
1310
1193
  });
1311
- var SummaryEvent = z6.strictObject({
1194
+ var SummaryEvent = z3.strictObject({
1312
1195
  ...envelope,
1313
- type: z6.literal("summary"),
1314
- title: z6.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
1196
+ type: z3.literal("summary"),
1197
+ title: z3.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
1315
1198
  });
1316
- var ExternalUrlEvent = z6.strictObject({
1199
+ var ExternalUrlEvent = z3.strictObject({
1317
1200
  ...envelope,
1318
- type: z6.literal("external_url"),
1319
- label: z6.string().trim().min(1).max(255),
1320
- url: z6.url()
1201
+ type: z3.literal("external_url"),
1202
+ label: z3.string().trim().min(1).max(255),
1203
+ url: z3.url()
1321
1204
  });
1322
- var UsageEvent = z6.strictObject({
1205
+ var TurnUsage = z3.strictObject({
1206
+ inputTokens: z3.number().int().min(0).optional(),
1207
+ outputTokens: z3.number().int().min(0).optional(),
1208
+ costUsd: z3.number().min(0).optional().describe("This turn's cost in US dollars, as the agent reported it")
1209
+ });
1210
+ var UsageEvent = z3.strictObject({
1323
1211
  ...envelope,
1324
- type: z6.literal("usage"),
1325
- inputTokens: z6.number().int().min(0).optional(),
1326
- outputTokens: z6.number().int().min(0).optional(),
1327
- costUsd: z6.number().min(0).optional(),
1328
- contextUsed: z6.number().int().min(0).optional(),
1329
- contextSize: z6.number().int().min(0).optional()
1212
+ type: z3.literal("usage"),
1213
+ ...TurnUsage.shape,
1214
+ contextUsed: z3.number().int().min(0).optional(),
1215
+ contextSize: z3.number().int().min(0).optional()
1330
1216
  });
1331
1217
  var STATUS_EVENT_STATES = [
1332
1218
  "starting",
@@ -1334,12 +1220,12 @@ var STATUS_EVENT_STATES = [
1334
1220
  "waiting_permission",
1335
1221
  "finishing"
1336
1222
  ];
1337
- var StatusEvent = z6.strictObject({
1223
+ var StatusEvent = z3.strictObject({
1338
1224
  ...envelope,
1339
- type: z6.literal("status"),
1340
- state: z6.enum(STATUS_EVENT_STATES)
1225
+ type: z3.literal("status"),
1226
+ state: z3.enum(STATUS_EVENT_STATES)
1341
1227
  });
1342
- var SessionEvent = z6.discriminatedUnion("type", [
1228
+ var SessionEvent = z3.discriminatedUnion("type", [
1343
1229
  ThoughtEvent,
1344
1230
  ActionEvent,
1345
1231
  ElicitationEvent,
@@ -1351,16 +1237,137 @@ var SessionEvent = z6.discriminatedUnion("type", [
1351
1237
  UsageEvent,
1352
1238
  StatusEvent
1353
1239
  ]);
1354
- var EventsRequest = z6.strictObject({
1355
- workItemId: z6.number().int().positive(),
1356
- events: z6.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
1240
+ var EventsRequest = z3.strictObject({
1241
+ workItemId: z3.number().int().positive(),
1242
+ events: z3.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
1243
+ });
1244
+ var EventsResponse = z3.looseObject({
1245
+ accepted: z3.number().int().min(0),
1246
+ duplicates: z3.number().int().min(0),
1247
+ dropped: z3.number().int().min(0),
1248
+ cancelRequested: z3.boolean()
1249
+ });
1250
+
1251
+ // ../protocol/src/complete.ts
1252
+ var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
1253
+ var CompleteRequest = z4.strictObject({
1254
+ workItemId: z4.number().int().positive(),
1255
+ outcome: z4.enum(COMPLETE_OUTCOMES),
1256
+ error: z4.string().max(4096).nullable().optional(),
1257
+ acpSessionId: z4.string().max(255).nullable().optional(),
1258
+ branch: z4.string().max(255).nullable().optional(),
1259
+ answersConsumed: z4.array(z4.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt"),
1260
+ usage: TurnUsage.optional().describe("What the turn spent, in full; recorded even when the turn was stopped and its events were dropped")
1261
+ });
1262
+ var CompleteResponse = z4.looseObject({
1263
+ alreadyTerminal: z4.boolean()
1264
+ });
1265
+ // ../protocol/src/connect.ts
1266
+ import { z as z6 } from "zod";
1267
+
1268
+ // ../protocol/src/heartbeat.ts
1269
+ import { z as z5 } from "zod";
1270
+ var ACTIVE_STATES = ["starting", "running", "cancelling"];
1271
+ var ActiveItem = z5.strictObject({
1272
+ workItemId: z5.number().int().positive(),
1273
+ agentSessionId: z5.number().int().positive(),
1274
+ role: RunnerRole,
1275
+ agentId: z5.string().min(1).max(64),
1276
+ state: z5.enum(ACTIVE_STATES)
1277
+ });
1278
+ var MachineId = z5.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
1279
+ var HeartbeatRequest = z5.strictObject({
1280
+ protocolVersion: z5.number().int().positive(),
1281
+ daemonVersion: z5.string().trim().min(1).max(64),
1282
+ platform: z5.string().trim().min(1).max(64),
1283
+ hostname: z5.string().trim().min(1).max(256),
1284
+ draining: z5.boolean(),
1285
+ machineId: MachineId,
1286
+ limits: RunnerLimits,
1287
+ active: z5.array(ActiveItem).max(1000),
1288
+ capabilities: RunnerCapabilities,
1289
+ dryRun: z5.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
1290
+ });
1291
+ var DryRunRunner = z5.looseObject({
1292
+ id: z5.number().int().positive(),
1293
+ name: z5.string(),
1294
+ projectId: z5.number().int().positive(),
1295
+ projectSlug: z5.string()
1296
+ });
1297
+ var OFFER_KINDS = [
1298
+ "session_created",
1299
+ "prompt",
1300
+ "dispatch",
1301
+ "review"
1302
+ ];
1303
+ var Offer = z5.looseObject({
1304
+ workItemId: z5.number().int().positive(),
1305
+ agentSessionId: z5.number().int().positive(),
1306
+ kind: z5.enum(OFFER_KINDS),
1307
+ role: RunnerRole,
1308
+ offerExpiresAt: z5.number().int()
1309
+ });
1310
+ var CANCEL_REASONS = [
1311
+ "stop",
1312
+ "dashboard",
1313
+ "delegate_removed",
1314
+ "issue_closed",
1315
+ "unassigned",
1316
+ "integration_revoked",
1317
+ "superseded"
1318
+ ];
1319
+ var Cancel = z5.looseObject({
1320
+ workItemId: z5.number().int().positive(),
1321
+ reason: z5.union([z5.enum(CANCEL_REASONS), z5.string().min(1).max(64)]),
1322
+ by: z5.string().min(1).max(200).optional(),
1323
+ said: z5.string().min(1).max(2000).optional()
1357
1324
  });
1358
- var EventsResponse = z6.looseObject({
1359
- accepted: z6.number().int().min(0),
1360
- duplicates: z6.number().int().min(0),
1361
- dropped: z6.number().int().min(0),
1362
- cancelRequested: z6.boolean()
1325
+ var STOP_QUOTE_MAX = 500;
1326
+ function stopQuote(said) {
1327
+ const text = said?.trim();
1328
+ if (!text || /^stop[.!]*$/i.test(text))
1329
+ return "";
1330
+ const cut = text.length > STOP_QUOTE_MAX ? `${text.slice(0, STOP_QUOTE_MAX)}\u2026` : text;
1331
+ return `
1332
+
1333
+ > You said: ${cut.split(`
1334
+ `).join(`
1335
+ > `)}`;
1336
+ }
1337
+ var Answer = z5.looseObject({
1338
+ workItemId: z5.number().int().positive(),
1339
+ activityId: z5.number().int().positive(),
1340
+ body: z5.string()
1341
+ });
1342
+ var HeartbeatResponse = z5.looseObject({
1343
+ serverTime: z5.number().int(),
1344
+ heartbeatIntervalMs: z5.number().int().positive(),
1345
+ leaseMs: z5.number().int().positive(),
1346
+ offers: z5.array(Offer),
1347
+ cancels: z5.array(Cancel),
1348
+ unknownActive: z5.array(z5.number().int().positive()),
1349
+ answers: z5.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
1350
+ runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
1351
+ unknownSubscriptions: z5.looseObject({ teams: z5.array(z5.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
1352
+ });
1353
+
1354
+ // ../protocol/src/connect.ts
1355
+ var ConnectRequest = z6.strictObject({
1356
+ machineId: MachineId
1363
1357
  });
1358
+ var ConnectResponse = z6.looseObject({
1359
+ runnerId: z6.number().int().positive(),
1360
+ channel: z6.string().regex(/^runner:\d+$/),
1361
+ heartbeatIntervalMs: z6.number().int().positive(),
1362
+ leaseMs: z6.number().int().positive(),
1363
+ serverTime: z6.number().int()
1364
+ });
1365
+ var MACHINE_IN_USE = "MACHINE_IN_USE";
1366
+ var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
1367
+ var RunnerFrame = z6.object({ event: z6.enum(RUNNER_FRAME_EVENTS) });
1368
+ function runnerChannel(runnerId) {
1369
+ return `runner:${runnerId}`;
1370
+ }
1364
1371
  // ../protocol/src/otlp.ts
1365
1372
  import protobuf from "protobufjs/light.js";
1366
1373
  var SPAN_KIND = {
@@ -3922,9 +3929,9 @@ async function socketAnswers(socketPath, timeoutMs = 1500) {
3922
3929
  import { spawn as spawn3 } from "child_process";
3923
3930
  import {
3924
3931
  closeSync as closeSync3,
3925
- existsSync as existsSync17,
3932
+ existsSync as existsSync18,
3926
3933
  openSync as openSync3,
3927
- readFileSync as readFileSync11,
3934
+ readFileSync as readFileSync12,
3928
3935
  readSync,
3929
3936
  rmSync as rmSync9,
3930
3937
  statSync as statSync5
@@ -4159,8 +4166,8 @@ class DaemonLog {
4159
4166
 
4160
4167
  // src/daemon/main.ts
4161
4168
  import { spawn as spawn2 } from "child_process";
4162
- import { createHash as createHash2 } from "crypto";
4163
- import { existsSync as existsSync16, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync5 } from "fs";
4169
+ import { createHash as createHash3 } from "crypto";
4170
+ import { existsSync as existsSync17, readFileSync as readFileSync11, rmSync as rmSync8, writeFileSync as writeFileSync5 } from "fs";
4164
4171
  import { join as join10 } from "path";
4165
4172
 
4166
4173
  // src/acp/host.ts
@@ -5740,6 +5747,11 @@ class TurnMapper {
5740
5747
  lastPlan = "";
5741
5748
  unknown = new Set;
5742
5749
  lastActionSummary = null;
5750
+ costBaseline = null;
5751
+ replayedCost = null;
5752
+ costChecked = false;
5753
+ sessionCost = null;
5754
+ usage = {};
5743
5755
  recorded = {};
5744
5756
  constructor(options) {
5745
5757
  this.options = options;
@@ -5751,8 +5763,14 @@ class TurnMapper {
5751
5763
  return [...this.messages.values()].some((m) => m.trim());
5752
5764
  }
5753
5765
  update(update) {
5754
- if (this.replaying)
5766
+ if (this.replaying) {
5767
+ if (update.sessionUpdate === "usage_update") {
5768
+ const cost = usdCost(update.cost);
5769
+ if (cost !== null)
5770
+ this.replayedCost = cost;
5771
+ }
5755
5772
  return [];
5773
+ }
5756
5774
  const kind = update.sessionUpdate;
5757
5775
  switch (update.sessionUpdate) {
5758
5776
  case "agent_thought_chunk": {
@@ -5838,14 +5856,10 @@ class TurnMapper {
5838
5856
  return [{ type: "summary", title: truncate(title, 255) }];
5839
5857
  }
5840
5858
  case "usage_update": {
5841
- return [
5842
- {
5843
- type: "usage",
5844
- contextUsed: Math.max(0, Math.round(update.used)),
5845
- contextSize: Math.max(0, Math.round(update.size)),
5846
- ...update.cost && update.cost.currency.toUpperCase() === "USD" ? { costUsd: Math.max(0, update.cost.amount) } : {}
5847
- }
5848
- ];
5859
+ this.usage.contextUsed = Math.max(0, Math.round(update.used));
5860
+ this.usage.contextSize = Math.max(0, Math.round(update.size));
5861
+ this.recordCost(usdCost(update.cost));
5862
+ return [{ type: "usage", ...this.usage }];
5849
5863
  }
5850
5864
  case "current_mode_update":
5851
5865
  this.recorded.mode = update.currentModeId;
@@ -5861,6 +5875,47 @@ class TurnMapper {
5861
5875
  return [];
5862
5876
  }
5863
5877
  }
5878
+ acceptReplay() {
5879
+ if (this.replayedCost !== null)
5880
+ this.costBaseline = this.replayedCost;
5881
+ this.replayedCost = null;
5882
+ }
5883
+ recordCost(cumulative) {
5884
+ if (cumulative === null)
5885
+ return;
5886
+ if (!this.costChecked) {
5887
+ this.costChecked = true;
5888
+ if (this.costBaseline !== null && cumulative < this.costBaseline)
5889
+ this.costBaseline = 0;
5890
+ }
5891
+ this.sessionCost = cumulative;
5892
+ if (this.costBaseline !== null)
5893
+ this.usage.costUsd = roundUsd(Math.max(0, cumulative - this.costBaseline));
5894
+ }
5895
+ tokens(tokens) {
5896
+ const count = (n) => typeof n === "number" && Number.isFinite(n) ? Math.max(0, Math.round(n)) : undefined;
5897
+ const input = count(tokens?.inputTokens);
5898
+ const output = count(tokens?.outputTokens);
5899
+ if (input === undefined && output === undefined)
5900
+ return [];
5901
+ if (input !== undefined)
5902
+ this.usage.inputTokens = input;
5903
+ if (output !== undefined)
5904
+ this.usage.outputTokens = output;
5905
+ return [{ type: "usage", ...this.usage }];
5906
+ }
5907
+ turnUsage() {
5908
+ const { inputTokens, outputTokens, costUsd } = this.usage;
5909
+ const usage = {
5910
+ ...inputTokens !== undefined ? { inputTokens } : {},
5911
+ ...outputTokens !== undefined ? { outputTokens } : {},
5912
+ ...costUsd !== undefined ? { costUsd } : {}
5913
+ };
5914
+ return Object.keys(usage).length ? usage : undefined;
5915
+ }
5916
+ sessionCostUsd() {
5917
+ return this.sessionCost ?? this.costBaseline;
5918
+ }
5864
5919
  tick() {
5865
5920
  if (!this.thought)
5866
5921
  return [];
@@ -5963,6 +6018,14 @@ function callText(call) {
5963
6018
  return parts.join(`
5964
6019
  `);
5965
6020
  }
6021
+ function usdCost(cost) {
6022
+ if (!cost || cost.currency.toUpperCase() !== "USD")
6023
+ return null;
6024
+ return Number.isFinite(cost.amount) ? Math.max(0, cost.amount) : null;
6025
+ }
6026
+ function roundUsd(usd) {
6027
+ return Math.round(usd * 1e6) / 1e6;
6028
+ }
5966
6029
 
5967
6030
  // src/acp/mcp-servers.ts
5968
6031
  import { isAbsolute as isAbsolute7 } from "path";
@@ -6706,7 +6769,9 @@ class Turn {
6706
6769
  outcome,
6707
6770
  error: error ? error.slice(0, 4000) : null,
6708
6771
  acpSessionId: this.acpSessionId,
6709
- branch: this.branch
6772
+ branch: this.branch,
6773
+ usage: this.mapper.turnUsage(),
6774
+ acpCostUsd: this.acpSessionId ? this.mapper.sessionCostUsd() : null
6710
6775
  };
6711
6776
  }
6712
6777
  async start() {
@@ -6881,6 +6946,7 @@ class Turn {
6881
6946
  const directories = extra.length && caps.sessionCapabilities?.additionalDirectories ? { additionalDirectories: extra } : {};
6882
6947
  const mcpServers = this.mcpServers(caps);
6883
6948
  this.resumed = false;
6949
+ this.mapper.costBaseline = this.ctx.previousAcpCostUsd ?? null;
6884
6950
  if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
6885
6951
  try {
6886
6952
  if (caps.sessionCapabilities?.resume) {
@@ -6905,6 +6971,7 @@ class Turn {
6905
6971
  mcpServers,
6906
6972
  ...directories
6907
6973
  }), 0, ""));
6974
+ this.mapper.acceptReplay();
6908
6975
  this.resumed = true;
6909
6976
  await this.applyMode(answer);
6910
6977
  return;
@@ -6922,6 +6989,7 @@ class Turn {
6922
6989
  }
6923
6990
  const created = await this.otel.around("acp.session.new", { "hypotenuse.mcp.servers": mcpServers.length }, () => this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, ""));
6924
6991
  this.acpSessionId = created.sessionId;
6992
+ this.mapper.costBaseline = 0;
6925
6993
  await this.applyMode(created);
6926
6994
  }
6927
6995
  resumed = false;
@@ -7038,6 +7106,7 @@ class Turn {
7038
7106
  throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
7039
7107
  });
7040
7108
  const usage = answer.usage;
7109
+ this.emitAll(this.mapper.tokens(usage));
7041
7110
  this.otel.promptEnded({
7042
7111
  "gen_ai.response.finish_reasons": answer.stopReason,
7043
7112
  "gen_ai.usage.input_tokens": usage?.inputTokens,
@@ -7308,9 +7377,131 @@ function registrationHeartbeat() {
7308
7377
  };
7309
7378
  }
7310
7379
 
7380
+ // src/daemon/config-restart.ts
7381
+ class ConfigRestart {
7382
+ deps;
7383
+ timeoutMs;
7384
+ pending = null;
7385
+ generation = 0;
7386
+ takenOver = false;
7387
+ constructor(deps, timeoutMs) {
7388
+ this.deps = deps;
7389
+ this.timeoutMs = timeoutMs;
7390
+ }
7391
+ get isPending() {
7392
+ return this.pending !== null;
7393
+ }
7394
+ get isTakenOver() {
7395
+ return this.takenOver;
7396
+ }
7397
+ saved(outcome, path) {
7398
+ if (this.takenOver)
7399
+ return null;
7400
+ if (this.pending !== null) {
7401
+ if (!outcome.needsRestart) {
7402
+ this.callOff(outcome.invalid ? `${path} is not valid now; the running config stays` : "the change that needed it was undone");
7403
+ }
7404
+ return null;
7405
+ }
7406
+ if (!outcome.needsRestart)
7407
+ return null;
7408
+ const problem = this.deps.startProblem();
7409
+ if (problem) {
7410
+ this.deps.log(`config restart not started: the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
7411
+ return null;
7412
+ }
7413
+ const generation = ++this.generation;
7414
+ this.pending = generation;
7415
+ this.deps.pauseClaims();
7416
+ return this.wait(generation, path);
7417
+ }
7418
+ takeOver() {
7419
+ this.takenOver = true;
7420
+ if (this.pending === null)
7421
+ return;
7422
+ this.pending = null;
7423
+ this.deps.log("config restart called off: a stop or restart was asked for");
7424
+ }
7425
+ async wait(generation, path) {
7426
+ const idle = await this.deps.waitIdle(this.timeoutMs);
7427
+ if (!this.current(generation))
7428
+ return;
7429
+ const problem = this.deps.startProblem();
7430
+ if (problem) {
7431
+ this.callOff(`the daemon would not start on ${path} as it is now (${problem}); the running config stays`);
7432
+ return;
7433
+ }
7434
+ if (!idle)
7435
+ await this.deps.stopTurns();
7436
+ if (!this.current(generation))
7437
+ return;
7438
+ this.pending = null;
7439
+ this.deps.restart();
7440
+ }
7441
+ current(generation) {
7442
+ return this.pending === generation && !this.takenOver && !this.deps.exiting();
7443
+ }
7444
+ callOff(reason) {
7445
+ if (this.pending === null)
7446
+ return;
7447
+ this.pending = null;
7448
+ this.deps.resumeClaims();
7449
+ this.deps.log(`config restart called off: ${reason}`);
7450
+ }
7451
+ }
7452
+
7453
+ // src/daemon/config-watch.ts
7454
+ import { createHash as createHash2 } from "crypto";
7455
+ import { existsSync as existsSync13, readFileSync as readFileSync9, watch } from "fs";
7456
+ import { basename as basename3, dirname as dirname8 } from "path";
7457
+ var CONFIG_WATCH_DEBOUNCE_MS = 300;
7458
+ function digest(path) {
7459
+ if (!existsSync13(path))
7460
+ return null;
7461
+ try {
7462
+ return createHash2("sha256").update(readFileSync9(path)).digest("hex");
7463
+ } catch {
7464
+ return null;
7465
+ }
7466
+ }
7467
+ function watchConfigFile(path, onChange, options = {}) {
7468
+ const name = basename3(path);
7469
+ let last = digest(path);
7470
+ let timer = null;
7471
+ let watcher;
7472
+ const settle = () => {
7473
+ timer = null;
7474
+ const now = digest(path);
7475
+ if (now === null || now === last)
7476
+ return;
7477
+ last = now;
7478
+ onChange();
7479
+ };
7480
+ try {
7481
+ watcher = watch(dirname8(path), (_event, file) => {
7482
+ if (file !== null && file !== name)
7483
+ return;
7484
+ if (timer)
7485
+ clearTimeout(timer);
7486
+ timer = setTimeout(settle, options.debounceMs ?? CONFIG_WATCH_DEBOUNCE_MS);
7487
+ });
7488
+ } catch (error) {
7489
+ options.onError?.(error);
7490
+ return null;
7491
+ }
7492
+ watcher.on("error", (error) => options.onError?.(error));
7493
+ return {
7494
+ close() {
7495
+ if (timer)
7496
+ clearTimeout(timer);
7497
+ watcher.close();
7498
+ }
7499
+ };
7500
+ }
7501
+
7311
7502
  // src/daemon/control-socket.ts
7312
- import { chmodSync as chmodSync4, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
7313
- import { dirname as dirname8 } from "path";
7503
+ import { chmodSync as chmodSync4, existsSync as existsSync14, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
7504
+ import { dirname as dirname9 } from "path";
7314
7505
  var METHOD_NOT_FOUND = -32601;
7315
7506
  var INVALID_PARAMS = -32602;
7316
7507
  var INTERNAL_ERROR = -32603;
@@ -7335,8 +7526,8 @@ class ControlServer {
7335
7526
  this.onError = onError;
7336
7527
  }
7337
7528
  listen() {
7338
- mkdirSync8(dirname8(this.path), { recursive: true, mode: 448 });
7339
- if (existsSync13(this.path))
7529
+ mkdirSync8(dirname9(this.path), { recursive: true, mode: 448 });
7530
+ if (existsSync14(this.path))
7340
7531
  rmSync5(this.path, { force: true });
7341
7532
  const previous = process.umask(127);
7342
7533
  try {
@@ -7502,11 +7693,11 @@ import {
7502
7693
  closeSync as closeSync2,
7503
7694
  mkdirSync as mkdirSync9,
7504
7695
  openSync as openSync2,
7505
- readFileSync as readFileSync9,
7696
+ readFileSync as readFileSync10,
7506
7697
  rmSync as rmSync6,
7507
7698
  writeSync
7508
7699
  } from "fs";
7509
- import { dirname as dirname9 } from "path";
7700
+ import { dirname as dirname10 } from "path";
7510
7701
  var STARTING_WINDOW_MS = 30000;
7511
7702
  function looksLikeDaemon(args) {
7512
7703
  const program = args.findIndex((arg) => {
@@ -7518,7 +7709,7 @@ function looksLikeDaemon(args) {
7518
7709
  function processArgs(pid) {
7519
7710
  try {
7520
7711
  if (process.platform === "linux") {
7521
- return readFileSync9(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
7712
+ return readFileSync10(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
7522
7713
  }
7523
7714
  if (process.platform === "win32") {
7524
7715
  const out = Bun.spawnSync([
@@ -7553,7 +7744,7 @@ var realProbe = {
7553
7744
  function readLock(path) {
7554
7745
  let text;
7555
7746
  try {
7556
- text = readFileSync9(path, "utf8");
7747
+ text = readFileSync10(path, "utf8");
7557
7748
  } catch {
7558
7749
  return;
7559
7750
  }
@@ -7577,7 +7768,7 @@ async function inspectLock(path, probe = realProbe, now = Date.now) {
7577
7768
  return now() - record.startedAt < STARTING_WINDOW_MS ? { kind: "starting", record } : { kind: "hung", record };
7578
7769
  }
7579
7770
  function writeExclusive(path, record) {
7580
- mkdirSync9(dirname9(path), { recursive: true, mode: 448 });
7771
+ mkdirSync9(dirname10(path), { recursive: true, mode: 448 });
7581
7772
  let fd;
7582
7773
  try {
7583
7774
  fd = openSync2(path, "wx", 384);
@@ -7655,8 +7846,8 @@ function childArgs(args) {
7655
7846
 
7656
7847
  // src/daemon/state.ts
7657
7848
  import { Database } from "bun:sqlite";
7658
- import { chmodSync as chmodSync5, existsSync as existsSync14, mkdirSync as mkdirSync10 } from "fs";
7659
- import { dirname as dirname10 } from "path";
7849
+ import { chmodSync as chmodSync5, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
7850
+ import { dirname as dirname11 } from "path";
7660
7851
  var LIVE_LOCAL_STATES = [
7661
7852
  "claimed",
7662
7853
  "running"
@@ -7714,7 +7905,8 @@ var MIGRATIONS = [
7714
7905
  ALTER TABLE worktrees ADD COLUMN removed_at INTEGER;
7715
7906
  ALTER TABLE local_sessions ADD COLUMN last_answer TEXT;
7716
7907
  ALTER TABLE local_sessions ADD COLUMN ended_at INTEGER;
7717
- ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]';`
7908
+ ALTER TABLE local_sessions ADD COLUMN pull_requests TEXT NOT NULL DEFAULT '[]';`,
7909
+ `ALTER TABLE local_sessions ADD COLUMN acp_cost_usd REAL;`
7718
7910
  ];
7719
7911
  function toSession(row) {
7720
7912
  return {
@@ -7728,6 +7920,7 @@ function toSession(row) {
7728
7920
  nextSeq: row.next_seq,
7729
7921
  acpAgentId: row.acp_agent_id ?? null,
7730
7922
  acpSessionId: row.acp_session_id ?? null,
7923
+ acpCostUsd: row.acp_cost_usd ?? null,
7731
7924
  pid: row.pid ?? null,
7732
7925
  worktreeId: row.worktree_id ?? null,
7733
7926
  createdAt: row.created_at,
@@ -7778,9 +7971,9 @@ class StateDb {
7778
7971
  this.path = path;
7779
7972
  this.now = now;
7780
7973
  if (path !== ":memory:") {
7781
- mkdirSync10(dirname10(path), { recursive: true, mode: 448 });
7974
+ mkdirSync10(dirname11(path), { recursive: true, mode: 448 });
7782
7975
  }
7783
- const fresh = path === ":memory:" || !existsSync14(path);
7976
+ const fresh = path === ":memory:" || !existsSync15(path);
7784
7977
  this.db = new Database(path, { create: true, strict: true });
7785
7978
  if (fresh && path !== ":memory:")
7786
7979
  chmodSync5(path, 384);
@@ -7837,7 +8030,8 @@ class StateDb {
7837
8030
  setSessionState(agentSessionId, state, extra = {}) {
7838
8031
  this.db.query(`UPDATE local_sessions SET state = $state, updated_at = $now,
7839
8032
  pid = CASE WHEN $setPid THEN $pid ELSE pid END,
7840
- acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END
8033
+ acp_session_id = CASE WHEN $setAcp THEN $acp ELSE acp_session_id END,
8034
+ acp_cost_usd = CASE WHEN $setCost THEN $cost ELSE acp_cost_usd END
7841
8035
  WHERE agent_session_id = $id`).run({
7842
8036
  state,
7843
8037
  now: this.now(),
@@ -7845,7 +8039,9 @@ class StateDb {
7845
8039
  setPid: extra.pid !== undefined ? 1 : 0,
7846
8040
  pid: extra.pid ?? null,
7847
8041
  setAcp: extra.acpSessionId !== undefined ? 1 : 0,
7848
- acp: extra.acpSessionId ?? null
8042
+ acp: extra.acpSessionId ?? null,
8043
+ setCost: extra.acpCostUsd !== undefined ? 1 : 0,
8044
+ cost: extra.acpCostUsd ?? null
7849
8045
  });
7850
8046
  }
7851
8047
  session(agentSessionId) {
@@ -8598,7 +8794,7 @@ function resolvedConfigSecrets(config, env) {
8598
8794
  }
8599
8795
 
8600
8796
  // src/worktree/cleanup.ts
8601
- import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
8797
+ import { existsSync as existsSync16, readdirSync, rmSync as rmSync7 } from "fs";
8602
8798
  import { join as join9, relative as relative5, resolve as resolve4 } from "path";
8603
8799
 
8604
8800
  // src/worktree/merge-state.ts
@@ -8736,14 +8932,14 @@ class WorktreeCleaner {
8736
8932
  if (this.inUse(wt))
8737
8933
  return { reason: "in use", overridable: [] };
8738
8934
  const reasons = [];
8739
- if (wt.kind === "worktree" && existsSync15(wt.path)) {
8935
+ if (wt.kind === "worktree" && existsSync16(wt.path)) {
8740
8936
  const status = await this.git(["status", "--porcelain", "--", ".", `:(exclude)${KEEP_FILE}`], { cwd: wt.path });
8741
8937
  if (status.code !== 0 || status.stdout.trim())
8742
8938
  reasons.push("uncommitted changes");
8743
8939
  if (await this.hasUnpushed(wt))
8744
8940
  reasons.push("unpushed commits");
8745
8941
  }
8746
- if (existsSync15(join9(wt.path, KEEP_FILE)))
8942
+ if (existsSync16(join9(wt.path, KEEP_FILE)))
8747
8943
  reasons.push("kept by request");
8748
8944
  return { reason: reasons[0] ?? null, overridable: reasons };
8749
8945
  }
@@ -8800,7 +8996,7 @@ class WorktreeCleaner {
8800
8996
  }
8801
8997
  }
8802
8998
  const scratch = join9(root, SCRATCH);
8803
- if (existsSync15(scratch)) {
8999
+ if (existsSync16(scratch)) {
8804
9000
  for (const name of readdirSync(scratch)) {
8805
9001
  const path = join9(scratch, name);
8806
9002
  const id = Number(name);
@@ -8854,7 +9050,7 @@ class WorktreeCleaner {
8854
9050
  if (!options.dryRun)
8855
9051
  this.state.updateWorktree(wt.id, { keptReason: reason });
8856
9052
  };
8857
- if (!existsSync15(wt.path)) {
9053
+ if (!existsSync16(wt.path)) {
8858
9054
  if (!options.dryRun) {
8859
9055
  if (wt.repoPath)
8860
9056
  await this.git(["worktree", "prune"], { cwd: wt.repoPath });
@@ -10104,7 +10300,9 @@ class Supervisor {
10104
10300
  ...assignment,
10105
10301
  agentId: assignment.agentId ?? this.agentFor(assignment.role)
10106
10302
  }, this.config).agentId;
10107
- const previousAcpSessionId = state.session(assignment.agentSessionId)?.acpSessionId ?? null;
10303
+ const local = state.session(assignment.agentSessionId);
10304
+ const previousAcpSessionId = local?.acpSessionId ?? null;
10305
+ const previousAcpCostUsd = local?.acpCostUsd ?? null;
10108
10306
  state.claimSession({
10109
10307
  agentSessionId: assignment.agentSessionId,
10110
10308
  linearSessionId: assignment.externalSessionId,
@@ -10200,6 +10398,7 @@ class Supervisor {
10200
10398
  stateDir: this.options.stateDir,
10201
10399
  env: this.options.env,
10202
10400
  previousAcpSessionId,
10401
+ previousAcpCostUsd,
10203
10402
  onReply: (handler) => {
10204
10403
  slot.onReply = handler;
10205
10404
  },
@@ -10260,9 +10459,11 @@ class Supervisor {
10260
10459
  this.slots.delete(slot.workItemId);
10261
10460
  if (slot.nonce)
10262
10461
  this.broker.end(slot.nonce);
10462
+ const acp = acpSessionAfterTurn(result, slot.silent, this.options.state.session(slot.agentSessionId)?.acpSessionId ?? null);
10263
10463
  if (slot.silent) {
10264
10464
  this.options.state.setSessionState(slot.agentSessionId, "interrupted", {
10265
- pid: null
10465
+ pid: null,
10466
+ ...acp
10266
10467
  });
10267
10468
  } else {
10268
10469
  this.sink.complete({
@@ -10272,6 +10473,7 @@ class Supervisor {
10272
10473
  error: result.error ?? null,
10273
10474
  acpSessionId: result.acpSessionId ?? null,
10274
10475
  branch: result.branch ?? null,
10476
+ ...result.usage ? { usage: result.usage } : {},
10275
10477
  ...slot.consumed.length ? { answersConsumed: slot.consumed.slice(0, 100) } : {}
10276
10478
  },
10277
10479
  active: {
@@ -10283,10 +10485,7 @@ class Supervisor {
10283
10485
  }
10284
10486
  });
10285
10487
  this.options.state.recordTurnEnd(slot.agentSessionId, slot.lastAnswer);
10286
- this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", {
10287
- pid: null,
10288
- ...result.acpSessionId ? { acpSessionId: result.acpSessionId } : {}
10289
- });
10488
+ this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", { pid: null, ...acp });
10290
10489
  }
10291
10490
  this.options.log.info(`turn ended: ${slot.silent ? "taken away" : result.outcome}`, {
10292
10491
  session: slot.agentSessionId,
@@ -10368,6 +10567,14 @@ class Supervisor {
10368
10567
  this.link.beatNow();
10369
10568
  return { drained: this.slots.size === 0 && flushed, cancelled };
10370
10569
  }
10570
+ pauseClaims() {
10571
+ this.draining = true;
10572
+ this.link.beatNow();
10573
+ }
10574
+ resumeClaims() {
10575
+ this.draining = false;
10576
+ this.link.beatNow();
10577
+ }
10371
10578
  async waitIdle(timeoutMs) {
10372
10579
  const deadline = Date.now() + timeoutMs;
10373
10580
  while (this.slots.size > 0) {
@@ -10525,6 +10732,17 @@ class Supervisor {
10525
10732
  this.telemetryQueue.stop();
10526
10733
  }
10527
10734
  }
10735
+ function acpSessionAfterTurn(result, silent, recordedAcpSessionId) {
10736
+ if (!result.acpSessionId)
10737
+ return {};
10738
+ if (!silent) {
10739
+ return {
10740
+ acpSessionId: result.acpSessionId,
10741
+ acpCostUsd: result.acpCostUsd ?? null
10742
+ };
10743
+ }
10744
+ return result.acpSessionId === recordedAcpSessionId ? { acpCostUsd: result.acpCostUsd ?? null } : {};
10745
+ }
10528
10746
  function changedKeys(a, b, prefix = "") {
10529
10747
  const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
10530
10748
  if (isObject(a) && isObject(b)) {
@@ -10593,18 +10811,19 @@ function revokedMarker(stateDir) {
10593
10811
  return join10(stateDir, "revoked.json");
10594
10812
  }
10595
10813
  function fingerprint(token) {
10596
- return createHash2("sha256").update(token).digest("hex");
10814
+ return createHash3("sha256").update(token).digest("hex");
10597
10815
  }
10598
10816
  function wasRevoked(stateDir, token) {
10599
10817
  const file = revokedMarker(stateDir);
10600
- if (!existsSync16(file))
10818
+ if (!existsSync17(file))
10601
10819
  return false;
10602
10820
  try {
10603
- return JSON.parse(readFileSync10(file, "utf8")).token === fingerprint(token);
10821
+ return JSON.parse(readFileSync11(file, "utf8")).token === fingerprint(token);
10604
10822
  } catch {
10605
10823
  return false;
10606
10824
  }
10607
10825
  }
10826
+ var CONFIG_RESTART_DRAIN_MS = 10 * 60000;
10608
10827
  function readDaemonConfig(options, env) {
10609
10828
  const path = resolveConfigPath(options.config, env);
10610
10829
  const checked = checkConfig(path, { env, offline: true });
@@ -10677,19 +10896,55 @@ async function runDaemon(options) {
10677
10896
  const machine = realMachine(env);
10678
10897
  const api = new DaemonApi(origin, runner.token, (line) => log.debug(line));
10679
10898
  let exiting = false;
10899
+ let restartScheduled = false;
10900
+ let configHandOff = null;
10680
10901
  let supervisor;
10681
10902
  let control;
10903
+ let watcher = null;
10682
10904
  const teardown = async (drain) => {
10683
10905
  if (drain) {
10684
10906
  await supervisor.drain({ timeoutMs: 1e4, cancel: false });
10685
10907
  } else {
10686
10908
  await supervisor.sink.drain(2000);
10687
10909
  }
10910
+ watcher?.close();
10688
10911
  supervisor.close();
10689
10912
  control.close();
10690
10913
  releaseLock(paths.pidfile);
10691
10914
  state.close();
10692
10915
  };
10916
+ const restartSelf = (fromConfig = false) => {
10917
+ const service = env.HYPOTENUSE_SERVICE === "1";
10918
+ const answer = {
10919
+ restarting: true,
10920
+ mode: service ? "service" : "re-exec"
10921
+ };
10922
+ if (restartScheduled)
10923
+ return answer;
10924
+ restartScheduled = true;
10925
+ const handOff = setTimeout(() => {
10926
+ configHandOff = null;
10927
+ if (exiting)
10928
+ return;
10929
+ exiting = true;
10930
+ log.info(service ? "exiting for the service manager to restart" : "re-executing");
10931
+ teardown(false).then(() => {
10932
+ if (service)
10933
+ process.exit(EXIT_RESTART);
10934
+ const [command, ...prefix] = selfCommand();
10935
+ const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
10936
+ detached: true,
10937
+ stdio: "ignore",
10938
+ env
10939
+ });
10940
+ child.unref();
10941
+ process.exit(0);
10942
+ });
10943
+ }, 50);
10944
+ if (fromConfig)
10945
+ configHandOff = handOff;
10946
+ return answer;
10947
+ };
10693
10948
  const requestExit = (exit) => {
10694
10949
  if (exiting)
10695
10950
  return;
@@ -10725,6 +10980,31 @@ async function runDaemon(options) {
10725
10980
  ...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
10726
10981
  ]
10727
10982
  });
10983
+ const configRestart = new ConfigRestart({
10984
+ pauseClaims: () => supervisor.pauseClaims(),
10985
+ resumeClaims: () => supervisor.resumeClaims(),
10986
+ waitIdle: (ms) => supervisor.waitIdle(ms),
10987
+ stopTurns: async () => {
10988
+ await supervisor.drain({ timeoutMs: 0, cancel: true });
10989
+ },
10990
+ startProblem: () => {
10991
+ const read = readDaemonConfig(options, env);
10992
+ if (!read.config)
10993
+ return "the config is not valid";
10994
+ const file = credentialsPath(env);
10995
+ try {
10996
+ resolveDaemonRunner(read.config, loadCredentials(file), file);
10997
+ } catch (error) {
10998
+ return error instanceof Error ? error.message : String(error);
10999
+ }
11000
+ return null;
11001
+ },
11002
+ exiting: () => exiting,
11003
+ restart: () => {
11004
+ restartSelf(true);
11005
+ },
11006
+ log: (message) => log.info(message)
11007
+ }, CONFIG_RESTART_DRAIN_MS);
10728
11008
  control = new ControlServer(paths.socket, {
10729
11009
  status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
10730
11010
  "agents.list": (params) => supervisor.agents(Boolean(params.all)),
@@ -10742,30 +11022,23 @@ async function runDaemon(options) {
10742
11022
  const report = await supervisor.cleaner.run(cleanupParams(params));
10743
11023
  return report;
10744
11024
  },
10745
- drain: (params) => supervisor.drain({
10746
- timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : 600000,
10747
- cancel: params.cancel === true
10748
- }),
11025
+ drain: (params) => {
11026
+ configRestart.takeOver();
11027
+ if (configHandOff) {
11028
+ clearTimeout(configHandOff);
11029
+ configHandOff = null;
11030
+ restartScheduled = false;
11031
+ log.info("config restart called off: a stop or restart was asked for");
11032
+ }
11033
+ return supervisor.drain({
11034
+ timeoutMs: typeof params.timeoutMs === "number" ? params.timeoutMs : 600000,
11035
+ cancel: params.cancel === true
11036
+ });
11037
+ },
10749
11038
  reload: () => supervisor.reload(),
10750
11039
  restart: () => {
10751
- const service = env.HYPOTENUSE_SERVICE === "1";
10752
- setTimeout(() => {
10753
- exiting = true;
10754
- log.info(service ? "exiting for the service manager to restart" : "re-executing");
10755
- teardown(false).then(() => {
10756
- if (service)
10757
- process.exit(EXIT_RESTART);
10758
- const [command, ...prefix] = selfCommand();
10759
- const child = spawn2(command, [...prefix, ...childArgs(process.argv.slice(2))], {
10760
- detached: true,
10761
- stdio: "ignore",
10762
- env
10763
- });
10764
- child.unref();
10765
- process.exit(0);
10766
- });
10767
- }, 50);
10768
- return { restarting: true, mode: service ? "service" : "re-exec" };
11040
+ configRestart.takeOver();
11041
+ return restartSelf();
10769
11042
  },
10770
11043
  "mcp.hello": (params) => supervisor.broker.hello(params),
10771
11044
  "mcp.call": (params) => supervisor.broker.call(params)
@@ -10801,6 +11074,19 @@ async function runDaemon(options) {
10801
11074
  });
10802
11075
  report({ ready: true, pid: process.pid });
10803
11076
  rmSync8(revokedMarker(paths.stateDir), { force: true });
11077
+ watcher = watchConfigFile(paths.config, () => {
11078
+ if (exiting || restartScheduled || configRestart.isTakenOver)
11079
+ return;
11080
+ log.info(`${paths.config} changed; reloading`);
11081
+ const result = supervisor.reload();
11082
+ const needsRestart = !result.error && result.requiresRestart.length > 0;
11083
+ if (needsRestart && !configRestart.isPending) {
11084
+ log.info("restarting to apply the config change once running turns finish", { keys: result.requiresRestart.join(",") });
11085
+ }
11086
+ configRestart.saved({ needsRestart, invalid: Boolean(result.error) }, paths.config);
11087
+ }, {
11088
+ onError: (error) => log.warn(`not watching ${paths.config} for changes (\`hypotenuse daemon reload\` still applies them): ${error instanceof Error ? error.message : String(error)}`)
11089
+ });
10804
11090
  }
10805
11091
 
10806
11092
  // src/commands/daemon.ts
@@ -10962,22 +11248,22 @@ async function stopDaemon(ctx, options) {
10962
11248
  }
10963
11249
  async function tailLog(file, options) {
10964
11250
  const keep = (line) => !options.session || line.includes(` session=${options.session}`);
10965
- if (!existsSync17(file)) {
11251
+ if (!existsSync18(file)) {
10966
11252
  if (!options.follow)
10967
11253
  throw new Error(`${file} does not exist yet`);
10968
11254
  } else {
10969
- const all = readFileSync11(file, "utf8").split(`
11255
+ const all = readFileSync12(file, "utf8").split(`
10970
11256
  `).filter(Boolean).filter(keep);
10971
11257
  for (const line of all.slice(-options.lines))
10972
11258
  console.log(paintLogLine(line));
10973
11259
  }
10974
11260
  if (!options.follow)
10975
11261
  return;
10976
- let offset = existsSync17(file) ? statSync5(file).size : 0;
11262
+ let offset = existsSync18(file) ? statSync5(file).size : 0;
10977
11263
  let partial = "";
10978
11264
  for (;; ) {
10979
11265
  await Bun.sleep(500);
10980
- if (!existsSync17(file))
11266
+ if (!existsSync18(file))
10981
11267
  continue;
10982
11268
  const size = statSync5(file).size;
10983
11269
  if (size < offset)
@@ -11072,7 +11358,7 @@ function registerDaemonCommands(program) {
11072
11358
  const opts = this.opts();
11073
11359
  const located = locateDaemon(ctx);
11074
11360
  const sessionLog = opts.session && /^\d+$/.test(opts.session) ? sessionLogPath(located.stateDir, Number(opts.session)) : null;
11075
- if (sessionLog && existsSync17(sessionLog)) {
11361
+ if (sessionLog && existsSync18(sessionLog)) {
11076
11362
  await tailLog(sessionLog, {
11077
11363
  lines: Number(opts.lines) || 200,
11078
11364
  follow: Boolean(opts.follow)
@@ -11538,9 +11824,9 @@ function registerCheckUpdateCommand(program) {
11538
11824
  }
11539
11825
 
11540
11826
  // src/commands/config.ts
11541
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
11827
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "fs";
11542
11828
  import { hostname as hostname5 } from "os";
11543
- import { dirname as dirname11, resolve as resolve5 } from "path";
11829
+ import { dirname as dirname12, resolve as resolve5 } from "path";
11544
11830
 
11545
11831
  // src/config/template.toml
11546
11832
  var template_default = `#:schema https://www.hypotenuse.cloud/schemas/config-v1.json
@@ -11687,7 +11973,7 @@ function registerConfigCommands(program) {
11687
11973
  await runCommand(this, async (ctx) => {
11688
11974
  const opts = this.opts();
11689
11975
  const path = ctx.configPath;
11690
- if (existsSync18(path) && !opts.force) {
11976
+ if (existsSync19(path) && !opts.force) {
11691
11977
  throw new Error(`${path} already exists; pass --force to overwrite it`);
11692
11978
  }
11693
11979
  const machine = realMachine(ctx.env);
@@ -11704,7 +11990,7 @@ function registerConfigCommands(program) {
11704
11990
  repositories: repositoryBlocks(repos, ctx.env),
11705
11991
  cpus: navigator.hardwareConcurrency || 1
11706
11992
  });
11707
- mkdirSync11(dirname11(resolve5(path)), { recursive: true, mode: 448 });
11993
+ mkdirSync11(dirname12(resolve5(path)), { recursive: true, mode: 448 });
11708
11994
  writeFileSync6(path, text);
11709
11995
  if (!ctx.json)
11710
11996
  console.log(`Wrote ${tildify(path, ctx.env)}.`);
@@ -11744,15 +12030,118 @@ function registerConfigCommands(program) {
11744
12030
  });
11745
12031
  }
11746
12032
 
12033
+ // src/commands/cost.ts
12034
+ var COST_DIMENSIONS = [
12035
+ "session",
12036
+ "root-session",
12037
+ "issue",
12038
+ "runner",
12039
+ "owner",
12040
+ "issuer",
12041
+ "role",
12042
+ "agent",
12043
+ "repository",
12044
+ "day"
12045
+ ];
12046
+ function usd(usd) {
12047
+ if (usd === null)
12048
+ return "-";
12049
+ if (usd !== 0 && Math.abs(usd) < 0.01)
12050
+ return `$${usd.toPrecision(2)}`;
12051
+ return `$${usd.toFixed(2)}`;
12052
+ }
12053
+ function describeTotals(totals) {
12054
+ const turns = `${totals.turns} turn${totals.turns === 1 ? "" : "s"}`;
12055
+ const unpriced = totals.unpricedTurns ? ` (${totals.unpricedTurns} unpriced)` : "";
12056
+ const tokens = `${totals.inputTokens.toLocaleString("en-US")} in / ${totals.outputTokens.toLocaleString("en-US")} out`;
12057
+ return `${usd(totals.costUsd)} over ${turns}${unpriced}, ${tokens}`;
12058
+ }
12059
+ function withFilters(command) {
12060
+ return command.option("--session <id>", "only this agent session's turns").option("--root-session <id>", "only this manager session's turns and its dispatched workers'").option("--runner <id>", "only turns this runner ran").option("--owner <userId>", "only turns on runners this user owns").option("--issuer <userId>", "only turns this user asked for").option("--role <role>", "only turns in this role: manager or worker").option("--since <time>", "only turns that ended since: ISO-8601, epoch ms, or 7d / 12h / 30m").option("--until <time>", "only turns that ended before: ISO-8601, epoch ms, or 7d / 12h / 30m");
12061
+ }
12062
+ function filterQuery(command) {
12063
+ const opts = command.opts();
12064
+ return {
12065
+ agentSessionId: opts.session,
12066
+ rootAgentSessionId: opts.rootSession,
12067
+ runnerId: opts.runner,
12068
+ runnerUserId: opts.owner,
12069
+ issuerUserId: opts.issuer,
12070
+ role: opts.role,
12071
+ since: opts.since ? String(parseTime(opts.since)) : undefined,
12072
+ until: opts.until ? String(parseTime(opts.until)) : undefined
12073
+ };
12074
+ }
12075
+ function registerCostCommands(program) {
12076
+ const cost = program.command("cost").description("What the selected project's agent sessions cost, and whom it is attributed to");
12077
+ withPaging(withFilters(cost.command("list").description("List each turn's cost, newest first, with its session, runner, and role"))).action(async function() {
12078
+ await runCommand(this, async (ctx) => {
12079
+ const projectId = await ctx.projectId();
12080
+ const result = await ctx.signedInClient().request("GET", "/costs", {
12081
+ query: { projectId, ...filterQuery(this), ...pageQuery(this) }
12082
+ });
12083
+ emit(ctx, result, () => `${table(result.costRecords.map((r) => ({
12084
+ ended: when(r.endedAt),
12085
+ issue: r.issueIdentifier ?? "-",
12086
+ session: r.agentSessionId,
12087
+ role: r.role ?? "-",
12088
+ kind: r.kind,
12089
+ runner: r.runnerName,
12090
+ agent: r.agentId ?? "-",
12091
+ ending: r.ending,
12092
+ tokens: `${r.inputTokens ?? "-"} / ${r.outputTokens ?? "-"}`,
12093
+ cost: usd(r.costUsd)
12094
+ })), [
12095
+ { key: "ended" },
12096
+ { key: "issue" },
12097
+ { key: "session" },
12098
+ { key: "role" },
12099
+ { key: "kind" },
12100
+ { key: "runner" },
12101
+ { key: "agent" },
12102
+ { key: "ending" },
12103
+ { key: "tokens" },
12104
+ { key: "cost" }
12105
+ ])}
12106
+ total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
12107
+ });
12108
+ });
12109
+ withPaging(withFilters(cost.command("summary").description("Total the turns' cost by one dimension, most expensive first").option("--by <dimension>", `what to group by: ${COST_DIMENSIONS.join(", ")}`, "session"))).action(async function() {
12110
+ await runCommand(this, async (ctx) => {
12111
+ const projectId = await ctx.projectId();
12112
+ const { by } = this.opts();
12113
+ const result = await ctx.signedInClient().request("GET", "/costs/summary", {
12114
+ query: { projectId, by, ...filterQuery(this), ...pageQuery(this) }
12115
+ });
12116
+ emit(ctx, result, () => `${table(result.groups.map((g) => ({
12117
+ [result.by]: g.label ?? g.key ?? "(none)",
12118
+ turns: g.turns,
12119
+ unpriced: g.unpricedTurns,
12120
+ "input tokens": g.inputTokens,
12121
+ "output tokens": g.outputTokens,
12122
+ cost: usd(g.costUsd)
12123
+ })), [
12124
+ { key: result.by, maxWidth: 50 },
12125
+ { key: "turns" },
12126
+ { key: "unpriced" },
12127
+ { key: "input tokens" },
12128
+ { key: "output tokens" },
12129
+ { key: "cost" }
12130
+ ])}
12131
+ total ${describeTotals(result.total)}${pageFooter(result.pagination)}`);
12132
+ });
12133
+ });
12134
+ }
12135
+
11747
12136
  // src/commands/doctor.ts
11748
- import { existsSync as existsSync20, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
12137
+ import { existsSync as existsSync21, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
11749
12138
  import { hostname as hostname6 } from "os";
11750
- import { dirname as dirname13 } from "path";
12139
+ import { dirname as dirname14 } from "path";
11751
12140
 
11752
12141
  // src/commands/service.ts
11753
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
12142
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
11754
12143
  import { homedir as homedir3, userInfo } from "os";
11755
- import { dirname as dirname12, join as join11 } from "path";
12144
+ import { dirname as dirname13, join as join11 } from "path";
11756
12145
 
11757
12146
  // src/daemon/units/launchd.plist
11758
12147
  var launchd_default = `<?xml version="1.0" encoding="UTF-8"?>
@@ -11888,7 +12277,7 @@ function registerServiceCommands(program) {
11888
12277
  console.log(text);
11889
12278
  return;
11890
12279
  }
11891
- mkdirSync12(dirname12(path), { recursive: true });
12280
+ mkdirSync12(dirname13(path), { recursive: true });
11892
12281
  mkdirSync12(paths.stateDir, { recursive: true, mode: 448 });
11893
12282
  writeFileSync7(path, text);
11894
12283
  if (platform === "darwin") {
@@ -11909,7 +12298,7 @@ function registerServiceCommands(program) {
11909
12298
  service.command("uninstall").description("Stop the service and remove its unit file").action(async function() {
11910
12299
  await runCommand(this, async (ctx) => {
11911
12300
  const path = unitPath(process.platform, ctx.env);
11912
- if (!path || !existsSync19(path)) {
12301
+ if (!path || !existsSync20(path)) {
11913
12302
  emit(ctx, { removed: false }, () => "No service is installed.");
11914
12303
  return;
11915
12304
  }
@@ -11927,7 +12316,7 @@ function registerServiceCommands(program) {
11927
12316
  service.command("status").description("Show whether the service is installed and loaded, and the daemon's status").action(async function() {
11928
12317
  await runCommand(this, async (ctx) => {
11929
12318
  const path = unitPath(process.platform, ctx.env);
11930
- const installed = Boolean(path && existsSync19(path));
12319
+ const installed = Boolean(path && existsSync20(path));
11931
12320
  let manager = "not installed";
11932
12321
  if (installed && process.platform === "darwin") {
11933
12322
  manager = run(["launchctl", "print", `gui/${userInfo().uid}/${LAUNCHD_LABEL}`], true).includes("state = running") ? "running" : "loaded, not running";
@@ -12015,7 +12404,7 @@ async function runDoctor(ctx, options) {
12015
12404
  return list.checks;
12016
12405
  }
12017
12406
  let runner = null;
12018
- if (!existsSync20(ctx.credentialsPath)) {
12407
+ if (!existsSync21(ctx.credentialsPath)) {
12019
12408
  list.fail("credentials", `${tildify(ctx.credentialsPath, ctx.env)} does not exist \u2014 \`hypotenuse login\`, then \`hypotenuse runner register\``);
12020
12409
  } else {
12021
12410
  try {
@@ -12078,7 +12467,7 @@ async function runDoctor(ctx, options) {
12078
12467
  list.warn("daemon", "not running \u2014 `hypotenuse daemon start` or `hypotenuse service install`");
12079
12468
  }
12080
12469
  const unit = unitPath(process.platform, ctx.env);
12081
- if (unit && existsSync20(unit))
12470
+ if (unit && existsSync21(unit))
12082
12471
  list.ok("daemon", `service unit ${tildify(unit, ctx.env)}`);
12083
12472
  else if (unit)
12084
12473
  list.warn("daemon", "no service unit \u2014 `hypotenuse service install` starts the daemon at login");
@@ -12146,8 +12535,8 @@ async function runDoctor(ctx, options) {
12146
12535
  `).at(-1) ?? ""}`);
12147
12536
  }
12148
12537
  let probePath = config.sessions.worktree_root;
12149
- while (!existsSync20(probePath) && dirname13(probePath) !== probePath)
12150
- probePath = dirname13(probePath);
12538
+ while (!existsSync21(probePath) && dirname14(probePath) !== probePath)
12539
+ probePath = dirname14(probePath);
12151
12540
  try {
12152
12541
  const fs = statfsSync2(probePath);
12153
12542
  const free = fs.bavail * fs.bsize;
@@ -13511,6 +13900,11 @@ function describeSession(session, activities, tree = {}) {
13511
13900
  if (tree.issuer?.user) {
13512
13901
  lines.push(`issuer ${tree.issuer.user.name}`);
13513
13902
  }
13903
+ if (tree.cost?.turns)
13904
+ lines.push(`cost ${describeTotals(tree.cost)}`);
13905
+ if (tree.treeCost && tree.treeCost.turns > (tree.cost?.turns ?? 0)) {
13906
+ lines.push(`tree cost ${describeTotals(tree.treeCost)} with its workers`);
13907
+ }
13514
13908
  if (session.summary)
13515
13909
  lines.push(`summary ${session.summary}`);
13516
13910
  if (session.externalUrl)
@@ -13737,7 +14131,7 @@ function registerSessionCommands(program) {
13737
14131
  }
13738
14132
 
13739
14133
  // src/commands/skill.ts
13740
- import { existsSync as existsSync21, readFileSync as readFileSync12 } from "fs";
14134
+ import { existsSync as existsSync22, readFileSync as readFileSync13 } from "fs";
13741
14135
  import { join as join13 } from "path";
13742
14136
  var SKILL_FILE = "hypotenuse.md";
13743
14137
  async function readSkill() {
@@ -13746,8 +14140,8 @@ async function readSkill() {
13746
14140
  return embedded.text();
13747
14141
  for (const up of [[".."], ["..", ".."]]) {
13748
14142
  const path = join13(import.meta.dir, ...up, "skills", SKILL_FILE);
13749
- if (existsSync21(path))
13750
- return readFileSync12(path, "utf8");
14143
+ if (existsSync22(path))
14144
+ return readFileSync13(path, "utf8");
13751
14145
  }
13752
14146
  throw new Error(`This install has no skills/${SKILL_FILE}; reinstall hypotenuse.`);
13753
14147
  }
@@ -14245,6 +14639,7 @@ function buildProgram() {
14245
14639
  registerIntegrationCommands(program);
14246
14640
  registerSessionCommands(program);
14247
14641
  registerEventCommands(program);
14642
+ registerCostCommands(program);
14248
14643
  registerRunnerCommands(program);
14249
14644
  registerRoutingCommands(program);
14250
14645
  registerTelemetryCommands(program);