roger-roger 0.1.14 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roger-roger",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Bring yourself back to the conversation: your coding agents reach you by Slack, a sound or a spoken line when they finish, get stuck, or need a decision, and you answer from your phone.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,7 +100,7 @@ roger-roger notify --kind done --summary --message "Billing refactor finished an
100
100
 
101
101
  If you offered actions, `wait <id>` (in the background if you can) and do what the user picks. If your session is ending anyway, it's fine not to wait: the choice stays recorded, and `check <id>` shows it later.
102
102
 
103
- The configured methods run together: Slack posts while the speech is synthesized, then the sound plays and the speech follows straight after it. The call returns once the audio finishes. When several agents share the machine, only one of them speaks at a time: if another is mid-announcement, yours waits for it and then plays in full, so nothing is talked over and nothing is dropped. The wait shows up as `results.queued.waitedMs`; it is normal, not a failure. Exit code `0` means at least one method worked. Exit code `3` means not configured, so do section 1 first. Look at `results`: if a method failed (for example Slack `not_in_channel` or `invalid_auth`), tell the user briefly in your reply. Do not retry in a loop. If quiet hours muted anything, `mutedByQuietHours` lists it. That is expected, not a failure.
103
+ The configured methods run together: Slack posts while the speech is synthesized, then the sound plays and the speech follows straight after it. The call returns as soon as Slack has posted; the sound and the voice carry on in the daemon, so `results.sound` and `results.speech` say `playing: true` rather than how it went (a failure there is in `~/.roger-roger/daemon.log`). When several agents share the machine, only one of them speaks at a time: if another is mid-announcement, yours waits for it and then plays in full, so nothing is talked over and nothing is dropped. Exit code `0` means at least one method worked. Exit code `3` means not configured, so do section 1 first. Look at `results`: if a method failed (for example Slack `not_in_channel` or `invalid_auth`), tell the user briefly in your reply. Do not retry in a loop. If quiet hours muted anything, `mutedByQuietHours` lists it. That is expected, not a failure.
104
104
 
105
105
  ### Progress on long work
106
106
 
@@ -284,7 +284,7 @@ Judge it by what happens if you are wrong, not by whether it feels routine. A fo
284
284
  - `end [name]`: this session is finished. Do this when your work is done, so messages meant for someone else don't come to you.
285
285
  - `tray status|start|stop|install`: an icon in the system tray showing every agent, with a translucent panel behind it and snooze on the right-click menu. The native part is a separate package installed on request into `~/.roger-roger` (about 4 MB, nothing compiled); everything here works without it. Once installed, the daemon starts it by itself whenever it starts (`setup --tray off` to stop that).
286
286
  - `snooze [1h|off]`: mute sound and speech for a while; Slack keeps working. For "not now" moments that quiet hours don't cover.
