@arcadeai/hypotenuse 0.14.1 → 0.16.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.14.1",
346
+ version: "0.16.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",
@@ -377,7 +377,7 @@ var package_default = {
377
377
  lint: "tsc && biome check .",
378
378
  format: "tsc && biome check --write .",
379
379
  bundle: "bun build ./src/cli.ts --target bun --packages external --outfile dist/hypotenuse.js",
380
- build: "bun build --compile --minify --sourcemap ./src/cli.ts --outfile dist/hypotenuse",
380
+ build: "bun build --compile --minify --sourcemap ./src/cli.ts ./skills/hypotenuse.md --loader .md:file --asset-naming='[name].[ext]' --outfile dist/hypotenuse",
381
381
  prepack: "bun run bundle"
382
382
  },
383
383
  dependencies: {
@@ -2493,7 +2493,7 @@ function applyOverrides(config, overrides, env = process.env) {
2493
2493
  },
2494
2494
  log: {
2495
2495
  ...config.log,
2496
- level: overrides.verbose ? "debug" : envLevel && LOG_LEVELS.includes(envLevel) ? envLevel : config.log.level
2496
+ level: overrides.logLevel ?? (overrides.verbose ? "debug" : envLevel && LOG_LEVELS.includes(envLevel) ? envLevel : config.log.level)
2497
2497
  }
2498
2498
  };
2499
2499
  }
@@ -4037,6 +4037,124 @@ function changedFiles(cwd) {
4037
4037
  }
4038
4038
  }
4039
4039
 
