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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ca83918e01b4dc98c7b72f17da5cd31d859d863d61614f7eee74dd01e62ecb2
4
- data.tar.gz: d50f30441533aded38f243800dda29a884fd01fda135506dee034115eeb63e7a
3
+ metadata.gz: aadeb50868f7e15b7da9346e051f268ffad1c652c1e7297d54cb328380b030f6
4
+ data.tar.gz: cf62c6519dfd54bb7b9ee169c5e1b58f5bcf92b54993e5a61e954547180b6ec2
5
5
  SHA512:
6
- metadata.gz: 9dd870ff93dd579dd1bbbc7cffd7e9f62d7a5d25ba79d2f69adb6018ac1ae8061614b5e1d09db5bc817d65f8f6a33fc7557b8a6a035245d640e92373bcdc598d
7
- data.tar.gz: 71c5a463b794b2196fa3023a0a703ab6cd02da596f2b1f003ea1365db96f9a67e8d3ab3e1ddc51bdd470d76217395907565a2d53ef65dccf6fc0b411c04dee0a
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
- if remote?(session)
201
- confirm_remote(chat_id, session, command)
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
- # An agent isn't a shell command that finishes in a
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 = show_result ? capture_screen(session, lines: run_result_lines) : nil
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) }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentsControl
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agents_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sapar Kurmanov