aiur-cli 0.0.8 → 0.0.9-nightly.3c81f85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/aiur.js CHANGED
@@ -165,8 +165,8 @@ function ensureTmux() {
165
165
  // True only when the pinned opencode is on PATH; a wrong (e.g. newer,
166
166
  // unverified) version must be replaced with the pin, so presence alone
167
167
  // isn't enough.
168
- function opencodeMatchesPin() {
169
- const result = safeSpawn("opencode", ["--version"], { encoding: "utf8" });
168
+ function opencodeMatchesPin(command = "opencode") {
169
+ const result = safeSpawn(command, ["--version"], { encoding: "utf8" });
170
170
  return !result.error && result.status === 0 && (result.stdout || "").trim() === opencodeVersion();
171
171
  }
172
172
 
@@ -185,40 +185,60 @@ function installOpencode() {
185
185
  return !result.error && result.status === 0;
186
186
  }
187
187
 
188
+ // npm's global bin may be shadowed by another OpenCode on the incoming PATH.
189
+ // Verify the actual installed executable before making it the release's choice.
190
+ function selectGlobalOpencode() {
191
+ const result = safeSpawn("npm", ["prefix", "-g"], { encoding: "utf8" });
192
+ if (result.error || result.status !== 0) return false;
193
+ const prefix = (result.stdout || "").trim();
194
+ if (!path.isAbsolute(prefix)) return false;
195
+ const bin = path.join(prefix, "bin");
196
+ if (!opencodeMatchesPin(path.join(bin, "opencode"))) return false;
197
+ process.env.PATH = `${bin}${path.delimiter}${process.env.PATH || ""}`;
198
+ return true;
199
+ }
200
+
188
201
  // opencode powers the interactive "take the wheel" panes. Non-fatal: everything
189
202
  // else works without it.
190
203
  function ensureOpencode() {
191
- if (opencodeMatchesPin()) return;
204
+ if (opencodeMatchesPin() || selectGlobalOpencode()) return;
192
205
  if (process.env.AIUR_SKIP_OPENCODE_INSTALL !== "1") {
193
206
  process.stderr.write(
194
207
  `aiur: provisioning opencode@${opencodeVersion()} (one-time, for the interactive 'take the wheel' panes)…\n`,
195
208
  );
196
209
  installOpencode();
197
- if (opencodeMatchesPin()) return;
210
+ if (opencodeMatchesPin() || selectGlobalOpencode()) return;
198
211
  }
199
212
  process.stderr.write(
200
- "aiur: opencode was not found on PATH. The interactive 'take the wheel' " +
213
+ `aiur: opencode@${opencodeVersion()} is unavailable. The interactive 'take the wheel' ` +
201
214
  "feature needs it; everything else works without it. " +
202
- "Install from https://opencode.ai\n",
215
+ `Install with: npm install -g opencode-ai@${opencodeVersion()}, ` +
216
+ "and put npm's global bin ahead of other opencode binaries on PATH.\n",
203
217
  );
204
218
  }
205
219
 
206
- // `init` and `--version` run as foreground one-shots that never start the
207
- // tmux-backed UI, so their tmux/opencode provisioning is irrelevant — and tmux
208
- // may legitimately be absent on a machine that only ever runs `aiur init`.
209
- function isForegroundOneShot(argv) {
210
- for (const arg of argv) {
211
- if (arg === "--version") return true;
212
- if (arg.startsWith("-")) continue;
213
- return arg === "init";
214
- }
215
- return false;
220
+ // Match the shared engine's first-argument dispatch: only session launch paths
221
+ // need interactive tools. Unknown commands still reach the engine's usage error.
222
+ function requiresInteractiveTools(argv) {
223
+ const command = argv[0] || "";
224
+ if (["", "run", "restart", "--bg"].includes(command)) return true;
225
+ const nonLaunchCommands = new Set([
226
+ "__identity", "help", "-h", "-help", "--h", "--help", "--version", "--todo", "--only",
227
+ "init", "findings", "ask", "asks", "status", "usage", "agents", "commands", "units",
228
+ "build-orders", "analytics", "github-cost", "github-usage", "alerts", "watch", "set",
229
+ "upgrade", "pause", "resume", "reset-budget", "message", "cleanup-stale", "stop",
230
+ "executor-answer", "executor-escalate", "executor-moot", "executor-listen", "executor-wait",
231
+ "executor-emit", "executor-subscribe", "executor-unsubscribe", "executor-subscriptions",
232
+ "executor-roster", "executor-fast-forward", "executor-claim", "executor-release", "executor-revoke",
233
+ ]);
234
+ if (nonLaunchCommands.has(command)) return false;
235
+ return command.startsWith("-") || fs.existsSync(command);
216
236
  }
217
237
 
218
238
  function main() {
219
239
  const releaseDir = resolveReleaseDir();
220
240
 
221
- if (!isForegroundOneShot(process.argv.slice(2))) {
241
+ if (requiresInteractiveTools(process.argv.slice(2))) {
222
242
  ensureTmux();
223
243
  ensureOpencode();
224
244
  }
@@ -560,6 +560,46 @@ parse_todo_args() {
560
560
  [ "$saw_todo" -eq 1 ] && [ "${#parsed_targets[@]}" -gt 0 ]
561
561
  }
562
562
 
563
+ # `--todo` is the one control command whose runtime scales with its arguments
564
+ # rather than with daemon state: every requested ID is a GitHub issue fetch, and
565
+ # `--only` adds an active-ticket enumeration plus up to 50 serial label DELETEs.
566
+ # On a repo with a deep `agent:todo` queue that is minutes of network work, and
567
+ # the shared 10s budget killed it every time — labels half-applied, outcome
568
+ # reported as unknown, `--only` scoping unusable (#2519). Size the watchdog to
569
+ # the work instead. An explicit operator override still wins outright.
570
+ todo_rpc_seconds_base=15
571
+ todo_rpc_seconds_per_id=3
572
+ todo_rpc_seconds_only=90
573
+ # The daemon is handed a budget this many seconds shorter than the watchdog, so
574
+ # it always stops itself and prints its summary and exit marker before the
575
+ # watchdog can discard them.
576
+ todo_rpc_grace_seconds=10
577
+
578
+ todo_rpc_seconds() {
579
+ local id_count="$1" only="$2" seconds
580
+
581
+ # Only a usable override wins. Everywhere else an unset, zero, or malformed
582
+ # value falls back to the shared 10s default; here that default is the bug, so
583
+ # fall back to work-proportional sizing instead.
584
+ if [[ "${AIUR_CONTROL_RPC_TIMEOUT_SECONDS:-}" =~ ^[1-9][0-9]*$ ]]; then
585
+ printf '%s' "$AIUR_CONTROL_RPC_TIMEOUT_SECONDS"
586
+ return 0
587
+ fi
588
+
589
+ seconds=$((todo_rpc_seconds_base + todo_rpc_seconds_per_id * id_count))
590
+ [ "$only" -eq 1 ] && seconds=$((seconds + todo_rpc_seconds_only))
591
+ printf '%s' "$seconds"
592
+ }
593
+
594
+ # Never hand the daemon a non-positive budget: an operator who sets the override
595
+ # below the grace window should still get a bounded run rather than an unlimited
596
+ # one.
597
+ todo_daemon_budget_ms() {
598
+ local seconds=$(($1 - todo_rpc_grace_seconds))
599
+ [ "$seconds" -ge 1 ] || seconds=1
600
+ printf '%s' $((seconds * 1000))
601
+ }
602
+
563
603
  run_todo() {
564
604
  if ! parse_todo_args "$@"; then
565
605
  echo "aiur: --todo expects one or more numeric issue IDs, optionally followed by --only" >&2
@@ -568,7 +608,13 @@ run_todo() {
568
608
 
569
609
  local only_arg="false"
570
610
  [ "$parsed_todo_only" -eq 1 ] && only_arg="true"
571
- run_control_rpc "Aiur.AgentControlCLI.todo($(elixir_list_literal "${parsed_targets[@]}"), only: $only_arg, emit_exit_marker: true)"
611
+
612
+ local seconds budget_ms
613
+ seconds="$(todo_rpc_seconds "${#parsed_targets[@]}" "$parsed_todo_only")"
614
+ budget_ms="$(todo_daemon_budget_ms "$seconds")"
615
+
616
+ AIUR_CONTROL_RPC_TIMEOUT_SECONDS="$seconds" \
617
+ run_control_rpc "Aiur.AgentControlCLI.todo($(elixir_list_literal "${parsed_targets[@]}"), only: $only_arg, budget_ms: $budget_ms, emit_exit_marker: true)"
572
618
  }
573
619
 
574
620
  # --- one-shot: findings (distribution-free, no daemon/tmux) -------------------
@@ -677,22 +723,10 @@ scrub_run_only_env() {
677
723
  }
678
724
 
679
725
  run_argv=()
680
- # Default dashboard bind host. Prefer this machine's Tailscale IPv4 so the
681
- # dashboard is reachable across the tailnet by default (when config omits it);
682
- # fall back to loopback when Tailscale is absent, or when dashboard credentials
683
- # are unset (a non-loopback bind requires them, so we stay on loopback rather
684
- # than refuse to start). The BEAM applies this below an explicit `server.host`,
685
- # while an explicit `--host` remains the highest-precedence value.
726
+ # Default dashboard bind host. Remote access is an explicit operator choice.
727
+ # The BEAM applies this below `server.host`; `--host` has highest precedence.
686
728
  default_dashboard_host() {
687
- local ip=""
688
- if command -v tailscale >/dev/null 2>&1; then
689
- ip="$(tailscale ip -4 2>/dev/null | grep -m1 -E '^100\.[0-9]+\.[0-9]+\.[0-9]+$' || true)"
690
- fi
691
- if [ -n "$ip" ] && [ -n "${AIUR_DASHBOARD_USERNAME:-}" ] && [ -n "${AIUR_DASHBOARD_PASSWORD:-}" ]; then
692
- printf '%s' "$ip"
693
- else
694
- printf '127.0.0.1'
695
- fi
729
+ printf '%s' "${AIUR_DEFAULT_DASHBOARD_HOST:-127.0.0.1}"
696
730
  }
697
731
 
698
732
  build_run_argv() {
@@ -834,9 +868,10 @@ run_session() {
834
868
  # supplied. `aiur --bg --interactive` opts back into the full terminal stack
835
869
  # for an attachable background session.
836
870
  build_run_argv "$mode" "$@"
837
- local no_dashboard=0
871
+ local no_dashboard=0 surface_mode=interactive
838
872
  for run_arg in "${run_argv[@]}"; do
839
873
  [ "$run_arg" = "--no-dashboard" ] && no_dashboard=1
874
+ [ "$run_arg" = "--headless" ] && surface_mode=headless
840
875
  done
841
876
  write_argv "${run_argv[@]}"
842
877
  export AIUR_ARGV_FILE="$argv_file"
@@ -1043,7 +1078,7 @@ run_session() {
1043
1078
  exit 1
1044
1079
  fi
1045
1080
 
1046
- write_aiur_instance_record "$session" "$socket"
1081
+ write_aiur_instance_record "$session" "$socket" replace "$surface_mode"
1047
1082
  release_aiur_launch_lock "$launch_lock"
1048
1083
  _session_launch_lock=""
1049
1084
  print_config_status "$startup_capture"
@@ -1549,7 +1584,7 @@ release_aiur_launch_lock() {
1549
1584
  }
1550
1585
 
1551
1586
  write_aiur_instance_record() {
1552
- local session="$1" socket="$2" write_mode="${3:-replace}" record_dir record tmp root
1587
+ local session="$1" socket="$2" write_mode="${3:-replace}" surface_mode="${4:-unknown}" record_dir record tmp root
1553
1588
  record_dir="$(aiur_instances_dir)"
1554
1589
  record="$(aiur_instance_record_path)"
1555
1590
  root="$(canonical_workspace_root "${AIUR_PROJECT_ROOT:-}")"
@@ -1561,6 +1596,7 @@ write_aiur_instance_record() {
1561
1596
  printf 'AIUR_RECORD_SESSION=%q\n' "$session"
1562
1597
  printf 'AIUR_RECORD_SOCKET=%q\n' "$socket"
1563
1598
  printf 'AIUR_RECORD_AGENT_TMPFILE=%q\n' "${AIUR_AGENT_TMPFILE:-}"
1599
+ printf 'AIUR_RECORD_SURFACE_MODE=%q\n' "$surface_mode"
1564
1600
  printf 'AIUR_RECORD_WORKSPACE_ROOT_FILE=%q\n' "${AIUR_WORKSPACE_ROOT_FILE:-}"
1565
1601
  printf 'AIUR_RECORD_PROJECT_ROOT=%q\n' "$root"
1566
1602
  printf 'AIUR_RECORD_PROJECT_ROOT_SOURCE=%q\n' "${AIUR_PROJECT_ROOT_SOURCE:-}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiur-cli",
3
- "version": "0.0.8",
3
+ "version": "0.0.9-nightly.3c81f85",
4
4
  "description": "AI Unit Runtime for Executors.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/aiur-team/aiur",
@@ -35,8 +35,8 @@
35
35
  ],
36
36
  "opencodeVersion": "1.17.10",
37
37
  "optionalDependencies": {
38
- "aiur-cli-linux-x64": "0.0.8",
39
- "aiur-cli-linux-arm64": "0.0.8",
40
- "aiur-cli-darwin-arm64": "0.0.8"
38
+ "aiur-cli-linux-x64": "0.0.9-nightly.3c81f85",
39
+ "aiur-cli-linux-arm64": "0.0.9-nightly.3c81f85",
40
+ "aiur-cli-darwin-arm64": "0.0.9-nightly.3c81f85"
41
41
  }
42
42
  }