agents_control 0.1.0 → 0.2.0

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: 7396050fbeeb69af3c8fd481608facce44bce68072dc043f03fa0bc34138de5b
4
- data.tar.gz: '05597674bc05d4bb1142e1295c264f737caf7be5afee724616ec4e0673db3870'
3
+ metadata.gz: b71d165050803893091b3bce0857cb4a9cac7875e28289e0aaebc6bf4a1aa820
4
+ data.tar.gz: a301e0e972625e0eb57a79e80799e21edf2232d24ef8bbbfb74840484d1feb7e
5
5
  SHA512:
6
- metadata.gz: efe615074ece6b5898eecaaa66fdc882c46d2b6a74c715b18768b347ab3af3ef180774930dc46d5648461a006b96059bd265c67f676e45ed2eadcc0017bc6ee2
7
- data.tar.gz: 6137a2654888b576860d1a8463e5aff520ace959c687974e6bd20e3425ba40f98673391517c30dc7d1c54a58bbbbad070aa2253891d6553458f774354c9c6543
6
+ metadata.gz: b6a5dd4eb521026c1084ade954fbe7d7a8f26426e3d68d78077a3d69e71addc0c40165181eaaff963a225434a71167e0ed02f68382fe8ed4605a46f8e781bc54
7
+ data.tar.gz: 7077e2aa298e86630f4c619e67a6bde0be85a2316f86c43b46dbd354353bc9404f91a0db8aa86ab9f8fcf1f0c3d6e8cee101c1b55cbf53080810f52d2ba37df1
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # agents_control
2
2
 
