agents_control 0.2.0 → 0.2.3
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.
- checksums.yaml +4 -4
- data/README.md +29 -14
- data/lib/agents_control/channels/telegram/router.rb +67 -7
- data/lib/agents_control/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aadeb50868f7e15b7da9346e051f268ffad1c652c1e7297d54cb328380b030f6
|
|
4
|
+
data.tar.gz: cf62c6519dfd54bb7b9ee169c5e1b58f5bcf92b54993e5a61e954547180b6ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dce0f05882260d26dddc1a65f392ef0cf8ad7351b1ccbd07e29d2378c2871077434669e4f3c7248b682fd10aa72aedbfbbfae5739703222040bd3741e9a2be4a
|
|
7
|
+
data.tar.gz: f3e61a373a6252b920e6086b9e9b01591855fecc8708ffd059e0f87d412fce749d97cc445a6553aa42d69e404c60973ab1eb6551c2db60ae3362cce2756964f7
|
data/README.md
CHANGED
|
@@ -104,20 +104,35 @@ Bot commands:
|
|
|
104
104
|
|
|
105
105
|
The number `N` comes from the last list shown.
|
|
106
106
|
|
|
107
|
-
`/run` treats the command itself as the reference
|
|
108
|
-
echoes back whatever's typed, so it looks for that
|
|
109
|
-
screen and shows from there — sharper than diffing
|
|
110
|
-
it still works even if something else wrote to the
|
|
111
|
-
between, since it doesn't need the screen from right
|
|
112
|
-
relate to the screen after at all. A short reply
|
|
113
|
-
add -p`) isn't a safe anchor on its own — too
|
|
114
|
-
something unrelated — so those fall back to a
|
|
115
|
-
instead, and when even that can't cleanly tell
|
|
116
|
-
current screen outright: seeing the result,
|
|
117
|
-
stale context around it, beats not seeing it
|
|
118
|
-
captured either way is a `/settings` option
|
|
119
|
-
200 by default) — a command whose output
|
|
120
|
-
last N lines, same as the default gets cut
|
|
107
|
+
Against a plain tab, `/run` treats the command itself as the reference
|
|
108
|
+
point: the shell echoes back whatever's typed, so it looks for that
|
|
109
|
+
exact text on screen and shows from there — sharper than diffing
|
|
110
|
+
screenshots, and it still works even if something else wrote to the
|
|
111
|
+
same tab in between, since it doesn't need the screen from right
|
|
112
|
+
before typing to relate to the screen after at all. A short reply
|
|
113
|
+
("y", "n" mid `git add -p`) isn't a safe anchor on its own — too
|
|
114
|
+
likely to match something unrelated — so those fall back to a
|
|
115
|
+
before/after diff instead, and when even that can't cleanly tell
|
|
116
|
+
what's new, to the current screen outright: seeing the result,
|
|
117
|
+
possibly with a little stale context around it, beats not seeing it
|
|
118
|
+
at all. How much gets captured either way is a `/settings` option
|
|
119
|
+
(`terminal.run_result_lines`, 200 by default) — a command whose output
|
|
120
|
+
runs longer just gets its last N lines, same as the default gets cut
|
|
121
|
+
by a screen that's too tall.
|
|
122
|
+
|
|
123
|
+
Against an agent, `/run` just confirms the send — an agent isn't a
|
|
124
|
+
shell command that finishes in a couple of seconds, so there's no
|
|
125
|
+
"result" to capture yet by the time it would look. Hooks already own
|
|
126
|
+
telling Telegram when it's actually done or needs something, the same
|
|
127
|
+
as replying to one of its own questions.
|
|
128
|
+
|
|
129
|
+
A tab stuck inside `less`, `vim`, a REPL, or anything else that reads
|
|
130
|
+
keystrokes as its own input rather than a line to submit gets a
|
|
131
|
+
confirmation first instead of a blind send — the text would go to
|
|
132
|
+
whatever's actually running there, not run as a command. A pager not
|
|
133
|
+
on that recognized list still gets caught: a bare `:` as the entire
|
|
134
|
+
last line is the one thing practically every pager agrees on for
|
|
135
|
+
"waiting on you," and a real shell prompt never looks like that.
|
|
121
136
|
|
|
122
137
|
The result stays a live target: replying to it — "y", "n", anything —
|
|
123
138
|
types straight into that same pane and shows what came back, so
|
|
@@ -14,6 +14,18 @@ module AgentsControl
|
|
|
14
14
|
# they get a separate confirmation.
|
|
15
15
|
REMOTE_COMMANDS = %w[ssh mosh].freeze
|
|
16
16
|
|
|
17
|
+
# Programs that read keystrokes as their own input, not as a
|
|
18
|
+
# line to submit to a shell. Sending a command into one of
|
|
19
|
+
# these wouldn't run it — it'd feed the letters to whatever's
|
|
20
|
+
# already there instead: pager navigation, an editor's insert
|
|
21
|
+
# mode, a REPL evaluating each character-by-character. Confirmed
|
|
22
|
+
# against a real incident: `git log`'s pager was still open,
|
|
23
|
+
# "git status" went in as keystrokes, and nothing about it ran
|
|
24
|
+
# as a command at all.
|
|
25
|
+
INTERACTIVE_COMMANDS = %w[less more most vim vi nvim emacs nano pico
|
|
26
|
+
man top htop irb pry python python3 node
|
|
27
|
+
mysql psql sqlite3].freeze
|
|
28
|
+
|
|
17
29
|
# Shared list for the Telegram menu and for /help: a menu that's
|
|
18
30
|
# drifted from reality is worse than no menu. The third element
|
|
19
31
|
# is the argument syntax, needed only in /help; setMyCommands
|
|
@@ -196,11 +208,29 @@ module AgentsControl
|
|
|
196
208
|
|
|
197
209
|
with_session(chat_id, number) do |session|
|
|
198
210
|
next say(chat_id, "This session has no terminal — nothing to run there.") if session.terminalless?
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
next confirm_remote(chat_id, session, command) if remote?(session)
|
|
212
|
+
|
|
213
|
+
# An agent isn't a shell command that finishes in a couple
|
|
214
|
+
# of seconds — it's a real task, and hooks already own
|
|
215
|
+
# telling Telegram when it's actually done or needs
|
|
216
|
+
# something. Capturing a "result" a moment after typing
|
|
217
|
+
# would just catch it mid-thought and fall back to dumping
|
|
218
|
+
# its whole transcript, which isn't a result at all — same
|
|
219
|
+
# reasoning as replying to an agent's own question
|
|
220
|
+
# (type_into_session).
|
|
221
|
+
next execute(chat_id, session, command, show_result: false) if session.agent?
|
|
222
|
+
next confirm_interactive(chat_id, session, command) if interactive?(session)
|
|
223
|
+
|
|
224
|
+
# One capture serves two purposes: it's checked for a
|
|
225
|
+
# pager's bare `:` prompt right here, and reused as
|
|
226
|
+
# execute()'s own "before" snapshot if it turns out clean —
|
|
227
|
+
# capturing twice would desync anything that reads the
|
|
228
|
+
# screen expecting to see it change between calls.
|
|
229
|
+
before = capture_screen(session, lines: run_result_lines)
|
|
230
|
+
if paused_for_input?(before)
|
|
231
|
+
confirm_interactive(chat_id, session, command)
|
|
202
232
|
else
|
|
203
|
-
execute(chat_id, session, command, show_result: true)
|
|
233
|
+
execute(chat_id, session, command, show_result: true, before: before)
|
|
204
234
|
end
|
|
205
235
|
end
|
|
206
236
|
end
|
|
@@ -211,6 +241,21 @@ module AgentsControl
|
|
|
211
241
|
REMOTE_COMMANDS.include?(session.foreground_command.to_s)
|
|
212
242
|
end
|
|
213
243
|
|
|
244
|
+
def interactive?(session)
|
|
245
|
+
INTERACTIVE_COMMANDS.include?(session.foreground_command.to_s)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Backstop for interactive? missing a pager by name — a custom
|
|
249
|
+
# $PAGER, or anything else not on that list. A bare `:` as the
|
|
250
|
+
# entire last line is the one thing practically every pager in
|
|
251
|
+
# the less/more lineage agrees on for "waiting on you"; a real
|
|
252
|
+
# shell prompt always has more on that line than a single
|
|
253
|
+
# colon. Takes the screen rather than the session so the
|
|
254
|
+
# caller's own capture can be reused instead of taking another.
|
|
255
|
+
def paused_for_input?(screen)
|
|
256
|
+
screen.to_s.rstrip.lines.last.to_s.strip == ":"
|
|
257
|
+
end
|
|
258
|
+
|
|
214
259
|
def confirm_remote(chat_id, session, command)
|
|
215
260
|
key = @store.put({ "action" => "run", "session_id" => session.id, "text" => command },
|
|
216
261
|
ttl: 300)
|
|
@@ -225,6 +270,21 @@ module AgentsControl
|
|
|
225
270
|
markup: markup)
|
|
226
271
|
end
|
|
227
272
|
|
|
273
|
+
def confirm_interactive(chat_id, session, command)
|
|
274
|
+
key = @store.put({ "action" => "run", "session_id" => session.id, "text" => command },
|
|
275
|
+
ttl: 300)
|
|
276
|
+
|
|
277
|
+
markup = { inline_keyboard: [[
|
|
278
|
+
{ text: "⚠️ Send anyway", callback_data: key },
|
|
279
|
+
{ text: "cancel", callback_data: @store.put({ "action" => "cancel" }, ttl: 300) }
|
|
280
|
+
]] }
|
|
281
|
+
|
|
282
|
+
say(chat_id, "#{session.label} is inside #{session.foreground_command} right now, " \
|
|
283
|
+
"not at a shell prompt — the text would go to that, not run as a command:\n\n" \
|
|
284
|
+
"`#{command}`",
|
|
285
|
+
markup: markup)
|
|
286
|
+
end
|
|
287
|
+
|
|
228
288
|
# Enter is sent as a separate call, not tacked onto the same
|
|
229
289
|
# input: a merged call can fail to send multi-line text at all.
|
|
230
290
|
TYPING_PAUSE = 0.4
|
|
@@ -241,9 +301,9 @@ module AgentsControl
|
|
|
241
301
|
|
|
242
302
|
def run_result_lines = @config.get("terminal.run_result_lines", 200)
|
|
243
303
|
|
|
244
|
-
def execute(chat_id, session, command, show_result: false)
|
|
304
|
+
def execute(chat_id, session, command, show_result: false, before: nil)
|
|
245
305
|
backend = @registry.backend_for(session)
|
|
246
|
-
before
|
|
306
|
+
before ||= capture_screen(session, lines: run_result_lines) if show_result
|
|
247
307
|
|
|
248
308
|
ok = backend.send_text(session.id, command, newline: false) &&
|
|
249
309
|
sleep(TYPING_PAUSE).then { backend.send_text(session.id, "", newline: true) }
|
|
@@ -514,7 +574,7 @@ module AgentsControl
|
|
|
514
574
|
case payload["action"]
|
|
515
575
|
when "focus" then focus_session(chat_id, session)
|
|
516
576
|
when "screen" then show_screen(chat_id, session)
|
|
517
|
-
when "run" then execute(chat_id, session, payload["text"], show_result:
|
|
577
|
+
when "run" then execute(chat_id, session, payload["text"], show_result: !session.agent?)
|
|
518
578
|
when "close_confirm" then say(chat_id, "Close #{session.label}?",
|
|
519
579
|
markup: @keyboards.confirm("close", session))
|
|
520
580
|
when "close" then close_session(chat_id, session)
|