4040
+ // src/daemon/log.ts
4041
+ import {
4042
+ appendFileSync as appendFileSync2,
4043
+ chmodSync as chmodSync2,
4044
+ existsSync as existsSync7,
4045
+ mkdirSync as mkdirSync3,
4046
+ renameSync as renameSync2,
4047
+ rmSync as rmSync2,
4048
+ statSync as statSync3
4049
+ } from "fs";
4050
+ import { dirname as dirname5 } from "path";
4051
+ var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
4052
+ var LOG_KEEP = 5;
4053
+ var RANK = {
4054
+ error: 0,
4055
+ warn: 1,
4056
+ info: 2,
4057
+ debug: 3,
4058
+ trace: 4
4059
+ };
4060
+ var LEVEL_TOKEN = {
4061
+ error: "status-error",
4062
+ warn: "status-waiting",
4063
+ info: "status-active",
4064
+ debug: "text-tertiary",
4065
+ trace: "text-tertiary"
4066
+ };
4067
+ var LINE = /^(\S+) (error|warn |info |debug|trace) (.*)$/;
4068
+ var FIELD_KEY = / ([A-Za-z][\w.]*)=/g;
4069
+ function paintLogLine(line, mode = getMode()) {
4070
+ if (mode.depth === "none")
4071
+ return line;
4072
+ const match = LINE.exec(line);
4073
+ if (!match)
4074
+ return line;
4075
+ const [, time = "", padded = "", rest = ""] = match;
4076
+ const level = padded.trim();
4077
+ const { color, dim } = createTheme(mode);
4078
+ const token = LEVEL_TOKEN[level];
4079
+ const body = rest.replace(FIELD_KEY, (_, key) => ` ${dim(`${key}=`)}`);
4080
+ const message = level === "error" || level === "warn" ? color(token, body) : level === "debug" || level === "trace" ? dim(body) : body;
4081
+ return `${dim(time)} ${color(token, padded)} ${message}`;
4082
+ }
4083
+ var PREVIEW_LIMIT = 200;
4084
+ function preview(text, level) {
4085
+ const line = text.replace(/\s+/g, " ").trim();
4086
+ return level !== "trace" && line.length > PREVIEW_LIMIT ? `${line.slice(0, PREVIEW_LIMIT - 1)}\u2026` : line;
4087
+ }
4088
+
4089
+ class DaemonLog {
4090
+ file;
4091
+ level;
4092
+ echo;
4093
+ paint;
4094
+ bytes = 0;
4095
+ constructor(file, level, echo, paint = Boolean(process.stderr.isTTY) || process.argv.includes("--force-color") || isTruthyEnv(process.env.FORCE_COLOR)) {
4096
+ this.file = file;
4097
+ this.level = level;
4098
+ this.echo = echo;
4099
+ this.paint = paint;
4100
+ if (file) {
4101
+ mkdirSync3(dirname5(file), { recursive: true, mode: 448 });
4102
+ this.bytes = existsSync7(file) ? statSync3(file).size : 0;
4103
+ }
4104
+ }
4105
+ enabled(level) {
4106
+ return RANK[level] <= RANK[this.level];
4107
+ }
4108
+ write(level, message, fields = {}) {
4109
+ if (!this.enabled(level))
4110
+ return;
4111
+ const extra = Object.entries(fields).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${typeof v === "string" && /\s/.test(v) ? JSON.stringify(v) : v}`).join(" ");
4112
+ const line = `${new Date().toISOString()} ${level.padEnd(5)} ${message}${extra ? ` ${extra}` : ""}
4113
+ `;
4114
+ if (this.echo)
4115
+ process.stderr.write(this.paint ? `${paintLogLine(line.slice(0, -1))}
4116
+ ` : line);
4117
+ if (!this.file)
4118
+ return;
4119
+ try {
4120
+ if (this.bytes + line.length > LOG_ROTATE_BYTES)
4121
+ this.rotate();
4122
+ const fresh = !existsSync7(this.file);
4123
+ appendFileSync2(this.file, line, { mode: 384 });
4124
+ if (fresh)
4125
+ chmodSync2(this.file, 384);
4126
+ this.bytes += Buffer.byteLength(line);
4127
+ } catch {}
4128
+ }
4129
+ rotate() {
4130
+ if (!this.file)
4131
+ return;
4132
+ rmSync2(`${this.file}.${LOG_KEEP}`, { force: true });
4133
+ for (let i = LOG_KEEP - 1;i >= 1; i--) {
4134
+ if (existsSync7(`${this.file}.${i}`))
4135
+ renameSync2(`${this.file}.${i}`, `${this.file}.${i + 1}`);
4136
+ }
4137
+ if (existsSync7(this.file))
4138
+ renameSync2(this.file, `${this.file}.1`);
4139
+ this.bytes = 0;
4140
+ }
4141
+ error(message, fields) {
4142
+ this.write("error", message, fields);
4143
+ }
4144
+ warn(message, fields) {
4145
+ this.write("warn", message, fields);
4146
+ }
4147
+ info(message, fields) {
4148
+ this.write("info", message, fields);
4149
+ }
4150
+ debug(message, fields) {
4151
+ this.write("debug", message, fields);
4152
+ }
4153
+ trace(message, fields) {
4154
+ this.write("trace", message, fields);
4155
+ }
4156
+ }
4157
+
4040
4158
  // src/daemon/main.ts
4041
4159
  import { spawn as spawn2 } from "child_process";
4042
4160
  import { createHash as createHash2 } from "crypto";
@@ -4044,7 +4162,7 @@ import { existsSync as existsSync16, readFileSync as readFileSync10, rmSync as r
4044
4162
  import { join as join10 } from "path";
4045
4163
 
4046
4164
  // src/acp/host.ts
4047
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
4165
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
4048
4166
  import { join as join8 } from "path";
4049
4167
 
4050
4168
  // src/telemetry/acp.ts
@@ -4431,7 +4549,7 @@ class NativeReceiver {
4431
4549
  }
4432
4550
 
4433
4551
  // src/worktree/co-author.ts
4434
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
4552
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "fs";
4435
4553
  import { join as join5 } from "path";
4436
4554
  var GIT_HOOK_NAMES = [
4437
4555
  "applypatch-msg",
@@ -4477,13 +4595,13 @@ exit 0
4477
4595
  `;
4478
4596
  function installCoAuthorHooks(stateDir) {
4479
4597
  const dir = join5(stateDir, "git-hooks");
4480
- mkdirSync3(dir, { recursive: true, mode: 448 });
4598
+ mkdirSync4(dir, { recursive: true, mode: 448 });
4481
4599
  for (const name of GIT_HOOK_NAMES) {
4482
4600
  const path = join5(dir, name);
4483
4601
  const temporary = `${path}.${process.pid}.tmp`;
4484
4602
  writeFileSync2(temporary, HOOK_SCRIPT, { mode: 493 });
4485
- chmodSync2(temporary, 493);
4486
- renameSync2(temporary, path);
4603
+ chmodSync3(temporary, 493);
4604
+ renameSync3(temporary, path);
4487
4605
  }
4488
4606
  return dir;
4489
4607
  }
@@ -4500,7 +4618,7 @@ function coAuthorEnvironment(env, hooksDir, coAuthor) {
4500
4618
  }
4501
4619
 
4502
4620
  // src/worktree/manager.ts
4503
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
4621
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync4 } from "fs";
4504
4622
  import { join as join6 } from "path";
4505
4623
 
4506
4624
  // src/worktree/branch.ts
@@ -4526,12 +4644,12 @@ function issueDirectory(identifier, fallback) {
4526
4644
  }
4527
4645
 
4528
4646
  // src/worktree/disk.ts
4529
- import { existsSync as existsSync7, statfsSync } from "fs";
4530
- import { dirname as dirname5 } from "path";
4647
+ import { existsSync as existsSync8, statfsSync } from "fs";
4648
+ import { dirname as dirname6 } from "path";
4531
4649
  function freeBytes(path, statfs = statfsSync) {
4532
4650
  let current = path;
4533
- while (!existsSync7(current)) {
4534
- const parent = dirname5(current);
4651
+ while (!existsSync8(current)) {
4652
+ const parent = dirname6(current);
4535
4653
  if (parent === current)
4536
4654
  break;
4537
4655
  current = parent;
@@ -4600,7 +4718,7 @@ function stderrTail(result, lines = 20) {
4600
4718
  }
4601
4719
 
4602
4720
  // src/worktree/lock.ts
4603
- import { existsSync as existsSync8, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
4721
+ import { existsSync as existsSync9, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
4604
4722
 
4605
4723
  class KeyedMutex {
4606
4724
  tails = new Map;
@@ -4631,7 +4749,7 @@ function pidAlive(pid) {
4631
4749
  }
4632
4750
  }
4633
4751
  function readCheckoutLock(file) {
4634
- if (!existsSync8(file))
4752
+ if (!existsSync9(file))
4635
4753
  return null;
4636
4754
  try {
4637
4755
  return JSON.parse(readFileSync5(file, "utf8"));
@@ -4654,7 +4772,7 @@ function takeCheckoutLock(file, agentSessionId, pid = process.pid) {
4654
4772
  function releaseCheckoutLock(file, agentSessionId) {
4655
4773
  const held = readCheckoutLock(file);
4656
4774
  if (!held || held.agentSessionId === agentSessionId)
4657
- rmSync2(file, { force: true });
4775
+ rmSync3(file, { force: true });
4658
4776
  }
4659
4777
 
4660
4778
  // src/worktree/setup.ts
@@ -4823,7 +4941,7 @@ class WorktreeManager {
4823
4941
  this.refuseIfShort(request);
4824
4942
  await this.fetchBase(request, repository, baseBranch);
4825
4943
  const path = this.worktreePath(request, repository);
4826
- mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
4944
+ mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
4827
4945
  await this.git(["worktree", "prune"], { cwd: repository.path });
4828
4946
  await this.must(["worktree", "add", "--detach", path, `origin/${baseBranch}`], repository.path, `create a read-only checkout of ${baseBranch}`);
4829
4947
  const created = this.state.insertWorktree({
@@ -4870,7 +4988,7 @@ class WorktreeManager {
4870
4988
  async scratch(request) {
4871
4989
  const id = request.assignment.agentSessionId;
4872
4990
  const path = join6(request.config.sessions.worktree_root, SCRATCH, String(id));
4873
- mkdirSync4(path, { recursive: true, mode: 448 });
4991
+ mkdirSync5(path, { recursive: true, mode: 448 });
4874
4992
  const known = this.state.worktreeAt(path);
4875
4993
  let row;
4876
4994
  if (known && known.state !== "removed") {
@@ -4950,7 +5068,7 @@ ${stderrTail(fetched)}
4950
5068
  }
4951
5069
  fetchAge(repoPath) {
4952
5070
  try {
4953
- const age = this.now() - statSync3(join6(repoPath, ".git", "FETCH_HEAD")).mtimeMs;
5071
+ const age = this.now() - statSync4(join6(repoPath, ".git", "FETCH_HEAD")).mtimeMs;
4954
5072
  if (age < 60000)
4955
5073
  return null;
4956
5074
  const units = [
@@ -5007,7 +5125,7 @@ ${stderrTail(fetched)}
5007
5125
  for (let n = 1;; n++) {
5008
5126
  const path = join6(parent, suffixed(dir, n));
5009
5127
  const row = this.state.worktreeAt(path);
5010
- if (!existsSync9(path) && (!row || row.state === "removed"))
5128
+ if (!existsSync10(path) && (!row || row.state === "removed"))
5011
5129
  return path;
5012
5130
  }
5013
5131
  }
@@ -5062,7 +5180,7 @@ ${stderrTail(fetched)}
5062
5180
  const reachable = await this.fetchBase(request, repository, baseBranch);
5063
5181
  const { branch, local, remote } = await this.chooseBranch(request, repository, repoConfig, reachable);
5064
5182
  const path = this.worktreePath(request, repository);
5065
- mkdirSync4(join6(path, ".."), { recursive: true, mode: 448 });
5183
+ mkdirSync5(join6(path, ".."), { recursive: true, mode: 448 });
5066
5184
  await this.git(["worktree", "prune"], { cwd: repository.path });
5067
5185
  let add;
5068
5186
  if (local) {
@@ -5106,7 +5224,7 @@ ${stderrTail(fetched)}
5106
5224
  reusable(agentSessionId) {
5107
5225
  const session = this.state.session(agentSessionId);
5108
5226
  const row = session?.worktreeId ? this.state.worktree(session.worktreeId) : null;
5109
- if (!row || row.kind !== "worktree" || row.state === "removed" || !existsSync9(row.path))
5227
+ if (!row || row.kind !== "worktree" || row.state === "removed" || !existsSync10(row.path))
5110
5228
  return null;
5111
5229
  return row;
5112
5230
  }
@@ -5440,15 +5558,15 @@ class QuestionQueue {
5440
5558
  // src/acp/fs.ts
5441
5559
  import { randomUUID } from "crypto";
5442
5560
  import {
5443
- existsSync as existsSync10,
5444
- mkdirSync as mkdirSync5,
5561
+ existsSync as existsSync11,
5562
+ mkdirSync as mkdirSync6,
5445
5563
  readFileSync as readFileSync6,
5446
5564
  realpathSync,
5447
- renameSync as renameSync3,
5448
- rmSync as rmSync3,
5565
+ renameSync as renameSync4,
5566
+ rmSync as rmSync4,
5449
5567
  writeFileSync as writeFileSync4
5450
5568
  } from "fs";
5451
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
5569
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
5452
5570
 
5453
5571
  class OutsideWorktree extends Error {
5454
5572
  path;
@@ -5486,11 +5604,11 @@ class ConfinedFs {
5486
5604
  return real;
5487
5605
  }
5488
5606
  resolveParent(absolute) {
5489
- let dir = dirname6(absolute);
5607
+ let dir = dirname7(absolute);
5490
5608
  const missing = [];
5491
- while (!existsSync10(dir) && dirname6(dir) !== dir) {
5609
+ while (!existsSync11(dir) && dirname7(dir) !== dir) {
5492
5610
  missing.unshift(basename2(dir));
5493
- dir = dirname6(dir);
5611
+ dir = dirname7(dir);
5494
5612
  }
5495
5613
  let real;
5496
5614
  try {
@@ -5524,20 +5642,20 @@ class ConfinedFs {
5524
5642
  write(path, content) {
5525
5643
  const absolute = this.absolute(path);
5526
5644
  let target;
5527
- if (existsSync10(absolute)) {
5645
+ if (existsSync11(absolute)) {
5528
5646
  target = realpathSync(absolute);
5529
5647
  if (!this.inside(target))
5530
5648
  throw new OutsideWorktree(path);
5531
5649
  } else {
5532
5650
  target = join7(this.resolveParent(absolute), basename2(absolute));
5533
5651
  }
5534
- mkdirSync5(dirname6(target), { recursive: true });
5535
- const temp = join7(dirname6(target), `.${basename2(target)}.${randomUUID()}.tmp`);
5652
+ mkdirSync6(dirname7(target), { recursive: true });
5653
+ const temp = join7(dirname7(target), `.${basename2(target)}.${randomUUID()}.tmp`);
5536
5654
  try {
5537
5655
  writeFileSync4(temp, content);
5538
- renameSync3(temp, target);
5656
+ renameSync4(temp, target);
5539
5657
  } catch (error) {
5540
- rmSync3(temp, { force: true });
5658
+ rmSync4(temp, { force: true });
5541
5659
  throw error;
5542
5660
  }
5543
5661
  }
@@ -6490,7 +6608,7 @@ class Turn {
6490
6608
  this.cwd = this.workspace.cwd;
6491
6609
  this.branch = this.workspace.branch;
6492
6610
  } else {
6493
- mkdirSync6(this.cwd, { recursive: true, mode: 448 });
6611
+ mkdirSync7(this.cwd, { recursive: true, mode: 448 });
6494
6612
  }
6495
6613
  if (this.endReason)
6496
6614
  throw new TurnFailure("stopped");
@@ -6818,6 +6936,13 @@ class Turn {
6818
6936
  "hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
6819
6937
  "hypotenuse.acp.resumed": this.resumed
6820
6938
  }, promptText);
6939
+ if (ctx.log.enabled("debug")) {
6940
+ ctx.log.debug(`prompting ${this.agentId}: ${preview(promptText, ctx.log.level)}`, {
6941
+ session: ctx.assignment.agentSessionId,
6942
+ workItem: ctx.assignment.workItemId,
6943
+ bytes: Buffer.byteLength(promptText)
6944
+ });
6945
+ }
6821
6946
  const request = conn.prompt(sessionId, blocks);
6822
6947
  this.promptSettled = request.then(() => {}, () => {});
6823
6948
  const answer = await this.guard(request, 0, "").catch((error) => {
@@ -7028,7 +7153,7 @@ class Turn {
7028
7153
 
7029
7154
  // src/runner/machine.ts
7030
7155
  import { createHash } from "crypto";
7031
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
7156
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
7032
7157
  import { hostname as hostname2 } from "os";
7033
7158
  var MAX_NAME = 64;
7034
7159
  function defaultRunnerName(host = hostname2()) {
@@ -7041,7 +7166,7 @@ function rawMachineId(env = process.env) {
7041
7166
  try {
7042
7167
  if (process.platform === "linux") {
7043
7168
  for (const path of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
7044
- if (existsSync11(path)) {
7169
+ if (existsSync12(path)) {
7045
7170
  const id = readFileSync8(path, "utf8").trim();
7046
7171
  if (id)
7047
7172
  return id;
@@ -7098,8 +7223,8 @@ function registrationHeartbeat() {
7098
7223
  }
7099
7224
 
7100
7225
  // src/daemon/control-socket.ts
7101
- import { chmodSync as chmodSync3, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "fs";
7102
- import { dirname as dirname7 } from "path";
7226
+ import { chmodSync as chmodSync4, existsSync as existsSync13, mkdirSync as mkdirSync8, rmSync as rmSync5 } from "fs";
7227
+ import { dirname as dirname8 } from "path";
7103
7228
  var METHOD_NOT_FOUND = -32601;
7104
7229
  var INVALID_PARAMS = -32602;
7105
7230
  var INTERNAL_ERROR = -32603;
@@ -7124,9 +7249,9 @@ class ControlServer {
7124
7249
  this.onError = onError;
7125
7250
  }
7126
7251
  listen() {
7127
- mkdirSync7(dirname7(this.path), { recursive: true, mode: 448 });
7128
- if (existsSync12(this.path))
7129
- rmSync4(this.path, { force: true });
7252
+ mkdirSync8(dirname8(this.path), { recursive: true, mode: 448 });
7253
+ if (existsSync13(this.path))
7254
+ rmSync5(this.path, { force: true });
7130
7255
  const previous = process.umask(127);
7131
7256
  try {
7132
7257
  this.listener = Bun.listen({
@@ -7142,12 +7267,12 @@ class ControlServer {
7142
7267
  } finally {
7143
7268
  process.umask(previous);
7144
7269
  }
7145
- chmodSync3(this.path, 384);
7270
+ chmodSync4(this.path, 384);
7146
7271
  }
7147
7272
  close() {
7148
7273
  this.listener?.stop(true);
7149
7274
  this.listener = null;
7150
- rmSync4(this.path, { force: true });
7275
+ rmSync5(this.path, { force: true });
7151
7276
  }
7152
7277
  onData(socket, data) {
7153
7278
  socket.data.buffer += data.toString("utf8");
@@ -7286,90 +7411,6 @@ class DaemonApi {
7286
7411
  }
7287
7412
  }
7288
7413
 
7289
- // src/daemon/log.ts
7290
- import {
7291
- appendFileSync as appendFileSync2,
7292
- chmodSync as chmodSync4,
7293
- existsSync as existsSync13,
7294
- mkdirSync as mkdirSync8,
7295
- renameSync as renameSync4,
7296
- rmSync as rmSync5,
7297
- statSync as statSync4
7298
- } from "fs";
7299
- import { dirname as dirname8 } from "path";
7300
- var LOG_ROTATE_BYTES = 10 * 1024 * 1024;
7301
- var LOG_KEEP = 5;
7302
- var RANK = {
7303
- error: 0,
7304
- warn: 1,
7305
- info: 2,
7306
- debug: 3,
7307
- trace: 4
7308
- };
7309
-
7310
- class DaemonLog {
7311
- file;
7312
- level;
7313
- echo;
7314
- bytes = 0;
7315
- constructor(file, level, echo) {
7316
- this.file = file;
7317
- this.level = level;
7318
- this.echo = echo;
7319
- if (file) {
7320
- mkdirSync8(dirname8(file), { recursive: true, mode: 448 });
7321
- this.bytes = existsSync13(file) ? statSync4(file).size : 0;
7322
- }
7323
- }
7324
- enabled(level) {
7325
- return RANK[level] <= RANK[this.level];
7326
- }
7327
- write(level, message, fields = {}) {
7328
- if (!this.enabled(level))
7329
- return;
7330
- const extra = Object.entries(fields).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${typeof v === "string" && /\s/.test(v) ? JSON.stringify(v) : v}`).join(" ");
7331
- const line = `${new Date().toISOString()} ${level.padEnd(5)} ${message}${extra ? ` ${extra}` : ""}
7332
- `;
7333
- if (this.echo)
7334
- process.stderr.write(line);
7335
- if (!this.file)
7336
- return;
7337
- try {
7338
- if (this.bytes + line.length > LOG_ROTATE_BYTES)
7339
- this.rotate();
7340
- const fresh = !existsSync13(this.file);
7341
- appendFileSync2(this.file, line, { mode: 384 });
7342
- if (fresh)
7343
- chmodSync4(this.file, 384);
7344
- this.bytes += Buffer.byteLength(line);
7345
- } catch {}
7346
- }
7347
- rotate() {
7348
- if (!this.file)
7349
- return;
7350
- rmSync5(`${this.file}.${LOG_KEEP}`, { force: true });
7351
- for (let i = LOG_KEEP - 1;i >= 1; i--) {
7352
- if (existsSync13(`${this.file}.${i}`))
7353
- renameSync4(`${this.file}.${i}`, `${this.file}.${i + 1}`);
7354
- }
7355
- if (existsSync13(this.file))
7356
- renameSync4(this.file, `${this.file}.1`);
7357
- this.bytes = 0;
7358
- }
7359
- error(message, fields) {
7360
- this.write("error", message, fields);
7361
- }
7362
- warn(message, fields) {
7363
- this.write("warn", message, fields);
7364
- }
7365
- info(message, fields) {
7366
- this.write("info", message, fields);
7367
- }
7368
- debug(message, fields) {
7369
- this.write("debug", message, fields);
7370
- }
7371
- }
7372
-
7373
7414
  // src/daemon/pidfile.ts
7374
7415
  import {
7375
7416
  closeSync as closeSync2,
@@ -8942,6 +8983,46 @@ class Backoff {
8942
8983
  // src/daemon/event-sink.ts
8943
8984
  var BATCH_EVENTS = 50;
8944
8985
  var BATCH_BYTES = 64 * 1024;
8986
+ function describeEvent(event) {
8987
+ let text;
8988
+ switch (event.type) {
8989
+ case "thought":
8990
+ case "response":
8991
+ case "error":
8992
+ text = event.body;
8993
+ break;
8994
+ case "elicitation":
8995
+ text = event.signal ? `[${event.signal}] ${event.body}` : event.body;
8996
+ break;
8997
+ case "action":
8998
+ text = `${event.action} ${event.parameter}${event.result ? ` \u2192 ${event.result}` : ""}`;
8999
+ break;
9000
+ case "plan": {
9001
+ const done = event.entries.filter((e) => e.status === "completed").length;
9002
+ const now = event.entries.find((e) => e.status === "in_progress");
9003
+ text = `${done}/${event.entries.length} done${now ? `; now: ${now.content}` : ""}`;
9004
+ break;
9005
+ }
9006
+ case "summary":
9007
+ text = event.title;
9008
+ break;
9009
+ case "external_url":
9010
+ text = `${event.label} ${event.url}`;
9011
+ break;
9012
+ case "usage":
9013
+ text = [
9014
+ event.inputTokens !== undefined && `in=${event.inputTokens}`,
9015
+ event.outputTokens !== undefined && `out=${event.outputTokens}`,
9016
+ event.costUsd !== undefined && `cost=$${event.costUsd}`,
9017
+ event.contextUsed !== undefined && `context=${event.contextUsed}${event.contextSize ? `/${event.contextSize}` : ""}`
9018
+ ].filter(Boolean).join(" ");
9019
+ break;
9020
+ case "status":
9021
+ text = event.state;
9022
+ break;
9023
+ }
9024
+ return text;
9025
+ }
8945
9026
 
8946
9027
  class EventSink {
8947
9028
  state;
@@ -8971,13 +9052,14 @@ class EventSink {
8971
9052
  ephemeral,
8972
9053
  payload: { ...event, eventId, at: Date.now() }
8973
9054
  });
8974
- this.log.debug("event queued", {
8975
- session: target.agentSessionId,
8976
- workItem: target.workItemId,
8977
- type: event.type,
8978
- seq,
8979
- ephemeral
8980
- });
9055
+ if (this.log.enabled("debug")) {
9056
+ this.log.debug(`agent ${event.type}: ${preview(describeEvent(event), this.log.level)}`, {
9057
+ session: target.agentSessionId,
9058
+ workItem: target.workItemId,
9059
+ seq,
9060
+ ephemeral
9061
+ });
9062
+ }
8981
9063
  this.kick();
8982
9064
  return { eventId, seq };
8983
9065
  }
@@ -9872,6 +9954,14 @@ class Supervisor {
9872
9954
  });
9873
9955
  if (taken)
9874
9956
  slot.consumed.push(reply.activityId);
9957
+ const { log } = this.options;
9958
+ if (log.enabled("debug")) {
9959
+ log.debug(`reply: ${preview(reply.body, log.level)}`, {
9960
+ session: slot.agentSessionId,
9961
+ workItem: slot.workItemId,
9962
+ activity: reply.activityId
9963
+ });
9964
+ }
9875
9965
  this.options.log.info(taken ? "a reply answered the turn's question" : "a reply did not answer the turn's question; it becomes the next prompt", { session: slot.agentSessionId, workItem: slot.workItemId });
9876
9966
  }
9877
9967
  agentFor(role) {
@@ -10681,7 +10771,7 @@ function notRunning(ctx, error) {
10681
10771
  }
10682
10772
  throw error;
10683
10773
  }
10684
- async function startBackground(ctx) {
10774
+ async function startBackground(ctx, logLevel) {
10685
10775
  const location = locateDaemon(ctx);
10686
10776
  const lock = await inspectLock(location.pidfile);
10687
10777
  if (lock.kind === "running" || lock.kind === "starting" || lock.kind === "hung") {
@@ -10696,7 +10786,8 @@ async function startBackground(ctx) {
10696
10786
  ...o.project ? ["--project", o.project] : [],
10697
10787
  ...o.verbose ? ["--verbose"] : [],
10698
10788
  "daemon",
10699
- "start"
10789
+ "start",
10790
+ ...logLevel ? ["--log-level", logLevel] : []
10700
10791
  ]);
10701
10792
  const child = spawn3(command, [...prefix, ...args], {
10702
10793
  detached: true,
@@ -10792,7 +10883,7 @@ async function tailLog(file, options) {
10792
10883
  const all = readFileSync11(file, "utf8").split(`
10793
10884
  `).filter(Boolean).filter(keep);
10794
10885
  for (const line of all.slice(-options.lines))
10795
- console.log(line);
10886
+ console.log(paintLogLine(line));
10796
10887
  }
10797
10888
  if (!options.follow)
10798
10889
  return;
@@ -10818,12 +10909,15 @@ async function tailLog(file, options) {
10818
10909
  partial = lines.pop() ?? "";
10819
10910
  for (const line of lines)
10820
10911
  if (line && keep(line))
10821
- console.log(line);
10912
+ console.log(paintLogLine(line));
10822
10913
  }
10823
10914
  }
10915
+ function logLevelOption() {
10916
+ return new Option("--log-level <level>", "the log level, over -v, HYPOTENUSE_LOG_LEVEL, and the config file").choices(LOG_LEVELS);
10917
+ }
10824
10918
  function registerDaemonCommands(program) {
10825
10919
  const daemon = program.command("daemon").description("Run this machine as a runner");
10826
- daemon.command("start").description("Start the daemon (in the background unless --foreground)").option("--foreground", "run in this process; what a service manager runs").addOption(new Option("--detached-child").hideHelp()).action(async function() {
10920
+ daemon.command("start").description("Start the daemon (in the background unless --foreground)").option("--foreground", "run in this process; what a service manager runs").addOption(logLevelOption()).addOption(new Option("--detached-child").hideHelp()).action(async function() {
10827
10921
  const opts = this.opts();
10828
10922
  if (opts.foreground) {
10829
10923
  const globals = this.optsWithGlobals();
@@ -10833,12 +10927,13 @@ function registerDaemonCommands(program) {
10833
10927
  url: globals.url,
10834
10928
  project: globals.project,
10835
10929
  verbose: globals.verbose,
10930
+ logLevel: opts.logLevel,
10836
10931
  detachedChild: Boolean(opts.detachedChild)
10837
10932
  });
10838
10933
  return;
10839
10934
  }
10840
10935
  await runCommand(this, async (ctx) => {
10841
- const pid = await startBackground(ctx);
10936
+ const pid = await startBackground(ctx, opts.logLevel);
10842
10937
  const { log } = locateDaemon(ctx);
10843
10938
  emit(ctx, { started: true, pid }, () => `Started the daemon (pid ${pid}). Logs: ${tildify(log, ctx.env)}`);
10844
10939
  });
@@ -10858,7 +10953,7 @@ function registerDaemonCommands(program) {
10858
10953
  }
10859
10954
  });
10860
10955
  });
10861
- daemon.command("restart").description("Stop the daemon (draining) and start it again").option("--timeout <duration>", "how long running turns may take before they are stopped", "10m").option("--cancel", "stop running turns at once").action(async function() {
10956
+ daemon.command("restart").description("Stop the daemon (draining) and start it again").option("--timeout <duration>", "how long running turns may take before they are stopped", "10m").option("--cancel", "stop running turns at once").addOption(logLevelOption()).action(async function() {
10862
10957
  await runCommand(this, async (ctx) => {
10863
10958
  const opts = this.opts();
10864
10959
  try {
@@ -10871,7 +10966,7 @@ function registerDaemonCommands(program) {
10871
10966
  if (!(error instanceof DaemonNotRunning))
10872
10967
  throw error;
10873
10968
  }
10874
- const pid = await startBackground(ctx);
10969
+ const pid = await startBackground(ctx, opts.logLevel);
10875
10970
  emit(ctx, { restarted: true, pid }, () => `Restarted the daemon (pid ${pid}).`);
10876
10971
  });
10877
10972
  });
@@ -12026,7 +12121,7 @@ function turnPlace(event) {
12026
12121
  }
12027
12122
  return t.runner ? `${t.status} on ${t.runner.name}` : t.status;
12028
12123
  }
12029
- function describeEvent(event, payload) {
12124
+ function describeEvent2(event, payload) {
12030
12125
  const lines = [
12031
12126
  `${event.eventType} ${event.action}${event.issueIdentifier ? ` ${event.issueIdentifier}` : ""}`,
12032
12127
  `outcome ${event.outcome}`,
@@ -12093,7 +12188,7 @@ function registerEventCommands(program) {
12093
12188
  await runCommand(this, async (ctx) => {
12094
12189
  const projectId = await ctx.projectId();
12095
12190
  const result = await ctx.signedInClient().request("GET", `/trigger-event/${id}`, { query: { projectId } });
12096
- emit(ctx, result, () => describeEvent(result.triggerEvent, result.payload));
12191
+ emit(ctx, result, () => describeEvent2(result.triggerEvent, result.payload));
12097
12192
  });
12098
12193
  });
12099
12194
  }
@@ -13563,6 +13658,30 @@ function registerSessionCommands(program) {
13563
13658
  });
13564
13659
  }
13565
13660
 
13661
+ // src/commands/skill.ts
13662
+ import { existsSync as existsSync21, readFileSync as readFileSync12 } from "fs";
13663
+ import { join as join13 } from "path";
13664
+ var SKILL_FILE = "hypotenuse.md";
13665
+ async function readSkill() {
13666
+ const embedded = Bun.embeddedFiles.find((file) => file.name === SKILL_FILE);
13667
+ if (embedded)
13668
+ return embedded.text();
13669
+ for (const up of [[".."], ["..", ".."]]) {
13670
+ const path = join13(import.meta.dir, ...up, "skills", SKILL_FILE);
13671
+ if (existsSync21(path))
13672
+ return readFileSync12(path, "utf8");
13673
+ }
13674
+ throw new Error(`This install has no skills/${SKILL_FILE}; reinstall hypotenuse.`);
13675
+ }
13676
+ function registerSkillCommand(program) {
13677
+ program.command("skill").description("Print the agent skill for using this CLI (save it as SKILL.md)").action(async function() {
13678
+ await runCommand(this, async (ctx) => {
13679
+ const skill = await readSkill();
13680
+ emit(ctx, { skill }, () => skill.trimEnd());
13681
+ });
13682
+ });
13683
+ }
13684
+
13566
13685
  // src/commands/tag.ts
13567
13686
  function registerTagCommands(program) {
13568
13687
  const tag = program.command("tag").description("The selected project's permission tags");
@@ -14059,6 +14178,7 @@ function buildProgram() {
14059
14178
  registerServiceCommands(program);
14060
14179
  registerDoctorCommand(program);
14061
14180
  registerCheckUpdateCommand(program);
14181
+ registerSkillCommand(program);
14062
14182
  registerUpgradeCommand(program);
14063
14183
  registerMcpCommand(program);
14064
14184
  return program;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadeai/hypotenuse",
3
- "version": "0.14.1",
3
+ "version": "0.16.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",
@@ -34,7 +34,7 @@
34
34
  "lint": "tsc && biome check .",
35
35
  "format": "tsc && biome check --write .",
36
36
  "bundle": "bun build ./src/cli.ts --target bun --packages external --outfile dist/hypotenuse.js",
37
- "build": "bun build --compile --minify --sourcemap ./src/cli.ts --outfile dist/hypotenuse",
37
+ "build": "bun build --compile --minify --sourcemap ./src/cli.ts ./skills/hypotenuse.md --loader .md:file --asset-naming='[name].[ext]' --outfile dist/hypotenuse",
38
38
  "prepack": "bun run bundle"
39
39
  },
40
40
  "dependencies": {
@@ -41,13 +41,14 @@ server. It calls the same HTTP actions the dashboard does, as the person who sig
41
41
  | `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
42
  | `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
43
  | `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. |
44
- | `hypotenuse daemon start\|stop\|restart\|status\|logs\|reload` | The runner daemon. `start` backgrounds it and waits until it is ready (`--foreground` stays attached, as a service manager runs it); `stop [--timeout 10m] [--cancel]` lets running turns finish (or stops them) and exits, `--force` signals it without its control socket; `status` exits 3 when it is not running; `logs [-f] [-n 200] [--session <id>]` (`--session` shows that session's agent log: the agent's stderr and ACP trace); `reload` re-reads the config and says what needs a restart. One daemon per machine. |
44
+ | `hypotenuse daemon start\|stop\|restart\|status\|logs\|reload` | The runner daemon. `start` backgrounds it and waits until it is ready (`--foreground` stays attached, as a service manager runs it; `--log-level <level>` overrides `[log] level`, and `debug` logs every agent activity); `stop [--timeout 10m] [--cancel]` lets running turns finish (or stops them) and exits, `--force` signals it without its control socket; `status` exits 3 when it is not running; `logs [-f] [-n 200] [--session <id>]` (`--session` shows that session's agent log: the agent's stderr and ACP trace); `reload` re-reads the config and says what needs a restart. One daemon per machine. |
45
45
  | `hypotenuse agents list\|kill\|login` | The coding agents this machine runs. `list [--all]` shows the agent sessions the daemon is running (session, issue, role, agent, state, how long, pid, worktree); `kill <session-id\|ENG-123> [--reason]` stops that session's running turn, which says so in Linear (with the reason); both exit 3 when the daemon is not running. `login <agent> [--method <id>]` logs a coding agent in on this machine — a terminal login runs attached, an agent-run one over ACP — then confirms it can open a session (exit 1 if not). |
46
46
  | `hypotenuse worktrees list\|cleanup` | The git worktrees (and scratch directories) the daemon made for its sessions, one per session under `sessions.worktree_root`. `list` shows each one's repository, branch, issue, state, last use, size, and whether it is safe to remove now (not in use, no uncommitted changes, no unpushed commits, no `.hypotenuse-keep` file). `cleanup [--dry-run] [--all] [--older-than 7d] [--force]` removes what the cleanup policy allows and prints every kept one with its reason; `--all` ignores the policy, `--force` overrides every rule but "in use". Both work without a running daemon, from `state.db` and the disk. |
47
47
  | `hypotenuse service install\|uninstall\|status` | Run the daemon as a user service (launchd on macOS, systemd user unit on Linux) with the `PATH` of the installing shell; `install --print` shows the unit without installing it. Windows: prints the foreground command and the Task Scheduler recipe. |
48
48
  | `hypotenuse doctor [--agent <id>] [--offline]` | A checklist of this machine's runner setup — version, tools, config, credentials, server and token, daemon and service, agents, repositories, disk — exit 1 when a check fails. `--agent <id>` starts that agent, runs the ACP `initialize` handshake, and opens a session — a failure names `hypotenuse agents login <id>`. |
49
49
  | `hypotenuse mcp --session <id> --nonce <n>` | Hidden: the stdio MCP server the daemon hands a coding agent for one turn — the Linear tools scoped to that session. Not for running by hand. |
50
50
  | `hypotenuse check-update` | Whether a newer release exists, with its changelog. |
51
+ | `hypotenuse skill` | Print this skill, the same text on every install (npm or binary); `--json` wraps it as `{ skill }`. |
51
52
  | `hypotenuse upgrade [--no-restart]` | Upgrade in place, by however it was installed (npm, bun, or binary), then drain and restart a running daemon on the new version. |
52
53
 
53
54
  ## Examples