roger-roger 0.1.1 → 0.1.2
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/README.md +2 -0
- package/package.json +1 -1
- package/skills/roger-roger/SKILL.md +2 -0
- package/skills/roger-roger/herdr-plugin.toml +1 -1
- package/skills/roger-roger/scripts/help.mjs +210 -0
- package/skills/roger-roger/scripts/install.mjs +300 -29
- package/skills/roger-roger/scripts/lib.mjs +9 -0
- package/skills/roger-roger/scripts/roger-roger.mjs +18 -65
- package/skills/roger-roger/scripts/slackcli.mjs +4 -0
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ It asks a few questions in the terminal and does the rest. First it offers to pu
|
|
|
16
16
|
|
|
17
17
|
If you'd rather install first and set up after, that works too: `npm install -g roger-roger`, then `roger-roger install`.
|
|
18
18
|
|
|
19
|
+
To take it off again, `npx roger-roger@latest uninstall` (or `roger-roger uninstall`): it lists what it finds, you tick what should go, and it removes the hooks, the skill links, the Herdr plugin, the daemon and the tray, your settings and tokens, and the global command. The Slack CLI is offered too, unticked. `roger-roger help` shows every command, and `roger-roger help notify` one command's flags.
|
|
20
|
+
|
|
19
21
|
Other ways in:
|
|
20
22
|
|
|
21
23
|
- **skills.sh**: `npx skills add code-red-app/roger-roger` copies the skill alone. The agent runs it as `node <skill-dir>/scripts/roger-roger.mjs` and, on first use, asks you to run `npx roger-roger install`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roger-roger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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": {
|
|
@@ -261,6 +261,8 @@ Judge it by what happens if you are wrong, not by whether it feels routine. A fo
|
|
|
261
261
|
## Other commands
|
|
262
262
|
|
|
263
263
|
- `install`: the interactive setup, for the user to run in a terminal (section 1). `install --yes` asks nothing: it links the skill for every agent on the machine and registers with Herdr, and prints what it did.
|
|
264
|
+
- `uninstall`: takes it out again, for the user to run in a terminal (`npx roger-roger@latest uninstall`): hooks, skill links, the Herdr plugin, the daemon and the tray, settings and tokens, the global command. `--yes` removes all of that without asking (`--slack-cli` adds the Slack CLI). Never run it for the user unasked.
|
|
265
|
+
- `help [command]`: the commands, and one command's flags.
|
|
264
266
|
- `status`: current config, whether the tokens are present, the daemon, running sessions, and every valid option value.
|
|
265
267
|
- `slack-setup [status|install-cli|login|create]`: the Slack steps `install` runs, one at a time, for repairing a connection by hand. `create` again is safe: it updates the same app, which is what to do after the manifest changed or when a scope is missing.
|
|
266
268
|
- `sessions [--all]`: which agents are running, what each is called, and what each is doing. `sessions rename sage plum` gives one a different name (the user's request, not yours to change on a whim).
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
id = "roger-roger"
|
|
8
8
|
name = "roger-roger"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.2"
|
|
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"]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// `roger-roger help`: the commands, grouped by what they are for, laid out to the terminal's width
|
|
2
|
+
// and coloured when it is one. `help <command>` shows that command's flags. The text lives here as
|
|
3
|
+
// data, so the overview, the per-command pages and the tests all read the same thing.
|
|
4
|
+
|
|
5
|
+
import { VERSION } from "./lib.mjs";
|
|
6
|
+
import { coloursOn, makeStyle, width } from "./tui.mjs";
|
|
7
|
+
|
|
8
|
+
export const TAGLINE = "Bring yourself back to the conversation: your agents reach you by Slack, a sound, or a voice.";
|
|
9
|
+
|
|
10
|
+
/** Every command: where it belongs, how it is called, what it does, and (for the busy ones) its flags. */
|
|
11
|
+
export const COMMANDS = [
|
|
12
|
+
// ---- setting up
|
|
13
|
+
{ section: "Setting up", name: "install", usage: "[--yes]", summary: "Set everything up in the terminal: the command on your PATH, the skill for every agent, Herdr, Slack, the sound and the voice, hooks, tray. Run it again to change anything. --yes asks nothing and only links the skill.", flags: [["--yes", "no questions: link the skill for every agent on the machine and register with Herdr, then print what was done"]] },
|
|
14
|
+
{ section: "Setting up", name: "uninstall", usage: "[--yes]", summary: "Take it out again: hooks, links, Herdr, the daemon and the tray, settings and tokens, the global command.", flags: [["--yes", "no questions: remove everything except the Slack CLI, then print what was done"]] },
|
|
15
|
+
{ section: "Setting up", name: "status", usage: "", summary: "Settings, whether the tokens are there, the daemon, who is running, and every valid option value." },
|
|
16
|
+
{
|
|
17
|
+
section: "Setting up", name: "setup", usage: "[flags]", summary: "Change any setting; the rest are kept.",
|
|
18
|
+
flags: [
|
|
19
|
+
["--methods slack,sound,speech", "how to be reached, any combination"],
|
|
20
|
+
["--slack-target U0123|C0123|#name", "your member ID for DMs, or a channel"],
|
|
21
|
+
["--slack-mention U0123", "who to mention in channel posts"],
|
|
22
|
+
["--sound NAME", "chime, ding, marimba, bubble, alert"],
|
|
23
|
+
["--voice NAME", "a voice of the speech provider (see voices)"],
|
|
24
|
+
["--speech auto|brief|same", "what is spoken: the agent's choice, one sentence, or the whole message"],
|
|
25
|
+
["--when auto|done-and-blocked|blocked-only|on-request", "when agents notify"],
|
|
26
|
+
["--speech-provider gemini|openai|elevenlabs", "who makes the voice"],
|
|
27
|
+
["--speech-key-var NAME|auto", "the environment variable holding that provider's key"],
|
|
28
|
+
["--model ID", "the provider's speech model"],
|
|
29
|
+
["--speech-volume 0-150", "the voice's volume"],
|
|
30
|
+
["--duck on|off, --duck-level 0-100", "turn other sound down while the voice speaks (Windows)"],
|
|
31
|
+
["--local-fallback true|false", "use the computer's voice when the provider fails"],
|
|
32
|
+
["--say-who on|off", "start a spoken line with the agent's name"],
|
|
33
|
+
["--session-label on|off", "the small name · machine · agent line under Slack messages"],
|
|
34
|
+
["--remind 15m|off", "remind about an unanswered question after this long"],
|
|
35
|
+
["--on-expire recommended|nothing", "what an unanswered question settles as"],
|
|
36
|
+
["--quiet-hours 22:00-08:00|off, --quiet-mute speech,sound", "when, and what, to keep quiet"],
|
|
37
|
+
["--permission-pings on|off", "also ping for ordinary permission prompts in the terminal"],
|
|
38
|
+
["--terminal-wait 20s", "how long a terminal question waits before the ping (OpenCode, Codex)"],
|
|
39
|
+
["--tray auto|off", "start the tray with the daemon once it is installed"],
|
|
40
|
+
["--notes TEXT", "anything else the agent should follow"],
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{ section: "Setting up", name: "slack-setup", usage: "[status|install-cli|login|create]", summary: "The Slack steps one at a time, for repairing a connection by hand. `next` says which comes next." },
|
|
44
|
+
{ section: "Setting up", name: "hooks", usage: "status|install|uninstall [claude|opencode|codex|all]", summary: "Ping you when an agent asks a question in its own terminal and can't tell you itself." },
|
|
45
|
+
{ section: "Setting up", name: "daemon", usage: "status|start|stop", summary: "The one process behind everything. It starts on demand; this is for looking at it, or restarting it after a change to the code." },
|
|
46
|
+
{ section: "Setting up", name: "tray", usage: "status|start|stop|install", summary: "An icon showing every agent, a panel behind it, snooze on right-click. The native part is installed on request (about 4 MB)." },
|
|
47
|
+
|
|
48
|
+
// ---- reaching the user
|
|
49
|
+
{
|
|
50
|
+
section: "Reaching the user", name: "notify", usage: "--kind K --message TEXT [flags]", summary: "Post to Slack, play the sound, speak the line: any or all, as configured.",
|
|
51
|
+
flags: [
|
|
52
|
+
["--kind done|blocked|input|milestone|error|info", "sets the Slack emoji"],
|
|
53
|
+
["--message TEXT", "the written message, self-contained (Slack mrkdwn works)"],
|
|
54
|
+
["--say TEXT", "the spoken line; short, no paths or URLs. Gemini tone tags like [positive] work"],
|
|
55
|
+
["--only sound,speech", "this call only, some of the methods"],
|
|
56
|
+
["--project NAME", "the label shown in Slack (default: the directory name)"],
|
|
57
|
+
["--attach FILE", "a file to post with the message; repeatable"],
|
|
58
|
+
["--summary, --field \"Label=value\"", "an end-of-task card with key facts, how long it took, and every decision"],
|
|
59
|
+
["--actions \"A|B\", --recommend A", "follow-up buttons; `wait` the returned id like a question"],
|
|
60
|
+
["--reply-to TS", "post in a thread: the replyTo that came with the user's message"],
|
|
61
|
+
["--then-listen [25m]", "post, then stay reachable until the user says something"],
|
|
62
|
+
],
|
|
63
|
+
example: "roger-roger notify --kind done --message \"Tests green, PR ready.\" --say \"[positive] All green.\" --session \"billing refactor\" --agent-model \"opus 5\"",
|
|
64
|
+
},
|
|
65
|
+
{ section: "Reaching the user", name: "progress", usage: "--message TEXT [--key NAME] [--done]", summary: "One Slack message, edited in place as the work moves on, with Pause, Stop and Note buttons." },
|
|
66
|
+
{ section: "Reaching the user", name: "control", usage: "[--key NAME] [--wait DURATION]", summary: "Read Pause, Stop and new messages without posting. --wait blocks while paused." },
|
|
67
|
+
{ section: "Reaching the user", name: "listen", usage: "[--wait 25m]", summary: "Park until the user says something. Run it in the background as the last thing you do." },
|
|
68
|
+
{ section: "Reaching the user", name: "snooze", usage: "[1h|off]", summary: "Sound and speech off for a while; Slack keeps working." },
|
|
69
|
+
|
|
70
|
+
// ---- asking
|
|
71
|
+
{
|
|
72
|
+
section: "Asking the user", name: "ask", usage: "--question TEXT --choices \"A|B\" [flags]", summary: "A question with a button per option in Slack. Returns an id at once; `wait` for the answer.",
|
|
73
|
+
flags: [
|
|
74
|
+
["--question TEXT", "self-contained"],
|
|
75
|
+
["--choices \"A|B\"", "2 to 10 options"],
|
|
76
|
+
["--recommend A", "the option you'd pick, shown in green; with --multi, several"],
|
|
77
|
+
["--multi", "checkboxes and a Submit button instead of one button per option"],
|
|
78
|
+
["--details TEXT", "context under the question"],
|
|
79
|
+
["--draft TEXT | --draft-file PATH", "text to approve or edit before use: a commit message, a PR description"],
|
|
80
|
+
["--expires 45m", "how long the question stays open"],
|
|
81
|
+
["--on-expire recommended|nothing", "settle as the recommended option, or just expire. Use nothing for anything hard to undo"],
|
|
82
|
+
["--remind 15m|off", "reminders for this question"],
|
|
83
|
+
["--other true|false", "the Other… button for a typed answer"],
|
|
84
|
+
["--reply-to TS", "ask in a thread"],
|
|
85
|
+
["--wait [DURATION]", "block until settled, as `wait` would"],
|
|
86
|
+
["--say, --only, --kind, --attach", "as for notify"],
|
|
87
|
+
],
|
|
88
|
+
example: "roger-roger ask --question \"Deploy now?\" --choices \"Yes|Tomorrow|No\" --recommend Yes --on-expire nothing --wait 10m",
|
|
89
|
+
},
|
|
90
|
+
{ section: "Asking the user", name: "wait", usage: "ID [--timeout DURATION]", summary: "Block until answered, expired or cancelled. Exit 0 answered, 5 expired or cancelled, 4 still pending after --timeout, 6 the user sent a message instead." },
|
|
91
|
+
{ section: "Asking the user", name: "check", usage: "ID", summary: "The question's state, without waiting." },
|
|
92
|
+
{ section: "Asking the user", name: "cancel", usage: "ID [--reason TEXT]", summary: "Withdraw a question; its Slack message stops offering buttons." },
|
|
93
|
+
{ section: "Asking the user", name: "decisions", usage: "[--all]", summary: "Pending questions; --all includes recent closed ones." },
|
|
94
|
+
|
|
95
|
+
// ---- sessions
|
|
96
|
+
{ section: "Sessions", name: "sessions", usage: "[--all] | rename NAME NEWNAME", summary: "Who is running and what each is called. Rename gives one a different colour name." },
|
|
97
|
+
{ section: "Sessions", name: "end", usage: "[NAME]", summary: "This session is finished; stop routing messages to it." },
|
|
98
|
+
{ section: "Sessions", name: "inbox", usage: "", summary: "Messages the user sent this session, and anything left for the next agent." },
|
|
99
|
+
|
|
100
|
+
// ---- sounds and voices
|
|
101
|
+
{ section: "Sounds and voices", name: "play", usage: "[SOUND]", summary: "Hear a sound (default: the configured one)." },
|
|
102
|
+
{ section: "Sounds and voices", name: "say", usage: "TEXT [--voice NAME] [--local | --test]", summary: "Hear a voice. --test insists on the provider: its voice or an error, never the computer's." },
|
|
103
|
+
{ section: "Sounds and voices", name: "voices", usage: "[--refresh]", summary: "The provider's voices and models; --refresh asks the provider." },
|
|
104
|
+
{ section: "Sounds and voices", name: "preview-sounds", usage: "[SOUND...] [--voice NAME] [--local]", summary: "Each sound announced by name, then played." },
|
|
105
|
+
{ section: "Sounds and voices", name: "preview-voices", usage: "[VOICE...] [--all] [--text TEXT]", summary: "Each voice introduces itself." },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export const ANY_COMMAND = [
|
|
109
|
+
["--session NAME", "what this work is called, and how the session is recognised between commands: always pass it"],
|
|
110
|
+
["--agent-model MODEL", "your model, short: \"opus 5\", \"gpt-6\""],
|
|
111
|
+
["--agent NAME", "your agent product, when it can't be detected"],
|
|
112
|
+
["--session-id ID, --agent-pid PID", "when the agent knows better than the environment does"],
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/** Text folded to `max` columns at spaces; a word longer than that stands alone. */
|
|
116
|
+
export function wrap(text, max) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
let line = "";
|
|
119
|
+
for (const word of String(text).split(/\s+/).filter(Boolean)) {
|
|
120
|
+
if (line && width(line) + 1 + width(word) > max) {
|
|
121
|
+
lines.push(line);
|
|
122
|
+
line = word;
|
|
123
|
+
} else line = line ? `${line} ${word}` : word;
|
|
124
|
+
}
|
|
125
|
+
if (line) lines.push(line);
|
|
126
|
+
return lines.length ? lines : [""];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A two-column table: the left column padded to one width, the right wrapped beside it. */
|
|
130
|
+
function table(rows, { indent, columns, c, left = (s) => s, right = (s) => s, gap = 3, maxLeft = 34 }) {
|
|
131
|
+
const leftWidth = Math.min(maxLeft, Math.max(...rows.map(([l]) => width(l))));
|
|
132
|
+
const rightWidth = Math.max(20, columns - indent - leftWidth - gap);
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const [l, r] of rows) {
|
|
135
|
+
const lines = wrap(r, rightWidth);
|
|
136
|
+
if (width(l) > leftWidth) {
|
|
137
|
+
out.push(" ".repeat(indent) + left(l));
|
|
138
|
+
for (const line of lines) out.push(" ".repeat(indent + leftWidth + gap) + right(line));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const pad = " ".repeat(leftWidth - width(l) + gap);
|
|
142
|
+
out.push(" ".repeat(indent) + left(l) + pad + right(lines[0]));
|
|
143
|
+
for (const line of lines.slice(1)) out.push(" ".repeat(indent + leftWidth + gap) + right(line));
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The overview, or one command's page. Returns the text; the caller prints it. */
|
|
149
|
+
export function helpText(command = "", { columns = process.stdout.columns || 100, colours = coloursOn() } = {}) {
|
|
150
|
+
const c = makeStyle(colours);
|
|
151
|
+
const cols = Math.max(60, Math.min(columns, 110));
|
|
152
|
+
const out = [""];
|
|
153
|
+
const title = `${c.bold("roger-roger")}${VERSION ? " " + c.dim(VERSION) : ""}`;
|
|
154
|
+
|
|
155
|
+
if (command) {
|
|
156
|
+
const cmd = COMMANDS.find((x) => x.name === command);
|
|
157
|
+
if (!cmd) return null;
|
|
158
|
+
out.push(` ${title}`);
|
|
159
|
+
out.push("");
|
|
160
|
+
out.push(` ${c.cyan(c.bold(cmd.name))}${cmd.usage ? " " + c.dim(cmd.usage) : ""}`);
|
|
161
|
+
for (const line of wrap(cmd.summary, cols - 2)) out.push(` ${line}`);
|
|
162
|
+
if (cmd.flags?.length) {
|
|
163
|
+
out.push("");
|
|
164
|
+
out.push(` ${c.bold("Flags")}`);
|
|
165
|
+
out.push(...table(cmd.flags, { indent: 4, columns: cols, c, left: c.cyan }));
|
|
166
|
+
}
|
|
167
|
+
if (cmd.example) {
|
|
168
|
+
out.push("");
|
|
169
|
+
out.push(` ${c.bold("Example")}`);
|
|
170
|
+
for (const line of wrap(cmd.example, cols - 4)) out.push(` ${c.dim(line)}`);
|
|
171
|
+
}
|
|
172
|
+
out.push("");
|
|
173
|
+
out.push(` ${c.bold("Any command")}`);
|
|
174
|
+
out.push(...table(ANY_COMMAND, { indent: 4, columns: cols, c, left: c.cyan }));
|
|
175
|
+
out.push("");
|
|
176
|
+
return out.join("\n") + "\n";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
out.push(` ${title}`);
|
|
180
|
+
for (const line of wrap(TAGLINE, cols - 2)) out.push(` ${c.dim(line)}`);
|
|
181
|
+
out.push("");
|
|
182
|
+
out.push(` ${c.bold("Usage")}`);
|
|
183
|
+
out.push(...table([["roger-roger <command> [flags]", "every command prints one JSON object"], ["roger-roger help <command>", "that command's flags"]], {
|
|
184
|
+
indent: 4, columns: cols, c, maxLeft: 30, left: (l) => c.cyan("roger-roger") + " " + c.dim(l.slice("roger-roger ".length)),
|
|
185
|
+
}));
|
|
186
|
+
const sections = [...new Set(COMMANDS.map((x) => x.section))];
|
|
187
|
+
for (const section of sections) {
|
|
188
|
+
out.push("");
|
|
189
|
+
out.push(` ${c.bold(section)}`);
|
|
190
|
+
const rows = COMMANDS.filter((x) => x.section === section).map((x) => [x.usage ? `${x.name} ${x.usage}` : x.name, x.summary]);
|
|
191
|
+
const styled = rows.map(([l, r]) => [l, r]);
|
|
192
|
+
out.push(...table(styled, {
|
|
193
|
+
indent: 4, columns: cols, c, maxLeft: 30,
|
|
194
|
+
left: (l) => {
|
|
195
|
+
const [name, ...rest] = l.split(" ");
|
|
196
|
+
return c.cyan(name) + (rest.length ? " " + c.dim(rest.join(" ")) : "");
|
|
197
|
+
},
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
out.push("");
|
|
201
|
+
out.push(` ${c.bold("Any command")}`);
|
|
202
|
+
out.push(...table(ANY_COMMAND, { indent: 4, columns: cols, c, left: c.cyan, maxLeft: 30 }));
|
|
203
|
+
out.push("");
|
|
204
|
+
out.push(` ${c.bold("Start here")}`);
|
|
205
|
+
out.push(...table([["npx roger-roger@latest install", "with nothing installed yet"], ["roger-roger install", "once the command is on your PATH; the same setup, to change anything"]], {
|
|
206
|
+
indent: 4, columns: cols, c, maxLeft: 30, left: c.cyan, right: c.dim,
|
|
207
|
+
}));
|
|
208
|
+
out.push("");
|
|
209
|
+
return out.join("\n") + "\n";
|
|
210
|
+
}
|
|
@@ -11,23 +11,15 @@ import fs from "node:fs";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { SKILL_DIR, VOICES, applySetup, configPath, listSounds, loadConfig, saveConfig, voiceSample } from "./lib.mjs";
|
|
15
|
-
import { PROVIDERS, SPEECH_SETUP, catalog
|
|
16
|
-
import { saveKey } from "./speechkey.mjs";
|
|
14
|
+
import { SKILL_DIR, VERSION, VOICES, applySetup, configPath, listSounds, loadConfig, rogerRogerHome, saveConfig, voiceSample } from "./lib.mjs";
|
|
15
|
+
import { PROVIDERS, SPEECH_SETUP, catalog } from "./tts.mjs";
|
|
16
|
+
import { envValue, hasSavedKey, saveKey } from "./speechkey.mjs";
|
|
17
17
|
import { createApp, loginFinish, loginStart, setupStatus } from "./slackapp.mjs";
|
|
18
|
-
import { installCli } from "./slackcli.mjs";
|
|
19
|
-
import { call } from "./client.mjs";
|
|
18
|
+
import { cliInstallDir, findCli, installCli } from "./slackcli.mjs";
|
|
19
|
+
import { call, connect, request } from "./client.mjs";
|
|
20
20
|
import { identity } from "./agent.mjs";
|
|
21
21
|
import { Cancelled, createUI } from "./tui.mjs";
|
|
22
22
|
|
|
23
|
-
export const VERSION = (() => {
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(fs.readFileSync(path.join(SKILL_DIR, "..", "..", "package.json"), "utf8")).version ?? "";
|
|
26
|
-
} catch {
|
|
27
|
-
return "";
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
|
|
31
23
|
// ---------------------------------------------------------------- where skills live
|
|
32
24
|
|
|
33
25
|
/** The agents that read skills from a folder in the home directory, and where. */
|
|
@@ -318,24 +310,43 @@ async function chooseSpeech(ui, { config, persist }) {
|
|
|
318
310
|
const p = PROVIDERS[provider];
|
|
319
311
|
// Saved now so the previews below speak with this provider, not the one configured before.
|
|
320
312
|
await persist({ "speech-provider": provider });
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
313
|
+
|
|
314
|
+
// Where the key is: the user says, rather than the setup assuming from what happens to be set.
|
|
315
|
+
const keyFile = path.join(rogerRogerHome(), "keys.json");
|
|
316
|
+
const chosenBefore = config?.speechProvider === provider ? String(config?.speechKeyVar ?? "") : "";
|
|
317
|
+
const describe = (name) => (process.env[name] ? "set in your environment" : hasSavedKey(name) ? "a key pasted into an earlier setup" : envValue(name) ? "set in your saved variables" : "not set");
|
|
318
|
+
const known = [...new Set([...p.keyVars, ...(chosenBefore ? [chosenBefore] : [])])];
|
|
319
|
+
const pick = await ui.select({
|
|
320
|
+
message: `Where is your ${p.label} API key?`,
|
|
321
|
+
initial: chosenBefore ? `var:${chosenBefore}` : known.find((name) => envValue(name)) ? `var:${known.find((name) => envValue(name))}` : "paste",
|
|
322
|
+
choices: [
|
|
323
|
+
...known.map((name) => ({ value: `var:${name}`, label: name, hint: describe(name) })),
|
|
324
|
+
{ value: "other", label: "Another variable…", hint: "you name it" },
|
|
325
|
+
{ value: "paste", label: "Paste it now", hint: `kept in ${keyFile}, readable by you only` },
|
|
326
|
+
{ value: "none", label: "No key for now", hint: "the computer's own voice speaks until there is one" },
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
let keyVar = "";
|
|
330
|
+
let hasKey = false;
|
|
331
|
+
if (pick.startsWith("var:") || pick === "other") {
|
|
332
|
+
keyVar = pick === "other"
|
|
333
|
+
? (await ui.text({ message: "Which variable?", placeholder: "MY_GEMINI_KEY", validate: (v) => (/^[A-Za-z_][A-Za-z0-9_]*$/.test(v) ? "" : "a variable name: letters, digits and underscores") })).toUpperCase()
|
|
334
|
+
: pick.slice(4);
|
|
335
|
+
hasKey = Boolean(envValue(keyVar));
|
|
336
|
+
if (hasKey) ui.done(`The key in ${keyVar} will be used`);
|
|
337
|
+
else ui.done(`${keyVar} is empty right now`, "set it and the voice will work; the computer's own voice speaks until then");
|
|
338
|
+
} else if (pick === "paste") {
|
|
339
|
+
const pasted = await ui.text({ message: `${p.label} API key`, mask: true, placeholder: "paste it, or leave empty for no key" });
|
|
330
340
|
if (pasted) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
ui.done("Key saved", `${
|
|
334
|
-
} else
|
|
335
|
-
|
|
336
|
-
|
|
341
|
+
saveKey(p.keyVars[0], pasted);
|
|
342
|
+
hasKey = true;
|
|
343
|
+
ui.done("Key saved", `${keyFile}; setting ${p.keyVars[0]} in your environment later works too`);
|
|
344
|
+
} else ui.done("Nothing pasted", "the computer's own voice speaks until a key is set");
|
|
345
|
+
} else {
|
|
346
|
+
ui.done("No key", "the computer's own voice speaks until a key is set");
|
|
337
347
|
}
|
|
338
|
-
|
|
348
|
+
// The named variable, or the provider's usual ones when the key was pasted or there is none.
|
|
349
|
+
await persist({ "speech-key-var": keyVar || "auto" });
|
|
339
350
|
|
|
340
351
|
const all = catalog(provider).voices;
|
|
341
352
|
const choice = (v) => ({
|
|
@@ -616,6 +627,266 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
616
627
|
}
|
|
617
628
|
}
|
|
618
629
|
|
|
630
|
+
// ---------------------------------------------------------------- uninstall
|
|
631
|
+
|
|
632
|
+
/** The skill as agents see it: links (always ours, by name) and folders that hold a SKILL.md. */
|
|
633
|
+
export function skillFootprint(home = os.homedir(), fsx = fs) {
|
|
634
|
+
return skillLinks(home).filter((a) => {
|
|
635
|
+
try {
|
|
636
|
+
const st = fsx.lstatSync(a.link);
|
|
637
|
+
return st.isSymbolicLink() || (st.isDirectory() && fsx.existsSync(path.join(a.link, "SKILL.md")));
|
|
638
|
+
} catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Remove a link, or a copy. */
|
|
645
|
+
export function removeSkillLink(link, fsx = fs) {
|
|
646
|
+
if (fsx.lstatSync(link).isSymbolicLink()) fsx.unlinkSync(link);
|
|
647
|
+
else fsx.rmSync(link, { recursive: true, force: true });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Whether Herdr has the plugin, so uninstall can say so rather than guess. */
|
|
651
|
+
export function herdrHasPlugin(herdr) {
|
|
652
|
+
try {
|
|
653
|
+
const r = spawnSync(herdr.command, ["plugin", "list", "--json"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
654
|
+
return (JSON.parse(r.stdout)?.result?.plugins ?? []).some((p) => p.plugin_id === "roger-roger");
|
|
655
|
+
} catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function herdrUnlink(herdr) {
|
|
661
|
+
const r = spawnSync(herdr.command, ["plugin", "unlink", "roger-roger"], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
|
|
662
|
+
if (r.status === 0) return { ok: true };
|
|
663
|
+
const text = `${r.stderr}${r.stdout}`.trim();
|
|
664
|
+
let message = text;
|
|
665
|
+
try {
|
|
666
|
+
message = JSON.parse(text)?.error?.message ?? text;
|
|
667
|
+
} catch {}
|
|
668
|
+
return /not (found|linked|installed)|unknown plugin/i.test(message) ? { ok: true, note: message } : { ok: false, error: message };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** `npm uninstall -g roger-roger`. Throws with npm's last words when it fails. */
|
|
672
|
+
export function uninstallGlobally(env = process.env) {
|
|
673
|
+
const r = npmSync(["uninstall", "-g", "roger-roger", "--no-audit", "--no-fund", "--loglevel", "error"], env, 300_000);
|
|
674
|
+
if (r.status !== 0) throw new Error((r.stderr || r.stdout || r.error?.message || `npm exited with ${r.status}`).trim().split(/\r?\n/).slice(-3).join(" "));
|
|
675
|
+
return r.stdout.trim();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Where the Slack CLI that `install` put on the machine is, if it is there at all. */
|
|
679
|
+
export async function slackCliFootprint(home = os.homedir()) {
|
|
680
|
+
const dir = cliInstallDir();
|
|
681
|
+
const cli = await findCli();
|
|
682
|
+
const ours = Boolean(cli && norm(cli.command).startsWith(norm(dir)));
|
|
683
|
+
return { dir, login: path.join(home, ".slack"), cli, ours, present: ours || fs.existsSync(dir) };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Take the Slack CLI out: the binary, its login, and (Windows) the PATH entry `install` added. */
|
|
687
|
+
export function removeSlackCli({ home = os.homedir(), fsx = fs } = {}) {
|
|
688
|
+
const { dir, login } = { dir: cliInstallDir(), login: path.join(home, ".slack") };
|
|
689
|
+
fsx.rmSync(dir, { recursive: true, force: true });
|
|
690
|
+
fsx.rmSync(login, { recursive: true, force: true });
|
|
691
|
+
if (process.platform === "win32") {
|
|
692
|
+
spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
|
693
|
+
"$p = [Environment]::GetEnvironmentVariable('Path', 'User'); $b = Join-Path $env:AG_DIR 'bin'; " +
|
|
694
|
+
"$new = ($p -split ';' | Where-Object { $_ -and $_.ToLower() -ne $b.ToLower() }) -join ';'; " +
|
|
695
|
+
"if ($new -ne $p) { [Environment]::SetEnvironmentVariable('Path', $new, 'User') }",
|
|
696
|
+
], { env: { ...process.env, AG_DIR: dir }, windowsHide: true, timeout: 20_000 });
|
|
697
|
+
} else {
|
|
698
|
+
const link = path.join(home, ".local", "bin", "slack");
|
|
699
|
+
try {
|
|
700
|
+
if (norm(fsx.readlinkSync(link)).startsWith(norm(dir))) fsx.unlinkSync(link);
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Stop the tray and the daemon if one is running; never start one to do it. */
|
|
706
|
+
async function stopDaemon(env = process.env) {
|
|
707
|
+
let socket;
|
|
708
|
+
try {
|
|
709
|
+
socket = await connect({ start: false, env });
|
|
710
|
+
} catch {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
await request(socket, { cmd: "tray", args: { _: ["stop"] } }).catch(() => {});
|
|
715
|
+
await request(socket, { cmd: "stop" }).catch(() => {});
|
|
716
|
+
return true;
|
|
717
|
+
} finally {
|
|
718
|
+
socket.end();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** What is on this machine, so uninstall can offer exactly that. */
|
|
723
|
+
export async function uninstallFindings({ home = os.homedir(), env = process.env, hooks = null, script = "" } = {}) {
|
|
724
|
+
const herdr = findHerdr(env);
|
|
725
|
+
const hookAgents = Object.entries(hooks?.agents ?? {}).filter(([, a]) => {
|
|
726
|
+
try {
|
|
727
|
+
return a.status({ script }).installed || a.status({ script }).partial;
|
|
728
|
+
} catch {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}).map(([name]) => name);
|
|
732
|
+
const global = globalCopy(env);
|
|
733
|
+
return {
|
|
734
|
+
links: skillFootprint(home),
|
|
735
|
+
herdr: herdr && herdrHasPlugin(herdr) ? herdr : null,
|
|
736
|
+
hookAgents,
|
|
737
|
+
home: [rogerRogerHome(env), path.join(home, ".attention")].filter((d) => fs.existsSync(d)),
|
|
738
|
+
global: global?.present ? global : null,
|
|
739
|
+
slackCli: await slackCliFootprint(home),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Everything but the Slack CLI, with no questions. Returns what was done. */
|
|
744
|
+
export async function quietUninstall({ home = os.homedir(), env = process.env, hooks = null, script = "", slackCli = false } = {}) {
|
|
745
|
+
const found = await uninstallFindings({ home, env, hooks, script });
|
|
746
|
+
const done = { daemonStopped: await stopDaemon(env), hooks: [], links: [], herdr: null, home: [], slackCli: false, global: null };
|
|
747
|
+
for (const name of found.hookAgents) {
|
|
748
|
+
try {
|
|
749
|
+
hooks.agents[name].uninstall();
|
|
750
|
+
done.hooks.push(name);
|
|
751
|
+
} catch (e) {
|
|
752
|
+
done.hooks.push({ agent: name, error: e.message });
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (found.herdr) done.herdr = herdrUnlink(found.herdr);
|
|
756
|
+
for (const a of found.links) {
|
|
757
|
+
try {
|
|
758
|
+
removeSkillLink(a.link);
|
|
759
|
+
done.links.push(a.link);
|
|
760
|
+
} catch (e) {
|
|
761
|
+
done.links.push({ link: a.link, error: e.message });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
for (const dir of found.home) {
|
|
765
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
766
|
+
done.home.push(dir);
|
|
767
|
+
}
|
|
768
|
+
if (slackCli && found.slackCli.present) {
|
|
769
|
+
removeSlackCli({ home });
|
|
770
|
+
done.slackCli = true;
|
|
771
|
+
}
|
|
772
|
+
if (found.global) {
|
|
773
|
+
try {
|
|
774
|
+
uninstallGlobally(env);
|
|
775
|
+
done.global = { removed: true, dir: found.global.dir };
|
|
776
|
+
} catch (e) {
|
|
777
|
+
done.global = { removed: false, error: e.message };
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return done;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** The conversation for taking it out. Returns the process exit code. */
|
|
784
|
+
export async function runUninstall(args, { script, hooks, input = process.stdin, output = process.stdout, env = process.env, home = os.homedir() } = {}) {
|
|
785
|
+
const ui = createUI({ input, output, env });
|
|
786
|
+
const c = ui.style;
|
|
787
|
+
|
|
788
|
+
if (args.yes || !ui.interactive) {
|
|
789
|
+
const done = await quietUninstall({ home, env, hooks, script, slackCli: Boolean(args["slack-cli"]) });
|
|
790
|
+
output.write(JSON.stringify({ ok: true, ...done }, null, 2) + "\n");
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
ui.intro(`roger-roger uninstall ${VERSION ? c.dim(VERSION) : ""}`, "Takes roger-roger off this machine. You choose what goes.");
|
|
796
|
+
const found = await ui.run("Looking around", () => uninstallFindings({ home, env, hooks, script }), { done: () => "Looked around" });
|
|
797
|
+
const labels = { claude: "Claude Code", opencode: "OpenCode", codex: "Codex" };
|
|
798
|
+
const choices = [];
|
|
799
|
+
if (found.links.length) choices.push({ value: "links", label: "The skill, for agents", hint: found.links.map((a) => a.label).join(", "), checked: true });
|
|
800
|
+
if (found.hookAgents.length) choices.push({ value: "hooks", label: "Terminal-question hooks", hint: found.hookAgents.map((n) => labels[n] ?? n).join(", "), checked: true });
|
|
801
|
+
if (found.herdr) choices.push({ value: "herdr", label: "The Herdr plugin", hint: "herdr plugin unlink", checked: true });
|
|
802
|
+
if (found.home.length) choices.push({ value: "home", label: "Settings, Slack tokens, state", hint: found.home.join(", "), checked: true });
|
|
803
|
+
if (found.global) choices.push({ value: "global", label: "The roger-roger command", hint: `npm uninstall -g (${found.global.dir})`, checked: true });
|
|
804
|
+
if (found.slackCli.present) choices.push({ value: "slack-cli", label: "The Slack CLI", hint: `${found.slackCli.dir} and its login in ~/.slack; Slack's tool, so off by default`, checked: false });
|
|
805
|
+
if (!choices.length) {
|
|
806
|
+
ui.outro([c.bold("Nothing of roger-roger's is on this machine."), "", "If it is somewhere unusual, remove it by hand, and see `roger-roger help uninstall`."]);
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
const what = await ui.multiselect({ message: "What should go?", choices });
|
|
810
|
+
if (!what.length) {
|
|
811
|
+
ui.outro([c.bold("Nothing removed.")]);
|
|
812
|
+
return 0;
|
|
813
|
+
}
|
|
814
|
+
const sure = await ui.confirm({ message: "Remove that now?", hint: "there is no undo", initial: true });
|
|
815
|
+
if (!sure) {
|
|
816
|
+
ui.outro([c.bold("Nothing removed.")]);
|
|
817
|
+
return 0;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const removed = [];
|
|
821
|
+
if (await stopDaemon(env)) ui.done("Stopped the daemon and the tray");
|
|
822
|
+
if (what.includes("hooks")) {
|
|
823
|
+
for (const name of found.hookAgents) {
|
|
824
|
+
try {
|
|
825
|
+
hooks.agents[name].uninstall();
|
|
826
|
+
ui.done(`${labels[name] ?? name}: hooks removed`);
|
|
827
|
+
removed.push(`hooks: ${labels[name] ?? name}`);
|
|
828
|
+
} catch (e) {
|
|
829
|
+
ui.failed(`${labels[name] ?? name}: hooks not removed`, e.message);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (what.includes("herdr")) {
|
|
834
|
+
const r = herdrUnlink(found.herdr);
|
|
835
|
+
if (r.ok) {
|
|
836
|
+
ui.done("Herdr plugin unlinked", r.note ?? "");
|
|
837
|
+
removed.push("Herdr plugin");
|
|
838
|
+
} else ui.failed("Herdr would not unlink the plugin", r.error);
|
|
839
|
+
}
|
|
840
|
+
if (what.includes("links")) {
|
|
841
|
+
for (const a of found.links) {
|
|
842
|
+
try {
|
|
843
|
+
removeSkillLink(a.link);
|
|
844
|
+
ui.done(`${a.label}: skill removed`, a.link);
|
|
845
|
+
removed.push(a.label);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
ui.failed(`${a.label}: not removed`, e.message);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (what.includes("home")) {
|
|
852
|
+
for (const dir of found.home) {
|
|
853
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
854
|
+
ui.done("Removed", dir);
|
|
855
|
+
removed.push(dir);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (what.includes("slack-cli")) {
|
|
859
|
+
removeSlackCli({ home });
|
|
860
|
+
ui.done("Slack CLI removed", `${found.slackCli.dir}, ~/.slack, and its PATH entry`);
|
|
861
|
+
removed.push("Slack CLI");
|
|
862
|
+
}
|
|
863
|
+
if (what.includes("global")) {
|
|
864
|
+
try {
|
|
865
|
+
await ui.run("Removing the roger-roger command", () => uninstallGlobally(env), { done: () => "The roger-roger command is gone" });
|
|
866
|
+
removed.push("the roger-roger command");
|
|
867
|
+
} catch {
|
|
868
|
+
// ui.run has already said what npm said.
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
ui.outro([
|
|
872
|
+
c.bold("Done."),
|
|
873
|
+
"",
|
|
874
|
+
...removed.map((s) => `${c.green("•")} ${s}`),
|
|
875
|
+
"",
|
|
876
|
+
c.dim("Lines about roger-roger in your own CLAUDE.md or AGENTS.md are yours, so they were left alone."),
|
|
877
|
+
`Back any time: ${c.cyan("npx roger-roger@latest install")}`,
|
|
878
|
+
]);
|
|
879
|
+
return 0;
|
|
880
|
+
} catch (e) {
|
|
881
|
+
if (e instanceof Cancelled) {
|
|
882
|
+
output.write(os.EOL + `${ui.rail.end} Stopped. Nothing more was removed.` + os.EOL + os.EOL);
|
|
883
|
+
return 130;
|
|
884
|
+
}
|
|
885
|
+
ui.failed("Uninstall stopped", e.message);
|
|
886
|
+
return 1;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
619
890
|
/** One line on what a test notification managed. */
|
|
620
891
|
function describeResults(results = {}) {
|
|
621
892
|
const parts = [];
|
|
@@ -13,6 +13,15 @@ import { detectAgent } from "./agent.mjs";
|
|
|
13
13
|
export const SKILL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
14
|
export const SOUNDS_DIR = path.join(SKILL_DIR, "sounds");
|
|
15
15
|
|
|
16
|
+
/** The package's version, when the skill sits inside the package; empty for a bare skills.sh copy. */
|
|
17
|
+
export const VERSION = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(path.join(SKILL_DIR, "..", "..", "package.json"), "utf8")).version ?? "";
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
16
25
|
export const METHODS = ["slack", "sound", "speech"];
|
|
17
26
|
export const KINDS = ["done", "blocked", "input", "milestone", "error", "info"];
|
|
18
27
|
export const SPEECH_MODES = ["auto", "same", "brief"];
|
|
@@ -30,7 +30,8 @@ import { fileURLToPath } from "node:url";
|
|
|
30
30
|
import { claudeHookRequest, claudeSettingsPath, claudeStatus, installClaude, readStdin, uninstallClaude } from "./hooks.mjs";
|
|
31
31
|
import { codexHome, codexHookRequest, codexStatus, installCodex, uninstallCodex } from "./hooks-codex.mjs";
|
|
32
32
|
import { installOpencode, opencodeConfigDir, opencodeHookRequest, opencodeStatus, uninstallOpencode } from "./hooks-opencode.mjs";
|
|
33
|
-
import { runInstall } from "./install.mjs";
|
|
33
|
+
import { runInstall, runUninstall } from "./install.mjs";
|
|
34
|
+
import { helpText } from "./help.mjs";
|
|
34
35
|
|
|
35
36
|
function out(obj, code = 0) {
|
|
36
37
|
process.stdout.write(JSON.stringify(obj, null, 2) + "\n");
|
|
@@ -321,6 +322,17 @@ const AGENTS = {
|
|
|
321
322
|
},
|
|
322
323
|
};
|
|
323
324
|
|
|
325
|
+
/** `help`, `help <command>`, `--help`, or a command that doesn't exist: the overview, or one command's page. */
|
|
326
|
+
function help(command, args) {
|
|
327
|
+
const wanted = command === "help" ? args._[0] : "";
|
|
328
|
+
const unknown = command && !["help", "--help", "-h", undefined].includes(command);
|
|
329
|
+
if (unknown) process.stderr.write(`roger-roger: unknown command "${command}"\n`);
|
|
330
|
+
const page = wanted ? helpText(wanted) : null;
|
|
331
|
+
if (wanted && !page) process.stderr.write(`roger-roger: no such command "${wanted}"\n`);
|
|
332
|
+
process.stdout.write(page ?? helpText());
|
|
333
|
+
process.exitCode = unknown || (wanted && !page) ? 2 : 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
324
336
|
/** Ask the daemon, or do it here when the daemon is from a copy of the skill that predates hooks. */
|
|
325
337
|
async function sendPermission(args, session) {
|
|
326
338
|
try {
|
|
@@ -409,68 +421,7 @@ function hooksCommand(args) {
|
|
|
409
421
|
return out({ ok: !failed, agents: results }, failed ? 1 : 0);
|
|
410
422
|
}
|
|
411
423
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
install [--yes] set everything up in the terminal: link the skill for every agent,
|
|
415
|
-
connect Slack, choose the sound and the voice, hooks, tray.
|
|
416
|
-
--yes (or no terminal) only links the skill and registers with Herdr
|
|
417
|
-
status config, daemon, running sessions, every valid option (JSON)
|
|
418
|
-
setup [--methods slack,sound,speech] [--sound NAME] [--voice NAME]
|
|
419
|
-
[--speech auto|same|brief] [--when auto|done-and-blocked|blocked-only|on-request]
|
|
420
|
-
[--slack-target U0123|C0123|#channel] [--slack-mention U0123]
|
|
421
|
-
[--local-fallback true|false] [--model ID] [--notes TEXT]
|
|
422
|
-
[--remind 15m|off] [--on-expire recommended|nothing] [--say-who on|off]
|
|
423
|
-
[--quiet-hours 22:00-08:00|off] [--quiet-mute speech,sound,slack] [--session-label on|off]
|
|
424
|
-
[--speech-provider gemini|openai|elevenlabs] [--speech-key-var NAME|auto]
|
|
425
|
-
[--speech-volume 0-150] [--duck on|off] [--duck-level 0-100] [--tray auto|off]
|
|
426
|
-
[--permission-pings on|off] [--terminal-wait 20s]
|
|
427
|
-
slack-setup [status|install-cli|login|create]
|
|
428
|
-
create the roger-roger Slack app with the Slack CLI
|
|
429
|
-
|
|
430
|
-
notify --kind done|blocked|input|milestone|error|info --message TEXT
|
|
431
|
-
[--say TEXT] [--only sound,speech] [--project NAME] [--attach FILE]
|
|
432
|
-
[--summary] [--field "Label=value"] [--actions "A|B"] [--recommend A]
|
|
433
|
-
[--reply-to TS] [--then-listen 25m]
|
|
434
|
-
[--session NAME] [--agent-model MODEL]
|
|
435
|
-
progress --message TEXT [--key NAME] [--done]
|
|
436
|
-
one Slack message, edited in place as the work moves on
|
|
437
|
-
control [--key NAME] [--wait DURATION]
|
|
438
|
-
read Pause / Stop and new messages without posting
|
|
439
|
-
inbox messages the user sent this session, and anything unclaimed
|
|
440
|
-
snooze [1h|off] mute the sound and the speech for a while; Slack keeps working
|
|
441
|
-
tray status|start|stop|install an icon showing every agent, and a panel behind it
|
|
442
|
-
listen [--wait 25m] park until the user says something; run it in the background as
|
|
443
|
-
the last thing you do, so a message reaches you while you're idle
|
|
444
|
-
|
|
445
|
-
ask --question TEXT --choices "A|B" [--recommend A] [--details TEXT] [--multi]
|
|
446
|
-
[--draft TEXT|--draft-file PATH] [--expires 45m] [--remind 15m]
|
|
447
|
-
[--on-expire recommended|nothing] [--other true|false] [--wait [DURATION]] [--reply-to TS]
|
|
448
|
-
wait ID [--timeout DURATION] block until answered/expired/cancelled (exit 0 / 5, 4 = timed out,
|
|
449
|
-
6 = the user sent a message instead)
|
|
450
|
-
check ID the current state, without waiting
|
|
451
|
-
cancel ID [--reason TEXT] withdraw a question
|
|
452
|
-
decisions [--all] pending questions (and recent closed ones with --all)
|
|
453
|
-
|
|
454
|
-
sessions [--all] which agents are running, and what each is called
|
|
455
|
-
sessions rename NAME NEWNAME give a session a different colour name
|
|
456
|
-
end [NAME] this session (or NAME) is finished; stop routing messages to it
|
|
457
|
-
|
|
458
|
-
play [SOUND] preview a sound
|
|
459
|
-
say TEXT [--voice NAME] [--local | --test]
|
|
460
|
-
--test checks the provider and key: its voice or an error, never the local one
|
|
461
|
-
voices [--refresh] the speech provider's voices and models (--refresh asks the provider)
|
|
462
|
-
preview-sounds [SOUND...] [--voice NAME] [--local]
|
|
463
|
-
announce each sound by name, then play it
|
|
464
|
-
preview-voices [VOICE...] [--all] [--text TEXT]
|
|
465
|
-
|
|
466
|
-
daemon status|start|stop the always-on process behind all of the above
|
|
467
|
-
hooks status|install|uninstall [claude|opencode|all]
|
|
468
|
-
ping you when an agent asks a question in the terminal
|
|
469
|
-
|
|
470
|
-
Any command takes --session NAME (what this work is called, and how the session is recognised
|
|
471
|
-
between commands), --agent-model MODEL, --agent NAME, and, if the agent knows better than the
|
|
472
|
-
environment does, --session-id ID and --agent-pid PID.
|
|
473
|
-
`;
|
|
424
|
+
// The help text lives in help.mjs, laid out for the terminal.
|
|
474
425
|
|
|
475
426
|
async function main() {
|
|
476
427
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -486,6 +437,9 @@ async function main() {
|
|
|
486
437
|
case "install":
|
|
487
438
|
process.exitCode = await runInstall(args, { script: SCRIPT, hooks: { agents: AGENTS } });
|
|
488
439
|
return;
|
|
440
|
+
case "uninstall":
|
|
441
|
+
process.exitCode = await runUninstall(args, { script: SCRIPT, hooks: { agents: AGENTS } });
|
|
442
|
+
return;
|
|
489
443
|
case "status": return status();
|
|
490
444
|
case "setup": return setup(args);
|
|
491
445
|
case "slack-setup": return slackSetup(args);
|
|
@@ -521,8 +475,7 @@ async function main() {
|
|
|
521
475
|
return run(command, args);
|
|
522
476
|
|
|
523
477
|
default:
|
|
524
|
-
|
|
525
|
-
process.exitCode = command && command !== "help" ? 2 : 0;
|
|
478
|
+
return help(command, args);
|
|
526
479
|
}
|
|
527
480
|
}
|
|
528
481
|
|
|
@@ -44,6 +44,10 @@ export function capture(cmd, args, { input, env, cwd, timeout = 120_000, detache
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Where the official installers put the binary, per platform. */
|
|
47
|
+
export function cliInstallDir() {
|
|
48
|
+
return installDir();
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
function installDir() {
|
|
48
52
|
if (process.platform === "win32") return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "slack-cli");
|
|
49
53
|
return path.join(os.homedir(), ".slack");
|