wogiflow 2.26.1 → 2.26.2
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/lib/wogi-claude +55 -14
- package/lib/wogi-claude-expect.exp +96 -29
- package/package.json +1 -1
package/lib/wogi-claude
CHANGED
|
@@ -41,27 +41,68 @@ set -u
|
|
|
41
41
|
WOGI_CLAUDE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
42
42
|
WOGI_EXPECT_SCRIPT="$WOGI_CLAUDE_DIR/wogi-claude-expect.exp"
|
|
43
43
|
|
|
44
|
-
# Detect whether to use the expect wrapper
|
|
45
|
-
#
|
|
46
|
-
# 1. WOGI_USE_EXPECT=1 is explicitly set (opt-in)
|
|
47
|
-
# 2. WOGI_NO_EXPECT is NOT set (legacy escape hatch still honored)
|
|
48
|
-
# 3. `expect` is on PATH and the wogi-claude-expect.exp script exists
|
|
49
|
-
# 4. The args include --dangerously-load-development-channels (the only
|
|
50
|
-
# flag that triggers the dialog we want to auto-dismiss)
|
|
44
|
+
# Detect whether to use the expect wrapper for auto-dismissing the
|
|
45
|
+
# --dangerously-load-development-channels modal.
|
|
51
46
|
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# opt-in
|
|
47
|
+
# Precedence (highest to lowest):
|
|
48
|
+
# 1. WOGI_NO_EXPECT=1 → always OFF (kill switch)
|
|
49
|
+
# 2. Workspace worker mode → ON automatically (headless, cannot Enter by hand)
|
|
50
|
+
# 3. WOGI_USE_EXPECT=1 → ON (explicit opt-in for interactive users)
|
|
51
|
+
# 4. Default → OFF (interactive users get the native Claude Code dialog)
|
|
52
|
+
#
|
|
53
|
+
# Worker auto-enable (v2.26.2): WOGI_WORKSPACE_ROOT + WOGI_REPO_NAME (worker
|
|
54
|
+
# side) are set by `flow workspace start` before spawning this wrapper, so
|
|
55
|
+
# detection here is reliable. Interactive users never set these vars, so their
|
|
56
|
+
# default remains opt-in — the v2.22.3 regression (expect's text match miss on
|
|
57
|
+
# Ink ANSI output) is bounded to users who explicitly asked for expect.
|
|
58
|
+
#
|
|
59
|
+
# The rewritten wogi-claude-expect.exp (v2.26.2) replaces the old brittle
|
|
60
|
+
# per-chunk text match with: rolling buffer + ANSI strip + bounded elapsed-time
|
|
61
|
+
# window. Misses fall back to the same failure mode as running claude without
|
|
62
|
+
# the wrapper (dialog stays up until someone presses Enter) — no unsafe blind
|
|
63
|
+
# keystrokes injected into server-mode Claude.
|
|
64
|
+
|
|
65
|
+
__wogi_is_worker=0
|
|
66
|
+
if [ -n "${WOGI_WORKSPACE_ROOT:-}" ] && [ -n "${WOGI_REPO_NAME:-}" ] && \
|
|
67
|
+
[ "${WOGI_REPO_NAME}" != "manager" ]; then
|
|
68
|
+
__wogi_is_worker=1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
__wogi_wants_expect=0
|
|
72
|
+
if [ -z "${WOGI_NO_EXPECT:-}" ]; then
|
|
73
|
+
if [ "$__wogi_is_worker" -eq 1 ] || [ "${WOGI_USE_EXPECT:-}" = "1" ]; then
|
|
74
|
+
__wogi_wants_expect=1
|
|
75
|
+
fi
|
|
76
|
+
fi
|
|
77
|
+
|
|
56
78
|
__wogi_use_expect=0
|
|
57
|
-
if [ "$
|
|
58
|
-
|
|
79
|
+
if [ "$__wogi_wants_expect" -eq 1 ]; then
|
|
80
|
+
# The dialog only fires when --dangerously-load-development-channels is in
|
|
81
|
+
# argv; skip the expect dance otherwise.
|
|
82
|
+
__wogi_has_flag=0
|
|
59
83
|
for arg in "$@"; do
|
|
60
84
|
if [ "$arg" = "--dangerously-load-development-channels" ]; then
|
|
61
|
-
|
|
85
|
+
__wogi_has_flag=1
|
|
62
86
|
break
|
|
63
87
|
fi
|
|
64
88
|
done
|
|
89
|
+
if [ "$__wogi_has_flag" -eq 1 ]; then
|
|
90
|
+
if command -v expect >/dev/null 2>&1 && [ -x "$WOGI_EXPECT_SCRIPT" ]; then
|
|
91
|
+
__wogi_use_expect=1
|
|
92
|
+
if [ "$__wogi_is_worker" -eq 1 ]; then
|
|
93
|
+
echo "[wogi-claude] worker mode detected — auto-enabled expect-based dialog dismissal" >&2
|
|
94
|
+
fi
|
|
95
|
+
elif [ "$__wogi_is_worker" -eq 1 ]; then
|
|
96
|
+
# Headless worker + missing expect = the dialog WILL deadlock this
|
|
97
|
+
# worker on restart. Warn loudly so the operator can install expect,
|
|
98
|
+
# but still start claude (better than failing the worker outright).
|
|
99
|
+
echo "[wogi-claude] WARNING: worker mode detected (repo '${WOGI_REPO_NAME}') but 'expect' is not installed." >&2
|
|
100
|
+
echo "[wogi-claude] The --dangerously-load-development-channels dialog will block this worker on the next restart." >&2
|
|
101
|
+
echo "[wogi-claude] Install expect to enable headless auto-dismiss:" >&2
|
|
102
|
+
echo "[wogi-claude] macOS: brew install expect" >&2
|
|
103
|
+
echo "[wogi-claude] Debian/Ubuntu: apt install expect" >&2
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
65
106
|
fi
|
|
66
107
|
|
|
67
108
|
# run_claude — invoke claude, routing through expect when we can auto-dismiss
|
|
@@ -8,21 +8,38 @@
|
|
|
8
8
|
# - The CLI is launched with --dangerously-load-development-channels
|
|
9
9
|
#
|
|
10
10
|
# There's no Claude Code setting that persists an "accepted" state
|
|
11
|
-
# (verified via decompiled source 2026-04-17)
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# (verified via decompiled source 2026-04-17) and no --accept-dev-channels
|
|
12
|
+
# flag exists in `claude --help`.
|
|
13
|
+
#
|
|
14
|
+
# v2.22.x implementation (DEPRECATED): `-re "Loading development channels"`
|
|
15
|
+
# matched per-chunk output. Ink paints the dialog in fragmented writes
|
|
16
|
+
# interleaved with ANSI color codes, so the literal phrase rarely arrived
|
|
17
|
+
# in a single buffer and the regex missed. Workers deadlocked.
|
|
18
|
+
#
|
|
19
|
+
# v2.26.2 rewrite (this file) solves it with three properties:
|
|
20
|
+
# 1. Rolling buffer — accumulate every chunk into one growing string,
|
|
21
|
+
# match against the whole buffer on each iteration, not per-chunk.
|
|
22
|
+
# 2. ANSI strip — remove CSI / OSC escape sequences before matching,
|
|
23
|
+
# so the color-interleaved Ink output normalizes to plain text.
|
|
24
|
+
# 3. Bounded elapsed-time window — stop accumulating after
|
|
25
|
+
# WOGI_EXPECT_TIMEOUT seconds (default 30). Without this, a very
|
|
26
|
+
# chatty claude startup with no dialog would keep exp_continue'ing
|
|
27
|
+
# forever until the per-iteration timeout.
|
|
28
|
+
#
|
|
29
|
+
# NO BLIND FALLBACK. If the window elapses without matching, we hand off
|
|
30
|
+
# to interact unchanged. Sending a speculative \r to claude in server:
|
|
31
|
+
# mode mid-startup is not safe (server-mode input handling is not a
|
|
32
|
+
# standard REPL and could corrupt the handshake). Miss = same failure
|
|
33
|
+
# mode as running claude without this wrapper. Worker retries via
|
|
34
|
+
# wogi-claude's restart loop.
|
|
16
35
|
#
|
|
17
36
|
# Usage (invoked from lib/wogi-claude):
|
|
18
37
|
# expect wogi-claude-expect.exp /absolute/path/to/claude [args...]
|
|
19
38
|
#
|
|
20
39
|
# Disable at runtime: set WOGI_NO_EXPECT=1. The wrapper then execs claude
|
|
21
|
-
# directly and the user sees the dialog as before
|
|
40
|
+
# directly and the user sees the dialog as before.
|
|
22
41
|
|
|
23
42
|
set timeout 30
|
|
24
|
-
|
|
25
|
-
# Allow WOGI_EXPECT_TIMEOUT override (rarely needed)
|
|
26
43
|
if {[info exists env(WOGI_EXPECT_TIMEOUT)]} {
|
|
27
44
|
set timeout $env(WOGI_EXPECT_TIMEOUT)
|
|
28
45
|
}
|
|
@@ -40,34 +57,84 @@ set claude_bin [lindex $argv 0]
|
|
|
40
57
|
set claude_args [lrange $argv 1 end]
|
|
41
58
|
|
|
42
59
|
# Spawn claude in a pseudo-TTY so its Ink UI renders normally.
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
60
|
+
# Use {*} list-splice rather than `eval spawn` — `eval` reparses its
|
|
61
|
+
# arguments as Tcl script, which lets an argument containing bracket syntax
|
|
62
|
+
# (e.g. `[exec attacker-cmd]`) escape to command execution. The splice form
|
|
63
|
+
# expands the list without reparsing.
|
|
64
|
+
spawn $claude_bin {*}$claude_args
|
|
46
65
|
|
|
47
|
-
#
|
|
48
|
-
# the default-highlighted "I am using this for local development" option.
|
|
66
|
+
# --- Dialog dismissal watch ---
|
|
49
67
|
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
68
|
+
# dialog_buf accumulates raw stdout chunks. After each chunk we strip ANSI
|
|
69
|
+
# escapes into `plain` and substring-search for the dialog title text. If
|
|
70
|
+
# found, we send Enter (accepts the default-highlighted "I am using this
|
|
71
|
+
# for local development" option). Otherwise we exp_continue until either
|
|
72
|
+
# the total elapsed time exceeds $timeout or EOF.
|
|
73
|
+
set dialog_buf ""
|
|
74
|
+
set start_ts [clock seconds]
|
|
75
|
+
|
|
53
76
|
expect {
|
|
54
|
-
-re "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
-re "(.+)" {
|
|
78
|
+
append dialog_buf $expect_out(1,string)
|
|
79
|
+
|
|
80
|
+
# Strip ANSI CSI sequences (colors, cursor moves): ESC [ ... letter
|
|
81
|
+
regsub -all {\x1b\[[0-9;?]*[a-zA-Z]} $dialog_buf "" plain
|
|
82
|
+
# Strip 8-bit CSI form (0x9B byte instead of ESC [), same terminator
|
|
83
|
+
regsub -all {\x9b[0-9;?]*[a-zA-Z]} $plain "" plain
|
|
84
|
+
# Strip OSC sequences (titles, hyperlinks): ESC ] ... BEL
|
|
85
|
+
regsub -all {\x1b\][^\x07]*\x07} $plain "" plain
|
|
86
|
+
# Strip ISO 2022 charset-selection sequences: ESC ( B, ESC ) 0, etc.
|
|
87
|
+
regsub -all {\x1b[\(\)\*\+\-\.\/][\x20-\x7e]} $plain "" plain
|
|
88
|
+
# Strip bare ESC that didn't belong to a recognized sequence
|
|
89
|
+
regsub -all {\x1b} $plain "" plain
|
|
90
|
+
|
|
91
|
+
if {[string first "Loading development channels" $plain] >= 0} {
|
|
92
|
+
# Let Ink finish rendering the select-input component before
|
|
93
|
+
# sending Enter — without this the keystroke can land before
|
|
94
|
+
# the keyboard listener binds and gets dropped.
|
|
95
|
+
after 250
|
|
96
|
+
send "\r"
|
|
97
|
+
# Fall through to interact — dialog is dismissed, user drives
|
|
98
|
+
# from here.
|
|
99
|
+
} else {
|
|
100
|
+
# Bound total accumulation by elapsed wall-clock time so we
|
|
101
|
+
# don't exp_continue forever on a chatty startup with no
|
|
102
|
+
# dialog.
|
|
103
|
+
set elapsed [expr {[clock seconds] - $start_ts}]
|
|
104
|
+
if {$elapsed < $timeout} {
|
|
105
|
+
# Cap buffer at 64KB to prevent runaway memory on a very
|
|
106
|
+
# long startup that never shows the dialog. Keep the tail
|
|
107
|
+
# half so any late-arriving title text still matches.
|
|
108
|
+
if {[string length $dialog_buf] > 65536} {
|
|
109
|
+
set dialog_buf [string range $dialog_buf 32768 end]
|
|
110
|
+
}
|
|
111
|
+
exp_continue
|
|
112
|
+
}
|
|
113
|
+
# Elapsed >= timeout: fall through to interact without
|
|
114
|
+
# dismissing. Same failure mode as running claude without
|
|
115
|
+
# this wrapper.
|
|
116
|
+
}
|
|
60
117
|
}
|
|
61
118
|
timeout { }
|
|
62
119
|
eof { exit }
|
|
63
120
|
}
|
|
64
121
|
|
|
65
|
-
# Hand off: user's keystrokes flow to claude, claude's output flows
|
|
66
|
-
#
|
|
67
|
-
|
|
122
|
+
# Hand off: user's keystrokes flow to claude, claude's output flows to
|
|
123
|
+
# the user's terminal. interact blocks until claude exits.
|
|
124
|
+
#
|
|
125
|
+
# Test hook: set WOGI_EXPECT_NO_INTERACT=1 to substitute `expect eof`.
|
|
126
|
+
# `interact` requires a real TTY on stdin; under node:test / CI with
|
|
127
|
+
# pipe-backed stdin it closes the PTY before our sent \r flushes to the
|
|
128
|
+
# child. The test harness sets this env var so the behavioral tests (dialog
|
|
129
|
+
# dismissal, ANSI fragmentation) can actually observe the child receiving
|
|
130
|
+
# the keystroke. Production callers MUST NOT set this — users need
|
|
131
|
+
# interact to drive claude after the dialog dismisses.
|
|
132
|
+
if {[info exists env(WOGI_EXPECT_NO_INTERACT)] && $env(WOGI_EXPECT_NO_INTERACT) eq "1"} {
|
|
133
|
+
expect eof
|
|
134
|
+
} else {
|
|
135
|
+
interact
|
|
136
|
+
}
|
|
68
137
|
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# after `interact`, but a plain exit is sufficient since the wrapper
|
|
72
|
-
# only cares about the restart flag file, not exit code).
|
|
138
|
+
# Pass claude's exit status — wrapper cares about the restart flag file,
|
|
139
|
+
# not exit code, so a plain exit suffices.
|
|
73
140
|
exit
|