agents_control 0.2.1 → 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 +8 -0
- data/lib/agents_control/channels/telegram/router.rb +66 -14
- 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
|
@@ -126,6 +126,14 @@ shell command that finishes in a couple of seconds, so there's no
|
|
|
126
126
|
telling Telegram when it's actually done or needs something, the same
|
|
127
127
|
as replying to one of its own questions.
|
|
128
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.
|
|
136
|
+
|
|
129
137
|
The result stays a live target: replying to it — "y", "n", anything —
|
|
130
138
|
types straight into that same pane and shows what came back, so
|
|
131
139
|
something like `git add -p`'s hunk-by-hunk prompts works as an actual
|
|
@@ -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,19 +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
|
-
|
|
204
|
-
# couple of seconds — it's a real task, and hooks already
|
|
205
|
-
# own telling Telegram when it's actually done or needs
|
|
206
|
-
# something. Capturing a "result" a moment after typing
|
|
207
|
-
# would just catch it mid-thought and fall back to
|
|
208
|
-
# dumping its whole transcript, which isn't a result at
|
|
209
|
-
# all — same reasoning as replying to an agent's own
|
|
210
|
-
# question (type_into_session).
|
|
211
|
-
execute(chat_id, session, command, show_result: !session.agent?)
|
|
233
|
+
execute(chat_id, session, command, show_result: true, before: before)
|
|
212
234
|
end
|
|
213
235
|
end
|
|
214
236
|
end
|
|
@@ -219,6 +241,21 @@ module AgentsControl
|
|
|
219
241
|
REMOTE_COMMANDS.include?(session.foreground_command.to_s)
|
|
220
242
|
end
|
|
221
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
|
+
|
|
222
259
|
def confirm_remote(chat_id, session, command)
|
|
223
260
|
key = @store.put({ "action" => "run", "session_id" => session.id, "text" => command },
|
|
224
261
|
ttl: 300)
|
|
@@ -233,6 +270,21 @@ module AgentsControl
|
|
|
233
270
|
markup: markup)
|
|
234
271
|
end
|
|
235
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
|
+
|
|
236
288
|
# Enter is sent as a separate call, not tacked onto the same
|
|
237
289
|
# input: a merged call can fail to send multi-line text at all.
|
|
238
290
|
TYPING_PAUSE = 0.4
|
|
@@ -249,9 +301,9 @@ module AgentsControl
|
|
|
249
301
|
|
|
250
302
|
def run_result_lines = @config.get("terminal.run_result_lines", 200)
|
|
251
303
|
|
|
252
|
-
def execute(chat_id, session, command, show_result: false)
|
|
304
|
+
def execute(chat_id, session, command, show_result: false, before: nil)
|
|
253
305
|
backend = @registry.backend_for(session)
|
|
254
|
-
before
|
|
306
|
+
before ||= capture_screen(session, lines: run_result_lines) if show_result
|
|
255
307
|
|
|
256
308
|
ok = backend.send_text(session.id, command, newline: false) &&
|
|
257
309
|
sleep(TYPING_PAUSE).then { backend.send_text(session.id, "", newline: true) }
|