aiur-cli 0.0.9-nightly.5a0d430 → 0.0.9-nightly.5febfcb

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", "pr", "findings", "ask", "asks", "status", "usage", "agents", "commands", "units",
228
+ "build-orders", "queue", "analytics", "experiments", "github-cost", "github-usage", "alerts", "watch", "set",
229
+ "upgrade", "pause", "resume", "park", "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
  }
@@ -0,0 +1,12 @@
1
+ # Read-only capability report through the daemon's local control RPC.
2
+ cmd_capabilities() {
3
+ local opts="" arg
4
+ for arg in "$@"; do
5
+ case "$arg" in
6
+ --json) opts="json: true" ;;
7
+ -*) echo "aiur: capabilities received an unknown option: $arg" >&2; exit 64 ;;
8
+ *) echo "aiur: capabilities does not accept positional arguments" >&2; exit 64 ;;
9
+ esac
10
+ done
11
+ run_control_rpc "Aiur.AgentControlCLI.capabilities([$opts])"
12
+ }
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced by the shared engine and the daemon pane.
3
+
4
+ supervisor_is_alive() {
5
+ local file="${1:-}" pid=""
6
+ [ -n "$file" ] && [ -r "$file" ] || return 1
7
+ IFS= read -r pid <"$file" || true
8
+ [[ "$pid" =~ ^[1-9][0-9]*$ ]] && kill -0 "$pid" 2>/dev/null
9
+ }
10
+
11
+ record_daemon_recovery() {
12
+ local name="$1" message="$2" since="$3" quiet="${4:-}" ledger="" timestamp
13
+ timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
14
+ printf '%s %s; down since %s\n' "$timestamp" "$message" "$since" >>"$AIUR_LOGS_ROOT/log/aiur.recovery"
15
+ [ -z "$quiet" ] || return 0
16
+ IFS= read -r ledger <"$AIUR_ALERT_LEDGER_PATH_FILE" || true
17
+ [ -n "$ledger" ] || return 0
18
+ mkdir -p "$(dirname "$ledger")"
19
+ printf '{"event":"alert","agent":"system","timestamp":"%s","name":"system.daemon.%s","topic":"system.daemon.%s","message":"%s","reason":"Daemon down since %s","severity":"warning","needs_attention":true}\n' \
20
+ "$timestamp" "$name" "$name" "$(json_escape_alert_field "$message")" "$since" >>"$ledger"
21
+ }
22
+
23
+ supervise_daemon() {
24
+ local supervisor_file="$1" capture="$2" baseline="$3"
25
+ shift 3
26
+ local status delay=1 started down_since="" child pane prev_dump giving_up crashes=() now t kept first_dump
27
+ local max_crashes="${AIUR_DAEMON_MAX_RESTARTS:-5}" crash_window=600
28
+ printf '%s\n' "$$" >"$supervisor_file"
29
+ release_bin="$AIUR_RELEASE_DIR/bin/aiur"
30
+ mkdir -p "$AIUR_LOGS_ROOT/log"
31
+ exec 3<&0
32
+ while :; do
33
+ started="$SECONDS"
34
+ (set +e; "$@" 2>&1 | tee -a "$capture"; exit "${PIPESTATUS[0]}") <&3 &
35
+ child=$!
36
+ if [ -n "$down_since" ]; then
37
+ # Bound readiness probes; a boot attempt is not evidence of recovery.
38
+ while kill -0 "$child" 2>/dev/null; do
39
+ if run_release_rpc_with_timeout 'if Enum.any?(Application.started_applications(), fn {app, _, _} -> app == :aiur end), do: IO.puts("__AIUR_RECOVERED__")' &&
40
+ [[ "$AIUR_CONTROL_RPC_OUTPUT" == *"__AIUR_RECOVERED__"* ]]; then
41
+ record_daemon_recovery restarted 'Daemon restarted after crash' "$down_since"
42
+ rm -f "$(aiur_crash_marker_path)"
43
+ down_since=""
44
+ break
45
+ fi
46
+ sleep 1
47
+ done
48
+ fi
49
+ if wait "$child"; then status=0; else status=$?; fi
50
+ # Initial boot failures stay visible to the launcher; normal halts stay stopped.
51
+ if [ "$status" -eq 0 ] || [ -f "$(aiur_stop_sentinel_path)" ] || ! grep -qx ready "$supervisor_file"; then
52
+ rm -f "$supervisor_file"
53
+ return "$status"
54
+ fi
55
+ # One needs-attention alert per outage; later attempts only reach the recovery log.
56
+ local quiet=1
57
+ [ -n "$down_since" ] || { down_since="$(date -u +%Y-%m-%dT%H:%M:%SZ)"; quiet=""; }
58
+ record_beam_crash "$RELEASE_NODE" "$AIUR_LOGS_ROOT" "$(aiur_crash_marker_path)" "$AIUR_ALERT_LEDGER_PATH_FILE" "$baseline"
59
+ now="$SECONDS"; kept=()
60
+ for t in "${crashes[@]}"; do [ "$((now - t))" -ge "$crash_window" ] || kept+=("$t"); done
61
+ crashes=("${kept[@]}" "$now")
62
+ giving_up=""
63
+ [ "${#crashes[@]}" -le "$max_crashes" ] || giving_up=1
64
+ if [ -n "$giving_up" ]; then
65
+ record_daemon_recovery gave-up "Daemon crashed ${#crashes[@]} times within ${crash_window}s; not restarting" "$down_since"
66
+ else
67
+ record_daemon_recovery down "Daemon down; retrying in ${delay}s (exit $status)" "$down_since" "$quiet"
68
+ fi
69
+ # Archive before another VM can overwrite even an operator-selected dump path.
70
+ if [ -f "$ERL_CRASH_DUMP" ]; then
71
+ mv "$ERL_CRASH_DUMP" "$ERL_CRASH_DUMP.$(date -u +%Y%m%dT%H%M%SZ).$$.$SECONDS" || return 1
72
+ # Keep only the first and the latest dump; the middle ones are GBs of repeats.
73
+ first_dump="" prev_dump=""
74
+ for t in "$ERL_CRASH_DUMP".*; do
75
+ [ -f "$t" ] || continue
76
+ [ -n "$first_dump" ] || { first_dump="$t"; continue; }
77
+ [ -z "$prev_dump" ] || rm -f "$prev_dump"
78
+ prev_dump="$t"
79
+ done
80
+ fi
81
+ [ -z "$giving_up" ] || { rm -f "$supervisor_file"; return "$status"; }
82
+ crash_dump_identity "$ERL_CRASH_DUMP" >"$baseline" || :
83
+ reap_aiur_agents "" "$AIUR_AGENT_TMPFILE"
84
+ reap_workspace_cwd_from_file "$AIUR_WORKSPACE_ROOT_FILE"
85
+ : >"$AIUR_AGENT_TMPFILE"
86
+ # Keep this pane, remove the dead VM's UI/agent panes before rebuilding the UI.
87
+ while IFS= read -r pane; do
88
+ [ "$pane" = "${TMUX_PANE:-}" ] || tmux -L "$AIUR_TMUX_SOCKET" kill-pane -t "$pane" 2>/dev/null || true
89
+ done < <(tmux -L "$AIUR_TMUX_SOCKET" list-panes -a -F '#{pane_id}' 2>/dev/null)
90
+ [ "$((SECONDS - started))" -lt 60 ] || delay=1
91
+ sleep "$delay"
92
+ [ ! -f "$(aiur_stop_sentinel_path)" ] || { rm -f "$supervisor_file"; return 0; }
93
+ [ "$delay" -ge 30 ] || delay=$((delay * 2))
94
+ [ "$delay" -le 30 ] || delay=30
95
+ done
96
+ }