287
- - `hush`: stop the voice or sound playing right now (the user's Stop button in the tray does the same). Only that announcement; the next one plays, and the Slack message stays. If a notification comes back with `results.interrupted`, the user stopped it: don't send it again.
287
+ - `hush`: stop the voice or sound playing right now (the user's Stop button in the tray does the same). Only that announcement; the next one plays, and the Slack message stays. If the user stopped one of yours, don't send it again.
288
288
  - `hooks status|install|uninstall [claude|opencode|codex|all]`: the terminal-question hooks. When you use your own question tool (Claude Code's question dialog, OpenCode's question tool, Codex's `request_user_input`) or present a plan to approve, you are frozen until it's answered and can't send anything yourself; with the hooks, the agent itself runs roger-roger, and once the question has gone unanswered for a little while the user gets it in Slack with its options. `roger-roger install` offers them; without a name, `hooks install` covers the agents on this machine. What each gets: Claude Code, three entries in `~/.claude/settings.json`; OpenCode, a small plugin at `~/.config/opencode/plugins/roger-roger.js` (restart OpenCode to load it); Codex, three entries in `~/.codex/hooks.json`, to trust in `/hooks`. They follow the user's `when`, quiet hours and snooze like any notification, and log each run to `~/.roger-roger/hooks.log`. Claude Code pings once nobody has typed for a few seconds; OpenCode and Codex can't tell, so they wait `setup --terminal-wait` (default 20s) and ping only if it's still unanswered. Ordinary permission prompts (leave to run a command) don't ping unless the user asks: `setup --permission-pings on` (Claude Code and OpenCode; Codex gives no signal when one is answered). Run `hooks install` again after the skill moves, so they point at the new copy; `hooks status` says if they don't.
289
289
  - `daemon status|start|stop`: the always-on process behind everything. It starts on demand, so this is only for checking on it or restarting it after a change to the skill's code.
290
290
  - `play [sound]`: preview a sound (defaults to the configured one).
@@ -6,7 +6,7 @@
6
6
 
7
7
  id = "roger-roger"
8
8
  name = "roger-roger"
9
- version = "0.1.14"
9
+ version = "0.1.15"
10
10
  min_herdr_version = "0.9.0"
11
11
  description = "Slack, a sound or a spoken line from the agents in your panes, and questions you answer from your phone"
12
12
  platforms = ["linux", "macos", "windows"]
@@ -141,6 +141,11 @@ function ctxFor(connection, identity, about) {
141
141
  log,
142
142
  daemon: true,
143
143
  emit: (event, data) => send(connection, { n: 1, event, data }),
144
+ /** Carry on with something after the reply has gone. It counts as in flight, so a handover waits for it. */
145
+ later(what, work) {
146
+ state.inFlight++;
147
+ work.catch((e) => log(`${what} failed: ${e.stack ?? e.message}`)).finally(() => state.inFlight--);
148
+ },
144
149
  /** When this agent last ran a real command. A parked `listen` gives up once it is awake again. */
145
150
  lastActivity() {
146
151
  return session ? state.activity.get(session.id) ?? 0 : 0;
@@ -112,6 +112,19 @@ function draftOf(args) {
112
112
  return draft.trim() ? draft : "";
113
113
  }
114
114
 
115
+ /**
116
+ * The sound and the voice. The daemon plays them after it has answered, so the agent isn't held for
117
+ * the synthesis and the whole announcement; it gets `{ playing: true }` per method instead. Without
118
+ * a daemon the command has to stay for them, because ending the process would cut them off.
119
+ */
120
+ async function announce(ctx, opts) {
121
+ const audible = opts.methods.filter((m) => m === "sound" || m === "speech");
122
+ if (!audible.length) return {};
123
+ if (!ctx.later) return playAlert(opts);
124
+ ctx.later("announcement", playAlert(opts));
125
+ return Object.fromEntries(audible.map((m) => [m, { ok: true, playing: true }]));
126
+ }
127
+
115
128
  /** Note what the agent is doing, so the picker can say "waiting on you" next to its name. */
116
129
  function setState(ctx, state) {
117
130
  if (ctx.session) sessions.touch(ctx.session.id, { state });
@@ -180,7 +193,7 @@ export async function notify(ctx, args) {
180
193
  })().catch((e) => (results.slack = { ok: Boolean(results.slack?.ok), error: e.message }))
181
194
  : null;
182
195
 
183
- Object.assign(results, await playAlert({ methods, config, text, prepared, who: ctx.session?.nickname }));
196
+ Object.assign(results, await announce(ctx, { methods, config, text, prepared, who: ctx.session?.nickname }));
184
197
  await slackJob;
185
198
  const succeeded = Object.values(results).some((r) => r.ok);
186
199
  if (succeeded) activity.record({ from: "agent", kind, agent: activity.agentOf(ctx.session), text: message });
@@ -852,7 +865,7 @@ export async function ask(ctx, args) {
852
865
  setState(ctx, "waiting");
853
866
  activity.record({ from: "agent", kind: "question", agent: activity.agentOf(ctx.session), text: question, detail: choices.join(" / ") });
854
867
 
855
- const audio = await playAlert({ methods: audible, config, text, prepared, who: ctx.session?.nickname });
868
+ const audio = await announce(ctx, { methods: audible, config, text, prepared, who: ctx.session?.nickname });
856
869
  const extra = { audio, ...(attached ? { attached } : {}), ...(muted.length ? { mutedByQuietHours: muted } : {}) };
857
870
  if (args.wait !== undefined) return waitFor(ctx, id, parseDuration(args.wait, Infinity), extra);
858
871
  // Waiting costs the agent nothing: run it in the background and carry on. It always comes back,