3
+ [![Gem Version](https://img.shields.io/gem/v/agents_control)](https://rubygems.org/gems/agents_control)
4
+
3
5
  A remote for iTerm2 and its AI agents (Claude Code, Codex) from Telegram:
4
6
  tab list, commands, screen, new sessions. For Claude Code — also the
5
7
  agent's questions and permission requests: it stops, buttons show up in
@@ -26,15 +28,15 @@ outside: the port only binds to 127.0.0.1, there are no incoming connections.
26
28
  On macOS, terminal control (sending commands, reading the screen,
27
29
  creating tabs) works through iTerm2 or tmux — Terminal.app isn't
28
30
  supported. Notifications about questions and permissions aren't tied to
29
- a terminal at all: their source is the agent's hooks, which work
31
+ a terminal at all: their source is Claude Code's hooks, which work
30
32
  everywhere, including sessions with no terminal (a VS Code session, for
31
- instance), but for now only for Claude Code.
33
+ instance).
32
34
 
33
35
  ## Why
34
36
 
35
37
  You need to run a command in a session, check the screen, or switch to
36
38
  a tab, and you're not at the computer — now you can do that from
37
- Telegram. If the session is Claude Code, there's a bonus too: the agent
39
+ Telegram. If the session is Claude Code, there's a bonus too: it
38
40
  stops and waits for an answer — the question and its buttons arrive in
39
41
  Telegram, no need to go home just to say "continue."
40
42
 
@@ -59,6 +61,12 @@ cd agents_control
59
61
  bundle install
60
62
  ```
61
63
 
64
+ Or as a RubyGem:
65
+
66
+ ```sh
67
+ gem install agents_control
68
+ ```
69
+
62
70
  Or straight from Claude Code, as a plugin — the repo doubles as a marketplace:
63
71
 
64
72
  ```
@@ -69,31 +77,7 @@ Or straight from Claude Code, as a plugin — the repo doubles as a marketplace:
69
77
  The plugin doesn't replace the install above — it's just a way to find
70
78
  the tool and get install instructions without leaving Claude Code.
71
79
 
72
- ## Usage
73
-
74
- ```sh
75
- agents_control # opens the console and stays in the tab
76
- ```
77
-
78
- The tool lives in a tab: while it's open, it listens to Telegram and
79
- receives agent events. Commands inside start with a slash, same as the bot's:
80
-
81
- ```
82
- > /sessions sessions with a live agent
83
- > /tabs all terminal tabs
84
- > /away intercept agent questions (before stepping out)
85
- > /settings settings; /settings away — toggle
86
- > /doctor check that everything is in place
87
- > /quit quit
88
- ```
89
-
90
- State icons: `⏳` working · `▸` at a shell prompt · `🖥` no terminal
91
- (VS Code) · `·` everything else.
92
-
93
- One-off commands exist too — `agents_control sessions`, `doctor`,
94
- `daemon` — but the normal way to run it is an open console.
95
-
96
- ### Telegram
80
+ ## Telegram
97
81
 
98
82
  Create a bot with [@BotFather](https://t.me/BotFather) and run the wizard:
99
83
 
@@ -113,13 +97,38 @@ Bot commands:
113
97
  | `/tabs` | all terminal tabs |
114
98
  | `/screen N` | show a tab's screen |
115
99
  | `/focus N` | switch to a tab |
116
- | `/run N command` | run a command in a tab |
100
+ | `/run N command` | run a command in a tab and show the result |
117
101
  | `/new [directory]` | create a tab |
118
102
  | `/away` | intercept agent questions |
119
103
  | `/status` | current status |
120
104
 
121
105
  The number `N` comes from the last list shown.
122
106
 
107
+ `/run` treats the command itself as the reference point: the shell
108
+ echoes back whatever's typed, so it looks for that exact text on
109
+ screen and shows from there — sharper than diffing screenshots, and
110
+ it still works even if something else wrote to the same tab in
111
+ between, since it doesn't need the screen from right before typing to
112
+ relate to the screen after at all. A short reply ("y", "n" mid `git
113
+ add -p`) isn't a safe anchor on its own — too likely to match
114
+ something unrelated — so those fall back to a before/after diff
115
+ instead, and when even that can't cleanly tell what's new, to the
116
+ current screen outright: seeing the result, possibly with a little
117
+ stale context around it, beats not seeing it at all. How much gets
118
+ captured either way is a `/settings` option (`terminal.run_result_lines`,
119
+ 200 by default) — a command whose output runs longer just gets its
120
+ last N lines, same as the default gets cut by a screen that's too tall.
121
+
122
+ The result stays a live target: replying to it — "y", "n", anything —
123
+ types straight into that same pane and shows what came back, so
124
+ something like `git add -p`'s hunk-by-hunk prompts works as an actual
125
+ back-and-forth over Telegram, not a one-shot fire-and-forget.
126
+
127
+ Both `/run` and `/screen` head their reply with the tab's label and
128
+ tty (`valkyrie · ttys017`) — several tabs can share a label when
129
+ they're open on the same project, and the tty is what actually tells
130
+ them apart.
131
+
123
132
  This list also populates Telegram's own `/` command menu automatically
124
133
  — `setup` and every daemon start publish it via the Bot API, no manual
125
134
  BotFather step needed. If the menu still shows only `/start` after
@@ -129,15 +138,16 @@ to force it to refresh.
129
138
 
130
139
  ### Two modes
131
140
 
132
- While you're at the keyboard, intercepting agent questions is
133
- counterproductive: you'll answer in the terminal faster than you can
134
- reach for your phone, and a blocked hook keeps the dialog from ever
135
- appearing on screen. So there are two modes:
141
+ This whole section is about Claude Code specifically — its hooks are
142
+ what makes any of it possible. While you're at the keyboard,
143
+ intercepting its questions is counterproductive: you'll answer in the
144
+ terminal faster than you can reach for your phone, and a blocked hook
145
+ keeps the dialog from ever appearing on screen. So there are two modes:
136
146
 
137
147
  - **present** (default) — questions are mirrored to Telegram but stay
138
148
  in the terminal;
139
149
  - **away** (`/away`) — a question arrives with buttons and waits for a
140
- reply; the agent stands by until you answer or time runs out.
150
+ reply; Claude Code stands by until you answer or time runs out.
141
151
 
142
152
  A question can also be answered by replying directly to the message —
143
153
  it goes to the right session, even with several tabs open.
@@ -159,14 +169,14 @@ the wrong place.
159
169
 
160
170
  A "continue" reply is sent automatically, but tool permissions aren't.
161
171
  These are two independent settings on purpose: merged into one, they'd
162
- produce an agent that approves itself everything while nobody's
163
- watching. A question that offers a choice ("rewrite it or leave it?")
164
- is never answered automatically, even if it contains the word "continue."
172
+ let it approve itself everything while nobody's watching. A question
173
+ that offers a choice ("rewrite it or leave it?") is never answered
174
+ automatically, even if it contains the word "continue."
165
175
 
166
176
  ### Hooks
167
177
 
168
178
  The daemon connects hooks on start and removes them on stop — otherwise
169
- the agent prints a warning about an unreachable address in every
179
+ Claude Code prints a warning about an unreachable address in every
170
180
  session. If the daemon crashed and the hooks are still there:
171
181
 
172
182
  ```sh
@@ -177,6 +187,30 @@ agents_control hooks uninstall
177
187
  Entries in `~/.claude/settings.json` are tagged, and other settings
178
188
  aren't touched: installing and removing return the file to exactly its original shape.
179
189
 
190
+ ## Usage
191
+
192
+ ```sh
193
+ agents_control # opens the console and stays in the tab
194
+ ```
195
+
196
+ The tool lives in a tab: while it's open, it listens to Telegram and
197
+ receives agent events. Commands inside start with a slash, same as the bot's:
198
+
199
+ ```
200
+ > /sessions sessions with a live agent
201
+ > /tabs all terminal tabs
202
+ > /away intercept agent questions (before stepping out)
203
+ > /settings settings; /settings away — toggle
204
+ > /doctor check that everything is in place
205
+ > /quit quit
206
+ ```
207
+
208
+ State icons: `⏳` working · `▸` at a shell prompt · `🖥` no terminal
209
+ (VS Code) · `·` everything else.
210
+
211
+ One-off commands exist too — `agents_control sessions`, `doctor`,
212
+ `daemon` — but the normal way to run it is an open console.
213
+
180
214
  ## Rate-limit anchors
181
215
 
182
216
  A five-hour window starts at the minute of the first message and
@@ -199,7 +233,7 @@ catches this and suggests `pmset repeat wakeorpoweron`.
199
233
 
200
234
  ## Watchers
201
235
 
202
- Hooks see the agent's own decisions, but not everything: the CLI's own
236
+ Hooks see Claude Code's own decisions, but not everything: the CLI's own
203
237
  local menus (model switch, folder trust) and text on screen (a
204
238
  rate-limit message) aren't covered by hooks at all — these events never
205
239
  produce a single hook call. Two independent watchers handle them,
@@ -20,6 +20,15 @@ module AgentsControl
20
20
  # working remote control.
21
21
  ACTION_TTL = 3600
22
22
 
23
+ # Numbers, unlike buttons, are typed by hand into later messages
24
+ # — /run N doesn't stay tied to the message the list came in on,
25
+ # so it has to survive much longer than a single quick trip out.
26
+ # A closed session is still handled gracefully regardless of how
27
+ # long the mapping lives (find just returns nil), so the only
28
+ # cost of a longer TTL is a number eventually pointing at a tab
29
+ # that's since closed — a clear, expected error either way.
30
+ LIST_TTL = 86_400
31
+
23
32
  def initialize(store:)
24
33
  @store = store
25
34
  end
@@ -78,7 +87,7 @@ module AgentsControl
78
87
  # land on the same row in a shorter, filtered list.
79
88
  def render_list(sessions, chat_id:, title:, universe: sessions)
80
89
  numbers = numbered(universe)
81
- @store.put(numbers.invert, ttl: ACTION_TTL, key: index_key(chat_id))
90
+ @store.put(numbers.invert, ttl: LIST_TTL, key: index_key(chat_id))
82
91
 
83
92
  lines = sessions.map do |session|
84
93
  "#{numbers[session.id].to_s.rjust(2)}. #{marker(session)} #{describe(session)}"
@@ -35,13 +35,14 @@ module AgentsControl
35
35
  HELP = (["Commands:", ""] +
36
36
  COMMANDS.map { |name, text, hint| "/#{name}#{hint ? " #{hint}" : ''} — #{text}" }).join("\n")
37
37
 
38
- def initialize(api:, registry:, store:, config:, keyboards: nil, pending: nil)
38
+ def initialize(api:, registry:, store:, config:, keyboards: nil, pending: nil, logger: nil)
39
39
  @api = api
40
40
  @registry = registry
41
41
  @store = store
42
42
  @config = config
43
43
  @keyboards = keyboards || Keyboards.new(store: store)
44
44
  @pending = pending
45
+ @logger = logger
45
46
  end
46
47
 
47
48
  def handle(update)
@@ -95,17 +96,25 @@ module AgentsControl
95
96
  end
96
97
 
97
98
  # First, the exact session by session_id — unambiguous even when
98
- # several tabs share one cwd. Matching by cwd is the fallback for
99
- # when that tab has already closed.
99
+ # several tabs share one cwd, and works for any session type:
100
+ # /run and /screen remember the terminal's own session id, not
101
+ # an agent's hook id, so this resolves directly whether the
102
+ # target is an agent or a plain shell tab. Matching by cwd is
103
+ # the fallback for when that tab has already closed — an agent
104
+ # match wins there if one exists (the common case: replying to
105
+ # an agent's own notification), otherwise any matching tab does
106
+ # (a plain tab mid-interactive-command has no agent to prefer).
100
107
  def type_into_session(chat_id, target, text)
101
108
  exact = @registry.refresh.find(target["session_id"])
102
- return execute(chat_id, exact, text) if exact&.agent? && !exact.terminalless?
109
+ return execute(chat_id, exact, text, show_result: !exact.agent?) if exact && !exact.terminalless?
103
110
 
104
- matching = @registry.agents.select { |s| s.cwd == target["cwd"] && !s.terminalless? }
111
+ candidates = @registry.sessions.select { |s| s.cwd == target["cwd"] && !s.terminalless? }
112
+ agent_matches = candidates.select(&:agent?)
113
+ matching = agent_matches.empty? ? candidates : agent_matches
105
114
 
106
115
  case matching.size
107
116
  when 0 then say(chat_id, "#{target['label']} isn't waiting anymore, and I couldn't find the tab.")
108
- when 1 then execute(chat_id, matching.first, text)
117
+ when 1 then execute(chat_id, matching.first, text, show_result: !matching.first.agent?)
109
118
  else say(chat_id, "#{target['label']} has several tabs — use /run NUMBER to pick one.")
110
119
  end
111
120
  end
@@ -188,7 +197,11 @@ module AgentsControl
188
197
  with_session(chat_id, number) do |session|
189
198
  next say(chat_id, "This session has no terminal — nothing to run there.") if session.terminalless?
190
199
 
191
- remote?(session) ? confirm_remote(chat_id, session, command) : execute(chat_id, session, command)
200
+ if remote?(session)
201
+ confirm_remote(chat_id, session, command)
202
+ else
203
+ execute(chat_id, session, command, show_result: true)
204
+ end
192
205
  end
193
206
  end
194
207
 
@@ -216,13 +229,125 @@ module AgentsControl
216
229
  # input: a merged call can fail to send multi-line text at all.
217
230
  TYPING_PAUSE = 0.4
218
231
 
219
- def execute(chat_id, session, command)
232
+ # How long to wait after the command lands before reading the
233
+ # screen back. There's no way to know when a command actually
234
+ # finishes without polling for it, so this is a fixed guess
235
+ # tuned for the common case (git status, ls, a quick build
236
+ # check) — long enough for those, short enough not to feel
237
+ # laggy. A slower command just gets caught mid-flight, same as
238
+ # looking over someone's shoulder while it's still running;
239
+ # /screen still shows the up-to-date state a moment later.
240
+ RUN_SETTLE_PAUSE = 1.2
241
+
242
+ def run_result_lines = @config.get("terminal.run_result_lines", 200)
243
+
244
+ def execute(chat_id, session, command, show_result: false)
220
245
  backend = @registry.backend_for(session)
246
+ before = show_result ? capture_screen(session, lines: run_result_lines) : nil
221
247
 
222
248
  ok = backend.send_text(session.id, command, newline: false) &&
223
249
  sleep(TYPING_PAUSE).then { backend.send_text(session.id, "", newline: true) }
224
250
 
225
- say(chat_id, ok ? "Sent to #{session.label}." : "Couldn't send.")
251
+ return say(chat_id, "Couldn't send.") unless ok
252
+ return say(chat_id, "Sent to #{session.label}.") unless show_result
253
+
254
+ sleep(RUN_SETTLE_PAUSE)
255
+ show_command_result(chat_id, session, since: before, command: command)
256
+ end
257
+
258
+ # Shows what's new since `since` (the screen right before the
259
+ # command was typed) rather than an arbitrary tail slice of the
260
+ # current screen — otherwise a short result could be padded out
261
+ # with whatever the terminal already had on it a moment ago.
262
+ def show_command_result(chat_id, session, since:, command:)
263
+ after = capture_screen(session, lines: run_result_lines)
264
+ text = new_output_since(after, since, command: command)
265
+ log("run result: session=#{session.id} before=#{since.to_s.length}b " \
266
+ "after=#{after.to_s.length}b prefix_match=#{after.to_s.start_with?(since.to_s)} " \
267
+ "command_anchor=#{command.to_s.strip.length >= MIN_COMMAND_ANCHOR && after.to_s.include?(command.to_s.strip)} " \
268
+ "result=#{text.to_s.length}b")
269
+
270
+ if text && !text.empty?
271
+ sent = say_chunked(chat_id, "🖥 #{session_descriptor(session)}\n\n", text, code: true)
272
+ return sent.each { |msg| remember_reply(chat_id, msg, session) }
273
+ end
274
+
275
+ # new_output_since only comes back empty when the capture
276
+ # itself was empty — a genuinely blank screen. The transcript
277
+ # fallback below is keyed by cwd, not by this specific session
278
+ # — several tabs (or VS Code sessions) can sit on the exact
279
+ # same project directory, and it'll pick whichever of them
280
+ # wrote to its transcript most recently. For a plain shell tab
281
+ # that's cross-session noise: a result from a totally
282
+ # different session shown as if it were this one's. Only
283
+ # worth the ambiguity when there's genuinely nothing else to
284
+ # offer — an agent (its own conversation is at least
285
+ # thematically the right session) or a session with no
286
+ # terminal at all (nothing else exists to show).
287
+ return say(chat_id, "The screen is empty.") unless session.agent? || session.terminalless?
288
+
289
+ show_agent_context(chat_id, session)
290
+ end
291
+
292
+ # A one- or two-character reply ("y", "n", "q" mid `git add -p`)
293
+ # turns up constant false matches in ordinary output — not a
294
+ # safe anchor at all. Anything shorter than this falls straight
295
+ # through to the prefix/marker fallback instead.
296
+ MIN_COMMAND_ANCHOR = 3
297
+
298
+ # Returns whatever showed up in `after` beyond `since`.
299
+ #
300
+ # The command itself is the sharpest anchor available: the
301
+ # shell echoes it back verbatim, and unlike the prompt (which
302
+ # looks identical after every command) the command text is
303
+ # specific to this exact invocation. rindex, not index — the
304
+ # *last* occurrence is the one nearest to now, i.e. this send,
305
+ # not some earlier time the same command happened to run. This
306
+ # is also what makes it more reliable than prefix/marker
307
+ # matching when the tab is shared with a human actively typing
308
+ # in it: `since` doesn't have to relate to `after` at all for
309
+ # this to work, since it just locates the command's own echo
310
+ # directly.
311
+ #
312
+ # An exact prefix match is the fallback: unambiguous whenever
313
+ # nothing scrolled out of the capture window between the two
314
+ # snapshots, since `after` is then simply `since` with new
315
+ # content appended and there's nothing to search for at all.
316
+ #
317
+ # Marker search (since's own last two lines, most recent
318
+ # occurrence) is the last resort, for whatever's too short to
319
+ # anchor on and doesn't fit as a literal prefix either.
320
+ #
321
+ # No matter which method resolves it, an anchor is never
322
+ # allowed to produce silence: if it lands on nothing (the
323
+ # reference point itself, unchanged, or a boundary right at the
324
+ # tail), what's actually on screen right now is still real and
325
+ # still worth showing — the whole point of /run is seeing the
326
+ # result, and reporting "no new output" when there's visibly a
327
+ # result sitting right there is a worse failure than
328
+ # occasionally including a line of stale context.
329
+ def new_output_since(after, since, command: nil)
330
+ after_s = after.to_s
331
+ since_s = since.to_s
332
+
333
+ needle = command.to_s.strip
334
+ if needle.length >= MIN_COMMAND_ANCHOR
335
+ index = after_s.rindex(needle)
336
+ return after_s[index..].to_s if index
337
+ end
338
+
339
+ return after_s if since_s.strip.empty?
340
+
341
+ tail = if after_s.start_with?(since_s)
342
+ after_s[since_s.length..]
343
+ else
344
+ marker = since_s.rstrip.lines.last(2).join
345
+ index = marker.strip.empty? ? nil : after_s.rindex(marker)
346
+ index ? after_s[(index + marker.length)..] : nil
347
+ end
348
+
349
+ tail = tail.to_s.sub(/\A\n+/, "")
350
+ tail.empty? ? after_s : tail
226
351
  end
227
352
 
228
353
  def create_tab(chat_id, directory)
@@ -389,7 +514,7 @@ module AgentsControl
389
514
  case payload["action"]
390
515
  when "focus" then focus_session(chat_id, session)
391
516
  when "screen" then show_screen(chat_id, session)
392
- when "run" then execute(chat_id, session, payload["text"])
517
+ when "run" then execute(chat_id, session, payload["text"], show_result: true)
393
518
  when "close_confirm" then say(chat_id, "Close #{session.label}?",
394
519
  markup: @keyboards.confirm("close", session))
395
520
  when "close" then close_session(chat_id, session)
@@ -406,23 +531,35 @@ module AgentsControl
406
531
  # just finished lines from the transcript. The transcript stays
407
532
  # the fallback for terminalless sessions and for a freshly
408
533
  # created tab's blank screen.
409
- def show_screen(chat_id, session)
410
- text = capture_screen(session)
534
+ def show_screen(chat_id, session, lines: @config.get("terminal.context_lines", 80))
535
+ text = capture_screen(session, lines: lines)
411
536
  if text && !text.empty?
412
- sent = say_chunked(chat_id, "", text, code: true)
537
+ sent = say_chunked(chat_id, "🖥 #{session_descriptor(session)}\n\n", text, code: true)
413
538
  return sent.each { |msg| remember_reply(chat_id, msg, session) }
414
539
  end
415
540
 
541
+ return say(chat_id, "The screen is empty.") unless session.agent? || session.terminalless?
542
+
416
543
  show_agent_context(chat_id, session)
417
544
  end
418
545
 
419
- def capture_screen(session)
420
- @registry.backend_for(session).capture(session.id,
421
- lines: @config.get("terminal.context_lines", 80))
546
+ def capture_screen(session, lines: @config.get("terminal.context_lines", 80))
547
+ @registry.backend_for(session).capture(session.id, lines: lines)
422
548
  rescue Terminals::Unsupported
423
549
  nil
424
550
  end
425
551
 
552
+ # The label alone isn't always enough to tell tabs apart — several
553
+ # can share the same project directory and so the same label. The
554
+ # tty is what actually distinguishes them, and it's the same
555
+ # identifier a terminal's own window/tab chrome would show.
556
+ def session_descriptor(session)
557
+ return session.label if session.terminalless?
558
+
559
+ tty = session.tty.to_s.sub(%r{\A/dev/}, "")
560
+ tty.empty? ? session.label : "#{session.label} · #{tty}"
561
+ end
562
+
426
563
  def show_agent_context(chat_id, session)
427
564
  transcript = Transcript.for_cwd(session.cwd, root: transcript_root)
428
565
 
@@ -476,6 +613,10 @@ module AgentsControl
476
613
 
477
614
  @api.send_message(chat_id: chat_id, text: text, reply_markup: markup)
478
615
  end
616
+
617
+ def log(message)
618
+ @logger&.puts("[#{Time.now.strftime('%H:%M:%S')}] #{message}")
619
+ end
479
620
  end
480
621
  end
481
622
  end
@@ -30,7 +30,9 @@ module AgentsControl
30
30
  { key: "answers.reply_timeout", label: "Reply timeout",
31
31
  values: [300, 600, 900, 1800, 3600], unit: "s", default: 900 },
32
32
  { key: "terminal.context_lines", label: "Context lines",
33
- values: [40, 80, 200, 500], unit: "", default: 80 }
33
+ values: [40, 80, 200, 500], unit: "", default: 80 },
34
+ { key: "terminal.run_result_lines", label: "Command result lines",
35
+ values: [100, 200, 500, 1000], unit: "", default: 200 }
34
36
  ].freeze
35
37
 
36
38
  def initialize(store:, config:)
@@ -234,7 +234,7 @@ module AgentsControl
234
234
  def start_bot
235
235
  router = Channels::Telegram::Router.new(
236
236
  api: api, registry: Registry.new, store: @store,
237
- config: @config, pending: pending
237
+ config: @config, pending: pending, logger: @logger
238
238
  )
239
239
 
240
240
  @bot = Channels::Telegram::Bot.new(api: api, router: router, store: @store,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentsControl
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sapar Kurmanov
@@ -24,8 +24,8 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.3'
26
26
  description: |
27
- A daemon that catches the moments a terminal AI agent stops and waits
28
- on a human, and relays them to Telegram — with the question's text and
27
+ A daemon that catches the moments Claude Code stops and waits on a
28
+ human, and relays them to Telegram — with the question's text and
29
29
  reply buttons. Plus a remote for the terminal itself: list tabs, create
30
30
  them, send commands.
31
31
  executables:
@@ -81,7 +81,12 @@ files:
81
81
  homepage: https://github.com/saparjohnick/agents_control
82
82
  licenses:
83
83
  - Apache-2.0
84
- metadata: {}
84
+ metadata:
85
+ source_code_uri: https://github.com/saparjohnick/agents_control
86
+ bug_tracker_uri: https://github.com/saparjohnick/agents_control/issues
87
+ documentation_uri: https://github.com/saparjohnick/agents_control/blob/main/README.md
88
+ changelog_uri: https://github.com/saparjohnick/agents_control/releases
89
+ rubygems_mfa_required: 'true'
85
90
  rdoc_options: []
86
91
  require_paths:
87
92
  - lib
@@ -98,5 +103,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
103
  requirements: []
99
104
  rubygems_version: 4.0.16
100
105
  specification_version: 4
101
- summary: Control terminal AI agents from Telegram
106
+ summary: Control terminal AI agents (Claude Code, Codex) from Telegram
102
107
  test_files: []