@astrofoundry/pi-astro 0.31.0 → 0.33.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.
- package/README.md +3 -2
- package/agents/media.md +1 -1
- package/agents/release-reviewer.md +23 -0
- package/extensions/astro-discord/admin.ts +1 -1
- package/extensions/astro-discord/config.ts +55 -20
- package/extensions/astro-discord/daily-report.test.ts +35 -13
- package/extensions/astro-discord/daily-report.ts +0 -57
- package/extensions/astro-discord/discord.test.ts +81 -2
- package/extensions/astro-discord/gateway.ts +9 -2
- package/extensions/astro-discord/index.ts +391 -39
- package/extensions/astro-discord/media.test.ts +1 -0
- package/extensions/astro-discord/media.ts +5 -4
- package/extensions/astro-discord/reports.ts +49 -0
- package/extensions/astro-discord/rest.ts +11 -0
- package/extensions/astro-discord/restarts.ts +109 -0
- package/extensions/astro-discord/tools-report.test.ts +344 -0
- package/extensions/astro-discord/tools-report.ts +669 -0
- package/extensions/astro-subagents/agents.test.ts +1 -0
- package/extensions/astro-subagents/agents.ts +2 -1
- package/extensions/astro-subagents/child.test.ts +6 -0
- package/extensions/astro-subagents/child.ts +2 -1
- package/package.json +1 -1
- package/skills/media/SKILL.md +1 -1
- package/specialists/AGENTS.md +4 -3
- package/specialists/README.md +26 -6
- package/specialists/entry/specialist-cli.sh +2 -1
- package/specialists/entry/specialist-install.sh +459 -0
- package/specialists/entry/specialist-install.test.ts +95 -0
|
@@ -14,6 +14,7 @@ export interface AgentConfig {
|
|
|
14
14
|
/** Name callers use: `astro.<name>` for bundled agents, `<name>` otherwise. */
|
|
15
15
|
name: string;
|
|
16
16
|
description: string;
|
|
17
|
+
/** Strict allowlist; undefined means Pi's defaults and an empty list means no tools at all (`tools: none`). */
|
|
17
18
|
tools?: string[];
|
|
18
19
|
skills: string[];
|
|
19
20
|
model?: string;
|
|
@@ -101,7 +102,7 @@ export function parseAgentFile(content: string, filePath: string, source: AgentS
|
|
|
101
102
|
return {
|
|
102
103
|
name: source === "bundled" ? `${BUNDLED_NAMESPACE}.${localName}` : localName,
|
|
103
104
|
description: frontmatter.description.trim(),
|
|
104
|
-
tools: tools.length > 0 ? tools : undefined,
|
|
105
|
+
tools: tools.length === 1 && tools[0] === "none" ? [] : tools.length > 0 ? tools : undefined,
|
|
105
106
|
skills: parseList(frontmatter.skills),
|
|
106
107
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
107
108
|
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
@@ -53,6 +53,12 @@ describe("child arguments", () => {
|
|
|
53
53
|
expect(args).toEqual(["--mode", "json", "-p", "--no-session", "--model", "anthropic/claude-x", "--thinking", "high", "--append-system-prompt", "/tmp/p.md", "--", "Task: t"]);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it("disables every tool for an agent declared with tools: none", () => {
|
|
57
|
+
const args = buildChildArgs({ agent: { ...agent, tools: [] }, task: "t", promptFile: null, defaults: {} });
|
|
58
|
+
expect(args).toContain("--no-tools");
|
|
59
|
+
expect(args).not.toContain("--tools");
|
|
60
|
+
});
|
|
61
|
+
|
|
56
62
|
it("does not pass the parent's thinking level to an agent that pins its own model", () => {
|
|
57
63
|
const pinned: AgentConfig = { ...agent, model: "m" };
|
|
58
64
|
expect(buildChildArgs({ agent: pinned, task: "t", promptFile: null, defaults: { thinkingLevel: "high" } })).not.toContain("--thinking");
|
|
@@ -104,7 +104,8 @@ export function buildChildArgs({ agent, task, promptFile, defaults, session, fil
|
|
|
104
104
|
if (model) args.push("--model", model);
|
|
105
105
|
const thinking = agent.thinking ?? (agent.model ? undefined : defaults.thinkingLevel);
|
|
106
106
|
if (thinking) args.push("--thinking", thinking);
|
|
107
|
-
if (agent.tools
|
|
107
|
+
if (agent.tools?.length === 0) args.push("--no-tools");
|
|
108
|
+
else if (agent.tools) args.push("--tools", agent.tools.join(","));
|
|
108
109
|
if (!agent.inheritProjectContext) args.push("--no-context-files");
|
|
109
110
|
if (!agent.inheritSkills) args.push("--no-skills");
|
|
110
111
|
if (promptFile) args.push(agent.systemPromptMode === "append" ? "--append-system-prompt" : "--system-prompt", promptFile);
|
package/package.json
CHANGED
package/skills/media/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ Answer "what is trending" with `trending` and list the items in this format, one
|
|
|
41
41
|
|
|
42
42
|
`{rank}. [{title} ({year})](<https://www.imdb.com/title/{imdb_id}/>) — {imdb_rating} · 🔥{popularity rounded} {✅ in library | ❌ not | ❓ unknown}`
|
|
43
43
|
|
|
44
|
-
Leave out the rating part when it is null. Add nothing else
|
|
44
|
+
Leave out the rating part when it is null. Add nothing else. When the user then names items by number ("add 2 and 5 from the movies"), map the numbers to that list's `imdb_id` values; if the list is not in this conversation, run `trending` again.
|
|
45
45
|
|
|
46
46
|
## Failures
|
|
47
47
|
|
package/specialists/AGENTS.md
CHANGED
|
@@ -25,11 +25,11 @@ specialists/<name>/run.ts one wrapper per specialist (arcane, identity, ne
|
|
|
25
25
|
specialists/wrappers.test.ts validators, tables, help
|
|
26
26
|
specialists/provision/ provision.sh (Vaultwarden to files)
|
|
27
27
|
specialists/install/ install.sh (package to ~/.specialists/bin, arcane-cli download)
|
|
28
|
-
specialists/entry/ specialist-cli.sh (sudo entry point), remote/ (forced commands for SSH targets, the VPS sudoers list and its install script)
|
|
28
|
+
specialists/entry/ specialist-cli.sh (sudo entry point), specialist-install.sh (root installer for Node, wrappers, arcane-cli, trendarr), remote/ (forced commands for SSH targets, the VPS sudoers list and its install script)
|
|
29
29
|
agents/<name>.md, skills/<name>/SKILL.md
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Wrappers ship as TypeScript and run under Node 24 type stripping: erasable syntax only (no enums, namespaces, parameter properties), explicit `.ts` import extensions. `install.sh`
|
|
32
|
+
Wrappers ship as TypeScript and run under Node 24 type stripping: erasable syntax only (no enums, namespaces, parameter properties), explicit `.ts` import extensions. `install.sh` (first install) and `specialist-install wrappers` (updates) copy `lib/` and each `run.ts` into `~/.specialists/bin/`; the entry point runs them with the root-owned Node, `/usr/local/libexec/specialist-node/current/bin/node`.
|
|
33
33
|
|
|
34
34
|
## Rules for wrapper code
|
|
35
35
|
|
|
@@ -53,6 +53,7 @@ Wrappers ship as TypeScript and run under Node 24 type stripping: erasable synta
|
|
|
53
53
|
- The wrapper's repository commands are the `git` group; `gitops` must stay free because it is an `arcane-cli` command group (server-side syncs).
|
|
54
54
|
- Every passthrough call writes a private per-call copy of `arcanecli.yml` with `jwt_token` set, because some `arcane-cli` commands validate the config file before reading `ARCANE_TOKEN`. The copy is removed after the call.
|
|
55
55
|
- Every wrapper answers `--help` without reading config, so agents can discover subcommands before anything else is set up.
|
|
56
|
+
- `entry/specialist-install.sh` runs as root, so it runs only `/usr/bin`, `/bin`, `/usr/sbin`, `/sbin` and the Node it installed, never Homebrew or anything else the main user can write; it checks that `/usr/local/libexec/specialist-node` and its parents belong to root and are writable by nobody else. Arguments are checked before the root test (tests exercise that part). Downloads go to a root-only temporary directory and are verified before use: Node against `SHASUMS256.txt` and the LTS flag in `index.json`, npm tarballs against the registry's `sha512` integrity, arcane-cli against the release's checksums file. Inside `/Users/specialist` it works only as `specialist` (`sudo -u specialist -H`), from a staging directory root hands over, so a link the specialist plants cannot redirect a root write. Each component switches by rename after a check (every installed wrapper's `--help`, `--version` of the new binary) and restores the previous copy when the check fails. Older versions are refused; `rollback` swaps current and previous. Homebrew's `bw` and `jq` (provision) and `gcloud` (edge) still run as `specialist`; that is a separate issue.
|
|
56
57
|
- `main()` waits for stdout and stderr to flush before `process.exit`: on a pipe, exiting right after a write drops everything past the 64 KiB buffer, which cut large answers such as `projects list --all` until 2026-09-25 (`lib/flush.test.ts`).
|
|
57
58
|
- `arcane update-report [--all]` is `lib/image-updates.ts` over `lib/registry.ts`: a fresh `images updates check --all`, then `projects list --all` for the stack and service of each image reference, then the registries' tag lists and manifest digests (anonymous bearer tokens from each registry's challenge; digests by HEAD, which Docker Hub does not count as a pull; lscr.io is read from ghcr.io). A floating or major-series tag is resolved to the release tag whose digest matches the running and the latest build, searching the newest tags of the three commonest release shapes; a fixed tag is compared with the newest tag of the same shape; a digest pin with the newest release. Checks older than one minute before the run count as stale and show as unknown. Private images (anonymous access refused with 401 or 403) are excluded from rows, counts, and table, listed in `privateImages`, and counted in the summary line. The table is fixed-width text for a code block, because Discord renders no Markdown tables.
|
|
58
59
|
- `media` runs trendarr unchanged: `trendarrCli` in `media.json` is its `dist/cli.js`, started with `process.execPath` because sudo's PATH has no node. Each call gets `HOME` set to a fresh `0700` directory under `~/.specialists/work/media/` that holds only `.trendarr/config.json` (`0600`, trendarr's own schema, built from the note fields and `media.json`) and is removed in `finally`; trendarr reads no environment variables and has no config flag, and Node's `os.homedir()` follows `$HOME` on POSIX. Every call passes `--json`; `init` is refused. An add prints trendarr's per-ID `results` and exits 0 even when trendarr exits 1 for a failed ID, because the gate shows only stderr on failure; other failures carry trendarr's stderr without its `warning:` lines, which go into the JSON as `warnings`. The bridge's slash commands and Add buttons call the same wrapper through the entry point (`extensions/astro-discord/media.ts`).
|
|
@@ -66,7 +67,7 @@ Wrappers ship as TypeScript and run under Node 24 type stripping: erasable synta
|
|
|
66
67
|
|
|
67
68
|
1. Identity: a Vaultwarden note named like the specialist; a Zitadel service user when the service accepts OIDC, else the service's own key; SSH key per target when the only path is SSH.
|
|
68
69
|
2. `specialists/<name>/run.ts` with `main("<name>", command)`, typed subcommands, `--help`, and tables for any fixed remote commands. Add the entry script under `entry/remote/` and its copy in the homelab repository.
|
|
69
|
-
3. `install.sh`: add the name to the copy loop and the `rm -rf` line. `entry/specialist-cli.sh`: add the name to the allowlist, then reinstall
|
|
70
|
+
3. `install.sh`: add the name to the copy loop and the `rm -rf` line. `entry/specialist-cli.sh` and `entry/specialist-install.sh`: add the name to the allowlist and to `SERVICES` (a test keeps them equal), then reinstall both root-owned copies on the host.
|
|
70
71
|
4. `agents/<name>.md` with `tools: read, <name>, subagent`, `skills: <name>`, `systemPromptMode: replace`, `inheritProjectContext: false`, `inheritSkills: false`, `maxSubagentDepth: 1`. `skills/<name>/SKILL.md` with services, subcommands, output shapes, limits, and when to stop.
|
|
71
72
|
5. Tests: validators and tables in `wrappers.test.ts`; anything shared in `lib/`.
|
|
72
73
|
6. Host: note fields, `provision`, config file, `specialists.json` entry, target entry script. Document the fields in the homelab `03-cortex-macmini/ROTATION.md` and host facts in `03-cortex-macmini/README.md`.
|
package/specialists/README.md
CHANGED
|
@@ -21,6 +21,7 @@ Call one with `/run astro.<name> -- <task>` or through the `subagent` tool (both
|
|
|
21
21
|
|
|
22
22
|
- Credentials and CLIs belong to a dedicated macOS user `specialist` on the host. Agents run as the main user and reach the wrappers only through `sudo -n -u specialist -H /usr/local/bin/specialist-cli`, the single passwordless sudo rule. General sudo asks for a password.
|
|
23
23
|
- Credentials come from Vaultwarden (`homelab` / `specialists`, one Secure Note per specialist) through `specialist-cli provision`, into `0600` files. Wrappers never call `bw`.
|
|
24
|
+
- The wrappers run on a root-owned Node in `/usr/local/libexec/specialist-node/` (latest LTS from nodejs.org), not on Homebrew's, because the main user owns all of `/opt/homebrew` and could otherwise swap the interpreter that reads every secret. `/usr/local/sbin/specialist-install` (root, one passwordless sudo rule) installs that Node, the wrappers, arcane-cli, and trendarr from downloads it checks against the publisher's checksums; it refuses older versions and keeps the previous one for `rollback`. Still open: `provision` runs Homebrew's `bw` and `jq`, and `edge` runs Homebrew's `gcloud`, both as `specialist`.
|
|
24
25
|
- Arcane is reached with a Zitadel service user: a fresh token per call, exchanged for a 300 s Arcane token. Other services use API keys, a PAT, tokens, or SSH keys that the targets pin to forced commands. The VPS is reached through `gcloud compute start-iap-tunnel` with a service account that may only open IAP tunnels and read instances; its gcloud state lives in `/Users/specialist/.specialists/work/gcloud`.
|
|
25
26
|
- Every call is logged in `/Users/specialist/.specialists/audit.log`.
|
|
26
27
|
- The `specialist-gate` extension registers the tool only inside the matching specialist child and blocks other agents from calling the CLIs or reading `/Users/specialist`. `security-guard` carries the same blocks.
|
|
@@ -29,7 +30,7 @@ Limits: `network` writes only firewall policies on UniFi. `security` is read-onl
|
|
|
29
30
|
|
|
30
31
|
## Enable specialists on a host
|
|
31
32
|
|
|
32
|
-
Host: Cortex (macOS
|
|
33
|
+
Host: Cortex (macOS on Apple silicon; Homebrew `bw`, `jq`, `gcloud-cli` with `python@3.14`, git; the specialists' Node and trendarr come from `specialist-install`). Steps 1 to 3 are one-time on the services; 4 to 8 are per host.
|
|
33
34
|
|
|
34
35
|
1. **Vaultwarden.** Account `specialists@monadeo.com` with **Can view** on collection `specialists` in organization `homelab`. Secure Notes with Hidden fields (`_B64` fields hold `base64 -i <keyfile> | tr -d '\n'`):
|
|
35
36
|
- `arcane`: `ZITADEL_KEY_JSON`, `GITOPS_DEPLOY_KEY_B64`, `ZITADEL_USER_ID` (plain).
|
|
@@ -51,10 +52,14 @@ Host: Cortex (macOS, Homebrew `node`, `bw`, `jq`, `gcloud-cli` with `python@3.14
|
|
|
51
52
|
sudo chmod 0440 /etc/sudoers.d/cortex /etc/sudoers.d/specialists && sudo rm -f /etc/sudoers.d/cortex-nopasswd && sudo visudo -c
|
|
52
53
|
sudo sysadminctl -addUser specialist -fullName "Specialists" -home /Users/specialist -shell /bin/zsh -password -
|
|
53
54
|
sudo dscl . create /Users/specialist IsHidden 1 && sudo createhomedir -c -u specialist && sudo chmod 700 /Users/specialist
|
|
55
|
+
sudo install -d -m 0755 -o root -g wheel /usr/local/sbin
|
|
56
|
+
sudo install -m 0755 -o root -g wheel <package>/specialists/entry/specialist-install.sh /usr/local/sbin/specialist-install
|
|
57
|
+
printf 'cortex ALL=(root) NOPASSWD: /usr/local/sbin/specialist-install\n' > /tmp/specialist-install.sudoers && sudo visudo -cf /tmp/specialist-install.sudoers && sudo install -m 0440 -o root -g wheel /tmp/specialist-install.sudoers /etc/sudoers.d/specialist-install && rm /tmp/specialist-install.sudoers
|
|
58
|
+
sudo /usr/local/sbin/specialist-install node 24.22.0
|
|
54
59
|
sudo install -m 0755 -o root -g wheel <package>/specialists/entry/specialist-cli.sh /usr/local/bin/specialist-cli
|
|
55
60
|
brew install --cask gcloud-cli
|
|
56
61
|
```
|
|
57
|
-
Add `cortex ALL=(specialist) NOPASSWD: ALL` as `/etc/sudoers.d/specialists-setup` (mode `0440`) for the duration of steps 5 to 7, then delete it. `specialist-cli`
|
|
62
|
+
`specialist-cli` runs the wrappers with the Node that `specialist-install node` put in place, so that Node comes first. Add `cortex ALL=(specialist) NOPASSWD: ALL` as `/etc/sudoers.d/specialists-setup` (mode `0440`) for the duration of steps 5 to 7, then delete it. `specialist-cli` and `specialist-install` hold the same list of services; after an update that adds one (such as `media`), install both again with the same `sudo install` lines.
|
|
58
63
|
5. **Bootstrap**, as `specialist` (`sudo -u specialist -H zsh`, `umask 077`): `~/.config/specialists/client.env` with `BW_SERVER=https://vault.37pla.net`, `BW_CLIENTID`, `BW_CLIENTSECRET`; `~/.config/specialists/master-password`. Both `0600`.
|
|
59
64
|
6. **Install and configure**, as `specialist`: `sh <package>/specialists/install/install.sh <package> 2.12.0` (`<package>` is the installed pi-astro directory, `~/.pi/agent/npm/node_modules/@astrofoundry/pi-astro`). Write `~/.specialists/config/<name>.json` for every specialist:
|
|
60
65
|
```json
|
|
@@ -74,13 +79,28 @@ Host: Cortex (macOS, Homebrew `node`, `bw`, `jq`, `gcloud-cli` with `python@3.14
|
|
|
74
79
|
{ "apiUrl": "https://10.0.10.37:8006", "certSha256": "<from: specialist-cli proxmox -- fingerprint>", "node": "pulsar", "pulsarHost": "10.0.10.37", "pulsarUser": "root" }
|
|
75
80
|
{ "nexusHost": "10.0.40.38", "nexusUser": "nexus", "hermesHost": "10.0.40.31", "hermesUser": "hermes", "pulsarHost": "10.0.10.37", "pulsarUser": "root",
|
|
76
81
|
"europaHealthUrl": "http://10.0.60.5:8081/health", "hermesRepo": "NousResearch/hermes-agent" }
|
|
77
|
-
{ "trendarrCli": "
|
|
82
|
+
{ "trendarrCli": "/Users/specialist/.specialists/bin/trendarr/node_modules/@astrofoundry/trendarr/dist/cli.js",
|
|
78
83
|
"radarrUrl": "http://10.0.40.30:7878", "radarrQualityProfileId": 7, "radarrRootFolderPath": "/media/movies",
|
|
79
84
|
"sonarrUrl": "http://10.0.40.30:8989", "sonarrQualityProfileId": 7, "sonarrRootFolderPath": "/media/tv", "sonarrMonitor": "all",
|
|
80
85
|
"sabnzbdUrl": "http://10.0.40.30:8084", "maxEntries": 10 }
|
|
81
86
|
```
|
|
82
87
|
The `media` wrapper writes trendarr's `~/.trendarr/config.json` for each call into a private temporary HOME under `~/.specialists/work/media/` and removes it afterwards, so trendarr itself is never configured with `trendarr init`. Cortex reaches the three ports on `10.0.40.30` within its own VLAN.
|
|
83
|
-
Then `sudo -n -u specialist -H /usr/local/bin/specialist-cli provision`; it lists every note and field.
|
|
88
|
+
`sudo /usr/local/sbin/specialist-install trendarr <version>` installs trendarr behind the link `~/.specialists/bin/trendarr`, so that path stays valid across updates. Then `sudo -n -u specialist -H /usr/local/bin/specialist-cli provision`; it lists every note and field.
|
|
89
|
+
|
|
90
|
+
Later updates go through the installer, one component at a time, each checked before and after the switch:
|
|
91
|
+
```sh
|
|
92
|
+
sudo /usr/local/sbin/specialist-install node <LTS version>
|
|
93
|
+
sudo /usr/local/sbin/specialist-install wrappers <pi-astro version>
|
|
94
|
+
sudo /usr/local/sbin/specialist-install arcane-cli <Arcane server version>
|
|
95
|
+
sudo /usr/local/sbin/specialist-install trendarr <version>
|
|
96
|
+
sudo /usr/local/sbin/specialist-install rollback node|wrappers|arcane-cli|trendarr
|
|
97
|
+
sudo /usr/local/sbin/specialist-install status
|
|
98
|
+
```
|
|
99
|
+
- `node` accepts only an LTS release listed in `https://nodejs.org/dist/index.json`, checks the tarball against `SHASUMS256.txt`, and switches `current` only after every installed wrapper answers `--help` with it; `previous` keeps the last one.
|
|
100
|
+
- `wrappers` takes the pi-astro tarball from the npm registry, checks its `sha512` integrity, copies what `install.sh` copies, writes `~/.specialists/bin/VERSION`, and restores the old files if any wrapper fails `--help`. `install.sh` stays the first-time installer (it also writes the default config files).
|
|
101
|
+
- `arcane-cli` checks the release tarball against `arcane_<version>_checksums.txt`; it follows the Arcane server's version.
|
|
102
|
+
- `trendarr` installs with the root-owned Node's npm, without install scripts, into `trendarr-<version>`.
|
|
103
|
+
- A version older than the installed one is refused; `rollback` swaps back to the kept previous copy. `status` prints the installed versions as JSON.
|
|
84
104
|
7. **SSH targets.** Each target gets an entry script from `entry/remote/` as `/usr/local/sbin/<entry>` (`0755`, root) and the public key in the right `authorized_keys` as
|
|
85
105
|
`command="/usr/local/sbin/<entry>",no-port-forwarding,no-agent-forwarding,no-pty,no-X11-forwarding ssh-ed25519 ...`:
|
|
86
106
|
- VM 104, user `dmz`: `spc-identity-dmz-entry.sh`.
|
|
@@ -113,8 +133,8 @@ Check: `sudo -n -u specialist -H /usr/local/bin/specialist-cli arcane --caller t
|
|
|
113
133
|
|
|
114
134
|
The `astro-discord` extension gives Discord users access to the specialists from a channel. It runs in a headless Pi host on Cortex (LaunchAgent `com.astrofoundry.astro-discord`, user `cortex`), never in interactive sessions.
|
|
115
135
|
|
|
116
|
-
- Channels are configured one by one (`channels.<id>`): `trigger` is `always` (every message that names a specialist counts) or `mention` (only messages that start with `@Cortex` or reply to the bot); `specialists` is `*` or a list. A channel with one specialist needs no prefix: every message there is a task for it. In a channel with several, `dns list the zones` names the specialist; an addressed message without a name gets a reply asking for one. Threads inherit their parent channel's entry; unlisted channels are ignored. The bot replies at once with "<specialist> is working on your task" and shows the typing indicator until the answer replaces that reply. Reactions on your message: ⏳ running, 🔒 waiting for an owner's approval, ✅ done, ❌ failed. `help` shows the specialists you may use in that channel and the commands (`status` lists running tasks; owners in the admin channel also see the `config` commands). Each channel and specialist has one conversation: a reply to the bot's answer, or another task for that specialist within 30 minutes, continues it with everything the specialist saw before; otherwise a new one starts, and a task beginning with `new` forces that. Replied-to and forwarded messages (text, embeds, attachment names) travel with the task, so "check this" on a forwarded alert works. Image attachments on the message or on what it quotes (up to 4, 8 MiB each) are downloaded and passed to the specialist, which sees them; other file types are named only. Sessions live under `~/.config/astro-discord/sessions/<specialist>/` and are deleted after 7 days. Long answers arrive as a `.md` attachment. When the bridge restarts, open approval requests are marked cancelled and running tasks interrupted in Discord; ask again.
|
|
117
|
-
- Media: where the `media` specialist is present (`config channel <#channel> always media`), `/trending [type] [min_rating] [count]`, `/search <title> [type]`, and `/downloads <status|pause|resume>` run the wrapper without a model. Lists arrive as two messages, Movies and Series, each title with a button beside it: **Add** adds it at once and turns into Added, In library, or Not found (Retry after a failure); a greyed-out In library or No IMDb ID button needs nothing. Notes such as "downloads are paused" go only to the user who clicked. Owners and `access.media` users may use them; anyone else gets a refusal only they see, and in a channel without `media` the bot names the channels that have it. Plain messages in the channel still go to the agent, for requests by name ("add the new Dune"). At startup the bridge sets the three commands as the application's global commands (servers only).
|
|
136
|
+
- Channels are configured one by one (`channels.<id>`): `trigger` is `always` (every message that names a specialist counts) or `mention` (only messages that start with `@Cortex` or reply to the bot); `specialists` is `*` or a list. A channel with one specialist needs no prefix: every message there is a task for it. In a channel with several, `dns list the zones` names the specialist; an addressed message without a name gets a reply asking for one. Threads inherit their parent channel's entry; unlisted channels are ignored. The bot replies at once with "<specialist> is working on your task" and shows the typing indicator until the answer replaces that reply. Reactions on your message: ⏳ running, 🔒 waiting for an owner's approval, ✅ done, ❌ failed. `help` shows the specialists you may use in that channel and the commands (`status` lists running tasks; owners in the admin channel also see the `config` commands). Each channel and specialist has one conversation: a reply to the bot's answer, or another task for that specialist within 30 minutes, continues it with everything the specialist saw before; otherwise a new one starts, and a task beginning with `new` forces that. Replied-to and forwarded messages (text, embeds, attachment names) travel with the task, so "check this" on a forwarded alert works. Image attachments on the message or on what it quotes (up to 4, 8 MiB each) are downloaded and passed to the specialist, which sees them; other file types are named only. Sessions live under `~/.config/astro-discord/sessions/<specialist>/` and are deleted after 7 days. Long answers arrive as a `.md` attachment. When the bridge restarts, open approval requests are marked cancelled and running tasks interrupted in Discord; ask again. Scheduled reports are a list in the config, one entry per report: `"reports": [{ "report": "arcane", "channelId": "<id>", "time": "06:00", "timeZone": "Europe/Madrid" }, { "report": "tools", "channelId": "<admin channel id>", "time": "06:00", "timeZone": "Europe/Madrid" }]` (the earlier single `dailyReport` key is refused; move it into the list). The Arcane report posts an "Arcane Daily Report:" message every morning: each service with an update as current to target version, from a fresh `update-report`; replying to it asks the arcane specialist with the report as context. The tools report posts "Cortex Tools Report:": every tool on Cortex with a newer version (Homebrew packages installed on request and casks, Pi, pi-astro, and through `specialist-install` the wrappers, arcane-cli at the Arcane server's version, trendarr, and the specialist Node at the latest LTS), a release-notes verdict from `astro.release-reviewer` for the tools the specialists build on, an Update button per line, and one Upgrade everything for Homebrew. Only owners can press them; nothing is installed otherwise. After an update that needs it, the bridge, herdr, or Astrogate restarts as soon as nothing runs on it; after a day of waiting, a Restart now button appears. The bridge calls `sudo -n /usr/local/sbin/specialist-install` through the sudoers rule from step 4.
|
|
137
|
+
- Media: where the `media` specialist is present (`config channel <#channel> always media`), `/trending [type] [min_rating] [count]`, `/search <title> [type]`, and `/downloads <status|pause|resume|unpause>` run the wrapper without a model. Lists arrive as two messages, Movies and Series, each title with a button beside it: **Add** adds it at once and turns into Added, In library, or Not found (Retry after a failure); a greyed-out In library or No IMDb ID button needs nothing. Notes such as "downloads are paused" go only to the user who clicked. Owners and `access.media` users may use them; anyone else gets a refusal only they see, and in a channel without `media` the bot names the channels that have it. Plain messages in the channel still go to the agent, for requests by name ("add the new Dune"). At startup the bridge sets the three commands as the application's global commands (servers only).
|
|
118
138
|
- Access: `owners` may use every specialist and decide approvals; `access.<specialist>` lists extra users for that specialist; everyone else gets no reply. Owners change the configuration from the admin channel (`adminChannelId`) without a shell: `config show`, `config channel <#channel> <always|mention> <*|dns,edge>`, `config channel <#channel> remove`, `config access <specialist> add|remove <@user>`; the bot writes `~/.config/astro-discord/config.json` on Cortex, which is also editable by hand and re-read on every message. Deny the bot's role View Channel on channels it must never read; that is the boundary the map cannot provide.
|
|
119
139
|
- Approvals: a risky call (anything with `--confirm`, plus writes such as record or zone deletes, Zitadel changes, Pomerium deploys, service restarts, and on Arcane only destructive actions: `down`, deletes, prunes, image, volume, or network removal, `system upgrade`, `gitops delete`; help lookups never ask) pauses the specialist and posts Approve/Deny buttons; only owners' clicks count; no decision within `approvalTimeoutMinutes` denies it. The specialist then reports the denial. Ambiguous tasks get a question back instead of an action.
|
|
120
140
|
- Setup, as `cortex`: `d=$(mktemp -d) && cp ~/.pi/agent/npm/node_modules/@astrofoundry/pi-astro/extensions/astro-discord/*.ts "$d" && node --disable-warning=ExperimentalWarning "$d/setup.ts"` (Node does not strip types inside `node_modules`, so the files are copied first) prompts for the bot token and the ids, checks each against Discord, and writes `~/.config/astro-discord/{token,config.json,run.sh}` and the LaunchAgent; it prints the `launchctl bootstrap` line. After a pi-astro update restart the bridge with `launchctl kickstart -k gui/$(id -u)/com.astrofoundry.astro-discord`. Log: `~/Library/Logs/astro-discord.log`.
|
|
@@ -17,4 +17,5 @@ if [[ "${1:-}" == "--caller" ]]; then caller="${2:-unknown}"; shift 2; fi
|
|
|
17
17
|
|
|
18
18
|
runner="$HOME/.specialists/bin/$service/run.ts"
|
|
19
19
|
[[ -f "$runner" ]] || { print -u2 "specialist '$service' is not installed"; exit 2; }
|
|
20
|
-
|
|
20
|
+
# The root-owned Node from specialist-install; Homebrew's belongs to the cortex user.
|
|
21
|
+
exec /usr/local/libexec/specialist-node/current/bin/node --disable-warning=ExperimentalWarning "$runner" --caller "$caller" -- "$@"
|