@arcadeai/hypotenuse 0.20.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.20.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
-
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
1101
 
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()
1324
+ });
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")
1357
1352
  });
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()
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 = {
@@ -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,
@@ -7836,7 +7905,8 @@ var MIGRATIONS = [
7836
7905
  ALTER TABLE worktrees ADD COLUMN removed_at INTEGER;
7837
7906
  ALTER TABLE local_sessions ADD COLUMN last_answer TEXT;
7838
7907
  ALTER TABLE local_sessions ADD COLUMN ended_at INTEGER;
7839
- 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;`
7840
7910
  ];
7841
7911
  function toSession(row) {
7842
7912
  return {
@@ -7850,6 +7920,7 @@ function toSession(row) {
7850
7920
  nextSeq: row.next_seq,
7851
7921
  acpAgentId: row.acp_agent_id ?? null,
7852
7922
  acpSessionId: row.acp_session_id ?? null,
7923
+ acpCostUsd: row.acp_cost_usd ?? null,
7853
7924
  pid: row.pid ?? null,
7854
7925
  worktreeId: row.worktree_id ?? null,
7855
7926
  createdAt: row.created_at,
@@ -7959,7 +8030,8 @@ class StateDb {
7959
8030
  setSessionState(agentSessionId, state, extra = {}) {
7960
8031
  this.db.query(`UPDATE local_sessions SET state = $state, updated_at = $now,
7961
8032
  pid = CASE WHEN $setPid THEN $pid ELSE pid END,
7962
- 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
7963
8035
  WHERE agent_session_id = $id`).run({
7964
8036
  state,
7965
8037
  now: this.now(),
@@ -7967,7 +8039,9 @@ class StateDb {
7967
8039
  setPid: extra.pid !== undefined ? 1 : 0,
7968
8040
  pid: extra.pid ?? null,
7969
8041
  setAcp: extra.acpSessionId !== undefined ? 1 : 0,
7970
- acp: extra.acpSessionId ?? null
8042
+ acp: extra.acpSessionId ?? null,
8043
+ setCost: extra.acpCostUsd !== undefined ? 1 : 0,
8044
+ cost: extra.acpCostUsd ?? null
7971
8045
  });
7972
8046
  }
7973
8047
  session(agentSessionId) {
@@ -10226,7 +10300,9 @@ class Supervisor {
10226
10300
  ...assignment,
10227
10301
  agentId: assignment.agentId ?? this.agentFor(assignment.role)
10228
10302
  }, this.config).agentId;
10229
- 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;
10230
10306
  state.claimSession({
10231
10307
  agentSessionId: assignment.agentSessionId,
10232
10308
  linearSessionId: assignment.externalSessionId,
@@ -10322,6 +10398,7 @@ class Supervisor {
10322
10398
  stateDir: this.options.stateDir,
10323
10399
  env: this.options.env,
10324
10400
  previousAcpSessionId,
10401
+ previousAcpCostUsd,
10325
10402
  onReply: (handler) => {
10326
10403
  slot.onReply = handler;
10327
10404
  },
@@ -10382,9 +10459,11 @@ class Supervisor {
10382
10459
  this.slots.delete(slot.workItemId);
10383
10460
  if (slot.nonce)
10384
10461
  this.broker.end(slot.nonce);
10462
+ const acp = acpSessionAfterTurn(result, slot.silent, this.options.state.session(slot.agentSessionId)?.acpSessionId ?? null);
10385
10463
  if (slot.silent) {
10386
10464
  this.options.state.setSessionState(slot.agentSessionId, "interrupted", {
10387
- pid: null
10465
+ pid: null,
10466
+ ...acp
10388
10467
  });
10389
10468
  } else {
10390
10469
  this.sink.complete({
@@ -10394,6 +10473,7 @@ class Supervisor {
10394
10473
  error: result.error ?? null,
10395
10474
  acpSessionId: result.acpSessionId ?? null,
10396
10475
  branch: result.branch ?? null,
10476
+ ...result.usage ? { usage: result.usage } : {},
10397
10477
  ...slot.consumed.length ? { answersConsumed: slot.consumed.slice(0, 100) } : {}
10398
10478
  },
10399
10479
  active: {
@@ -10405,10 +10485,7 @@ class Supervisor {
10405
10485
  }
10406
10486
  });
10407
10487
  this.options.state.recordTurnEnd(slot.agentSessionId, slot.lastAnswer);
10408
- this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", {
10409
- pid: null,
10410
- ...result.acpSessionId ? { acpSessionId: result.acpSessionId } : {}
10411
- });
10488
+ this.options.state.setSessionState(slot.agentSessionId, result.outcome === "completed" ? "idle" : result.outcome === "cancelled" ? "cancelled" : "failed", { pid: null, ...acp });
10412
10489
  }
10413
10490
  this.options.log.info(`turn ended: ${slot.silent ? "taken away" : result.outcome}`, {
10414
10491
  session: slot.agentSessionId,
@@ -10655,6 +10732,17 @@ class Supervisor {
10655
10732
  this.telemetryQueue.stop();
10656
10733
  }
10657
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
+ }
10658
10746
  function changedKeys(a, b, prefix = "") {
10659
10747
  const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
10660
10748
  if (isObject(a) && isObject(b)) {
@@ -11942,6 +12030,109 @@ function registerConfigCommands(program) {
11942
12030
  });
11943
12031
  }
11944
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
+
11945
12136
  // src/commands/doctor.ts
11946
12137
  import { existsSync as existsSync21, statfsSync as statfsSync2, statSync as statSync6 } from "fs";
11947
12138
  import { hostname as hostname6 } from "os";
@@ -13709,6 +13900,11 @@ function describeSession(session, activities, tree = {}) {
13709
13900
  if (tree.issuer?.user) {
13710
13901
  lines.push(`issuer ${tree.issuer.user.name}`);
13711
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
+ }
13712
13908
  if (session.summary)
13713
13909
  lines.push(`summary ${session.summary}`);
13714
13910
  if (session.externalUrl)
@@ -14443,6 +14639,7 @@ function buildProgram() {
14443
14639
  registerIntegrationCommands(program);
14444
14640
  registerSessionCommands(program);
14445
14641
  registerEventCommands(program);
14642
+ registerCostCommands(program);
14446
14643
  registerRunnerCommands(program);
14447
14644
  registerRoutingCommands(program);
14448
14645
  registerTelemetryCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Hypotenuse — run coding agents on your machine for Linear agent sessions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hypotenuse
3
- description: Operate a Hypotenuse server from the terminal with the `hypotenuse` CLI — sign in, pick a project, manage members, invites, tags, connect trigger integrations (Linear), register and manage runners, run this machine as a runner (config file, daemon, user service, doctor), edit routing rules and settings, point the project's telemetry at OpenTelemetry collectors, link Linear users to members, read and stop agent sessions, and read the audit log. Use when asked to administer Hypotenuse projects, check what @hypotenuse did on an issue, or check who changed what.
3
+ description: Operate a Hypotenuse server from the terminal with the `hypotenuse` CLI — sign in, pick a project, manage members, invites, tags, connect trigger integrations (Linear), register and manage runners, run this machine as a runner (config file, daemon, user service, doctor), edit routing rules and settings, point the project's telemetry at OpenTelemetry collectors, link Linear users to members, read and stop agent sessions, see what they cost, and read the audit log. Use when asked to administer Hypotenuse projects, check what @hypotenuse did on an issue, or check who changed what.
4
4
  ---
5
5
 
6
6
  # hypotenuse
@@ -38,6 +38,7 @@ server. It calls the same HTTP actions the dashboard does, as the person who sig
38
38
  | `hypotenuse event list\|view` | The selected project's event log: every webhook its Linear workspace sent in the last 14 days and how each was routed. `list [--type AgentSessionEvent\|Issue\|…] [--outcome handled\|ignored\|duplicate_of_dispatch\|failed\|pending] [--hide-ignored] [--session <id>] [--since 12h]`, newest first, shows each event's issue, outcome, what it did, the role it was routed as and why (`worker (default)`, `manager (rule /manage)`, `worker (dispatched)`), and where its turn is now (`queued: no_capacity`, `running on <runner>`); `view <id>` shows one in full, with the payload Linear sent for project admins only. |
39
39
  | `hypotenuse runner register\|list\|view\|edit\|revoke` | The selected project's runners — machines that run coding agents. `register [--name <name>] [--replace] [--force]` registers **this** machine as the signed-in person, saves the runner token to `credentials.json` (never printed), and sends a first heartbeat; `--replace` revokes the runner of that name first. `list [--status online\|offline\|draining\|revoked] [--all]`; `view <id\|name>` shows capabilities, subscriptions, and whether its socket is attached; `edit <id\|name> [--name] [--enable\|--disable]` renames or pauses it; `revoke <id\|name> --yes` revokes it for good (its owner or an admin). |
40
40
  | `hypotenuse routing list\|add\|edit\|remove\|reorder\|preview\|settings` | The selected project's routing rules — the first enabled rule whose match holds decides whether a new session gets a **manager** or a **worker** (no match: worker). `list`; `add --name <name> --role manager\|worker [--label <l>] [--team <key>] [--linear-project <id>] [--prefix /manage] [--has-parent\|--no-parent] [--origin delegation\|mention] [--repository <repo>] [--disabled]` (labels and team keys are checked against the connected workspace); `edit <id>` with the same flags (any match flag replaces the whole match) plus `--enable\|--disable` and `--clear-repository`; `remove <id>`; `reorder <id...>` lists every rule id in the new order; `preview <ENG-123> [--prompt "/manage ..."] [--with-repository]` shows how the issue would route if delegated now — the matching rule, the role, every runner with whether it could take the work and its reason codes, and the repository (any member; `--with-repository` asks Linear to rank repositories, spending its API budget); `settings` shows the routing timers, `settings [--queue-timeout 30m] [--sticky-timeout 10m] [--offer-ttl 10s] [--stop-grace 30s] [--repository-confidence 0.7] [--max-review-rounds 3] [--reset <key>]` changes them. Changes are admin-only. |
41
+ | `hypotenuse cost list\|summary` | What the selected project's turns cost and whom it is attributed to, as the coding agents reported it (US dollars and tokens; Hypotenuse never prices tokens itself). `list`, newest first, prints one record per runner's claim of a turn — issue, session, role, kind, runner, agent, how it ended, tokens, cost — and the total; `summary --by session\|root-session\|issue\|runner\|owner\|issuer\|role\|agent\|repository\|day` totals them per group, most expensive first, counting turns with no reported cost as `unpriced`. Both take `--session <id>`, `--root-session <id>` (a manager and its workers), `--runner <id>`, `--owner <userId>`, `--issuer <userId>`, `--role manager\|worker`, `--since 30d`, `--until <time>`, and paging. Any member. |
41
42
  | `hypotenuse telemetry exporter list\|view\|add\|edit\|remove\|test` | Where the selected project's OpenTelemetry goes: each exporter is an OTLP/HTTP collector that receives every turn as one trace, at a content level (`metadata`, `content`, or `full` — full sends prompts and source code). `list` and `view <id>` show endpoint, level, signals, sample share, header names (never values), and the last export or error (any member); `add --name <name> --endpoint <https-url> [--header KEY=VALUE...] [--content <level>] [--signals traces,metrics,logs] [--sample 0.5] [--attribute KEY=VALUE...] [--no-agent-native] [--disabled]`; `edit <id>` with the same flags plus `--clear-headers` and `--enable\|--disable` (headers are kept unless given); `remove <id>`; `test <id>` sends a probe and prints each signal's answer, exiting 1 on a failure. Changes and tests are admin-only; header values are never printed. |
42
43
  | `hypotenuse identity list\|link\|unlink` | Links between people in the project's Linear workspace and project members; routing prefers the runners of whoever started a session. `list [--user <id>]` shows each link and how it was made (e-mail match, Linear sign-in, admin); `link --user <id> --linear-user <id> [--name] [--email]` links one (admin); `unlink <id>` removes your own link, or anyone's as an admin. |
43
44
  | `hypotenuse config init\|validate\|schema\|path` | The daemon's TOML config (`-c <path>`, else `HYPOTENUSE_CONFIG`, else `~/.config/hypotenuse/config.toml`). `init [--repo <path>...] [--force]` writes a commented starter filled in from this machine; `validate [--online] [--offline]` checks shape, references, and this machine (git clones, prompt files, agent commands), and with `--online` the server, runner token, project, and team keys — exit 0 valid (warnings allowed), 1 errors, 2 unreadable; `--json` prints `{ valid, errors: [{ path, message, hint? }], warnings }`. `schema` prints the JSON Schema; `path` prints the config, state directory, and credentials file in use. |
@@ -63,6 +64,7 @@ hypotenuse --json integration view linear | jq '.integration | {type, status, wo
63
64
  hypotenuse --json session list --issue ENG-123 | jq '.agentSessions[] | {id, status}'
64
65
  hypotenuse --json runner list | jq '.runners[] | {name, status, owner: .owner.name}'
65
66
  hypotenuse --json session view 42 | jq '.activities[] | {seq, direction, contentType, deliveryStatus}'
67
+ hypotenuse --json cost summary --by owner --since 7d | jq '.groups[] | {owner: .label, costUsd, turns}'
66
68
  hypotenuse --json config validate | jq -r '.errors[] | "\(.path): \(.message)"'
67
69
  hypotenuse daemon start && hypotenuse --json daemon status | jq '{connected: .server.connected, slots}'
68
70
  ```