openvibers/openvibe.link Commits

OpenVibe Link: the name, the command and the installer, with openvibe-node kept as the alias (#133)

OP OpenVibers committed 5 days ago · ee5bfe89b3 · 47 files, +408 −384

The product, the site-facing text, the service's display name, the tray, the first run, the CLI help, the docs and
the release title say OpenVibe Link. Every release now carries openvibe-link-<os>-<arch> and the same binary under
the old openvibe-node-<os>-<arch> name, and the installer puts openvibe-link in /usr/local/bin with openvibe-node as
a symlink to it. The service unit, data folders and wire identifiers stay openvibe-node so an upgrade keeps its
pairing and settings, per the plan's rename rule (2026-11-07 §5).

Changes

ddiff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
iindex df2f5d6..740b852 100644
---- a/.github/workflows/release.yml
++++ b/.github/workflows/release.yml
@@@ -33,7 +33,12 @@ jobs:
test -s dist/install.sh
test -s dist/openvibe-node-plugins.tar.gz
test -s dist/SHA256SUMS
++ # The new name and the alias it replaces are both in every release for a year (plan 2026-11-07 §5).
++ test -s dist/openvibe-link-linux-amd64
++ test -s dist/openvibe-node-linux-amd64
++ test -s dist/openvibe-link-windows-amd64.exe
++ test -s dist/openvibe-node-windows-amd64.exe
- name: publish the release
env:
GH_TOKEN: ${{ github.token }}
-- run: gh release create "${GITHUB_REF_NAME}" dist/* --title "OpenVibe Node ${GITHUB_REF_NAME}" --generate-notes
++ run: gh release create "${GITHUB_REF_NAME}" dist/* --title "OpenVibe Link ${GITHUB_REF_NAME}" --generate-notes
ddiff --git a/README.md b/README.md
iindex 23e89cd..845ca0c 100644
---- a/README.md
++++ b/README.md
@@@ -1,4 +1,8 @@
--# OpenVibe Node
++# OpenVibe Link
++
++OpenVibe Link is the new name of OpenVibe Node: the product, the installer and the app's text say Link; the repo is
++still `OpenVibe.Node`, and `openvibe-node` is kept as an alias for existing installs and scripts for a year. The
++service's internal unit name and folders stay `openvibe-node` so upgrades keep the same pairing and settings.
One service for your computers, servers, Raspberry Pis, phones and robots: pair it once with OpenVibe and products
like **OpenVibe.Bot** (robots: drive, cameras, sensors, with safety) and **OpenVibe.Actor** (computer control for AI
@@@ -7,11 +11,11 @@ agents, at the level you set on the machine) can use the device. It connects out
On a robot's Raspberry Pi (or other Linux computer), the one command:
```sh
--curl -fsSL https://node.openvibe.network/install | sh
++curl -fsSL https://openvibe.link/install | sh
```
--The same installer is also at <https://openvibe.bot/install>, which keeps working; `node.openvibe.network` is the
--Node's own address, where it also has a page that says what it is and how to install it.
++The same installer is also at <https://openvibe.bot/install>, which keeps working; `node.openvibe.network/install`
++and the old `node.openvibe.network` pages keep working and send you to openvibe.link.
It turns on I2C and SPI, installs the camera tools, finds the kit (an Adeept ADR036 4WD, a ROS 2 robot), shows a code
to approve at openvibe.bot/link, and pairs: the robot is made, drivable on its panel and, if you chose it, live on your
@@@ -20,7 +24,7 @@ OpenVibe.Live channel ([docs/install.md](docs/install.md#the-one-command-raspber
With a robot already added on openvibe.bot:
```sh
--curl -fsSL https://node.openvibe.network/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept
++curl -fsSL https://openvibe.link/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept
```
openvibe.bot shows this command with the robot's id and a fresh code. `--driver` is `adeept`, `adeept-mecanum`, `cozmo`, `ros2`,
@@@ -28,13 +32,13 @@ openvibe.bot shows this command with the robot's id and a fresh code. `--driver`
OpenVibe.Network gets `--network <URL> --pairing pair_… --code ABCD-1234` instead of `--robot`. Details, manual
install, Windows and configuration: [docs/install.md](docs/install.md).
--**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-node share 8790`
++**Put a port on the web** with [OpenVibe Connect](https://connect.openvibe.website): `openvibe-link share 8790`
serves the share this computer was picked for at `https://<name>--<you>.openvibe.website` behind OpenVibe sign-in (or
--`openvibe-node share --key ovc_… --port 8790` with a key), and `openvibe-node share add` keeps it up with the service;
--`openvibe-node status` lists each share ([docs/connect.md](docs/connect.md)).
++`openvibe-link share --key ovc_… --port 8790` with a key), and `openvibe-link share add` keeps it up with the service;
++`openvibe-link status` lists each share ([docs/connect.md](docs/connect.md)).
**Sell a local model** on [OpenVibe.Exchange](https://openvibe.exchange):
--`openvibe-node exchange serve --model qwen3:32b --public-url https://gpu.example.com/v1`. It serves the model from the
++`openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.com/v1`. It serves the model from the
local OpenAI-compatible server (Ollama by default at `127.0.0.1:11434`), registers the public https address through the
Exchange's endpoint-seller flow with your `OPENVIBE_DEVELOPER_KEY`, and removes the offer when you stop it with Ctrl-C.
The public URL must be reachable by the Exchange and is judged by its egress guard; keep it behind your own tunnel or
@@@ -42,18 +46,18 @@ reverse proxy. The local listener has no buyer authentication (the Exchange's en
bind it to loopback, which is the default.
```sh
--openvibe-node # guided first run: pair, check the link, report this machine's first job
--openvibe-node status # link, plugins, video, battery, shares, stop latch
--openvibe-node stop # kill switch: stop every motor now and keep it stopped
--openvibe-node resume
--openvibe-node quit # end the node cleanly (nothing latched), e.g. from a stream launcher
--openvibe-node approvals # the computer plugin at level ask: what waits; approve <id> / deny <id>
--openvibe-node tray # the same in your notification area, with Approve/Deny notifications (Linux, Windows)
--openvibe-node memory scan # what your coding tools remember: read-only, local, secrets removed
--openvibe-node memory upload # send what they remember to your own memory store, after you say yes
--openvibe-node memory sync # write your profile and rules back into the tools, one marked block per file, opt-in per tool
--openvibe-node skills sync # write the skills and rules you chose at openvibe.codes/skills into each tool, with a dry run and undo
--openvibe-node exchange serve --model qwen3:32b --public-url https://gpu.example.com/v1 # sell a local model
++openvibe-link # guided first run: pair, check the link, report this machine's first job
++openvibe-link status # link, plugins, video, battery, shares, stop latch
++openvibe-link stop # kill switch: stop every motor now and keep it stopped
++openvibe-link resume
++openvibe-link quit # end the node cleanly (nothing latched), e.g. from a stream launcher
++openvibe-link approvals # the computer plugin at level ask: what waits; approve <id> / deny <id>
++openvibe-link tray # the same in your notification area, with Approve/Deny notifications (Linux, Windows)
++openvibe-link memory scan # what your coding tools remember: read-only, local, secrets removed
++openvibe-link memory upload # send what they remember to your own memory store, after you say yes
++openvibe-link memory sync # write your profile and rules back into the tools, one marked block per file, opt-in per tool
++openvibe-link skills sync # write the skills and rules you chose at openvibe.codes/skills into each tool, with a dry run and undo
++openvibe-link exchange serve --model qwen3:32b --public-url https://gpu.example.com/v1 # sell a local model
```
## What is in here
@@@ -72,16 +76,16 @@ openvibe-node exchange serve --model qwen3:32b --public-url https://gpu.example.
pairing, the control link, drive/actuator commands, telemetry, heartbeat, deadman and e-stop — but no jobs or video.
**Community robots:** a profile people submit on openvibe.bot (`community.<name>`) is reviewed by an OpenVibe admin
--and signed by Bot (Ed25519 over its canonical JSON). `openvibe-node profile verify community.<name>` checks that
++and signed by Bot (Ed25519 over its canonical JSON). `openvibe-link profile verify community.<name>` checks that
signature against the key Bot publishes (`--key-id` pins one) and names the plugin the robots run; pairing with a robot
on a community profile runs the same check and warns when it fails. Install a community plugin only when it passes
([docs/plugins.md](docs/plugins.md#community-plugins)).
**Your computer for your agents:** `--computer observe|ask|trusted|full` adds the `computer` plugin (terminal, files,
screen, mouse and keyboard, and a browser of its own for OpenVibe.Actor), running as your own account, at the level you set on the machine; `ask` waits for
--your answer in the tray, a notification's Approve button or `openvibe-node approve <id>` ([docs/computer.md](docs/computer.md)).
++your answer in the tray, a notification's Approve button or `openvibe-link approve <id>` ([docs/computer.md](docs/computer.md)).
With it on, you can also watch the machine's screen live on openvibe.bot. Only you see it, it runs for up to 30
--minutes, and `openvibe-node stop` ends it at once ([the live screen](docs/computer.md#the-live-screen-screen_stream-node-v0120)).
++minutes, and `openvibe-link stop` ends it at once ([the live screen](docs/computer.md#the-live-screen-screen_stream-node-v0120)).
**Your own models for your OpenVibe work:** the `ai` plugin lets OpenVibe use a model you already run at home (Ollama,
LM Studio or llama.cpp's server) for your own work, at no cost and with no open port, once you turn on "Let my AI use
@@@ -95,18 +99,18 @@ the service's HOME to your home, and says how to sign in a tool that is not read
login's content.
**What the machine does for the network:** the owner sets the machine's roles on openvibe.network/machines
--(`codes.worker`, `desks.host`, `ai.host`, `bot.robot`). The Node reads them at its check-in, keeps them, and applies
++(`codes.worker`, `desks.host`, `ai.host`, `bot.robot`). Link reads them at its check-in, keeps them, and applies
the ones this machine was installed for: the job worker starts if the local configuration has it, and the machine says
plainly which role it could not honour. The local configuration always wins: a role never installs anything and never
turns on a feature you turned off here.
--**Link's own window:** the tray's **Open Link** (or `openvibe-node panel`) opens a small page on the machine itself
++**Link's own window:** the tray's **Open Link** (or `openvibe-link panel`) opens a small page on the machine itself
at `127.0.0.1` that shows what runs here, what this computer allows, your shares, the coding-agent CLIs and the Codes
tasks this machine ran today, and what happened, and lets you pause everything, stop one thing, turn a capability off,
share a port, or send files (not in this version). It is served only on loopback, with a per-run token in the address
fragment that every API call must send; see [docs/panel.md](docs/panel.md) for the security rules.
--**Link on your phone (Android, v0):** the same pairing and connection as the desktop Node, in an app: your agents'
++**Link on your phone (Android, v0):** the same pairing and connection as desktop Link, in an app: your agents'
approvals in your pocket (money, passwords, deletions and a send to a new recipient ask for a fingerprint first), the
Dock's activity read-only, and **Send to OpenVibe** from any app's share sheet to your Download drive. It pairs from
openvibe.bot/link/phone and shows up as a device of kind `phone`. No phone automation at all in this version: no
@@@ -115,7 +119,7 @@ AccessibilityService, no screen capture. Build, install and the security notes:
Docs: [protocol](docs/protocol.md) (the wire format with OpenVibe.Bot), [plugins](docs/plugins.md) (the driver
protocol, the bundled drivers, and where Actor's computer-control plugin attaches), [install](docs/install.md),
[panel](docs/panel.md) (Link's own window and its security rules),
--[memory](docs/memory.md) (what the coding tools on a machine remember, how the Node reads it and how it is uploaded to
++[memory](docs/memory.md) (what the coding tools on a machine remember, how Link reads it and how it is uploaded to
your own memory store), [skills](docs/skills.md) (the skills hub: which folders each tool reads, the marked block, the
dry run and the undo).
Decisions: [ADR-043](docs/ADR-043-bot-devices-and-control.md).
ddiff --git a/cmd/openvibe-node/desk_sandbox.go b/cmd/openvibe-node/desk_sandbox.go
iindex d6789bf..b8947c4 100644
---- a/cmd/openvibe-node/desk_sandbox.go
++++ b/cmd/openvibe-node/desk_sandbox.go
@@@ -20,7 +20,7 @@ type listFlag []string
func (l *listFlag) String() string { return strings.Join(*l, ",") }
func (l *listFlag) Set(v string) error { *l = append(*l, v); return nil }
--const deskSandboxUsage = `usage: openvibe-node desk-sandbox [--home DIR] [--memory-mb N] [--cpu-millis N] [--disk-mb N] [--pids N]
++const deskSandboxUsage = `usage: openvibe-link desk-sandbox [--home DIR] [--memory-mb N] [--cpu-millis N] [--disk-mb N] [--pids N]
[--bind PATH]... [--setenv NAME=VALUE]... [--egress public] -- PROGRAM [ARG]...
Runs one desk's program (its browser) confined, for a desk host that serves people other than its owner: in the
@@@ -33,7 +33,7 @@ program's code; 125 when the sandbox could not be set up.
// deskPipe answers the desk's two pipe ends (its fds 3 and 4); tests replace it.
var deskPipe = inheritedDeskPipe
--// cmdDeskSandbox is `openvibe-node desk-sandbox`; it parses its own flags, up to "--", so the program's flags (a
++// cmdDeskSandbox is `openvibe-link desk-sandbox`; it parses its own flags, up to "--", so the program's flags (a
// browser's) are never read as the Node's.
func cmdDeskSandbox(args []string, stderr io.Writer) int {
fs := flag.NewFlagSet("desk-sandbox", flag.ContinueOnError)
ddiff --git a/cmd/openvibe-node/desk_sandbox_test.go b/cmd/openvibe-node/desk_sandbox_test.go
iindex 26f7f64..5f8ebd1 100644
---- a/cmd/openvibe-node/desk_sandbox_test.go
++++ b/cmd/openvibe-node/desk_sandbox_test.go
@@@ -13,7 +13,7 @@ import (
// config with no worker.run_as.
func TestDeskSandboxCommand(t *testing.T) {
var out, errb bytes.Buffer
-- if code := run([]string{"desk-sandbox"}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "usage: openvibe-node desk-sandbox") {
++ if code := run([]string{"desk-sandbox"}, &out, &errb); code != 2 || !strings.Contains(errb.String(), "usage: openvibe-link desk-sandbox") {
t.Fatalf("no program: code %d, stderr %q", code, errb.String())
}
errb.Reset()
ddiff --git a/cmd/openvibe-node/first_run.go b/cmd/openvibe-node/first_run.go
iindex b505304..b670007 100644
---- a/cmd/openvibe-node/first_run.go
++++ b/cmd/openvibe-node/first_run.go
@@@ -23,12 +23,12 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/protocol"
)
--// The guided first run is what a bare `openvibe-node` does: pair (when needed), check the way to OpenVibe, run one
++// The guided first run is what a bare `openvibe-link` does: pair (when needed), check the way to OpenVibe, run one
// harmless first job (this machine's OS, CPUs, memory and free disk) and report one event per step to Bot over the
--// link the Node already uses. `openvibe-node first-run` is the same command, for scripts that want to say it.
++// link the Node already uses. `openvibe-link first-run` is the same command, for scripts that want to say it.
const firstRunDocs = "https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs/install.md"
--// guideArgs are the flags a bare `openvibe-node` (or `openvibe-node first-run`) takes. They mirror the pair flags so
++// guideArgs are the flags a bare `openvibe-link` (or `openvibe-link first-run`) takes. They mirror the pair flags so
// a machine's installer command can be pasted at the prompt, and so scripts can pass the code and Network pairing.
type guideArgs struct {
code, robot, name, server, network, pairing string
@@@ -56,7 +56,7 @@ func parseGuideFlags(args []string, g *globals, a *guideArgs, stderr io.Writer)
// never asks again once a credential exists.
func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader, stdout io.Writer) error {
if len(positional) > 1 {
-- return errors.New("usage: openvibe-node [--code CODE] [--network URL --pairing pair_…] [--home DIR]")
++ return errors.New("usage: openvibe-link [--code CODE] [--network URL --pairing pair_…] [--home DIR]")
}
if len(positional) == 1 && a.code == "" {
a.code = positional[0]
@@@ -71,7 +71,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
fmt.Fprintln(stdout, " sudo openvibe-node")
return fmt.Errorf("cannot write to %s: %w (run with sudo)", g.paths.StateDir, err)
}
-- fmt.Fprintln(stdout, "OpenVibe Node first run")
++ fmt.Fprintln(stdout, "OpenVibe Link first run")
fmt.Fprintln(stdout, "======================")
report, err := firstrun.Load(g.paths.FirstRunFile())
@@@ -118,7 +118,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
if err != nil {
fmt.Fprintln(stdout)
fmt.Fprintln(stdout, "That did not pair. Codes work once and expire; get a fresh one at openvibe.bot/machines")
-- fmt.Fprintln(stdout, "and run `openvibe-node` again.")
++ fmt.Fprintln(stdout, "and run `openvibe-link` again.")
return err
}
at := creds.PairedAt
@@@ -130,7 +130,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
} else if err != nil {
return err
} else {
-- // A pairing done by the installer (or `openvibe-node pair`) already has its time; keep it for the funnel.
++ // A pairing done by the installer (or `openvibe-link pair`) already has its time; keep it for the funnel.
if !report.Has(firstrun.StepPaired) {
at := creds.PairedAt
if at.IsZero() {
@@@ -198,7 +198,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
events := report.Events(version, info.OS, info.Arch)
if len(events) > 0 {
if err := sendFirstRun(g, creds, events); err != nil {
-- fmt.Fprintf(stdout, "Could not report this first run (%v). The report is kept; run `openvibe-node` again when the link is up.\n", err)
++ fmt.Fprintf(stdout, "Could not report this first run (%v). The report is kept; run `openvibe-link` again when the link is up.\n", err)
} else {
fmt.Fprintln(stdout, "Reported this first run to OpenVibe.Bot.")
}
@@@ -206,12 +206,12 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
// (d) What it can do next, in one line, with the docs.
fmt.Fprintln(stdout)
-- fmt.Fprintln(stdout, "Next: `openvibe-node status` shows this machine, `openvibe-node stop` stops everything, and")
++ fmt.Fprintln(stdout, "Next: `openvibe-link status` shows this machine, `openvibe-link stop` stops everything, and")
fmt.Fprintf(stdout, "your account lists it at https://openvibe.bot/machines. Docs: %s\n", firstRunDocs)
return nil
}
--// pairFromGuide redeems the code and stores the credential, the same two paths `openvibe-node pair` takes.
++// pairFromGuide redeems the code and stores the credential, the same two paths `openvibe-link pair` takes.
func pairFromGuide(g *globals, a pairArgs, stdout io.Writer) (*credentials.Credentials, error) {
cfg, noConfig, err := preparePair(g, a)
if err != nil {
@@@ -353,7 +353,7 @@ func parsePairingInput(line string) (pairArgs, error) {
}
}
if a.code == "" {
-- // `openvibe-node pair ABCD-1234` pasted without --code.
++ // `openvibe-link pair ABCD-1234` pasted without --code.
for i := 0; i+1 < len(fields); i++ {
if fields[i] == "pair" && !strings.HasPrefix(fields[i+1], "-") {
a.code = fields[i+1]
ddiff --git a/cmd/openvibe-node/first_run_test.go b/cmd/openvibe-node/first_run_test.go
iindex ef4f315..1f78013 100644
---- a/cmd/openvibe-node/first_run_test.go
++++ b/cmd/openvibe-node/first_run_test.go
@@@ -81,7 +81,7 @@ func TestFirstRunSuccessReportsOneEventPerStep(t *testing.T) {
if code != 0 {
t.Fatalf("exit %d, out %s err %s", code, out, errOut)
}
-- for _, want := range []string{"OpenVibe Node first run", "Paired as", "Self-check: OpenVibe.Bot answers.",
++ for _, want := range []string{"OpenVibe Link first run", "Paired as", "Self-check: OpenVibe.Bot answers.",
"First job: this machine", "system " + runtime.GOOS, "Reported this first run to OpenVibe.Bot.", "Next:", "docs/install.md"} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in output:\n%s", want, out)
@@@ -174,10 +174,10 @@ func TestParsePairingInput(t *testing.T) {
ok bool
}{
{"ABCD-1234", "ABCD-1234", "", "", "", true},
-- {"openvibe-node pair ABCD-1234", "ABCD-1234", "", "", "", true},
++ {"openvibe-link pair ABCD-1234", "ABCD-1234", "", "", "", true},
{"curl -fsSL https://openvibe.bot/install | sh -s -- --network https://openvibe.network --pairing pair_01J8Z4M2Q0R7T9YV3K6N8P1W2Y --code ABCD-1234 --worker",
"ABCD-1234", "https://openvibe.network", "pair_01J8Z4M2Q0R7T9YV3K6N8P1W2Y", "", true},
-- {"openvibe-node pair --code=ABCD-1234 --robot rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X", "ABCD-1234", "", "", "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X", true},
++ {"openvibe-link pair --code=ABCD-1234 --robot rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X", "ABCD-1234", "", "", "rob_01J8Z4M2Q0R7T9YV3K6N8P1W2X", true},
{"", "", "", "", "", false},
{"curl https://openvibe.bot/install | sh", "", "", "", "", false},
}
ddiff --git a/cmd/openvibe-node/main.go b/cmd/openvibe-node/main.go
iindex bd159d1..6332414 100644
---- a/cmd/openvibe-node/main.go
++++ b/cmd/openvibe-node/main.go
@@@ -1,23 +1,23 @@
--// Command openvibe-node connects a computer, server, Raspberry Pi or robot to OpenVibe.
++// Command openvibe-link connects a computer, server, Raspberry Pi or robot to OpenVibe.
//
--// openvibe-node guided first run: pair, self-check, one first job, then what to do next
--// openvibe-node pair <CODE> redeem a one-time pairing code and store the device credential
++// openvibe-link guided first run: pair, self-check, one first job, then what to do next
++// openvibe-link pair <CODE> redeem a one-time pairing code and store the device credential
// (--network/--pairing: pair on OpenVibe.Network and bind the device on Bot)
--// openvibe-node credential import take a rotated credential: reads the rotate response JSON on stdin
--// openvibe-node run run in the foreground (this is also what the service runs)
--// openvibe-node install install and start the system service
--// openvibe-node uninstall stop and remove the system service
--// openvibe-node status show the link, the plugins, the stop latch and video
--// openvibe-node stop local kill switch: stop every actuator and hold it stopped
--// openvibe-node resume release the local kill switch (and a server e-stop)
--// openvibe-node quit end the running node cleanly: every actuator stops, nothing is latched
--// openvibe-node plugins list plugins and their capabilities
--// openvibe-node memory scan [--tools a,b] [--project DIR …] [--json]
++// openvibe-link credential import take a rotated credential: reads the rotate response JSON on stdin
++// openvibe-link run run in the foreground (this is also what the service runs)
++// openvibe-link install install and start the system service
++// openvibe-link uninstall stop and remove the system service
++// openvibe-link status show the link, the plugins, the stop latch and video
++// openvibe-link stop local kill switch: stop every actuator and hold it stopped
++// openvibe-link resume release the local kill switch (and a server e-stop)
++// openvibe-link quit end the running node cleanly: every actuator stops, nothing is latched
++// openvibe-link plugins list plugins and their capabilities
++// openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]
// read the memory files of the coding tools (read-only, local)
--// openvibe-node skills sync [--tools a,b] [--project DIR …] [--dry-run]
++// openvibe-link skills sync [--tools a,b] [--project DIR …] [--dry-run]
// write the skills you chose in the hub into each tool's own folders
--// openvibe-node skills on|off|status declare/remove/check the skill runner (sandboxed skill code)
--// openvibe-node version
++// openvibe-link skills on|off|status declare/remove/check the skill runner (sandboxed skill code)
++// openvibe-link version
package main
import (
@@@ -54,56 +54,56 @@ import (
// version is set at build time: -ldflags "-X main.version=v0.1.0".
var version = "dev"
--const usage = `openvibe-node %s: connect this device to OpenVibe.
++const usage = `openvibe-link %s: connect this device to OpenVibe.
Usage:
-- openvibe-node [--code CODE] [--network URL --pairing pair_…] [--home DIR]
++ openvibe-link [--code CODE] [--network URL --pairing pair_…] [--home DIR]
guided first run: pair with a code from openvibe.bot/machines, check this machine can reach
-- OpenVibe, run one harmless first job, and report each step; the same as 'openvibe-node first-run'
-- openvibe-node pair <CODE> [--robot rob_…] [--name NAME] [--server URL] [--kind onboard|bridge] [--force]
-- openvibe-node pair --network URL --pairing pair_… --code CODE [--name NAME] [--server URL] [--force]
-- openvibe-node credential import store the credential from a rotate response, read as JSON on stdin
-- openvibe-node run [--dry-run]
-- openvibe-node install [--user NAME]
-- openvibe-node install --user-session --home DIR Windows: start at sign-in in your own session (Bluetooth bridges)
-- openvibe-node uninstall [--user-session]
-- openvibe-node status [--json]
-- openvibe-node stop stop every actuator now and hold them stopped
-- openvibe-node resume release the stop
-- openvibe-node quit end the running node cleanly: every actuator stops and nothing is latched, so the next
++ OpenVibe, run one harmless first job, and report each step; the same as 'openvibe-link first-run'
++ openvibe-link pair <CODE> [--robot rob_…] [--name NAME] [--server URL] [--kind onboard|bridge] [--force]
++ openvibe-link pair --network URL --pairing pair_… --code CODE [--name NAME] [--server URL] [--force]
++ openvibe-link credential import store the credential from a rotate response, read as JSON on stdin
++ openvibe-link run [--dry-run]
++ openvibe-link install [--user NAME]
++ openvibe-link install --user-session --home DIR Windows: start at sign-in in your own session (Bluetooth bridges)
++ openvibe-link uninstall [--user-session]
++ openvibe-link status [--json]
++ openvibe-link stop stop every actuator now and hold them stopped
++ openvibe-link resume release the stop
++ openvibe-link quit end the running node cleanly: every actuator stops and nothing is latched, so the next
start drives as before (stop is the kill switch, and holds until resume)
-- openvibe-node plugins [--json]
-- openvibe-node approvals [--json] what the computer plugin waits to be approved (control level "ask")
-- openvibe-node approve <ID> let it run
-- openvibe-node deny <ID> refuse it
-- openvibe-node tray [--autostart on|off]
++ openvibe-link plugins [--json]
++ openvibe-link approvals [--json] what the computer plugin waits to be approved (control level "ask")
++ openvibe-link approve <ID> let it run
++ openvibe-link deny <ID> refuse it
++ openvibe-link tray [--autostart on|off]
the Node in your notification area (Linux, Windows): what waits with Approve and Deny, stop,
resume, Open Link; --autostart starts it when you sign in
-- openvibe-node panel print Link's own window's address (loopback, this run's token)
-- openvibe-node profile verify community.<name> [--server URL] [--key-id ID]
++ openvibe-link panel print Link's own window's address (loopback, this run's token)
++ openvibe-link profile verify community.<name> [--server URL] [--key-id ID]
check a community robot profile against the signature Bot made when it was reviewed
-- openvibe-node desk-sandbox [--memory-mb N] [--cpu-millis N] [--bind PATH]... [--egress public] -- PROGRAM [ARG]...
++ openvibe-link desk-sandbox [--memory-mb N] [--cpu-millis N] [--bind PATH]... [--egress public] -- PROGRAM [ARG]...
run one desk's browser confined (a desk host that serves other people; the computer plugin
calls it, with --remote-debugging-pipe on fds 3 and 4)
-- openvibe-node memory scan [--tools a,b] [--project DIR …] [--json]
++ openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]
read the memory files of the coding tools on this machine
(read-only, local, secrets removed)
-- openvibe-node skills sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
-- openvibe-node skills sync --undo
++ openvibe-link skills sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
++ openvibe-link skills sync --undo
write the skills and rules you chose at openvibe.codes/skills
into each tool's own folders, inside one marked block
-- openvibe-node skills on|off|status [--dir DIR] [--node PATH] [--json]
++ openvibe-link skills on|off|status [--dir DIR] [--node PATH] [--json]
run a skill's code as a sandboxed job on this machine
([email protected]; off until "on")
-- openvibe-node exchange serve --model <id> [--public-url URL]
++ openvibe-link exchange serve --model <id> [--public-url URL]
serve a local model as an OpenVibe.Exchange seller offer
-- openvibe-node stream --probe --whip-url URL [--seconds N] [--size WxH] [--json]
++ openvibe-link stream --probe --whip-url URL [--seconds N] [--size WxH] [--json]
publish a QR of the millisecond clock for the headless latency viewer (the ticket comes from
OPENVIBE_STREAM_TICKET, never argv)
-- openvibe-node share --key ovc_… --port N put this machine's port N on the web (OpenVibe Connect),
++ openvibe-link share --key ovc_… --port N put this machine's port N on the web (OpenVibe Connect),
behind OpenVibe sign-in, until Ctrl-C
-- openvibe-node share add|list|remove … the shares the Node service keeps up (openvibe-node share)
-- openvibe-node version
++ openvibe-link share add|list|remove … the shares the Node service keeps up (openvibe-link share)
++ openvibe-link version
Global flags (before or after the command):
--home DIR keep config and state in DIR (also OPENVIBE_NODE_HOME)
@@@ -129,13 +129,13 @@ func run(args []string, stdout, stderr io.Writer) int {
func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
if len(args) > 0 && (args[0] == "-h" || args[0] == "--help" || args[0] == "help" || args[0] == "--version") {
if args[0] == "--version" {
-- fmt.Fprintf(stdout, "openvibe-node %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
++ fmt.Fprintf(stdout, "openvibe-link %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
return 0
}
fmt.Fprintf(stdout, usage, version)
return 0
}
-- // A bare `openvibe-node` (or flags with no command) is the guided first run; it needs its own flag set, because
++ // A bare `openvibe-link` (or flags with no command) is the guided first run; it needs its own flag set, because
// its flags include the pairing ones and there is no command word to hang them on.
if len(args) == 0 || args[0] == "first-run" || strings.HasPrefix(args[0], "-") {
g := &globals{}
@@@ -197,7 +197,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
shKey = fs.String("key", "", "the share's ovc_… connect key from connect.openvibe.website (share)")
shPort = fs.Int("port", 0, "the local port to share (share)")
shSrv = fs.String("connect-server", "", "OpenVibe Connect's tunnel URL; default wss://connect.openvibe.website/tunnel (share)")
-- shID = fs.String("share", "", "a share this computer serves itself, as OpenVibe Node: its shr_… id, no key (share)")
++ shID = fs.String("share", "", "a share this computer serves itself, as OpenVibe Link: its shr_… id, no key (share)")
)
// Accept flags anywhere: split positionals out.
var positional []string
@@@ -226,7 +226,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
positional, p.pairing = []string{p.pairing}, ""
}
if len(positional) != 1 {
-- fmt.Fprintln(stderr, "usage: openvibe-node pair <CODE> [--robot rob_…] [--name NAME]\n openvibe-node pair --network URL --pairing pair_… --code CODE [--name NAME]")
++ fmt.Fprintln(stderr, "usage: openvibe-link pair <CODE> [--robot rob_…] [--name NAME]\n openvibe-link pair --network URL --pairing pair_… --code CODE [--name NAME]")
return 2
}
p.code = positional[0]
@@@ -237,7 +237,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
}
case "credential":
if len(positional) != 1 || positional[0] != "import" {
-- fmt.Fprintln(stderr, "usage: openvibe-node credential import (reads the rotate response JSON on stdin)")
++ fmt.Fprintln(stderr, "usage: openvibe-link credential import (reads the rotate response JSON on stdin)")
return 2
}
err = cmdCredentialImport(g, stdin, stdout)
@@@ -275,7 +275,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
err = cmdApprovals(g, *asJSON, stdout)
case "approve", "deny":
if len(positional) != 1 {
-- fmt.Fprintf(stderr, "usage: openvibe-node %s <ID> (openvibe-node approvals lists the ids)\n", cmd)
++ fmt.Fprintf(stderr, "usage: openvibe-link %s <ID> (openvibe-link approvals lists the ids)\n", cmd)
return 2
}
err = cmdAnswer(g, cmd, positional[0], stdout)
@@@ -285,7 +285,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
err = cmdPanel(g, stdout)
case "profile":
if len(positional) != 2 || positional[0] != "verify" {
-- fmt.Fprintln(stderr, "usage: openvibe-node profile verify community.<name> [--server URL] [--key-id ID]")
++ fmt.Fprintln(stderr, "usage: openvibe-link profile verify community.<name> [--server URL] [--key-id ID]")
return 2
}
err = cmdProfileVerify(g, positional[1], *server, *keyID, stdout)
@@@ -295,7 +295,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
return 2
}
case "version", "--version":
-- fmt.Fprintf(stdout, "openvibe-node %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
++ fmt.Fprintf(stdout, "openvibe-link %s %s/%s\n", version, runtime.GOOS, runtime.GOARCH)
default:
fmt.Fprintf(stderr, "unknown command %q\n\n", cmd)
fmt.Fprintf(stderr, usage, version)
@@@ -356,7 +356,7 @@ func probeAll(cfg *config.Config, paths config.Paths, log *slog.Logger) (map[str
return out, failed
}
--// pairArgs are the flags of `openvibe-node pair`.
++// pairArgs are the flags of `openvibe-link pair`.
type pairArgs struct {
code, robot, name, server, kind string
network, pairing string
@@@ -471,14 +471,14 @@ func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {
}
chownLikeDir(g.paths.CredentialFile(), g.paths.ConfigDir)
fmt.Fprintf(stdout, "Paired as %s (robot %s). Credential saved to %s (mode 600).\n", creds.DeviceID, creds.RobotID, g.paths.CredentialFile())
-- fmt.Fprintln(stdout, "Confirm the device on openvibe.bot, then start it: `openvibe-node run` or `sudo openvibe-node install`.")
++ fmt.Fprintln(stdout, "Confirm the device on openvibe.bot, then start it: `openvibe-link run` or `sudo openvibe-link install`.")
reportCommunityProfile(ctx, cfg.Server, creds.Profile, stdout)
return finishPair(g, a, cfg, noConfig, stdout)
}
// cmdPairNetwork pairs through OpenVibe.Network: it redeems the code at <network>/api/v1/node-pairing, stores the
// node principal and credential, then binds the device on Bot (POST /api/v1/devices/bind with a node token). A
--// failed bind keeps the credential: `openvibe-node run` binds on start.
++// failed bind keeps the credential: `openvibe-link run` binds on start.
func cmdPairNetwork(g *globals, a pairArgs, stdout io.Writer) error {
code, err := link.NormalizeCode(a.code)
if err != nil {
@@@ -523,7 +523,7 @@ func cmdPairNetwork(g *globals, a pairArgs, stdout io.Writer) error {
fmt.Fprintf(stdout, "Bound as %s (a machine for your jobs, no robot).\n", bound.DeviceID)
}
}
-- fmt.Fprintln(stdout, "Start it: `openvibe-node run` or `sudo openvibe-node install`.")
++ fmt.Fprintln(stdout, "Start it: `openvibe-link run` or `sudo openvibe-link install`.")
return finishPair(g, a, cfg, noConfig, stdout)
}
@@@ -595,7 +595,7 @@ func cmdCredentialImport(g *globals, stdin io.Reader, stdout io.Writer) error {
}
old, err := credentials.Load(g.paths.CredentialFile(), nil)
if errors.Is(err, credentials.ErrNotPaired) {
-- return errors.New("this Node is not paired yet; run `openvibe-node pair <CODE>` first")
++ return errors.New("this Node is not paired yet; run `openvibe-link pair <CODE>` first")
}
if err != nil {
return err
@@@ -654,12 +654,12 @@ func cmdRun(g *globals, dryRun bool) error {
if err != nil {
return err
}
-- g.log.Info("openvibe-node starting", "version", version, "config", g.paths.ConfigDir, "plugins", len(cfg.Plugins))
++ g.log.Info("openvibe-link starting", "version", version, "config", g.paths.ConfigDir, "plugins", len(cfg.Plugins))
return service.Run(serviceOptions(g, ""), func(ctx context.Context) error {
if err := n.Run(ctx); !errors.Is(err, node.ErrQuit) {
return err
}
-- g.log.Info("openvibe-node exited: every actuator stopped, nothing latched")
++ g.log.Info("openvibe-link exited: every actuator stopped, nothing latched")
return service.ErrQuit
})
}
@@@ -700,14 +700,14 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
if err := service.Control(opt, "start"); err != nil {
return fmt.Errorf("installed, but could not start: %w", err)
}
-- fmt.Fprintln(stdout, "Installed and started the openvibe-node service.")
++ fmt.Fprintln(stdout, "Installed and started the openvibe-link service.")
// The guided first run's clock starts here. Best effort: a record that cannot be written never fails an install.
if r, rerr := firstrun.Load(g.paths.FirstRunFile()); rerr == nil && !r.Has(firstrun.StepInstalled) {
r.Upsert(firstrun.Step{Name: firstrun.StepInstalled, At: time.Now().UTC()})
_ = r.Save(g.paths.FirstRunFile())
}
if _, err := credentials.Load(g.paths.CredentialFile(), nil); err != nil {
-- fmt.Fprintln(stdout, "Not paired yet: run `sudo openvibe-node first-run` to pair and finish the setup.")
++ fmt.Fprintln(stdout, "Not paired yet: run `sudo openvibe-link first-run` to pair and finish the setup.")
}
return nil
}
@@@ -715,7 +715,7 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
func cmdStop(g *globals, stdout io.Writer) error {
resp, err := localctl.Call(g.paths.SocketPath(), localctl.Request{Cmd: localctl.CmdStop}, 2*time.Second)
if err == nil {
-- fmt.Fprintln(stdout, "Stopped. Every actuator is held stopped until `openvibe-node resume`.")
++ fmt.Fprintln(stdout, "Stopped. Every actuator is held stopped until `openvibe-link resume`.")
return nil
}
if resp.Error != "" {
@@@ -731,7 +731,7 @@ func cmdStop(g *globals, stdout io.Writer) error {
}
chownLikeDir(g.paths.LatchFile(), g.paths.StateDir) // the service account must be able to read it
fmt.Fprintln(stdout, "The node did not answer; the stop is latched in", g.paths.LatchFile())
-- fmt.Fprintln(stdout, "A running node obeys it within a quarter second; a starting node starts stopped. Release with `openvibe-node resume`.")
++ fmt.Fprintln(stdout, "A running node obeys it within a quarter second; a starting node starts stopped. Release with `openvibe-link resume`.")
return nil
}
@@@ -754,7 +754,7 @@ func cmdQuit(g *globals, stdout io.Writer) error {
}
c.Close()
}
-- return errors.New("the node took the quit but is still running after 20 s; `openvibe-node status` shows it")
++ return errors.New("the node took the quit but is still running after 20 s; `openvibe-link status` shows it")
}
func cmdResume(g *globals, stdout io.Writer) error {
@@@ -796,7 +796,7 @@ func cmdApprovals(g *globals, asJSON bool, stdout io.Writer) error {
// with their control, ANSI and bidi characters made visible.
fmt.Fprintf(stdout, "%q %s, %s ago: %s\n", a.ID, safety.DisplayText(a.Kind), age, safety.DisplayText(a.Summary))
}
-- fmt.Fprintln(stdout, "\nopenvibe-node approve <ID> lets one run; openvibe-node deny <ID> refuses it.")
++ fmt.Fprintln(stdout, "\nopenvibe-link approve <ID> lets one run; openvibe-link deny <ID> refuses it.")
return nil
}
@@@ -863,11 +863,11 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
if err := json.Unmarshal(resp.Data, &s); err != nil {
return err
}
-- fmt.Fprintf(stdout, "OpenVibe Node %s, running for %s (service: %s)\n", s.Version, s.Uptime, service.Status(serviceOptions(g, "")))
++ fmt.Fprintf(stdout, "OpenVibe Link %s, running for %s (service: %s)\n", s.Version, s.Uptime, service.Status(serviceOptions(g, "")))
if s.Paired {
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(s.DeviceID, s.Principal, s.Server))
} else {
-- fmt.Fprintln(stdout, "Device: not paired (openvibe-node pair <CODE>)")
++ fmt.Fprintln(stdout, "Device: not paired (openvibe-link pair <CODE>)")
}
link := "down"
if s.Link.Connected {
@@@ -893,7 +893,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
if s.Screen.LastError != "" && s.Screen.State != "live" {
fmt.Fprintf(stdout, " (%s)", s.Screen.LastError)
}
-- fmt.Fprintln(stdout, "; `openvibe-node stop` ends it")
++ fmt.Fprintln(stdout, "; `openvibe-link stop` ends it")
}
fmt.Fprintln(stdout, "Plugins:")
for _, p := range s.Plugins {
@@@ -932,13 +932,13 @@ func deviceLine(device, principal, server string) string {
func latchText(l safety.LatchState) string {
if l.Computer {
-- return "STOPPED for computer control (an e-stop on this computer); `openvibe-node resume` on this machine is required"
++ return "STOPPED for computer control (an e-stop on this computer); `openvibe-link resume` on this machine is required"
}
switch {
case l.Local && l.Remote:
-- return "STOPPED locally and by the server (" + l.RemoteReason + "); `openvibe-node resume` releases both"
++ return "STOPPED locally and by the server (" + l.RemoteReason + "); `openvibe-link resume` releases both"
case l.Local:
-- return "STOPPED locally; `openvibe-node resume` releases it"
++ return "STOPPED locally; `openvibe-link resume` releases it"
case l.Remote:
return "E-STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"
}
@@@ -966,11 +966,11 @@ func offlineStatus(g *globals, asJSON bool, stdout io.Writer) error {
if asJSON {
return json.NewEncoder(stdout).Encode(o)
}
-- fmt.Fprintf(stdout, "OpenVibe Node %s is not running (service: %s).\n", version, o.Service)
++ fmt.Fprintf(stdout, "OpenVibe Link %s is not running (service: %s).\n", version, o.Service)
if o.Paired {
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(o.DeviceID, o.Principal, o.Server))
} else {
-- fmt.Fprintln(stdout, "Device: not paired (openvibe-node pair <CODE>)")
++ fmt.Fprintln(stdout, "Device: not paired (openvibe-link pair <CODE>)")
}
fmt.Fprintf(stdout, "Stop: %s\n", latchText(o.Latch))
fmt.Fprintf(stdout, "Config: %s\n", o.Config)
@@@ -1064,7 +1064,7 @@ func reportCommunityProfile(ctx context.Context, server string, profile json.Raw
}
res, err := profilesig.Check(ctx, nil, server, p.ID, profile, "")
if err != nil {
-- fmt.Fprintf(stdout, "WARNING: the robot's community profile %s did not check out (%v). Do not install its driver until it does: openvibe-node profile verify %s\n", p.ID, err, p.ID)
++ fmt.Fprintf(stdout, "WARNING: the robot's community profile %s did not check out (%v). Do not install its driver until it does: openvibe-link profile verify %s\n", p.ID, err, p.ID)
return
}
fmt.Fprintf(stdout, "The robot's community profile %s (version %s) is the one Bot reviewed and signed (key %s).\n", res.ID, res.Version, res.KeyID)
ddiff --git a/cmd/openvibe-node/main_test.go b/cmd/openvibe-node/main_test.go
iindex c19712e..3215aab 100644
---- a/cmd/openvibe-node/main_test.go
++++ b/cmd/openvibe-node/main_test.go
@@@ -47,7 +47,7 @@ func shortHome(t *testing.T) string {
}
func TestHelpAndVersion(t *testing.T) {
-- if out, _, code := cli(t, "help"); code != 0 || !strings.Contains(out, "openvibe-node pair <CODE>") {
++ if out, _, code := cli(t, "help"); code != 0 || !strings.Contains(out, "openvibe-link pair <CODE>") {
t.Fatalf("%d %s", code, out)
}
if out, _, code := cli(t, "version"); code != 0 || !strings.Contains(out, runtime.GOOS) {
ddiff --git a/cmd/openvibe-node/memory.go b/cmd/openvibe-node/memory.go
iindex 2d5f4aa..b4058c0 100644
---- a/cmd/openvibe-node/memory.go
++++ b/cmd/openvibe-node/memory.go
@@@ -11,13 +11,13 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memory"
)
--// memoryUsage is what "openvibe-node memory" prints.
--const memoryUsage = "openvibe-node memory scan [--tools a,b] [--project DIR] [--json]\n" +
-- "openvibe-node memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
-- "openvibe-node memory status [--show]\n" +
-- "openvibe-node memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes] | --disable a,b | --undo\n" +
-- "openvibe-node memory export [--out FILE] [--scope SCOPE] [--force]\n" +
-- "openvibe-node memory import --file FILE [--dry-run] [--yes]\n" +
++// memoryUsage is what "openvibe-link memory" prints.
++const memoryUsage = "openvibe-link memory scan [--tools a,b] [--project DIR] [--json]\n" +
++ "openvibe-link memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
++ "openvibe-link memory status [--show]\n" +
++ "openvibe-link memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes] | --disable a,b | --undo\n" +
++ "openvibe-link memory export [--out FILE] [--scope SCOPE] [--force]\n" +
++ "openvibe-link memory import --file FILE [--dry-run] [--yes]\n" +
"\n" +
" scan reads the memory files of the coding tools on this machine and\n" +
" prints what it found. It is read-only and local: nothing is written\n" +
ddiff --git a/cmd/openvibe-node/memory_portable.go b/cmd/openvibe-node/memory_portable.go
iindex acb8747..a7e4d9e 100644
---- a/cmd/openvibe-node/memory_portable.go
++++ b/cmd/openvibe-node/memory_portable.go
@@@ -14,8 +14,8 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memorystore"
)
--// memoryExportUsage is what "openvibe-node memory export" prints.
--const memoryExportUsage = "openvibe-node memory export [--out FILE] [--scope SCOPE] [--force]\n" +
++// memoryExportUsage is what "openvibe-link memory export" prints.
++const memoryExportUsage = "openvibe-link memory export [--out FILE] [--scope SCOPE] [--force]\n" +
"\n" +
" Writes your one portable memory file: a JSON document with every note, its\n" +
" kind, scope, source and confidence, and no account id or note id. The same\n" +
@@@ -28,8 +28,8 @@ const memoryExportUsage = "openvibe-node memory export [--out FILE] [--scope SCO
" --endpoint URL the store (default " + memorystore.DefaultBaseURL + ")\n" +
" --token-env NAME the variable the access token is read from (default " + memorystore.DefaultTokenEnv + ")\n"
--// memoryImportUsage is what "openvibe-node memory import" prints.
--const memoryImportUsage = "openvibe-node memory import --file FILE [--dry-run] [--yes]\n" +
++// memoryImportUsage is what "openvibe-link memory import" prints.
++const memoryImportUsage = "openvibe-link memory import --file FILE [--dry-run] [--yes]\n" +
"\n" +
" Reads a portable memory file back into your store. The store validates the\n" +
" format, removes secrets again and merges a note that says the same thing as\n" +
ddiff --git a/cmd/openvibe-node/memory_sync.go b/cmd/openvibe-node/memory_sync.go
iindex 9b0da33..b0f47b0 100644
---- a/cmd/openvibe-node/memory_sync.go
++++ b/cmd/openvibe-node/memory_sync.go
@@@ -18,10 +18,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memoryupload"
)
--// memorySyncUsage is what "openvibe-node memory sync" prints.
--const memorySyncUsage = "openvibe-node memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
-- "openvibe-node memory sync --disable a,b\n" +
-- "openvibe-node memory sync --undo\n" +
++// memorySyncUsage is what "openvibe-link memory sync" prints.
++const memorySyncUsage = "openvibe-link memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
++ "openvibe-link memory sync --disable a,b\n" +
++ "openvibe-link memory sync --undo\n" +
"\n" +
" Writes your OpenVibe Memory profile and this project's rules into the\n" +
" places the coding tools read them: AGENTS.md (Codex and friends), a thin\n" +
ddiff --git a/cmd/openvibe-node/memory_upload.go b/cmd/openvibe-node/memory_upload.go
iindex 58e2dcc..7d04551 100644
---- a/cmd/openvibe-node/memory_upload.go
++++ b/cmd/openvibe-node/memory_upload.go
@@@ -16,8 +16,8 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memoryupload"
)
--// memoryUploadUsage is what "openvibe-node memory upload" prints.
--const memoryUploadUsage = "openvibe-node memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
++// memoryUploadUsage is what "openvibe-link memory upload" prints.
++const memoryUploadUsage = "openvibe-link memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
"\n" +
" Reads the memory files of the coding tools on this machine and sends the\n" +
" items to your own memory store (OpenVibe.AI). It shows what it found and\n" +
@@@ -31,8 +31,8 @@ const memoryUploadUsage = "openvibe-node memory upload [--tools a,b] [--project
" --token-env NAME the variable the access token is read from (default " + memorystore.DefaultTokenEnv + ")\n" +
" --user-home DIR read this directory as the person's home (default: yours)\n"
--// memoryStatusUsage is what "openvibe-node memory status" prints.
--const memoryStatusUsage = "openvibe-node memory status [--show]\n" +
++// memoryStatusUsage is what "openvibe-link memory status" prints.
++const memoryStatusUsage = "openvibe-link memory status [--show]\n" +
"\n" +
" Shows what your own memory store holds: how many items, per source and per\n" +
" scope. The text of an item is only printed with --show.\n" +
ddiff --git a/cmd/openvibe-node/session_other.go b/cmd/openvibe-node/session_other.go
iindex bb6150c..796fda7 100644
---- a/cmd/openvibe-node/session_other.go
++++ b/cmd/openvibe-node/session_other.go
@@@ -4,7 +4,7 @@ package main
import "errors"
--var errUserSessionWindowsOnly = errors.New("--user-session is for Windows; on Linux and macOS install the service (sudo openvibe-node install), or start `openvibe-node run --home DIR` from your desktop's autostart")
++var errUserSessionWindowsOnly = errors.New("--user-session is for Windows; on Linux and macOS install the service (sudo openvibe-link install), or start `openvibe-link run --home DIR` from your desktop's autostart")
func userSessionSupported() error { return errUserSessionWindowsOnly }
func registerUserSession(string, string) error { return errUserSessionWindowsOnly }
ddiff --git a/cmd/openvibe-node/share.go b/cmd/openvibe-node/share.go
iindex 261d45e..3cbca24 100644
---- a/cmd/openvibe-node/share.go
++++ b/cmd/openvibe-node/share.go
@@@ -17,7 +17,7 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/link"
)
--// shareArgs is `openvibe-node share …` (OpenVibe Connect, Website Connect v0).
++// shareArgs is `openvibe-link share …` (OpenVibe Connect, Website Connect v0).
type shareArgs struct {
key string
port int
@@@ -26,15 +26,15 @@ type shareArgs struct {
}
const shareUsage = `usage:
-- openvibe-node share <port> serve the share this computer was picked for on that port, until Ctrl-C
-- openvibe-node share add <port> keep that share up from the Node service
-- openvibe-node share --share shr_… [--port N] serve one by its id, until Ctrl-C (no key)
-- openvibe-node share add --share shr_… [--port N] keep one by its id up from the Node service
-- openvibe-node share mine the shares this computer was picked for
-- openvibe-node share --key ovc_… --port N serve a share with its connect key, until Ctrl-C
-- openvibe-node share add --key ovc_… --port N keep it up from the Node service (restart the service to start it)
-- openvibe-node share list the shares the service keeps up
-- openvibe-node share remove <PORT|KEY|ID|ADDRESS> stop keeping one up
++ openvibe-link share <port> serve the share this computer was picked for on that port, until Ctrl-C
++ openvibe-link share add <port> keep that share up from the Node service
++ openvibe-link share --share shr_… [--port N] serve one by its id, until Ctrl-C (no key)
++ openvibe-link share add --share shr_… [--port N] keep one by its id up from the Node service
++ openvibe-link share mine the shares this computer was picked for
++ openvibe-link share --key ovc_… --port N serve a share with its connect key, until Ctrl-C
++ openvibe-link share add --key ovc_… --port N keep it up from the Node service (restart the service to start it)
++ openvibe-link share list the shares the service keeps up
++ openvibe-link share remove <PORT|KEY|ID|ADDRESS> stop keeping one up
Make a share at https://connect.openvibe.website. On its page, pick this computer under "Or serve it from one of your
computers" to serve it with --share and no key (the computer must be added on openvibe.bot/machines). Without --port,
@@@ -91,7 +91,7 @@ func nodeShare(ctx context.Context, g *globals, a shareArgs) (connect.Share, fun
}
// shareByPort finds the one share the owner picked this computer for that names local port a.port (CON-4). It is the
--// `openvibe-node share <port>` form: the machine's own node token buys the tunnel token, so there is no key.
++// `openvibe-link share <port>` form: the machine's own node token buys the tunnel token, so there is no key.
func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
if a.port < 1 || a.port > 65535 {
return connect.Share{}, nil, fmt.Errorf("port %d is not a port", a.port)
@@@ -133,7 +133,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
if a.key == "" {
a.key = os.Getenv("OV_CONNECT_KEY")
}
-- // `openvibe-node share <port>`: the share this computer was picked for, found by the local port its owner set on
++ // `openvibe-link share <port>`: the share this computer was picked for, found by the local port its owner set on
// the share's page (CON-4). The page gives this form because there is no key to copy.
if a.share == "" && a.key == "" && a.port == 0 && len(positional) == 1 {
if p, err := strconv.Atoi(positional[0]); err == nil && p > 0 && p < 65536 {
@@@ -229,7 +229,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return err
}
if len(shares) == 0 {
-- fmt.Fprintln(stdout, "No saved shares. Add one with: openvibe-node share add --key ovc_… --port N")
++ fmt.Fprintln(stdout, "No saved shares. Add one with: openvibe-link share add --key ovc_… --port N")
return nil
}
for _, s := range shares {
ddiff --git a/cmd/openvibe-node/skills_runner.go b/cmd/openvibe-node/skills_runner.go
iindex d285157..f535c7e 100644
---- a/cmd/openvibe-node/skills_runner.go
++++ b/cmd/openvibe-node/skills_runner.go
@@@ -10,10 +10,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/skills"
)
--// skillsRunnerUsage is what "openvibe-node skills on|off|status" prints.
--var skillsRunnerUsage = "openvibe-node skills on [--dir DIR] [--node PATH]\n" +
-- "openvibe-node skills off\n" +
-- "openvibe-node skills status [--json]\n" +
++// skillsRunnerUsage is what "openvibe-link skills on|off|status" prints.
++var skillsRunnerUsage = "openvibe-link skills on [--dir DIR] [--node PATH]\n" +
++ "openvibe-link skills off\n" +
++ "openvibe-link skills status [--json]\n" +
"\n" +
" Runs one tool of a declared skill as a sandboxed function job on this\n" +
" machine (artifact [email protected], docs/worker.md). The job's\n" +
@@@ -106,7 +106,7 @@ func cmdSkillsRunner(args []string, stdout, stderr io.Writer) int {
if !s.On {
fmt.Fprintln(stdout, "skill runner: off")
fmt.Fprintf(stdout, " The job worker is %s.\n", onOff(s.WorkerEnabled))
-- fmt.Fprintln(stdout, " Turn it on: openvibe-node skills on (then restart the Node)")
++ fmt.Fprintln(stdout, " Turn it on: openvibe-link skills on (then restart the Node)")
break
}
fmt.Fprintf(stdout, "skill runner: on (%s@%s, %s)\n", s.Name, s.Version, s.Class)
ddiff --git a/cmd/openvibe-node/skills_sync.go b/cmd/openvibe-node/skills_sync.go
iindex ac6e409..77916e9 100644
---- a/cmd/openvibe-node/skills_sync.go
++++ b/cmd/openvibe-node/skills_sync.go
@@@ -17,10 +17,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/skillsync"
)
--// skillsSyncUsage is what "openvibe-node skills sync" prints.
--const skillsSyncUsage = "openvibe-node skills sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
-- "openvibe-node skills sync --disable a,b\n" +
-- "openvibe-node skills sync --undo\n" +
++// skillsSyncUsage is what "openvibe-link skills sync" prints.
++const skillsSyncUsage = "openvibe-link skills sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]\n" +
++ "openvibe-link skills sync --disable a,b\n" +
++ "openvibe-link skills sync --undo\n" +
"\n" +
" Writes the skills and rules you chose in the OpenVibe skills hub\n" +
" (openvibe.codes/skills) into the folders each coding tool reads:\n" +
ddiff --git a/cmd/openvibe-node/stream.go b/cmd/openvibe-node/stream.go
iindex 7188d57..f0b3706 100644
---- a/cmd/openvibe-node/stream.go
++++ b/cmd/openvibe-node/stream.go
@@@ -16,7 +16,7 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/stream"
)
--// cmdStream is `openvibe-node stream --probe`: it publishes a QR of the millisecond clock over WHIP, so a headless
++// cmdStream is `openvibe-link stream --probe`: it publishes a QR of the millisecond clock over WHIP, so a headless
// viewer can report the glass-to-glass latency. The ticket comes from the environment or stdin, never from argv.
func cmdStream(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("stream", flag.ContinueOnError)
@@@ -37,7 +37,7 @@ func cmdStream(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 2
}
if !*probe {
-- fmt.Fprintln(stderr, "usage: openvibe-node stream --probe --whip-url URL [--seconds N] [--size WxH] [--json]")
++ fmt.Fprintln(stderr, "usage: openvibe-link stream --probe --whip-url URL [--seconds N] [--size WxH] [--json]")
fmt.Fprintln(stderr, "the publish ticket is read from OPENVIBE_STREAM_TICKET, or with --ticket-stdin from stdin (never argv)")
return 2
}
ddiff --git a/cmd/openvibe-node/tray.go b/cmd/openvibe-node/tray.go
iindex 71f1a22..26b5392 100644
---- a/cmd/openvibe-node/tray.go
++++ b/cmd/openvibe-node/tray.go
@@@ -1,6 +1,6 @@
package main
--// `openvibe-node tray`: the Node in its owner's notification area. It runs in the graphical session (not in the
++// `openvibe-link tray`: the Node in its owner's notification area. It runs in the graphical session (not in the
// service), talks to the running Node over the local control socket like the other commands, and shows:
//
// - the state in the icon: blue when all is well, amber while a computer request waits for an answer, red while a
@@@ -64,7 +64,7 @@ type trayView struct {
// viewOf builds the view from the Node's status and approvals; err is the socket error (the Node is not running).
func viewOf(st *node.Status, list []node.Approval, err error) trayView {
if err != nil || st == nil {
-- return trayView{State: trayDown, Tooltip: "OpenVibe Node: not running", Header: "The Node is not running"}
++ return trayView{State: trayDown, Tooltip: "OpenVibe Link: not running", Header: "The Node is not running"}
}
v := trayView{Waiting: list, Panel: panelURL(st.Server)}
if lv, ok := st.Telemetry.Sensors["computer_level"].(string); ok {
@@@ -84,9 +84,9 @@ func viewOf(st *node.Status, list []node.Approval, err error) trayView {
v.Header = fmt.Sprintf("%d %s your answer", len(list), plural(len(list), "request waits for", "requests wait for"))
default:
v.State = trayOK
-- v.Header = "OpenVibe Node: " + link
++ v.Header = "OpenVibe Link: " + link
}
-- tip := []string{"OpenVibe Node: " + link}
++ tip := []string{"OpenVibe Link: " + link}
if v.Level != "" {
tip = append(tip, "computer level "+v.Level)
}
@@@ -267,7 +267,7 @@ func (t *trayCore) answer(a node.Approval, approved bool) error {
if approved && strings.Contains(err.Error(), "mouse or keyboard") {
t.note.Ask(a, "Not taken: the agent was using the mouse or keyboard. Answer again if it was you.")
} else {
-- t.note.Info("OpenVibe Node", err.Error())
++ t.note.Info("OpenVibe Link", err.Error())
}
return err
}
@@@ -349,7 +349,7 @@ func trayDesktopEntry(exe, home string) string {
if home != "" {
line += " --home " + shellQuote(home)
}
-- return "[Desktop Entry]\nType=Application\nName=OpenVibe Node\nComment=Approve what your agents ask to do on this computer\n" +
++ return "[Desktop Entry]\nType=Application\nName=OpenVibe Link\nComment=Approve what your agents ask to do on this computer\n" +
"Exec=" + line + "\nIcon=dialog-question\nTerminal=false\nX-GNOME-Autostart-enabled=true\nNoDisplay=false\n"
}
ddiff --git a/cmd/openvibe-node/tray_notify_linux.go b/cmd/openvibe-node/tray_notify_linux.go
iindex ce631fd..3477515 100644
---- a/cmd/openvibe-node/tray_notify_linux.go
++++ b/cmd/openvibe-node/tray_notify_linux.go
@@@ -138,7 +138,7 @@ func (n *dbusNotifier) Ask(a node.Approval, again string) {
}
hints := map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(2)), "desktop-entry": dbus.MakeVariant("openvibe-node-tray")}
var nid uint32
-- err := n.obj.Call(notifyIface+".Notify", 0, "OpenVibe Node", replaces, "dialog-question", "OpenVibe: your agent asks",
++ err := n.obj.Call(notifyIface+".Notify", 0, "OpenVibe Link", replaces, "dialog-question", "OpenVibe: your agent asks",
n.text(body), actions, hints, expire).Store(&nid)
if err != nil {
n.log.Warn("tray: notification failed", "err", err)
@@@ -165,7 +165,7 @@ func (n *dbusNotifier) Close(id string) {
func (n *dbusNotifier) Info(title, body string) {
hints := map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(1))}
-- n.obj.Call(notifyIface+".Notify", 0, "OpenVibe Node", uint32(0), "dialog-information", title, n.text(body), []string{}, hints, int32(6000))
++ n.obj.Call(notifyIface+".Notify", 0, "OpenVibe Link", uint32(0), "dialog-information", title, n.text(body), []string{}, hints, int32(6000))
}
func (n *dbusNotifier) Answers() <-chan trayAnswer { return n.answers }
ddiff --git a/cmd/openvibe-node/tray_systray.go b/cmd/openvibe-node/tray_systray.go
iindex a06b05c..387cde8 100644
---- a/cmd/openvibe-node/tray_systray.go
++++ b/cmd/openvibe-node/tray_systray.go
@@@ -43,7 +43,7 @@ type systrayUI struct {
func (u *systrayUI) build() {
u.link = systray.AddMenuItem("Open Link", "Link's own window: what runs here, what you allow, your shares")
systray.AddSeparator()
-- u.header = systray.AddMenuItem("OpenVibe Node", "")
++ u.header = systray.AddMenuItem("OpenVibe Link", "")
u.header.Disable()
u.level = systray.AddMenuItem("", "")
u.level.Disable()
@@@ -59,7 +59,7 @@ func (u *systrayUI) build() {
u.panel = systray.AddMenuItem("Open my machines page", "This machine on openvibe.bot")
u.panel.Hide()
systray.AddSeparator()
-- u.quit = systray.AddMenuItem("Quit the tray", "The Node keeps running; requests wait for `openvibe-node approve`")
++ u.quit = systray.AddMenuItem("Quit the tray", "The Node keeps running; requests wait for `openvibe-link approve`")
}
func (u *systrayUI) SetIcon(state string) {
@@@ -178,14 +178,14 @@ func (u *systrayUI) SetPanel(has bool) {
func runTray(g *globals) error {
if runtime.GOOS == "linux" && os.Getenv("DBUS_SESSION_BUS_ADDRESS") == "" && os.Getenv("XDG_RUNTIME_DIR") == "" {
-- return fmt.Errorf("the tray runs in your desktop session: start it there (openvibe-node tray --autostart on starts it when you sign in)")
++ return fmt.Errorf("the tray runs in your desktop session: start it there (openvibe-link tray --autostart on starts it when you sign in)")
}
ui := &systrayUI{}
core := newTrayCore(g.paths.SocketPath(), ui, newDesktopNotifier(g.log))
stopSig := make(chan os.Signal, 1)
signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM)
systray.Run(func() {
-- systray.SetTitle("OpenVibe Node")
++ systray.SetTitle("OpenVibe Link")
ui.build()
core.tick()
// The first time the tray runs, Link's window opens by itself: the person sees what the machine does.
@@@ -218,16 +218,16 @@ func trayLoop(core *trayCore, ui *systrayUI, stopSig <-chan os.Signal, g *global
core.tick()
case <-ui.stop.ClickedCh:
if err := core.stop(); err != nil {
-- core.note.Info("OpenVibe Node", "Could not stop: "+err.Error())
++ core.note.Info("OpenVibe Link", "Could not stop: "+err.Error())
}
core.tick()
case <-ui.link.ClickedCh:
if err := core.openPanel(); err != nil {
-- core.note.Info("OpenVibe Node", err.Error())
++ core.note.Info("OpenVibe Link", err.Error())
}
case <-ui.resume.ClickedCh:
if err := core.resume(); err != nil {
-- core.note.Info("OpenVibe Node", "Could not resume: "+err.Error())
++ core.note.Info("OpenVibe Link", "Could not resume: "+err.Error())
}
core.tick()
case <-ui.panel.ClickedCh:
@@@ -293,9 +293,9 @@ func trayAutostart(on bool, exe, home string, stdout io.Writer) error {
var out []byte
var err error
if on {
-- out, err = exec.Command("reg", "add", runKey, "/v", "OpenVibe Node tray", "/t", "REG_SZ", "/d", line, "/f").CombinedOutput()
++ out, err = exec.Command("reg", "add", runKey, "/v", "OpenVibe Link tray", "/t", "REG_SZ", "/d", line, "/f").CombinedOutput()
} else {
-- out, err = exec.Command("reg", "delete", runKey, "/v", "OpenVibe Node tray", "/f").CombinedOutput()
++ out, err = exec.Command("reg", "delete", runKey, "/v", "OpenVibe Link tray", "/f").CombinedOutput()
if err != nil && strings.Contains(strings.ToLower(string(out)), "unable to find") {
err = nil
}
ddiff --git a/cmd/openvibe-node/tray_test.go b/cmd/openvibe-node/tray_test.go
iindex 6eaebca..dca19d4 100644
---- a/cmd/openvibe-node/tray_test.go
++++ b/cmd/openvibe-node/tray_test.go
@@@ -26,7 +26,7 @@ func TestTrayViewStates(t *testing.T) {
st := &node.Status{Server: "https://openvibe.bot", Link: node.LinkStatus{Connected: true}}
st.Telemetry.Sensors = map[string]any{"computer_level": "ask"}
v := viewOf(st, nil, nil)
-- if v.State != trayOK || v.Level != "ask" || v.Panel != "https://openvibe.bot/machines" || v.Tooltip != "OpenVibe Node: online, computer level ask" {
++ if v.State != trayOK || v.Level != "ask" || v.Panel != "https://openvibe.bot/machines" || v.Tooltip != "OpenVibe Link: online, computer level ask" {
t.Fatalf("all well: %+v", v)
}
v = viewOf(st, []node.Approval{{ID: "a"}, {ID: "b"}}, nil)
ddiff --git a/docs/HANDOFF.md b/docs/HANDOFF.md
iindex ce9fee4..04d0a79 100644
---- a/docs/HANDOFF.md
++++ b/docs/HANDOFF.md
@@@ -1,6 +1,6 @@
--# Handoff: build the OpenVibe Node (cloud session brief)
++# Handoff: build the OpenVibe Link (cloud session brief)
--You are starting the OpenVibe Node from an empty repository. Read this whole file and `docs/ADR-043-bot-devices-and-control.md`
++You are starting the OpenVibe Link from an empty repository. Read this whole file and `docs/ADR-043-bot-devices-and-control.md`
first; they are binding. Work on a branch `node-core`, commit in small steps with clear messages, push, and open a pull
request when the core and both robot plugins pass their tests. Do not deploy anything; you have no access to OpenVibe's
servers and do not need it.
@@@ -29,9 +29,9 @@ and a **Cozmo**.
## The CLI
--`openvibe-node pair <CODE>` (redeems a one-time pairing code, stores the credential), `openvibe-node run` (foreground),
--`openvibe-node install` / `uninstall` (as a service), `openvibe-node status`, `openvibe-node stop` (**the local kill
--switch**: stops every plugin's actuators immediately and holds them stopped until `openvibe-node resume`), `openvibe-node
++`openvibe-link pair <CODE>` (redeems a one-time pairing code, stores the credential), `openvibe-link run` (foreground),
++`openvibe-link install` / `uninstall` (as a service), `openvibe-link status`, `openvibe-link stop` (**the local kill
++switch**: stops every plugin's actuators immediately and holds them stopped until `openvibe-link resume`), `openvibe-node
plugins` (list, with capabilities). Config in `/etc/openvibe-node/` (or the platform's equivalent); the credential file is
mode 0600 and its contents never appear in logs, errors or `status`.
@@@ -56,9 +56,9 @@ mode 0600 and its contents never appear in logs, errors or `status`.
capped by the profile's `max_command_ms`); the core nacks `expired` a command that arrives after it and gives the plugin
only the time left, and the plugin stops at that deadline unless a newer command arrived. Held controls are re-sent by the operator every 150 ms.
- Stop all actuators on: 2 missed heartbeats, link loss, core shutdown, plugin crash (the core restarts it with backoff
-- and it starts stopped), `openvibe-node stop`, an `estop`.
++ and it starts stopped), `openvibe-link stop`, an `estop`.
- Two latches, both persisted across restarts. The **remote** e-stop: a server `estop` sets it, and the owner's `estop`
-- `latched:false` or the local `resume` clears it. The **local** stop (`openvibe-node stop`, or a driver's `local_stop`):
++ `latched:false` or the local `resume` clears it. The **local** stop (`openvibe-link stop`, or a driver's `local_stop`):
only the local `resume` clears it, never the owner. Owner limits (`max_speed`, `max_turn`) clamp every value in the core before it reaches a plugin.
- Never run or expose a kit's stock control server (the Adeept kit ships one on 0.0.0.0:8888 with the fixed login
`admin:123456` and an MJPEG stream on :5000 — the Node replaces it; `install.sh` disables it if it finds it enabled).
@@@ -130,7 +130,7 @@ core. Say in `docs/` where it will attach.
## Done means
--`go test ./...` and the Python plugin tests green in CI on the PR; `openvibe-node run` with `dryrun` pairs against the fake
++`go test ./...` and the Python plugin tests green in CI on the PR; `openvibe-link run` with `dryrun` pairs against the fake
server, publishes a test pattern, obeys commands and stops on every safety path; the Adeept and Cozmo plugins pass their
tests with fake hardware; `docs/protocol.md`, `docs/plugins.md` and `docs/install.md` written; README says what the Node
is and how to install it.
ddiff --git a/docs/ai.md b/docs/ai.md
iindex b14f7e6..7a1e466 100644
---- a/docs/ai.md
++++ b/docs/ai.md
@@@ -2,7 +2,7 @@
If you already run a model at home (Ollama, LM Studio or llama.cpp's server on a gaming PC or a spare box with a GPU),
OpenVibe can use it for your own work: Vibe's answers, an agent's thinking, a summary. It costs nothing, and the prompt
--never leaves your network. The machine opens no port: its OpenVibe Node already connects out to OpenVibe.Bot, and the
++never leaves your network. The machine opens no port: its OpenVibe Link already connects out to OpenVibe.Bot, and the
request comes back over that connection.
## How it works
@@@ -43,7 +43,7 @@ With nothing else set, it uses the first of these that answers:
| LM Studio | `http://127.0.0.1:1234` |
| llama.cpp's server | `http://127.0.0.1:8080` |
--It offers every model the server lists. Restart the Node (`openvibe-node restart`), then turn on **Let my AI use this
++It offers every model the server lists. Restart the Node (`openvibe-link restart`), then turn on **Let my AI use this
computer's models** for the machine on openvibe.bot/machines.
| key | default | meaning |
ddiff --git a/docs/computer.md b/docs/computer.md
iindex 7d12c6e..8bd3eb4 100644
---- a/docs/computer.md
++++ b/docs/computer.md
@@@ -19,7 +19,7 @@ and adds the plugin with your home folder as its only folder. It cannot share a
worker's service runs as root).
By hand, or on another system, add it to `plugins` in the Node's `config.json` and run the Node as your own account
--(`openvibe-node run` in your session, or `openvibe-node install --user <you>`):
++(`openvibe-link run` in your session, or `openvibe-link install --user <you>`):
```json
{"name": "computer", "config": {"level": "ask", "roots": ["/home/you/projects"]}}
@@@ -52,22 +52,22 @@ plugin compares path names only: there a name that changes under it is still a n
(default 120 s) and never longer than the request's own deadline:
```sh
--openvibe-node approvals # what waits: id, kind, what it would do
--openvibe-node approve <ID> # let it run
--openvibe-node deny <ID> # refuse it
++openvibe-link approvals # what waits: id, kind, what it would do
++openvibe-link approve <ID> # let it run
++openvibe-link deny <ID> # refuse it
```
A request shows what it would do in full: every argument of a program, the program it resolves to, the folder, and
whether stdin is given with its size and digest; a write's path, mode, size and content digest; the whole text to type,
or its length and digest. Control, ANSI and bidi characters are escaped. A notification or dialog that cannot show all
--of it offers no **Approve** button: answer that request from `openvibe-node approvals`.
++of it offers no **Approve** button: answer that request from `openvibe-link approvals`.
**What an approval does not limit.** Approving a program lets it run as your account, with everything that account can
reach: the folders are the plugin's own limit, not a sandbox around the program. Read what it would do before you
approve it.
--**The tray** (Linux and Windows): `openvibe-node tray` puts the Node in your notification area. The installer's
--`--computer` starts it when you sign in (`openvibe-node tray --autostart on|off` does it by hand). It runs in your
++**The tray** (Linux and Windows): `openvibe-link tray` puts the Node in your notification area. The installer's
++`--computer` starts it when you sign in (`openvibe-link tray --autostart on|off` does it by hand). It runs in your
desktop session, not in the service, and talks to the Node over the same local socket as the commands above:
- the icon is blue when all is well, amber while a request waits, red while a stop is latched, grey when the Node does
@@@ -86,15 +86,15 @@ the command to answer with); on macOS a dialog with **Deny** as its default butt
expires. A prompt goes away once the request is answered anywhere else, expires or is stopped.
**An approval is not taken while the agent drives the mouse or keyboard**, nor for 2 seconds after: an approved click
--must not be able to press another request's Approve button. `openvibe-node approve` then answers "answer again in a
++must not be able to press another request's Approve button. `openvibe-link approve` then answers "answer again in a
moment", and the tray and the plugin ask again. Deny always goes through.
At most 8 requests wait at once. The panel on openvibe.bot sees `approval` and `approval_done` events, so it can say
that a request is waiting on the machine.
--**Kill switch:** `openvibe-node stop` (and an owner's e-stop) drops the plugin to `observe`: what runs is killed, what
++**Kill switch:** `openvibe-link stop` (and an owner's e-stop) drops the plugin to `observe`: what runs is killed, what
waits is denied, the agent's browser is closed, and the core also refuses `exec`, file writes, `input` and the browser's
--acting actions until `openvibe-node resume`. `halt` kills what runs. An e-stop on this computer always needs that local
++acting actions until `openvibe-link resume`. `halt` kills what runs. An e-stop on this computer always needs that local
resume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remote
latch, never this computer.
@@@ -124,10 +124,10 @@ You can watch your machine's screen live on openvibe.bot (**Your machines** →
- **How it works.** The plugin captures a JPEG frame with the same tools as `screen`, 1 to 5 times a second (2 by
default), at most 1600 px wide when Pillow is installed. The core encodes the frames (ffmpeg, else its built-in
encoder) and publishes them on a WebRTC session of their own, separate from a robot's camera.
--- **You are told.** The desktop says when it starts ("Your screen is live on OpenVibe"), and `openvibe-node status`
++- **You are told.** The desktop says when it starts ("Your screen is live on OpenVibe"), and `openvibe-link status`
shows `Screen: live … until …` while it runs.
- **It ends** when its time is up, when you press Stop, when capturing fails five times in a row, and at once on
-- `openvibe-node stop` or an e-stop. While a stop is latched, a new start is refused.
++ `openvibe-link stop` or an e-stop. While a stop is latched, a new start is refused.
- **Levels.** It runs at every level, like a screenshot. To never offer it, set `allow_screen_stream: false`.
- **Without Pillow** the capture tool must write JPEG itself: `screencapture`, `grim`, `import` and `scrot` do;
`gnome-screenshot` does not.
@@@ -202,7 +202,7 @@ An agent can work on a desk of its own instead of your screen (plan track K: Vib
`status` and `list` run at every level. What runs on the desk keeps its own rules: input and the browser still act.
- **Watching and taking over.** Start the live screen with `"desk": "private"` (your desktop is not told, since nothing of
yours is shown) and drive it the same way.
--- **It ends** on `stop`, and at once on `openvibe-node stop` or an e-stop: every process the desk started is ended by
++- **It ends** on `stop`, and at once on `openvibe-link stop` or an e-stop: every process the desk started is ended by
handle, never by name.
- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`
for screenshots. Not offered elsewhere; `allow_desk: false` turns it off.
@@@ -289,7 +289,7 @@ each desk's browser runs in the job worker's sandbox (docs/worker.md):
```
curl -fsSL https://node.openvibe.network/install | sudo sh -s -- --network … --pairing pair_… --code … --desk-host --shared
```
--- **Each desk sandboxed.** The plugin starts a desk's browser as `openvibe-node desk-sandbox -- <browser> …`: its own
++- **Each desk sandboxed.** The plugin starts a desk's browser as `openvibe-link desk-sandbox -- <browser> …`: its own
user, mount, network, PID and IPC namespaces, as the unprivileged `openvibe-job` (`worker.run_as`), in a cgroup of its
own with `desk_memory_mb` of memory (swap off: running out ends that desk alone), `desk_cpu_millis` of CPU,
`desk_pids` processes and a `desk_disk_mb` private `/tmp`, where its profile and downloads live. It sees only the
ddiff --git a/docs/connect.md b/docs/connect.md
iindex cfdcafc..46333bd 100644
---- a/docs/connect.md
++++ b/docs/connect.md
@@@ -12,12 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or ser
then on the computer:
```sh
--openvibe-node share 8790 # serve the share this computer was picked for on that port
--openvibe-node share mine # the shares this computer was picked for
--openvibe-node share --share shr_… # serve one now, until Ctrl-C
--openvibe-node share --share shr_… --port 8790 # the same, with the port named
--sudo openvibe-node share add --share shr_… # or keep it up with the Node service
--sudo openvibe-node share add 8790 # the same, found by its local port
++openvibe-link share 8790 # serve the share this computer was picked for on that port
++openvibe-link share mine # the shares this computer was picked for
++openvibe-link share --share shr_… # serve one now, until Ctrl-C
++openvibe-link share --share shr_… --port 8790 # the same, with the port named
++sudo openvibe-link share add --share shr_… # or keep it up with the Node service
++sudo openvibe-link share add 8790 # the same, found by its local port
```
There is no key to copy or leak. Before every connection the Node buys its node token for OpenVibe Connect (audience
@@@ -37,25 +37,25 @@ connect key once (`ovc_…`). Keep it like a password: it lets a computer serve
## Serve it now
```sh
--openvibe-node share --key ovc_… --port 8790
++openvibe-link share --key ovc_… --port 8790
```
It prints the address, then one line per request, until Ctrl-C. The key can also come from `OV_CONNECT_KEY`, which
keeps it off the command line and out of your shell history:
```sh
--OV_CONNECT_KEY=ovc_… openvibe-node share --port 8790
++OV_CONNECT_KEY=ovc_… openvibe-link share --port 8790
```
## Keep it up with the Node service
```sh
--openvibe-node share add --key ovc_… --port 8790
--openvibe-node share list
--openvibe-node share remove 8790 # or the key, or the address
++openvibe-link share add --key ovc_… --port 8790
++openvibe-link share list
++openvibe-link share remove 8790 # or the key, or the address
```
--`openvibe-node status` lists every share the service keeps up: its local port, its address on Connect, whether the
++`openvibe-link status` lists every share the service keeps up: its local port, its address on Connect, whether the
tunnel is connected (and why not), and how many requests the app has answered. A share with no address yet says so.
The saved shares are in the Node's config directory, `connect-shares.json`, readable only by the Node's account
ddiff --git a/docs/hardware-adeept.md b/docs/hardware-adeept.md
iindex b16505e..7ede3e0 100644
---- a/docs/hardware-adeept.md
++++ b/docs/hardware-adeept.md
@@@ -1,6 +1,6 @@
# Adeept ADR036 4WD Smart Car — hardware bring-up (O30)
--The device side is the OpenVibe Node (`openvibe-node`) with the `adeept_adr036` plugin. Bot serves the one-paste
++The device side is the OpenVibe Link (`openvibe-node`) with the `adeept_adr036` plugin. Bot serves the one-paste
installer; the panel is `openvibe.bot/panel/<rob_…>`. Nothing here runs the kit's stock control server — the Node
replaces it, and the installer disables the kit's own autostart (`docs/install.md`).
@@@ -43,16 +43,16 @@ Then check the written config (`cat /etc/openvibe-node/config.json`): `plugins`
`real` and `wheels: "ordinary"`, and `video.source` is `auto`. The config has no `video.whip_url`: the ingest URL +
publish key the pairing returned (O30: `https://ingest.openre.stream/whip/<key>`) is stored in
`/etc/openvibe-node/credential.json`, and video publishes there unless a `video.whip_url` in the config overrides it.
--`openvibe-node status --json` should show `link.connected: true` and
++`openvibe-link status --json` should show `link.connected: true` and
`video.state` at `connecting` or `live`.
## 3. Panel checklist
--Drive from openvibe.bot; watch `openvibe-node status --json` for telemetry.
++Drive from openvibe.bot; watch `openvibe-link status --json` for telemetry.
| Subsystem | Expected | Pass |
|---|---|---|
--| Motors (PCA9685 `0x5f`, 50 Hz; M1–M4 = 15/14, 12/13, 11/10, 8/9) | forward/back + steer; wheels stop at the 300 ms deadline and on `openvibe-node stop` | |
++| Motors (PCA9685 `0x5f`, 50 Hz; M1–M4 = 15/14, 12/13, 11/10, 8/9) | forward/back + steer; wheels stop at the 300 ms deadline and on `openvibe-link stop` | |
| Servos pan ch0 / tilt ch1 (500–2400 µs) | reach min → centre → max | |
| Battery (ADS7830 `0x48`, 2×18650 6.0–8.4 V) | telemetry `battery.volts` ≈ pack, `percent` sane | |
| Ultrasonic (`DistanceSensor(echo=24, trigger=23)`) | `sensors.distance_cm` changes with a hand | |
@@@ -70,7 +70,7 @@ widget and a `display` command is nacked.
If `rpicam-vid` is missing, `video.source` `auto` has nothing to fall back to: the Adeept plugin offers only the
H.264 camera command (no JPEG frames, no test pattern), so the core publishes **no video** — `journalctl -u openvibe-node` shows
--`camera program not found` and then `no camera to publish`, and `openvibe-node status --json` has no `video.source`.
++`camera program not found` and then `no camera to publish`, and `openvibe-link status --json` has no `video.source`.
Install `rpicam-apps` (noted in `docs/install.md`) and `sudo systemctl restart openvibe-node`. To check the WHIP path without the camera,
set `"video": {"source": "test"}` in `/etc/openvibe-node/config.json` and restart: the panel then shows the test
pattern. Report `video.state`, `video.last_error` and the journal lines above.
ddiff --git a/docs/hardware-cozmo.md b/docs/hardware-cozmo.md
iindex d08d953..b627b85 100644
---- a/docs/hardware-cozmo.md
++++ b/docs/hardware-cozmo.md
@@@ -48,7 +48,7 @@ Then check the written config (`cat /etc/openvibe-node/config.json`): `device_ki
`cozmo`, and `video.source` is `auto` (the plugin supplies JPEG camera frames). The config has no `video.whip_url`: the
ingest URL + publish key the pairing returned is stored in `/etc/openvibe-node/credential.json`, and video publishes
there unless a `video.whip_url` in the config overrides it. Setup waits `connect_timeout_s` (default 10 s) for the
--robot; if it is unreachable the plugin reports fault `not_connected`. `openvibe-node status --json` should show
++robot; if it is unreachable the plugin reports fault `not_connected`. `openvibe-link status --json` should show
`link.connected: true`, `sensors` (`head_deg`, `lift_mm`, `cliff`, `picked_up`, `cubes`) and `video.state` at
`connecting` or `live`.
@@@ -62,11 +62,11 @@ know a version works, allow it with `allow_firmware: [2381, <version>]` in the p
## 4. Panel checklist
--Drive from openvibe.bot; watch `openvibe-node status --json` for telemetry.
++Drive from openvibe.bot; watch `openvibe-link status --json` for telemetry.
| Subsystem | Expected | Pass |
|---|---|---|
--| Drive (`drive` `{"throttle": -1..1, "steer": -1..1}`) | left = throttle + steer, right = throttle − steer (steer > 0 turns right), × `max_wheel_mmps`; wheels stop at the deadline and on `openvibe-node stop` | |
++| Drive (`drive` `{"throttle": -1..1, "steer": -1..1}`) | left = throttle + steer, right = throttle − steer (steer > 0 turns right), × `max_wheel_mmps`; wheels stop at the deadline and on `openvibe-link stop` | |
| Head (`ptz` `{"tilt": -1..1}` / actuator `head`) | −1 = 25° down → +1 = 44.5° up (no pan: turn with `drive`) | |
| Lift (actuator `lift` 0..1) | 0 = 32 mm down → 1 = 92 mm up | |
| Say (`say` `{"text"}`) | espeak-ng / pico2wave played on Cozmo's speaker (ack = queued) | |
@@@ -83,7 +83,7 @@ Drive from openvibe.bot; watch `openvibe-node status --json` for telemetry.
- **No telemetry / `not_connected`**: confirm the second interface is joined to `Cozmo_XXXXXX`, the default route is
still on the internet NIC (`ip route`), the Cozmo app is closed everywhere, and the robot is on its charger.
- **No video**: the plugin writes JPEG frames to `frame_dir` (a temp dir by default) and announces each as
-- `{"op":"video","format":"jpeg","path":…,"seq":n}`; the core publishes them. Check `openvibe-node status --json` for
++ `{"op":"video","format":"jpeg","path":…,"seq":n}`; the core publishes them. Check `openvibe-link status --json` for
`video.state` / `video.last_error` and journalctl for `openvibe-node`. To check the WHIP path without the camera, set
`"video": {"source": "test"}` in `/etc/openvibe-node/config.json` and restart: the panel then shows the test pattern.
- **A command answers `nack`**: read the fault — `firmware_unsupported` is the firmware latch above; `cliff` and
ddiff --git a/docs/hardware-looi.md b/docs/hardware-looi.md
iindex f4f6a9f..ce38262 100644
---- a/docs/hardware-looi.md
++++ b/docs/hardware-looi.md
@@@ -1,4 +1,4 @@
--# LOOI on the OpenVibe Node
++# LOOI on the OpenVibe Link
LOOI is the tracked desk robot whose face is a phone. Its base talks Bluetooth Low Energy, so it is a **bridge**
robot: the Node runs on a computer next to it (Windows, macOS or Linux), and a driver plugin drives the base over
@@@ -63,8 +63,8 @@ The driver should send telemetry with `battery` (`{"percent": 0..100}` or `{"vol
- **A stop on the robot itself.** If the driver has its own stop (a button, a hotkey, a sensor it trusts), call
`self.local_stop("reason")` (or send `{"op": "local_stop", "reason": "…"}`). Every actuator stops at once, the
Node latches its **local** stop (persisted across restarts), and the panel shows the robot as stopped on the robot.
-- The owner's remote clear does not lift it: only `openvibe-node resume` on the bridge computer does, and then the
-- owner clears the panel's e-stop. Running `openvibe-node stop` does the same from a terminal.
++ The owner's remote clear does not lift it: only `openvibe-link resume` on the bridge computer does, and then the
++ owner clears the panel's e-stop. Running `openvibe-link stop` does the same from a terminal.
## Installing the driver
@@@ -81,8 +81,8 @@ run the Node in your own session:
```bat
set OVHOME=%LOCALAPPDATA%\OpenVibe Node
--openvibe-node pair XXXX-XXXX --robot rob_… --kind bridge --home "%OVHOME%"
--openvibe-node install --user-session --home "%OVHOME%"
++openvibe-link pair XXXX-XXXX --robot rob_… --kind bridge --home "%OVHOME%"
++openvibe-link install --user-session --home "%OVHOME%"
```
`config.json` in that directory:
@@@ -99,9 +99,9 @@ openvibe-node install --user-session --home "%OVHOME%"
`install --user-session` starts the Node when you sign in, in a console window that shows its log (Windows keeps no
service log yet). Closing the window stops the Node and every actuator; nothing restarts it until the next sign-in.
--`openvibe-node uninstall --user-session --home DIR` removes that directory's sign-in start (each Node directory has its own, since v0.37.2, so a test node never replaces a working one). On Windows a killed plugin takes the processes it
++`openvibe-link uninstall --user-session --home DIR` removes that directory's sign-in start (each Node directory has its own, since v0.37.2, so a test node never replaces a working one). On Windows a killed plugin takes the processes it
started with it (a Job Object), as it does on Linux and macOS, so a bridge helper cannot keep the Bluetooth link open
after the Node stops or restarts the plugin.
--Try the driver without the robot first: `openvibe-node plugins` asks each plugin to describe itself without touching
--hardware, and `openvibe-node run --dry-run` drives only the simulated plugin.
++Try the driver without the robot first: `openvibe-link plugins` asks each plugin to describe itself without touching
++hardware, and `openvibe-link run --dry-run` drives only the simulated plugin.
ddiff --git a/docs/install.md b/docs/install.md
iindex 8ca5e2c..26214b1 100644
---- a/docs/install.md
++++ b/docs/install.md
@@@ -1,6 +1,6 @@
--# Installing the OpenVibe Node
++# Installing OpenVibe Link
--The Node is one program, `openvibe-node`, plus Python plugins for robots. It connects **out** to OpenVibe (no open
++OpenVibe Link is one program, `openvibe-link` (`openvibe-node` is kept as an alias), plus Python plugins for robots. It connects **out** to OpenVibe (no open
ports, works behind any home router), and a local kill switch stops everything it drives.
## The one command (Raspberry Pi and other Linux robots)
@@@ -138,53 +138,54 @@ first job those counts: never a path, a username, an IP or a secret. Bot stores
many installs reach each step and how long they take (`/admin/first-run`), and the machine's page shows what the first
job found.
--`openvibe-node first-run` is the same command when a script wants to say it. `--code CODE`, `--network URL
++`openvibe-link first-run` is the same command when a script wants to say it. `--code CODE`, `--network URL
--pairing pair_…` and `--home DIR` work as flags instead of the prompt.
## By hand
```sh
--sudo install -m 755 openvibe-node-linux-arm64 /usr/local/bin/openvibe-node
--sudo openvibe-node pair ABCD-1234 --robot rob_… # stores /etc/openvibe-node/credential.json (mode 600)
++sudo install -m 755 openvibe-link-linux-arm64 /usr/local/bin/openvibe-link
++sudo ln -s openvibe-link /usr/local/bin/openvibe-node
++sudo openvibe-link pair ABCD-1234 --robot rob_… # stores /etc/openvibe-node/credential.json (mode 600)
# or, for a Network pairing:
--sudo openvibe-node pair --network https://openvibe.network --pairing pair_… --code ABCD-1234
--sudo openvibe-node install # systemd / launchd / Windows service, starts it
--openvibe-node status
++sudo openvibe-link pair --network https://openvibe.network --pairing pair_… --code ABCD-1234
++sudo openvibe-link install # systemd / launchd / Windows service, starts it
++openvibe-link status
```
Without root, everything goes to your user config directory (`~/.config/openvibe-node`), or to any directory with
`--home DIR` / `OPENVIBE_NODE_HOME`:
```sh
--openvibe-node --home ~/node pair ABCD-1234
--openvibe-node --home ~/node run --dry-run # foreground; Ctrl-C stops it
++openvibe-link --home ~/node pair ABCD-1234
++openvibe-link --home ~/node run --dry-run # foreground; Ctrl-C stops it
```
--**Windows**: download `openvibe-node-windows-amd64.exe`, then in an Administrator prompt `openvibe-node pair <CODE>`
--and `openvibe-node install` (the service runs as LocalSystem; files go to `%ProgramData%\OpenVibe Node`). Robot plugins
++**Windows**: download `openvibe-link-windows-amd64.exe` (or `openvibe-node-windows-amd64.exe`, the same program under its old name), then in an Administrator prompt `openvibe-link pair <CODE>`
++and `openvibe-link install` (the service runs as LocalSystem; files go to `%ProgramData%\OpenVibe Node`). Robot plugins
need Python; set `"python"` in `config.json`.
A driver that needs your signed-in session (Bluetooth through the Windows runtime, as a LOOI bridge does) runs the
--Node there instead, with no administrator: `openvibe-node pair <CODE> --home "%LOCALAPPDATA%\OpenVibe Node"`, then
--`openvibe-node install --user-session --home "%LOCALAPPDATA%\OpenVibe Node"`. It starts when you sign in, in a
--console window that shows its log; closing the window stops the Node and every actuator. `openvibe-node uninstall
++Node there instead, with no administrator: `openvibe-link pair <CODE> --home "%LOCALAPPDATA%\OpenVibe Node"`, then
++`openvibe-link install --user-session --home "%LOCALAPPDATA%\OpenVibe Node"`. It starts when you sign in, in a
++console window that shows its log; closing the window stops the Node and every actuator. `openvibe-link uninstall
--user-session` removes it. See [hardware-looi.md](hardware-looi.md).
## Commands
| command | what it does |
|-------------------------------------------|----------------------------------------------------------------------------|
--| `openvibe-node pair <CODE> [--force]` | redeem a pairing code (or `--code CODE`); `--robot rob_…` the robot it is for, `--name` the device name (default: hostname); `--server`, `--kind onboard\|bridge` override config. With `--network URL [--pairing pair_…]` the code is redeemed on OpenVibe.Network and the device bound on Bot |
--| `openvibe-node run [--dry-run]` | run in the foreground (what the service runs) |
--| `openvibe-node install [--user NAME]` | install and start the service |
--| `openvibe-node uninstall` | stop and remove the service (config and credential are kept) |
--| `openvibe-node install --user-session --home DIR` | Windows: start at sign-in in your own session instead of as a service |
--| `openvibe-node uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) |
--| `openvibe-node status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs |
--| `openvibe-node stop` | **kill switch**: stop every actuator now and hold it stopped |
--| `openvibe-node resume` | release the local stop (and a server e-stop) |
--| `openvibe-node quit` | end the running Node cleanly: every actuator stops, **nothing is latched**, and it waits until the Node has exited |
--| `openvibe-node plugins [--json]` | plugins and their capabilities |
++| `openvibe-link pair <CODE> [--force]` | redeem a pairing code (or `--code CODE`); `--robot rob_…` the robot it is for, `--name` the device name (default: hostname); `--server`, `--kind onboard\|bridge` override config. With `--network URL [--pairing pair_…]` the code is redeemed on OpenVibe.Network and the device bound on Bot |
++| `openvibe-link run [--dry-run]` | run in the foreground (what the service runs) |
++| `openvibe-link install [--user NAME]` | install and start the service |
++| `openvibe-link uninstall` | stop and remove the service (config and credential are kept) |
++| `openvibe-link install --user-session --home DIR` | Windows: start at sign-in in your own session instead of as a service |
++| `openvibe-link uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) |
++| `openvibe-link status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs |
++| `openvibe-link stop` | **kill switch**: stop every actuator now and hold it stopped |
++| `openvibe-link resume` | release the local stop (and a server e-stop) |
++| `openvibe-link quit` | end the running Node cleanly: every actuator stops, **nothing is latched**, and it waits until the Node has exited |
++| `openvibe-link plugins [--json]` | plugins and their capabilities |
`stop` works even when the Node does not answer: it latches `latch.json` in the state directory, which a running Node
obeys within a quarter second and a starting Node obeys before anything moves. The latch survives restarts.
@@@ -193,23 +194,23 @@ obeys within a quarter second and a starting Node obeys before anything moves. T
`stop` is a kill switch, not an exit: it holds every actuator stopped, across restarts, until `resume`. To end the Node
from a script (a stream launcher that starts and stops it with OBS, say), use `quit`. Every actuator stops, the Node
--exits, and nothing is latched, so the next `openvibe-node run` drives as before. `quit` waits until the process has
++exits, and nothing is latched, so the next `openvibe-link run` drives as before. `quit` waits until the process has
gone and succeeds when no Node is running, so a launcher can call it without checking first.
`quit` ends a Node started from a terminal, by a launcher, or at sign-in (`install --user-session`). A Node installed as
a system service refuses it, because systemd, launchd or Windows would start it again. Stop the service there instead
--(`sudo systemctl stop openvibe-node`, or Services on Windows), or remove it with `openvibe-node uninstall`. A launcher
--that owns the Node's life should run `openvibe-node run --home DIR` itself, with no service and no sign-in start.
++(`sudo systemctl stop openvibe-node`, or Services on Windows), or remove it with `openvibe-link uninstall`. A launcher
++that owns the Node's life should run `openvibe-link run --home DIR` itself, with no service and no sign-in start.
## Files
| Linux | macOS / Windows | contents |
|---------------------------------------|-------------------------------------------------------|-----------------------------------|
--| `/etc/openvibe-node/config.json` | `…/OpenVibe Node/config.json` | configuration |
--| `/etc/openvibe-node/credential.json` | `…/OpenVibe Node/credential.json` | device or node credential (mode 600) |
--| `/var/lib/openvibe-node/latch.json` | `…/OpenVibe Node/latch.json` | e-stop / kill switch latch |
--| `/var/lib/openvibe-node/node.sock` | `…/OpenVibe Node/node.sock` | local control socket (mode 600) |
--| `/var/lib/openvibe-node/venv` | `…/OpenVibe Node/venv` | plugin Python environment |
++| `/etc/openvibe-node/config.json` | `…/OpenVibe Link/config.json` | configuration |
++| `/etc/openvibe-node/credential.json` | `…/OpenVibe Link/credential.json` | device or node credential (mode 600) |
++| `/var/lib/openvibe-node/latch.json` | `…/OpenVibe Link/latch.json` | e-stop / kill switch latch |
++| `/var/lib/openvibe-node/node.sock` | `…/OpenVibe Link/node.sock` | local control socket (mode 600) |
++| `/var/lib/openvibe-node/venv` | `…/OpenVibe Link/venv` | plugin Python environment |
macOS: `/Library/Application Support/OpenVibe Node`. Windows: `%ProgramData%\OpenVibe Node`.
@@@ -270,22 +271,22 @@ internet (Ethernet, or a second Wi-Fi adapter). Install with `--driver cozmo`; t
## Troubleshooting
--- `openvibe-node status` shows the last link error. "the server refused the device credential" means it was revoked:
-- `sudo openvibe-node pair --force <NEW-CODE>`.
++- `openvibe-link status` shows the last link error. "the server refused the device credential" means it was revoked:
++ `sudo openvibe-link pair --force <NEW-CODE>`.
- A Network-paired machine whose node credential OpenVibe.Network refuses (revoked or removed) stops with "OpenVibe.Network
refused this machine's node credential" and exit status 78, which the systemd unit does not restart. Get a new
installer command on openvibe.bot and pair again with `--force`.
--- Logs: `journalctl -u openvibe-node -f` (Linux), `/usr/local/var/log/openvibe-node.err.log` (macOS). On Windows the
-- service's log is not kept yet; stop the service and run `openvibe-node run` in a terminal to watch it.
++- Logs: `journalctl -u openvibe-link -f` (Linux), `/usr/local/var/log/openvibe-node.err.log` (macOS). On Windows the
++ service's log is not kept yet; stop the service and run `openvibe-link run` in a terminal to watch it.
- A plugin in `faulted` state says why in `status` (missing library, not a Raspberry Pi, robot not reachable, firmware).
--- `openvibe-node plugins` works without the service running: it asks each plugin to describe itself without touching
++- `openvibe-link plugins` works without the service running: it asks each plugin to describe itself without touching
hardware.
## Uninstall
```sh
--sudo openvibe-node uninstall
--sudo rm -rf /usr/local/bin/openvibe-node /etc/openvibe-node /var/lib/openvibe-node
++sudo openvibe-link uninstall
++sudo rm -rf /usr/local/bin/openvibe-link /etc/openvibe-node /var/lib/openvibe-node
sudo userdel openvibe-node
```
ddiff --git a/docs/memory.md b/docs/memory.md
iindex 626a505..d8d9c0c 100644
---- a/docs/memory.md
++++ b/docs/memory.md
@@@ -21,12 +21,12 @@ memory comes later.
## The commands
```sh
--openvibe-node memory scan [--tools a,b] [--project DIR …] [--json]
--openvibe-node memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]
--openvibe-node memory status [--show]
--openvibe-node memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
--openvibe-node memory export [--out FILE] [--scope SCOPE] [--force]
--openvibe-node memory import --file FILE [--dry-run] [--yes]
++openvibe-link memory scan [--tools a,b] [--project DIR …] [--json]
++openvibe-link memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]
++openvibe-link memory status [--show]
++openvibe-link memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
++openvibe-link memory export [--out FILE] [--scope SCOPE] [--force]
++openvibe-link memory import --file FILE [--dry-run] [--yes]
```
| Flag | What it does |
@@@ -180,7 +180,7 @@ with no END line, and an `.env` block.
## The upload
```sh
--openvibe-node memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]
++openvibe-link memory upload [--tools a,b] [--project DIR …] [--dry-run] [--yes]
```
The upload runs the same scan as `memory scan` — the same adapters, the same
@@@ -253,7 +253,7 @@ batch is sent again: nothing is dropped without being said.
## The status
```sh
--openvibe-node memory status [--show]
++openvibe-link memory status [--show]
```
It shows what your own store holds: how many items there are, per source and
@@@ -362,9 +362,9 @@ upload:
## The sync: back into the tools
```sh
--openvibe-node memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
--openvibe-node memory sync --disable a,b
--openvibe-node memory sync --undo
++openvibe-link memory sync [--tools a,b] [--project DIR …] [--dry-run] [--yes]
++openvibe-link memory sync --disable a,b
++openvibe-link memory sync --undo
```
The sync is the other direction: it writes **your own** profile and the
@@@ -389,7 +389,7 @@ Every write is inside one block, and the text outside it is not touched:
```markdown
<!-- openvibe-memory:start -->
--<!-- Managed by OpenVibe Node from your OpenVibe Memory. … -->
++<!-- Managed by OpenVibe Link from your OpenVibe Memory. … -->
## About you
…the cached profile…
@@@ -441,8 +441,8 @@ the memory text.
## The portable file
```sh
--openvibe-node memory export [--out FILE] [--scope SCOPE] [--force]
--openvibe-node memory import --file FILE [--dry-run] [--yes]
++openvibe-link memory export [--out FILE] [--scope SCOPE] [--force]
++openvibe-link memory import --file FILE [--dry-run] [--yes]
```
`memory export` writes the store's one portable file, `openvibe.memory-export@1`
ddiff --git a/docs/panel.md b/docs/panel.md
iindex ecee4e8..d0c2267 100644
---- a/docs/panel.md
++++ b/docs/panel.md
@@@ -7,7 +7,7 @@ keeps, and what happened. The design and the JSON it reads are in
## Opening it
- The tray's **Open Link** (the first item) opens it. The first time the tray runs, it opens by itself.
--- `openvibe-node panel` prints its address for a terminal without a tray.
++- `openvibe-link panel` prints its address for a terminal without a tray.
The address is `http://127.0.0.1:<port>/panel/#t=<token>`: the port is picked free at each start and the token is new
for every run (32 random bytes). The token is in the fragment, so the browser never sends it in a request or in a
@@@ -32,7 +32,7 @@ A restart makes a new port and a new token: an old link stops working.
| Call | What it does | Today |
|---|---|---|
| `GET /panel/api/state` | Everything the page shows | Machine (with its paired id), running (waiting approvals, jobs, connected shares, desks), capabilities, shares, Codes (the agent CLIs here and the worker's tasks today), activity |
--| `POST /panel/api/pause` `{ "paused": true }` | The local kill switch, the same latch as `openvibe-node stop` / the tray | Real |
++| `POST /panel/api/pause` `{ "paused": true }` | The local kill switch, the same latch as `openvibe-link stop` / the tray | Real |
| `POST /panel/api/running/:id/stop` | Stops one running thing | Waiting approvals (deny), jobs (cancel), shares (end) |
| `POST /panel/api/codes/tasks/:id/stop` | Stops a local Codes task | Real: cancels the Run job in the worker, the same way Codes' own stop does; `409` when it is not running here |
| `POST /panel/api/capabilities/:id` `{ "on": false }` | Turns a capability off here | Real; persisted in `<state>/panel.json` |
ddiff --git a/docs/plugins.md b/docs/plugins.md
iindex b493289..1c4ea12 100644
---- a/docs/plugins.md
++++ b/docs/plugins.md
@@@ -23,7 +23,7 @@ hardware.
A bundled plugin needs only its `name`; the core runs `<state>/venv/bin/python -m openvibe_<name>`. Anything else needs
`command`. `config` is sent to the plugin in `hello`. The core adds `frame_dir` (where to write camera frames) unless
set, and the environment variables `OPENVIBE_NODE_STATE` and `OPENVIBE_NODE_CONFIG` (its state and config directories)
--and `OPENVIBE_NODE_EXE` (its own executable), so a command a plugin runs with it (`openvibe-node desk-sandbox`, the
++and `OPENVIBE_NODE_EXE` (its own executable), so a command a plugin runs with it (`openvibe-link desk-sandbox`, the
computer plugin's shared desks) reads the same config as the Node that started it.
## Lifecycle
@@@ -42,7 +42,7 @@ A plugin that exits is restarted with exponential backoff (1 s doubling to 30 s,
starts stopped; if the e-stop or the local kill switch is latched, the core sends `estop` right after `hello`.
A plugin that does not describe itself within 15 s, or stops reading stdin, is killed.
--Probe mode (`hello` with `"probe": true`) is used by `openvibe-node pair` and `openvibe-node plugins`: the plugin
++Probe mode (`hello` with `"probe": true`) is used by `openvibe-link pair` and `openvibe-link plugins`: the plugin
describes itself and waits for EOF without opening any hardware.
## Messages
@@@ -180,14 +180,14 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
- a `halt` (a change of driver, with its `turn_epoch`, or an operator's halt);
- the link to Bot dropping;
- the core shutting down (then stdin closes);
-- - `openvibe-node quit`;
++ - `openvibe-link quit`;
- a plugin missing its reply window.
- `estop` stops every actuator **and latches**. The core refuses `drive`, `ptz` and `actuator` itself (`estopped`,
`local_stop`), and so does the runtime, until `resume`. The latch:
-- - is set by Bot's e-stop (an owner or operator presses it), by `openvibe-node stop`, or by a driver's `local_stop`;
++ - is set by Bot's e-stop (an owner or operator presses it), by `openvibe-link stop`, or by a driver's `local_stop`;
- is written to disk, survives a Node restart, and is sent again at once to a plugin that restarts;
- is reapplied from Bot's config on every reconnect.
-- - Only the owner clears Bot's e-stop. A local stop is cleared only by `openvibe-node resume` on the machine.
++ - Only the owner clears Bot's e-stop. A local stop is cleared only by `openvibe-link resume` on the machine.
- A `halt` still passes while latched.
- The driver's own stops (a motion deadline passing, a second without a heartbeat, stdin closing) are `stop`s.
Heartbeat loss also refuses motion until heartbeats return.
@@@ -240,11 +240,11 @@ Asked next by the same authors (2026-10-27, P7 LOOI shadow integration), and cha
|---|---|---|---|
| `stop` | `halt` | Bot's halt: a turn ended, a viewer handoff, a release, a timeout, an unbind (with `turn_epoch`) | no |
| `stop` | `link_lost` | the link to Bot dropped (two missed heartbeats, a close) | no |
-- | `stop` | `shutdown` | the Node is stopping (service stop, `openvibe-node quit`), or the core is stopping this process | no |
++ | `stop` | `shutdown` | the Node is stopping (service stop, `openvibe-link quit`), or the core is stopping this process | no |
| `stop` | `timeout` | a command got no answer from the driver within its window | no |
| `estop` | `remote` | the platform's e-stop (a robot owner or staff), with its `reason` | yes, until the owner clears it |
-- | `estop` | `local` | `openvibe-node stop` on the device | yes, until `openvibe-node resume` |
-- | `estop` | `driver` | a driver's own `local_stop` (this plugin's or another's) | yes, until `openvibe-node resume` |
++ | `estop` | `local` | `openvibe-link stop` on the device | yes, until `openvibe-link resume` |
++ | `estop` | `driver` | a driver's own `local_stop` (this plugin's or another's) | yes, until `openvibe-link resume` |
| `estop` | `latched` | the latch was already set when the Node or this process started (`replayed: true`, `first_cause` when known) | yes |
| `resume` | `cleared` | the owner cleared the platform's latch | clears the core's and the runtime's refusal only |
@@@ -481,9 +481,9 @@ A plugin must stop every actuator itself — the core dying must never leave mot
**A driver's own stop.** A driver with a stop of its own (a stop button on the robot, a hotkey on a bridge computer,
a sensor it trusts) sends `local_stop` (Python: `self.local_stop("reason")`). The core stops every plugin at once and
latches its **local** stop with that reason, persisted across restarts; Bot shows the robot as stopped on the robot.
--Only `openvibe-node resume` on that machine releases it: the owner's remote clear does not, and nothing a plugin sends
++Only `openvibe-link resume` on that machine releases it: the owner's remote clear does not, and nothing a plugin sends
releases a latch. The Python runtime refuses motion from the moment it is called and ignores a `resume` that crosses it
--on the wire. `openvibe-node stop` sets the same latch from a terminal.
++on the wire. `openvibe-link stop` sets the same latch from a terminal.
A plugin whose setup can fail only because something is not up yet (the robot's computer still booting) sets
`retry_setup_s` (Python): the runtime sends the fault once, sets up again that often, and sends `ready` when it works,
@@@ -644,9 +644,9 @@ with the owner's control level (`observe`, `ask`, `trusted`, `full`) and folders
`screen` (not motion kinds: long deadlines, no clamping, not in `GuardedKinds`; the core refuses `exec` and file writes
while a stop is latched). Its `ack` carries the answer in `data`. The level is enforced in the plugin from its local
config, never from a command; `ask` holds a command (`Deferred` in the SDK), raises an `approval` event and answers
--once the owner runs `openvibe-node approve|deny <id>`, which the core forwards to the plugin as `{"op": "approval", "id",
++once the owner runs `openvibe-link approve|deny <id>`, which the core forwards to the plugin as `{"op": "approval", "id",
"approved"}`. The local kill switch drops it to `observe`. `input` (mouse and keyboard) and `browser` are kinds on the
--same plugin; the machine's own coding agents come later. While `openvibe-node tray` runs, the core sends the plugin
++same plugin; the machine's own coding agents come later. While `openvibe-link tray` runs, the core sends the plugin
`{"op": "tray", "attached": true, "ttl_s": 15}` every few seconds (and `attached: false` when it quits), and the plugin
leaves the desktop prompts to the tray.
@@@ -663,8 +663,8 @@ source the profile's page links to and lists it in the config's `plugins`, as wi
Before installing one, check that the profile is the one that was reviewed:
```sh
--openvibe-node profile verify community.ballbot # Bot from the config, else https://openvibe.bot
--openvibe-node profile verify community.ballbot --key-id a7a0f02462c68119 # accept only this signing key
++openvibe-link profile verify community.ballbot # Bot from the config, else https://openvibe.bot
++openvibe-link profile verify community.ballbot --key-id a7a0f02462c68119 # accept only this signing key
```
The Node fetches the profile (`GET /api/v1/profiles/:id`, with the `signature` Bot made on approval) and Bot's key
ddiff --git a/docs/protocol.md b/docs/protocol.md
iindex 3f8e7ee..cbc11dd 100644
---- a/docs/protocol.md
++++ b/docs/protocol.md
@@@ -45,7 +45,7 @@ RFC 9457 problems; the Node turns their `code` into a sentence: `bot.pairing_cod
(tests).
The credential and publish key are stored in `credential.json` (mode 0600, with the device and robot ids) and never
--appear in a URL, a log line, an error, `openvibe-node status` or telemetry (the Go type `credentials.Secret` prints as
++appear in a URL, a log line, an error, `openvibe-link status` or telemetry (the Go type `credentials.Secret` prints as
`[redacted]` in every format).
### Pairing through OpenVibe.Network
@@@ -126,7 +126,7 @@ body into the Node to take the new pair without pairing again:
```sh
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://openvibe.bot/api/v1/devices/dev_…/rotate \
-- | openvibe-node credential import
++ | openvibe-link credential import
```
`import` keeps every other stored field (server, WHIP URL, robot ids, profile, version), rewrites `credential.json`
@@@ -171,7 +171,7 @@ applied the Node runs nothing but `halt` (`nack not_ready`), so that latch is in
this machine was installed for; the local configuration always wins (a role never installs anything and never turns on
a feature the owner turned off here), and a role the Node does not know is ignored.
`estop` `latched:false` is the owner's clear: it clears the remote latch only, never the local kill switch. A computer
--plugin is the one exception to the remote latch's reach: an e-stop on a computer waits for `openvibe-node resume` on the
++plugin is the one exception to the remote latch's reach: an e-stop on a computer waits for `openvibe-link resume` on the
machine, whatever the server clears (docs/computer.md).
### Device → server
@@@ -256,7 +256,7 @@ reply window (an owner's approval on the machine plus the program's own run): ab
writing a file (`exec`, `fs` other than `list`/`stat`/`read`) are refused while the local kill switch is latched
(`nack local_stop`) or the remote e-stop is (`nack estopped`); capturing the screen and reading files are not. The
`computer` plugin applies its owner's control level, and its `ack` carries the answer in `data` (at most 512 KB of JSON,
--else `nack bad_value`). An `ask`-level command waits for `openvibe-node approve|deny <id>` on the machine; the plugin
++else `nack bad_value`). An `ask`-level command waits for `openvibe-link approve|deny <id>` on the machine; the plugin
raises an `approval` event while it waits and `approval_done` when it is answered or expires.
Commands are never queued while offline and never replayed after a reconnect.
@@@ -377,8 +377,8 @@ original expiry, [plugins.md](plugins.md) answer 9). Plugins may add their own;
|----------------------------------------------|----------------------------------------------------|-----------------------------------------|
| motion command's deadline passes | the plugin (its own timer) | no |
| 2 missed heartbeats / link closed | the core → `stop` to every plugin | no |
--| server `estop` | the core → `estop` to every plugin | yes, persisted; `estop` `latched:false` or `openvibe-node resume` |
--| `openvibe-node stop` (local kill switch) | the core → `estop` to every plugin | yes, persisted; only `openvibe-node resume` |
++| server `estop` | the core → `estop` to every plugin | yes, persisted; `estop` `latched:false` or `openvibe-link resume` |
++| `openvibe-link stop` (local kill switch) | the core → `estop` to every plugin | yes, persisted; only `openvibe-link resume` |
| core shutdown | `stop`, then stdin closed | no |
| core killed / crashed | the plugin: stdin EOF, or 1 s without a heartbeat | no |
| plugin crash | restarted with backoff; starts stopped; `estop` re-sent if latched | — |
ddiff --git a/docs/skills.md b/docs/skills.md
iindex 82186a6..d3161c6 100644
---- a/docs/skills.md
++++ b/docs/skills.md
@@@ -5,12 +5,12 @@ OpenVibe.Codes keeps skills, rules and MCP server records per person and per pro
tools already read, on this machine, inside one marked block.
```sh
--openvibe-node skills sync --tools claude-code,codex --project ~/code/my-project
--openvibe-node skills sync --tools claude-code --dry-run # the diffs, nothing written
--openvibe-node skills sync --undo # take every block out again
++openvibe-link skills sync --tools claude-code,codex --project ~/code/my-project
++openvibe-link skills sync --tools claude-code --dry-run # the diffs, nothing written
++openvibe-link skills sync --undo # take every block out again
```
--`OPENVIBE_TOKEN` must hold your OpenVibe access token (the same one `openvibe-node memory sync` uses). The sync asks
++`OPENVIBE_TOKEN` must hold your OpenVibe access token (the same one `openvibe-link memory sync` uses). The sync asks
`GET /api/v1/me/skills` on openvibe.codes, with `?project=<slug>` for a project, so it only ever sees what you chose
there.
ddiff --git a/docs/stream.md b/docs/stream.md
iindex 4d03fcc..ffe07ac 100644
---- a/docs/stream.md
++++ b/docs/stream.md
@@@ -16,7 +16,7 @@ curtain, and measures the glass-to-glass latency with a QR probe. The design is
The core mints the stream and its `wpt_…` ticket on Live with this machine's Network node token (audience
`openvibe.live`, capability `live.agent-stream.publish`). **The plugin never sees the WHIP URL or the ticket**, and
--neither does a log line, an error, `openvibe-node status` or the command's ack. The ticket is a
++neither does a log line, an error, `openvibe-link status` or the command's ack. The ticket is a
`credentials.Secret`.
## The command
@@@ -91,7 +91,7 @@ seconds, so a restarted plugin's first "clear" cannot uncover the desk.
```
# Live hands the desk a WHIP URL and a wpt_ ticket (create + state routes).
--OPENVIBE_STREAM_TICKET=wpt_… openvibe-node stream --probe \
++OPENVIBE_STREAM_TICKET=wpt_… openvibe-link stream --probe \
--whip-url https://whip.openvibe.live/whip/agent/ags_… --seconds 60
# The same ticket's view side, on a WHEP endpoint:
ddiff --git a/docs/worker.md b/docs/worker.md
iindex 7b8a870..529610c 100644
---- a/docs/worker.md
++++ b/docs/worker.md
@@@ -48,9 +48,9 @@ A skill that declares `runs: "sandbox"` has tools whose code runs as a **functio
skill runner. The runner is a declared function like the Codes runner, and it is **off until the owner turns it on**:
```sh
--sudo openvibe-node skills on # write the runner and declare it in worker.functions
--openvibe-node skills status [--json]
--sudo openvibe-node skills off
++sudo openvibe-link skills on # write the runner and declare it in worker.functions
++openvibe-link skills status [--json]
++sudo openvibe-link skills off
```
`skills on` finds a system Node.js (`/usr/bin/node`, `/usr/local/bin/node`, then `node` on PATH; `--node PATH` overrides),
@@@ -247,7 +247,7 @@ exactly as with the worker off: there is no weaker mode. A job whose sandbox fai
its function having run.
Requirements: Linux 5.14 or later with cgroup v2; the Node's cgroup **delegated** to it (`Delegate=yes` on its
--systemd unit, which the service `openvibe-node install` writes sets; running `install.sh` again rewrites an older
++systemd unit, which the service `openvibe-link install` writes sets; running `install.sh` again rewrites an older
install's service, or add it by hand: `sudo systemctl edit openvibe-node`, add `[Service]` and `Delegate=yes`,
restart). The Node moves itself into an `openvibe-node` leaf of that cgroup and makes one `openvibe-job-*` cgroup per
job beside it; a cgroup that holds any process other than the Node and its children (a login session's, say) is not
@@@ -302,11 +302,11 @@ AppArmor; a Node running as root with `run_as` is not affected.
Every kill is SIGKILL to the whole process group and to the leader, which is PID 1 of the job's PID namespace, so its
death also kills every process the job left behind. The watchdog kills at `ttl_ms` (from receipt), `wall_ms` (from
start), `cpu_ms`, `mem_bytes`, at `max_output_bytes` of stdout, on `job_cancel`, and the moment the e-stop or the local
--stop latches (`openvibe-node stop`); while the latch is set every job is refused, and `openvibe-node resume` admits
++stop latches (`openvibe-link stop`); while the latch is set every job is refused, and `openvibe-link resume` admits
them again. A Node shutting down kills its jobs (`stopped`); a job acked but not started yet never starts and gets its
`job_exit` (`stopped`) all the same.
--`openvibe-node status` lists the jobs not ended yet under `jobs` (`--json`: `id`, `function` as `name@version`,
++`openvibe-link status` lists the jobs not ended yet under `jobs` (`--json`: `id`, `function` as `name@version`,
`state` `admitted`, `running` or `stopping`, and `started_ms` once running); the list is empty when the worker is off.
## Isolation model and its limits
@@@ -420,16 +420,16 @@ name the new file.
- **What may be named.** An absolute path under the Node user's home, and a plain file. A path outside the home, a
directory or a symlink is refused when the config loads.
- **Which home.** The service's `HOME`, which `install.sh --plan-logins` sets to the owner's home. A
-- `sudo openvibe-node …` command uses the sudo user's home for the same check, so the CLI and the service see the same
++ `sudo openvibe-link …` command uses the sudo user's home for the same check, so the CLI and the service see the same
folder rather than root's.
- **A file that is not there is skipped**, not an error: the CLI it belongs to is simply not signed in. A file that is
there but cannot be read, or is not a plain file, ends the job: a half-copied credential is worse than none.
- **What the job sees.** `openvibe-codes logins` (the Codes job runner) reports each CLI as installed, signed in, and
with the limit windows its login file carries, and nothing else: no key, no token, no account name.
--### Desks in the sandbox (`openvibe-node desk-sandbox`, Node v0.32.0)
++### Desks in the sandbox (`openvibe-link desk-sandbox`, Node v0.32.0)
A desk host for other people (docs/computer.md, "Desk hosts for other people") runs each desk's browser in this same
--sandbox: `openvibe-node desk-sandbox [--memory-mb N] [--cpu-millis N] [--disk-mb N] [--pids N] [--egress public]
++sandbox: `openvibe-link desk-sandbox [--memory-mb N] [--cpu-millis N] [--disk-mb N] [--pids N] [--egress public]
[--bind PATH]... [--setenv NAME=VALUE]... -- PROGRAM [ARG]...`, which the computer plugin runs. What differs from a job:
- **It lives for hours and talks both ways.** Fds 3 and 4 pass through to the program (a browser's
ddiff --git a/install/install.sh b/install/install.sh
iindex 9e7ee0c..1a15f13 100755
---- a/install/install.sh
++++ b/install/install.sh
@@@ -1,12 +1,12 @@
#!/bin/sh
--# OpenVibe Node installer.
++# OpenVibe Link installer (the former OpenVibe Node; the openvibe-node command and service name keep working as aliases).
#
--# curl -fsSL https://node.openvibe.network/install | sh
--# curl -fsSL https://node.openvibe.network/install | sh -s -- --robot <rob_…> --code <CODE> [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none]
--# curl -fsSL https://node.openvibe.network/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> [--driver …]
--# curl -fsSL https://node.openvibe.network/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --worker
--# curl -fsSL https://node.openvibe.network/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --plan-logins
--# curl -fsSL https://node.openvibe.network/install | sh -s -- --setup <osk_…>
++# curl -fsSL https://openvibe.link/install | sh
++# curl -fsSL https://openvibe.link/install | sh -s -- --robot <rob_…> --code <CODE> [--driver adeept|adeept-mecanum|cozmo|looi|ros2|ros2-holonomic|none]
++# curl -fsSL https://openvibe.link/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> [--driver …]
++# curl -fsSL https://openvibe.link/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --worker
++# curl -fsSL https://openvibe.link/install | sh -s -- --network <URL> --pairing <pair_…> --code <CODE> --plan-logins
++# curl -fsSL https://openvibe.link/install | sh -s -- --setup <osk_…>
#
# (the same script is also at https://openvibe.bot/install, which keeps working)
#
@@@ -126,13 +126,13 @@ usage() {
echo " [--computer observe|ask|trusted|full] [--desk-host [--shared]] [--ai] [--ai-endpoint URL]"
echo " [--no-service] [--no-claim] [--setup osk_…]"
echo " [--local DIR] [--version TAG]"
-- echo " curl -fsSL https://node.openvibe.network/install | sh (finds the kit, then shows a code to approve on openvibe.bot/link)"
-- echo " curl -fsSL https://node.openvibe.network/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
-- echo " curl -fsSL https://node.openvibe.network/install | sh -s -- --network https://openvibe.network --pairing pair_… --code ABCD-1234"
-- echo " curl -fsSL https://node.openvibe.network/install | sh -s -- --network https://openvibe.network --pairing pair_… --code ABCD-1234 --plan-logins"
++ echo " curl -fsSL https://openvibe.link/install | sh (finds the kit, then shows a code to approve on openvibe.bot/link)"
++ echo " curl -fsSL https://openvibe.link/install | sh -s -- --robot rob_… --code ABCD-1234 --driver adeept"
++ echo " curl -fsSL https://openvibe.link/install | sh -s -- --network https://openvibe.network --pairing pair_… --code ABCD-1234"
++ echo " curl -fsSL https://openvibe.link/install | sh -s -- --network https://openvibe.network --pairing pair_… --code ABCD-1234 --plan-logins"
}
--say() { printf '%s\n' "openvibe-node: $*"; }
--die() { printf '%s\n' "openvibe-node: error: $*" >&2; exit 1; }
++say() { printf '%s\n' "openvibe-link: $*"; }
++die() { printf '%s\n' "openvibe-link: error: $*" >&2; exit 1; }
# --robot is the rob_… pairing target; it used to name the driver kind, which still works but is deprecated.
robot_arg() {
case "$1" in
@@@ -393,7 +393,7 @@ def call(method, path, body=None, headers=None):
with urllib.request.urlopen(req, timeout=20) as r:
return json.load(r)
def die(msg):
-- print(f'\nopenvibe-node: error: {msg}', file=sys.stderr)
++ print(f'\nopenvibe-link: error: {msg}', file=sys.stderr)
sys.exit(1)
setup_key = os.environ.get('OV_SETUP_KEY', '')
body = {'hostname': host, 'hardware': hw}
@@@ -859,9 +859,16 @@ if [ "$CLAIM" = 1 ] || [ "$DRIVER" = adeept ] || [ "$DRIVER" = adeept-mecanum ];
# ---- download ----
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
--BIN_NAME="openvibe-node-$OS-$ARCH"
++BIN_NAME="openvibe-link-$OS-$ARCH"
++OLD_BIN_NAME="openvibe-node-$OS-$ARCH"
if [ -n "$LOCAL" ]; then
-- cp "$LOCAL/$BIN_NAME" "$TMP/$BIN_NAME" || die "$LOCAL/$BIN_NAME not found"
++ if [ -f "$LOCAL/$BIN_NAME" ]; then
++ cp "$LOCAL/$BIN_NAME" "$TMP/$BIN_NAME" || die "$LOCAL/$BIN_NAME not found"
++ else
++ # A folder filled from a release before the rename still installs; the old name is an alias either way.
++ BIN_NAME="$OLD_BIN_NAME"
++ cp "$LOCAL/$BIN_NAME" "$TMP/$BIN_NAME" || die "$LOCAL/$BIN_NAME not found"
++ fi
cp "$LOCAL/openvibe-node-plugins.tar.gz" "$TMP/" || die "$LOCAL/openvibe-node-plugins.tar.gz not found"
else
if [ "$TAG" = latest ]; then
@@@ -875,7 +882,8 @@ else
else die "curl or wget is needed"; fi
}
say "downloading $BIN_NAME"
-- fetch "$BASE/$BIN_NAME" "$TMP/$BIN_NAME" || die "download failed: $BASE/$BIN_NAME"
++ # A release made before the rename has only the old asset name: install it as the alias, the same program.
++ fetch "$BASE/$BIN_NAME" "$TMP/$BIN_NAME" || { say "no $BIN_NAME in this release; trying $OLD_BIN_NAME"; BIN_NAME="$OLD_BIN_NAME"; fetch "$BASE/$BIN_NAME" "$TMP/$BIN_NAME"; } || die "download failed: $BASE/$BIN_NAME"
fetch "$BASE/openvibe-node-plugins.tar.gz" "$TMP/openvibe-node-plugins.tar.gz" || die "download failed: plugin bundle"
fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "download failed: SHA256SUMS"
( cd "$TMP"
@@@ -885,8 +893,10 @@ else
fi
# ---- binary ----
--$SUDO install -m 0755 "$TMP/$BIN_NAME" "$BIN_DIR/openvibe-node"
--say "installed $BIN_DIR/openvibe-node ($("$BIN_DIR/openvibe-node" version))"
++$SUDO install -m 0755 "$TMP/$BIN_NAME" "$BIN_DIR/openvibe-link"
++# The old command is a link to the same program for a year (plan 2026-11-07 §5), never a second install.
++$SUDO ln -sf openvibe-link "$BIN_DIR/openvibe-node" || $SUDO install -m 0755 "$TMP/$BIN_NAME" "$BIN_DIR/openvibe-node"
++say "installed $BIN_DIR/openvibe-link ($("$BIN_DIR/openvibe-link" version)); openvibe-node is an alias"
# ---- service account (Linux): hardware groups, no login ----
# A worker (--worker) runs the service as root: the job sandbox's run_as and its public egress (a veth, NAT and the
@@@ -1098,12 +1108,12 @@ elif [ -n "$COMPUTER" ]; then
# The tray starts when they sign in: what waits for approval with Approve and Deny, the stop and resume.
had_config=0
[ -d "$COMPUTER_HOME/.config" ] && had_config=1
-- if XDG_CONFIG_HOME="$COMPUTER_HOME/.config" "$BIN_DIR/openvibe-node" tray --autostart on >/dev/null 2>&1; then
++ if XDG_CONFIG_HOME="$COMPUTER_HOME/.config" "$BIN_DIR/openvibe-link" tray --autostart on >/dev/null 2>&1; then
if [ "$(id -u)" = 0 ]; then
[ "$had_config" = 1 ] || chown "$SVC_USER": "$COMPUTER_HOME/.config" 2>/dev/null || true
chown "$SVC_USER": "$COMPUTER_HOME/.config/autostart" "$COMPUTER_HOME/.config/autostart/openvibe-node-tray.desktop" 2>/dev/null || true
fi
-- say "the tray starts when $SVC_USER signs in (openvibe-node tray; openvibe-node tray --autostart off removes it)"
++ say "the tray starts when $SVC_USER signs in (openvibe-link tray; openvibe-link tray --autostart off removes it)"
fi
fi
@@@ -1194,19 +1204,19 @@ disable_stock_server
if [ "$SERVICE" = 1 ]; then
# An upgrade rewrites the service definition (this version's adds Delegate=yes, which the worker needs): the
# service manager refuses to install over an existing one. Config and credential are kept.
-- $SUDO "$BIN_DIR/openvibe-node" uninstall >/dev/null 2>&1 || true
++ $SUDO "$BIN_DIR/openvibe-link" uninstall >/dev/null 2>&1 || true
if [ -n "$SVC_USER" ]; then
-- $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER" >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install --user "$SVC_USER"
++ $SUDO "$BIN_DIR/openvibe-link" install --user "$SVC_USER" >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-link" install --user "$SVC_USER"
else
-- $SUDO "$BIN_DIR/openvibe-node" install >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-node" install
++ $SUDO "$BIN_DIR/openvibe-link" install >/dev/null 2>&1 || $SUDO "$BIN_DIR/openvibe-link" install
fi
say "service installed and started"
fi
# ---- the one command: the owner approves the code on openvibe.bot/link, then this machine pairs at once ----
if [ "$CLAIM" = 1 ]; then
-- if $SUDO "$BIN_DIR/openvibe-node" status --json 2>/dev/null | grep -q '"paired":true'; then
-- say "already paired; keeping it (to make it a new robot: sudo openvibe-node pair --force <CODE> with a code from openvibe.bot)"
++ if $SUDO "$BIN_DIR/openvibe-link" status --json 2>/dev/null | grep -q '"paired":true'; then
++ say "already paired; keeping it (to make it a new robot: sudo openvibe-link pair --force <CODE> with a code from openvibe.bot)"
CLAIM=0
else
claim_robot || die "not paired: nothing was added on openvibe.bot"
@@@ -1221,10 +1231,10 @@ fi
# ---- pair ----
if [ -n "$CODE" ]; then
-- if $SUDO "$BIN_DIR/openvibe-node" status --json 2>/dev/null | grep -q '"paired":true'; then
++ if $SUDO "$BIN_DIR/openvibe-link" status --json 2>/dev/null | grep -q '"paired":true'; then
say "already paired; keeping the credential (to pair again: sudo openvibe-node pair --force <CODE>, adding --network <URL> --pairing <pair_…> for a Network pairing)"
else
-- pair_with $SUDO "$BIN_DIR/openvibe-node"
++ pair_with $SUDO "$BIN_DIR/openvibe-link"
fi
else
say "not paired yet: get a code on openvibe.bot and run: sudo openvibe-node pair <CODE>"
@@@ -1235,7 +1245,7 @@ if [ "$CLAIM" = 1 ] && [ "$SERVICE" = 1 ]; then
say "checking the link, the driver and the camera (up to a minute)"
_status="" _i=0
while [ "$_i" -lt 30 ]; do
-- _status="$($SUDO "$BIN_DIR/openvibe-node" status --json 2>/dev/null || true)"
++ _status="$($SUDO "$BIN_DIR/openvibe-link" status --json 2>/dev/null || true)"
if printf '%s' "$_status" | grep -q '"connected":true' && printf '%s' "$_status" | grep -q '"state":"live"'; then break; fi
_i=$((_i + 1))
sleep 2
@@@ -1258,5 +1268,5 @@ print(" camera %s%s" % (v.get("state") or "off", (" (" + v["last_error"] + ")
[ "${LIVE:-0}" = 0 ] || say "its camera is on your OpenVibe.Live channel while it streams"
fi
--say "done. Check it with: sudo openvibe-node status Stop everything with: sudo openvibe-node stop"
--say "first run: sudo openvibe-node (checks the link and reports this machine's first job on openvibe.bot/machines)"
++say "done. Check it with: sudo openvibe-link status Stop everything with: sudo openvibe-link stop"
++say "first run: sudo openvibe-link (checks the link and reports this machine's first job on openvibe.bot/machines)"
ddiff --git a/internal/memorysync/sync.go b/internal/memorysync/sync.go
iindex 7064193..e762bf1 100644
---- a/internal/memorysync/sync.go
++++ b/internal/memorysync/sync.go
@@@ -119,7 +119,7 @@ func Body(content Content) string {
if text == "" && len(rules) == 0 {
return ""
}
-- b.WriteString("<!-- Managed by OpenVibe Node from your OpenVibe Memory. Edit the memory in OpenVibe, not in this block. This file may be shared or committed. -->\n")
++ b.WriteString("<!-- Managed by OpenVibe Link from your OpenVibe Memory. Edit the memory in OpenVibe, not in this block. This file may be shared or committed. -->\n")
if text != "" {
if isProject(content.Scope) {
b.WriteString("\n## About this project\n\n")
ddiff --git a/internal/service/service.go b/internal/service/service.go
iindex c2c724f..f2990cf 100644
---- a/internal/service/service.go
++++ b/internal/service/service.go
@@@ -15,8 +15,8 @@ import (
const (
Name = "openvibe-node"
-- DisplayName = "OpenVibe Node"
-- Description = "Connects this device to OpenVibe (robots, cameras, computer control) over one outbound link."
++ DisplayName = "OpenVibe Link"
++ Description = "OpenVibe Link connects this machine to OpenVibe (your agents, robots, cameras, computer control) over one outbound link."
)
// Program is the body of the service: run until ctx ends.
ddiff --git a/internal/skillsync/sync.go b/internal/skillsync/sync.go
iindex 44a8961..a8b2858 100644
---- a/internal/skillsync/sync.go
++++ b/internal/skillsync/sync.go
@@@ -105,7 +105,7 @@ func RulesBlock(rules []string) string {
return ""
}
var b strings.Builder
-- b.WriteString("<!-- Managed by OpenVibe Node from your OpenVibe skills hub. Edit the rules in OpenVibe, not in this block. -->\n")
++ b.WriteString("<!-- Managed by OpenVibe Link from your OpenVibe skills hub. Edit the rules in OpenVibe, not in this block. -->\n")
for _, rule := range clean {
b.WriteString("- " + rule + "\n")
}
ddiff --git a/plugins/ai/openvibe_ai/__init__.py b/plugins/ai/openvibe_ai/__init__.py
iindex 91b7b73..cd7469a 100644
---- a/plugins/ai/openvibe_ai/__init__.py
++++ b/plugins/ai/openvibe_ai/__init__.py
@@@ -1,4 +1,4 @@
--"""OpenVibe Node ai plugin: the person's own models for their own OpenVibe work (contracts 0.219.0, kind ``ai``).
++"""OpenVibe Link ai plugin: the person's own models for their own OpenVibe work (contracts 0.219.0, kind ``ai``).
Many people already run models at home (Ollama, llama.cpp's server, LM Studio). This plugin lets OpenVibe use them for
their owner, for Vibe's answers or an agent's thinking, without the prompt leaving the machine's network and with no
ddiff --git a/plugins/computer/openvibe_computer/__init__.py b/plugins/computer/openvibe_computer/__init__.py
iindex 7fa1620..f394cd9 100644
---- a/plugins/computer/openvibe_computer/__init__.py
++++ b/plugins/computer/openvibe_computer/__init__.py
@@@ -1,4 +1,4 @@
--"""OpenVibe Node computer plugin: the machine's terminal, files, screen and input for its owner's agents (plan T14/T17).
++"""OpenVibe Link computer plugin: the machine's terminal, files, screen and input for its owner's agents (plan T14/T17).
Off unless its owner lists it in the Node's config. The owner sets the **control level** on the machine itself; a cloud
command can never raise it:
@@@ -143,7 +143,7 @@ INPUT_GUARD_S = 2.0
# macOS dialog); the owner answers it in full with `openvibe-node approvals`.
NOTIFY_SUMMARY_MAX = 300
DIALOG_SUMMARY_MAX = 400
--FULL_VIEW_HINT = "openvibe-node approvals"
++FULL_VIEW_HINT = "openvibe-link approvals"
# Control, ANSI and bidi characters are escaped before any surface shows them: a summary can be a terminal, a
# notification or a dialog, and none of them may interpret what the agent wrote as layout or markup.
BIDI_CHARS = frozenset("\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069")
@@@ -1383,23 +1383,23 @@ class Computer(Plugin):
return
title = "OpenVibe: your agent asks"
summary = _plain(summary)
-- hint = "openvibe-node approve %s (or deny %s)" % (shlex.quote(_plain(cid)), shlex.quote(_plain(cid)))
++ hint = "openvibe-link approve %s (or deny %s)" % (shlex.quote(_plain(cid)), shlex.quote(_plain(cid)))
parse = None
if self.platform.startswith("linux") and (self.env.get("DISPLAY") or self.env.get("WAYLAND_DISPLAY")) and self.which("notify-send"):
# A notification cannot show an arbitrarily long request; when it is longer than this surface can show in
# full, it offers no button at all and points at the full view instead of approving a cut summary.
if self._notify_can_act() and len(summary) <= NOTIFY_SUMMARY_MAX:
-- argv = ["notify-send", "--app-name=OpenVibe Node", "--urgency=critical", "--wait",
++ argv = ["notify-send", "--app-name=OpenVibe Link", "--urgency=critical", "--wait",
"--expire-time=%d" % (self.approval_timeout_s * 1000), "--action=approve=Approve",
"--action=deny=Deny", "--", title, _markup(summary)]
parse = _parse_notify_send
elif len(summary) > NOTIFY_SUMMARY_MAX:
-- argv = ["notify-send", "--app-name=OpenVibe Node", "--urgency=critical",
++ argv = ["notify-send", "--app-name=OpenVibe Link", "--urgency=critical",
"--expire-time=%d" % (self.approval_timeout_s * 1000), "--", title,
_markup("This request is too long to show whole here. Run `%s` to read it and answer it.\n%s"
% (FULL_VIEW_HINT, hint))]
else:
-- argv = ["notify-send", "--app-name=OpenVibe Node", title, _markup("%s\n%s" % (summary, hint))]
++ argv = ["notify-send", "--app-name=OpenVibe Link", title, _markup("%s\n%s" % (summary, hint))]
elif self.platform == "darwin" and self.which("osascript"):
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
if len(summary) <= DIALOG_SUMMARY_MAX:
@@@ -2653,10 +2653,10 @@ class Computer(Plugin):
if not self.notify:
return
title = "Your screen is live on OpenVibe"
-- body = "Shown to your own account for %d min. Run `openvibe-node stop` to end it now." % max(1, seconds // 60)
++ body = "Shown to your own account for %d min. Run `openvibe-link stop` to end it now." % max(1, seconds // 60)
argv = None
if self.platform.startswith("linux") and (self.env.get("DISPLAY") or self.env.get("WAYLAND_DISPLAY")) and self.which("notify-send"):
-- argv = ["notify-send", "--app-name=OpenVibe Node", "--", title, body]
++ argv = ["notify-send", "--app-name=OpenVibe Link", "--", title, body]
elif self.platform == "darwin" and self.which("osascript"):
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
argv = ["osascript", "-e", "display notification %s with title %s" % (quote(body), quote(title))]
ddiff --git a/plugins/computer/tests/test_computer.py b/plugins/computer/tests/test_computer.py
iindex f1b4b2d..5fa16d7 100644
---- a/plugins/computer/tests/test_computer.py
++++ b/plugins/computer/tests/test_computer.py
@@@ -203,8 +203,8 @@ class ComputerTest(unittest.TestCase):
self.plugin.notify_actions = False # an older notify-send: no buttons, the command to answer with
cid = self.cmd("exec", {"argv": ["echo", "hi"]})
self.out.wait_for(lambda m: m.get("name") == "approval")
-- self.assertEqual(calls[0][:3], ["notify-send", "--app-name=OpenVibe Node", "OpenVibe: your agent asks"])
-- self.assertIn("openvibe-node approve %s" % cid, calls[0][3])
++ self.assertEqual(calls[0][:3], ["notify-send", "--app-name=OpenVibe Link", "OpenVibe: your agent asks"])
++ self.assertIn("openvibe-link approve %s" % cid, calls[0][3])
self.plugin.env = {}
self.cmd("exec", {"argv": ["echo", "again"]})
self.assertEqual(len(calls), 1, "no graphical session: no notification")
@@@ -728,7 +728,7 @@ class ComputerTest(unittest.TestCase):
self.out.wait_for(lambda m: m.get("name") == "approval" and m.get("id") == cid)
argv = prompts[0].argv
self.assertNotIn("--action=approve=Approve", argv, "no approval button on cut text")
-- self.assertIn("openvibe-node approvals", " ".join(argv))
++ self.assertIn("openvibe-link approvals", " ".join(argv))
def test_a_stale_prompt_answer_is_discarded(self):
# F6: an answer from the prompt instance that is no longer the one shown is not the owner's answer.
ddiff --git a/plugins/ros2/openvibe_ros2/__init__.py b/plugins/ros2/openvibe_ros2/__init__.py
iindex 6567527..fe6f36b 100644
---- a/plugins/ros2/openvibe_ros2/__init__.py
++++ b/plugins/ros2/openvibe_ros2/__init__.py
@@@ -1,4 +1,4 @@
--"""OpenVibe Node bridge plugin for ROS 2 robots.
++"""OpenVibe Link bridge plugin for ROS 2 robots.
The Node runs on the robot's computer (or next to it) and this plugin turns Bot's commands into ROS 2 messages and the
robot's topics into telemetry and camera frames:
ddiff --git a/plugins/sdk/openvibe_plugin/__init__.py b/plugins/sdk/openvibe_plugin/__init__.py
iindex 7448cd7..90ae7c6 100644
---- a/plugins/sdk/openvibe_plugin/__init__.py
++++ b/plugins/sdk/openvibe_plugin/__init__.py
@@@ -1,4 +1,4 @@
--"""OpenVibe Node plugin runtime.
++"""OpenVibe Link plugin runtime.
A plugin is an executable the Node core starts. It reads JSON lines from stdin and writes JSON lines to stdout (one
object per line; logs go to stderr). This module implements the parts every driver must get right, so a driver only
ddiff --git a/plugins/voice/openvibe_voice/__init__.py b/plugins/voice/openvibe_voice/__init__.py
iindex 652e6f3..fe3b06a 100644
---- a/plugins/voice/openvibe_voice/__init__.py
++++ b/plugins/voice/openvibe_voice/__init__.py
@@@ -1,4 +1,4 @@
--"""OpenVibe Node voice plugin: Vibe by voice on the person's own machine (contracts 0.214.0; consumer plan §8).
++"""OpenVibe Link voice plugin: Vibe by voice on the person's own machine (contracts 0.214.0; consumer plan §8).
The owner lets Vibe answer their voice on this machine and chooses a call word on openvibe.bot/machines; Bot's config
says so (the core passes it on as the ``voice`` op). While it is on, this plugin listens on the machine itself, and
ddiff --git a/scripts/dist.sh b/scripts/dist.sh
iindex 881a45e..d8ea8ae 100755
---- a/scripts/dist.sh
++++ b/scripts/dist.sh
@@@ -11,15 +11,19 @@ for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 wind
rest="${target#*/}"
arch="${rest%%/*}"
arm=""
-- name="openvibe-node-$os-$arch"
++ name="openvibe-link-$os-$arch"
if [ "$arch" = arm ]; then
arm="${rest#*/}"
-- name="openvibe-node-$os-armv$arm"
++ name="openvibe-link-$os-armv$arm"
fi
[ "$os" = windows ] && name="$name.exe"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" GOARM="$arm" \
go build -trimpath -ldflags "-s -w -X main.version=$VERSION" -o "dist/$name" ./cmd/openvibe-node
-- echo "built dist/$name"
++ # openvibe-node is kept as an alias for existing installs and scripts (plan 2026-11-07 §5): the same binary,
++ # under the old asset name, in every release for a year. The daemon's unit/service name does not change.
++ old="openvibe-node-${name#openvibe-link-}"
++ cp "dist/$name" "dist/$old"
++ echo "built dist/$name and dist/$old"
done
# The plugin bundle: the Python packages without their tests.
tar -czf dist/openvibe-node-plugins.tar.gz -C plugins \