roger-roger 0.1.0 → 0.1.1
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
CHANGED
|
@@ -6,18 +6,15 @@ It works with any agent that supports [Agent Skills](https://skills.sh), such as
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
npm install -g roger-roger
|
|
11
|
-
roger-roger install
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Or, without installing anything permanently:
|
|
9
|
+
One command, in any shell on Windows, macOS or Linux:
|
|
15
10
|
|
|
16
11
|
```sh
|
|
17
|
-
npx roger-roger install
|
|
12
|
+
npx roger-roger@latest install
|
|
18
13
|
```
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
It asks a few questions in the terminal and does the rest. First it offers to put the `roger-roger` command on your PATH (`npm install -g`), then it links the skill into every agent's skills folder it finds (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Copilot, and `~/.agents/skills`), registers with [Herdr](https://herdr.dev) as a plugin when it's installed, connects Slack, lets you hear the sounds and the voices before you choose, and installs the terminal-question hooks and the tray icon. Once it's on your PATH, `roger-roger install` runs the same setup again whenever you want to change something.
|
|
16
|
+
|
|
17
|
+
If you'd rather install first and set up after, that works too: `npm install -g roger-roger`, then `roger-roger install`.
|
|
21
18
|
|
|
22
19
|
Other ways in:
|
|
23
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roger-roger",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|
|
@@ -34,10 +34,10 @@ Every command prints a single JSON object.
|
|
|
34
34
|
Run `status` first. If `configured` is `false`, don't set things up in the conversation. Ask the user to run the setup in a terminal, and wait for them:
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
|
|
37
|
+
npx roger-roger@latest install
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
It asks everything in one sitting, with previews: how to be reached (Slack, sound, speech), connecting Slack (it creates the roger-roger Slack app; nobody copies tokens), which sound and which voice (space plays them), how much to say out loud, when to notify, the terminal-question hooks (`hooks`, under *Other commands*), and the tray. It also links the skill into every agent's skills folder and registers with Herdr when it's there. When the user says it's done, run `status` again and carry on.
|
|
40
|
+
That one line works in every shell (`roger-roger install` does the same once the command is on their PATH, which the setup offers first). It asks everything in one sitting, with previews: how to be reached (Slack, sound, speech), connecting Slack (it creates the roger-roger Slack app; nobody copies tokens), which sound and which voice (space plays them), how much to say out loud, when to notify, the terminal-question hooks (`hooks`, under *Other commands*), and the tray. It also links the skill into every agent's skills folder and registers with Herdr when it's there. When the user says it's done, run `status` again and carry on.
|
|
41
41
|
|
|
42
42
|
Only if they can't get to a terminal: `setup` takes every setting as a flag (below), and `slack-setup` does the Slack steps one at a time (`slack-setup` prints `next`: `install-cli`, `login` — it returns a `/slackauthticket …` command for the user to paste into Slack and you pass back the code they get with `--challenge CODE --ticket TICKET` — `create`, then `done`, with the user's member ID as `userId` for `--slack-target`).
|
|
43
43
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
id = "roger-roger"
|
|
8
8
|
name = "roger-roger"
|
|
9
|
-
version = "0.1.
|
|
9
|
+
version = "0.1.1"
|
|
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"]
|
|
@@ -103,6 +103,52 @@ export function applyLink(link, target, plan, fsx = fs, now = new Date()) {
|
|
|
103
103
|
return { ...plan, done: true };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ---------------------------------------------------------------- the global copy
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Run npm with fixed arguments (never anything typed). On Windows npm is a .cmd, which Node only
|
|
110
|
+
* runs through a shell, and a shell wants one command line rather than an argument list.
|
|
111
|
+
*/
|
|
112
|
+
function npmSync(argv, env, timeout) {
|
|
113
|
+
const options = { encoding: "utf8", windowsHide: true, timeout, env };
|
|
114
|
+
return process.platform === "win32"
|
|
115
|
+
? spawnSync(["npm", ...argv].join(" "), { ...options, shell: true })
|
|
116
|
+
: spawnSync("npm", argv, options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Where npm keeps global packages, and whether roger-roger is among them. Null when npm isn't around. */
|
|
120
|
+
export function globalCopy(env = process.env) {
|
|
121
|
+
try {
|
|
122
|
+
const r = npmSync(["root", "-g"], env, 20_000);
|
|
123
|
+
const root = (r.stdout ?? "").trim();
|
|
124
|
+
if (r.status !== 0 || !root) return null;
|
|
125
|
+
const pkg = path.join(root, "roger-roger");
|
|
126
|
+
return { root, dir: path.join(pkg, "skills", "roger-roger"), present: fs.existsSync(path.join(pkg, "package.json")) };
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Which copy of the skill everything should point at, and whether to offer a global install:
|
|
134
|
+
* the copy this runs from, unless that is a passing one (npx's cache, say) and there is no global
|
|
135
|
+
* copy yet. A global copy that exists beside a checkout or a skills.sh copy is left alone: whoever
|
|
136
|
+
* runs this copy meant this copy.
|
|
137
|
+
*/
|
|
138
|
+
export function pickTarget(running, global) {
|
|
139
|
+
if (!global) return { target: running, offer: false, reason: "npm was not found" };
|
|
140
|
+
if (norm(running) === norm(global.dir)) return { target: running, offer: false, reason: "global" };
|
|
141
|
+
if (global.present) return { target: running, offer: false, reason: "a global copy exists, but this one was run" };
|
|
142
|
+
return { target: running, offer: true, reason: "no global copy yet" };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `npm install -g roger-roger@latest`. Throws with npm's last words when it fails. */
|
|
146
|
+
export function installGlobally(env = process.env) {
|
|
147
|
+
const r = npmSync(["install", "-g", "roger-roger@latest", "--no-audit", "--no-fund", "--loglevel", "error"], env, 300_000);
|
|
148
|
+
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(" "));
|
|
149
|
+
return r.stdout.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
106
152
|
// ---------------------------------------------------------------- herdr
|
|
107
153
|
|
|
108
154
|
/** The `herdr` binary, from the pane's environment or PATH: `{ command, version }`, or null. */
|
|
@@ -357,13 +403,34 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
357
403
|
config ? "Settings from an earlier setup are the defaults below" : "First setup",
|
|
358
404
|
]);
|
|
359
405
|
|
|
406
|
+
// ---- the copy everything points at: the global one, if there is or is about to be one
|
|
407
|
+
let target = SKILL_DIR;
|
|
408
|
+
let hookScript = script;
|
|
409
|
+
const global = globalCopy(env);
|
|
410
|
+
const pick = pickTarget(SKILL_DIR, global);
|
|
411
|
+
if (pick.reason === "global") {
|
|
412
|
+
ui.done("The roger-roger command is on your PATH", global.dir);
|
|
413
|
+
} else if (pick.offer) {
|
|
414
|
+
const yes = await ui.confirm({ message: "Put the roger-roger command on your PATH?", hint: "npm install -g roger-roger, so you and your agents can run it from anywhere" });
|
|
415
|
+
if (yes) {
|
|
416
|
+
const done = await attempt(ui, "Installing globally", () => ui.run("Installing roger-roger with npm", () => installGlobally(env), { done: () => ({ text: "The roger-roger command is on your PATH", detail: global.dir }) }), { skipLabel: "Carry on with this copy" });
|
|
417
|
+
if (done !== null && fs.existsSync(path.join(global.dir, "scripts", "roger-roger.mjs"))) {
|
|
418
|
+
target = global.dir;
|
|
419
|
+
hookScript = path.join(global.dir, "scripts", "roger-roger.mjs");
|
|
420
|
+
// The daemon should come from the copy that stays, not from the one npx will throw away.
|
|
421
|
+
await daemon("stop").catch(() => {});
|
|
422
|
+
spawnSync(process.execPath, [hookScript, "daemon", "start"], { encoding: "utf8", windowsHide: true, timeout: 30_000, env });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
360
427
|
// ---- the skill, where agents look for it
|
|
361
428
|
const wanted = await ui.multiselect({
|
|
362
429
|
message: "Which agents should have the skill?",
|
|
363
430
|
choices: agents.map((a) => ({ value: a.id, label: a.label, hint: a.hint ?? `~/${a.dir.join("/")}/skills`, checked: true })),
|
|
364
431
|
});
|
|
365
432
|
for (const a of agents.filter((x) => wanted.includes(x.id))) {
|
|
366
|
-
const plan = planLink(a.link,
|
|
433
|
+
const plan = planLink(a.link, target);
|
|
367
434
|
if (plan.action === "replace") {
|
|
368
435
|
const yes = await ui.confirm({ message: `${a.label} has its own copy of the skill. Replace it with a link?`, hint: "the copy is moved aside, not deleted" });
|
|
369
436
|
if (!yes) {
|
|
@@ -376,7 +443,7 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
376
443
|
continue;
|
|
377
444
|
}
|
|
378
445
|
try {
|
|
379
|
-
const done = applyLink(a.link,
|
|
446
|
+
const done = applyLink(a.link, target, plan);
|
|
380
447
|
ui.done(`${a.label}: ${plan.action === "none" ? plan.note : "linked"}`, `${a.link}${done.movedTo ? ` (the copy is at ${done.movedTo})` : ""}`);
|
|
381
448
|
summary.push(a.label);
|
|
382
449
|
} catch (e) {
|
|
@@ -389,7 +456,7 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
389
456
|
if (herdr) {
|
|
390
457
|
const yes = await ui.confirm({ message: "Register with Herdr as a plugin?", hint: "starts the daemon with Herdr, adds snooze to its actions" });
|
|
391
458
|
if (yes) {
|
|
392
|
-
const r = herdrLink(herdr,
|
|
459
|
+
const r = herdrLink(herdr, target);
|
|
393
460
|
if (r.ok) {
|
|
394
461
|
ui.done("Registered with Herdr", r.note ?? "herdr plugin list shows it");
|
|
395
462
|
herdrDone = true;
|
|
@@ -469,7 +536,7 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
469
536
|
if (hookAgents.length && methods.includes("slack")) {
|
|
470
537
|
const labels = { claude: "Claude Code", opencode: "OpenCode", codex: "Codex" };
|
|
471
538
|
const installed = hookAgents.every(([, a]) => {
|
|
472
|
-
const s = a.status({ script });
|
|
539
|
+
const s = a.status({ script: hookScript });
|
|
473
540
|
return s.installed && !s.note;
|
|
474
541
|
});
|
|
475
542
|
if (installed) {
|
|
@@ -482,7 +549,7 @@ export async function runInstall(args, { script, hooks, input = process.stdin, o
|
|
|
482
549
|
if (yes) {
|
|
483
550
|
for (const [name, a] of hookAgents) {
|
|
484
551
|
try {
|
|
485
|
-
a.install({ script });
|
|
552
|
+
a.install({ script: hookScript });
|
|
486
553
|
ui.done(`${labels[name] ?? name}: hooks installed`, name === "codex" ? "open /hooks in Codex once and trust the three roger-roger hooks" : name === "opencode" ? "restart OpenCode to load the plugin" : "");
|
|
487
554
|
summary.push(`hooks: ${labels[name] ?? name}`);
|
|
488
555
|
} catch (e) {
|
|
@@ -9,18 +9,24 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
const METADATA_URL = "https://docs.slack.dev/tools/metadata.json";
|
|
10
10
|
const DOWNLOADS = "https://downloads.slack-edge.com/slack-cli";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* On Windows the Slack CLI hangs for ever when it shares the caller's console (seen from agent
|
|
14
|
+
* shells) and runs normally without one, so it is started detached. Nothing else is: a detached
|
|
15
|
+
* PowerShell comes back at once with exit code 0 and no output, before it has done anything, which
|
|
16
|
+
* is how an install once "succeeded" into an empty folder.
|
|
17
|
+
*/
|
|
18
|
+
export const detachOnWindows = (cmd) => process.platform === "win32" && /^slack(cli)?(\.exe|\.cmd)?$/i.test(path.basename(String(cmd)));
|
|
19
|
+
|
|
12
20
|
/** Run a command and collect its output. Never rejects; `code` is null if it couldn't start. */
|
|
13
|
-
export function capture(cmd, args, { input, env, cwd, timeout = 120_000 } = {}) {
|
|
21
|
+
export function capture(cmd, args, { input, env, cwd, timeout = 120_000, detached = detachOnWindows(cmd) } = {}) {
|
|
14
22
|
return new Promise((resolve) => {
|
|
15
23
|
let child;
|
|
16
24
|
try {
|
|
17
|
-
// Detached: on Windows the Slack CLI can hang indefinitely when it shares the caller's console
|
|
18
|
-
// (seen from agent shells); without a console it runs normally. Output is still piped back.
|
|
19
25
|
child = spawn(cmd, args, {
|
|
20
26
|
env: env ?? process.env,
|
|
21
27
|
cwd,
|
|
22
28
|
timeout,
|
|
23
|
-
detached
|
|
29
|
+
detached,
|
|
24
30
|
windowsHide: true,
|
|
25
31
|
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
26
32
|
});
|