openvibers/openvibe.link Commits
OpenVibe Link: the name, the command and the installer, with openvibe-node kept as the alias (#133)
Changes
@@@ -33,7 +33,12 @@ jobs:test -s dist/install.shtest -s dist/openvibe-node-plugins.tar.gztest -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 releaseenv: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@@@ -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 productslike **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 outOn 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 codeto 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-raspberWith 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, manualinstall, 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 thelocal OpenAI-compatible server (Ollama by default at `127.0.0.1:11434`), registers the public https address through theExchange'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 enbind 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 thatsignature against the key Bot publishes (`--key-id` pins one) and names the plugin the robots run; pairing with a roboton 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 readlogin'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 appliesthe ones this machine was installed for: the job worker starts if the local configuration has it, and the machine saysplainly which role it could not honour. The local configuration always wins: a role never installs anything and neverturns 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 itselfat `127.0.0.1` that shows what runs here, what this computer allows, your shares, the coding-agent CLIs and the Codestasks 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 addressfragment 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), theDock's activity read-only, and **Send to OpenVibe** from any app's share sheet to your Download drive. It pairs fromopenvibe.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 driverprotocol, 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 toyour own memory store), [skills](docs/skills.md) (the skills hub: which folders each tool reads, the marked block, thedry run and the undo).Decisions: [ADR-043](docs/ADR-043-bot-devices-and-control.md).@@@ -20,7 +20,7 @@ type listFlag []stringfunc (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)@@@ -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()@@@ -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.PairedAtif 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]@@@ -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},}@@@ -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 versionpackage mainimport (@@@ -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 nextstart 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 plugincalls 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 --undowrite the skills and rules you chose at openvibe.codes/skillsinto 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 fromOPENVIBE_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 versionGlobal 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) intshKey = 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) intpositional, 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) interr = 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) interr = 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) intreturn 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[strreturn 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 stringnetwork, 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 itfmt.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)@@@ -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) {@@@ -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" +@@@ -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" +@@@ -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" +@@@ -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" +@@@ -4,7 +4,7 @@ package mainimport "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 }@@@ -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 stringport 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 upMake a share at https://connect.openvibe.website. On its page, pick this computer under "Or serve it from one of yourcomputers" 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.Wrif 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.Wrreturn 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 {@@@ -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)@@@ -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" +@@@ -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}@@@ -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"}@@@ -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 }@@@ -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 *globalcore.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 []bytevar err errorif 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}@@@ -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)@@@ -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 pullrequest when the core and both robot plugins pass their tests. Do not deploy anything; you have no access to OpenVibe'sservers 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-nodeplugins` (list, with capabilities). Config in `/etc/openvibe-node/` (or the platform's equivalent); the credential file ismode 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 pluginonly 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 fakeserver, publishes a test pattern, obeys commands and stops on every safety path; the Adeept and Cozmo plugins pass theirtests with fake hardware; `docs/protocol.md`, `docs/plugins.md` and `docs/install.md` written; README says what the Nodeis and how to install it.@@@ -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 therequest 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 thiscomputer's models** for the machine on openvibe.bot/machines.| key | default | meaning |@@@ -19,7 +19,7 @@ and adds the plugin with your home folder as its only folder. It cannot share aworker'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, andwhether 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 canreach: the folders are the plugin's own limit, not a sandbox around the program. Read what it would do before youapprove 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 yourdesktop 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 buttexpires. 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 amoment", 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 saythat 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, whatwaits 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 localresume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remotelatch, 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 bydefault), at most 1600 px wide when Pillow is installed. The core encodes the frames (ffmpeg, else its built-inencoder) 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 ofyours 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 byhandle, 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 ownuser, mount, network, PID and IPC namespaces, as the unprivileged `openvibe-job` (`worker.run_as`), in a cgroup of itsown 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@@@ -12,12 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or serthen 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`, whichkeeps 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 thetunnel 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@@@ -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-pasteinstaller; the panel is `openvibe.bot/panel/<rob_…>`. Nothing here runs the kit's stock control server — the Nodereplaces 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 theH.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 testpattern. Report `video.state`, `video.last_error` and the journal lines above.@@@ -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`: theingest URL + publish key the pairing returned is stored in `/etc/openvibe-node/credential.json`, and video publishesthere 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 isstill 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@@@ -1,4 +1,4 @@--# LOOI on the OpenVibe Node++# LOOI on the OpenVibe LinkLOOI 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, theNode 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:```batset 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 noservice 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 itstarted with it (a Job Object), as it does on Linux and macOS, so a bridge helper cannot keep the Bluetooth link openafter 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.@@@ -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 openports, 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 storesmany installs reach each step and how long they take (`/admin/first-run`), and the machine's page shows what the firstjob 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 pluginsneed 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 Nodeobeys 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 Nodefrom 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 hasgone 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 asa 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.Networkrefused this machine's node credential" and exit status 78, which the systemd unit does not restart. Get a newinstaller 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 touchinghardware.## 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-nodesudo userdel openvibe-node```@@@ -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`@@@ -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 newfor 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` |@@@ -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) unlessset, 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`, thecomputer 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 plugindescribes 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 andlatches 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 sendsreleases 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 writeswhile a stop is latched). Its `ack` carries the answer in `data`. The level is enforced in the plugin from its localconfig, 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 pluginleaves 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 wiBefore 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@@@ -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:```shcurl -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 inthis machine was installed for; the local configuration always wins (a role never installs anything and never turns ona 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 themachine, 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): abwriting 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 pluginraises 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 | — |@@@ -5,12 +5,12 @@ OpenVibe.Codes keeps skills, rules and MCP server records per person and per protools 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 chosethere.@@@ -16,7 +16,7 @@ curtain, and measures the glass-to-glass latency with a QR probe. The design isThe 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:@@@ -48,9 +48,9 @@ A skill that declares `runs: "sandbox"` has tools whose code runs as a **functioskill 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 faiits 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 olderinstall'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 perjob 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 itsdeath also kills every process the job left behind. The watchdog kills at `ttl_ms` (from receipt), `wall_ms` (fromstart), `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` admitsthem 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, adirectory 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 samefolder 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 isthere 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, andwith 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@@@ -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"++ ficp "$LOCAL/openvibe-node-plugins.tar.gz" "$TMP/" || die "$LOCAL/openvibe-node-plugins.tar.gz not found"elseif [ "$TAG" = latest ]; then@@@ -875,7 +882,8 @@ elseelse 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 @@ elsefi# ---- 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; thenif [ "$(id -u)" = 0 ]; then[ "$had_config" = 1 ] || chown "$SVC_USER": "$COMPUTER_HOME/.config" 2>/dev/null || truechown "$SVC_USER": "$COMPUTER_HOME/.config/autostart" "$COMPUTER_HOME/.config/autostart/openvibe-node-tray.desktop" 2>/dev/null || truefi-- 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)"fifi@@@ -1194,19 +1204,19 @@ disable_stock_serverif [ "$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 || trueif [ -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" installfisay "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=0elseclaim_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'; thensay "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"fielsesay "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 ]; thensay "checking the link, the driver and the camera (up to a minute)"_status="" _i=0while [ "$_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)"@@@ -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")@@@ -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.@@@ -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")}@@@ -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 fortheir owner, for Vibe's answers or an agent's thinking, without the prompt leaving the machine's network and with no@@@ -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 cloudcommand 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 = 300DIALOG_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):returntitle = "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 = Noneif 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_sendelif 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:returntitle = "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 = Noneif 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))]@@@ -203,8 +203,8 @@ class ComputerTest(unittest.TestCase):self.plugin.notify_actions = False # an older notify-send: no buttons, the command to answer withcid = 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].argvself.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.@@@ -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 therobot's topics into telemetry and camera frames:@@@ -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 (oneobject per line; logs go to stderr). This module implements the parts every driver must get right, so a driver only@@@ -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 configsays so (the core passes it on as the ``voice`` op). While it is on, this plugin listens on the machine itself, and@@@ -11,15 +11,19 @@ for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 windrest="${target#*/}"arch="${rest%%/*}"arm=""-- name="openvibe-node-$os-$arch"++ name="openvibe-link-$os-$arch"if [ "$arch" = arm ]; thenarm="${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 \
@@ -33,7 +33,12 @@ jobs:
@@ -33,7 +33,12 @@ jobs:
test -s dist/install.sh
test -s dist/install.sh
test -s dist/openvibe-node-plugins.tar.gz
test -s dist/openvibe-node-plugins.tar.gz
test -s dist/SHA256SUMS
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
- name: publish the release
env:
env:
GH_TOKEN: ${{ github.token }}
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
@@ -1,4 +1,8 @@
@@ -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
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
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
@@ -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:
On a robot's Raspberry Pi (or other Linux computer), the one command:
```sh
```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
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
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
@@ -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:
With a robot already added on openvibe.bot:
```sh
```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`,
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`
@@ -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
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).
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
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):
**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
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.
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
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
@@ -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.
bind it to loopback, which is the default.
```sh
```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
## What is in here
@@ -72,16 +76,16 @@ openvibe-node exchange serve --model qwen3:32b --public-url https://gpu.example.
@@ -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.
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
**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
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
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)).
([docs/plugins.md](docs/plugins.md#community-plugins)).
**Your computer for your agents:** `--computer observe|ask|trusted|full` adds the `computer` plugin (terminal, files,
**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
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
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,
**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
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
@@ -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.
login's content.
**What the machine does for the network:** the owner sets the machine's roles on openvibe.network/machines
**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
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
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.
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
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,
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
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.
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
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
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
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:
@@ -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
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),
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),
[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
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).
dry run and the undo).
Decisions: [ADR-043](docs/ADR-043-bot-devices-and-control.md).
Decisions: [ADR-043](docs/ADR-043-bot-devices-and-control.md).
@@ -20,7 +20,7 @@ type listFlag []string
@@ -20,7 +20,7 @@ type listFlag []string
func (l *listFlag) String() string { return strings.Join(*l, ",") }
func (l *listFlag) String() string { return strings.Join(*l, ",") }
func (l *listFlag) Set(v string) error { *l = append(*l, v); return nil }
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]...
[--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
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.
@@ -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.
// deskPipe answers the desk's two pipe ends (its fds 3 and 4); tests replace it.
var deskPipe = inheritedDeskPipe
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.
// browser's) are never read as the Node's.
func cmdDeskSandbox(args []string, stderr io.Writer) int {
func cmdDeskSandbox(args []string, stderr io.Writer) int {
fs := flag.NewFlagSet("desk-sandbox", flag.ContinueOnError)
fs := flag.NewFlagSet("desk-sandbox", flag.ContinueOnError)
@@ -13,7 +13,7 @@ import (
@@ -13,7 +13,7 @@ import (
// config with no worker.run_as.
// config with no worker.run_as.
func TestDeskSandboxCommand(t *testing.T) {
func TestDeskSandboxCommand(t *testing.T) {
var out, errb bytes.Buffer
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())
t.Fatalf("no program: code %d, stderr %q", code, errb.String())
}
}
errb.Reset()
errb.Reset()
@@ -23,12 +23,12 @@ import (
@@ -23,12 +23,12 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/protocol"
"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
// 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"
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.
// a machine's installer command can be pasted at the prompt, and so scripts can pass the code and Network pairing.
type guideArgs struct {
type guideArgs struct {
code, robot, name, server, network, pairing string
code, robot, name, server, network, pairing string
@@ -56,7 +56,7 @@ func parseGuideFlags(args []string, g *globals, a *guideArgs, stderr io.Writer)
@@ -56,7 +56,7 @@ func parseGuideFlags(args []string, g *globals, a *guideArgs, stderr io.Writer)
// never asks again once a credential exists.
// never asks again once a credential exists.
func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader, stdout io.Writer) error {
func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader, stdout io.Writer) error {
if len(positional) > 1 {
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 == "" {
if len(positional) == 1 && a.code == "" {
a.code = positional[0]
a.code = positional[0]
@@ -71,7 +71,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
@@ -71,7 +71,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
fmt.Fprintln(stdout, " sudo openvibe-node")
fmt.Fprintln(stdout, " sudo openvibe-node")
return fmt.Errorf("cannot write to %s: %w (run with sudo)", g.paths.StateDir, err)
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, "======================")
fmt.Fprintln(stdout, "======================")
report, err := firstrun.Load(g.paths.FirstRunFile())
report, err := firstrun.Load(g.paths.FirstRunFile())
@@ -118,7 +118,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
@@ -118,7 +118,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
if err != nil {
if err != nil {
fmt.Fprintln(stdout)
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, "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
return err
}
}
at := creds.PairedAt
at := creds.PairedAt
@@ -130,7 +130,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
@@ -130,7 +130,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
} else if err != nil {
} else if err != nil {
return err
return err
} else {
} 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) {
if !report.Has(firstrun.StepPaired) {
at := creds.PairedAt
at := creds.PairedAt
if at.IsZero() {
if at.IsZero() {
@@ -198,7 +198,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
@@ -198,7 +198,7 @@ func cmdFirstRun(g *globals, a guideArgs, positional []string, stdin io.Reader,
events := report.Events(version, info.OS, info.Arch)
events := report.Events(version, info.OS, info.Arch)
if len(events) > 0 {
if len(events) > 0 {
if err := sendFirstRun(g, creds, events); err != nil {
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 {
} else {
fmt.Fprintln(stdout, "Reported this first run to OpenVibe.Bot.")
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,
@@ -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.
// (d) What it can do next, in one line, with the docs.
fmt.Fprintln(stdout)
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)
fmt.Fprintf(stdout, "your account lists it at https://openvibe.bot/machines. Docs: %s\n", firstRunDocs)
return nil
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) {
func pairFromGuide(g *globals, a pairArgs, stdout io.Writer) (*credentials.Credentials, error) {
cfg, noConfig, err := preparePair(g, a)
cfg, noConfig, err := preparePair(g, a)
if err != nil {
if err != nil {
@@ -353,7 +353,7 @@ func parsePairingInput(line string) (pairArgs, error) {
@@ -353,7 +353,7 @@ func parsePairingInput(line string) (pairArgs, error) {
}
}
}
}
if a.code == "" {
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++ {
for i := 0; i+1 < len(fields); i++ {
if fields[i] == "pair" && !strings.HasPrefix(fields[i+1], "-") {
if fields[i] == "pair" && !strings.HasPrefix(fields[i+1], "-") {
a.code = fields[i+1]
a.code = fields[i+1]
@@ -81,7 +81,7 @@ func TestFirstRunSuccessReportsOneEventPerStep(t *testing.T) {
@@ -81,7 +81,7 @@ func TestFirstRunSuccessReportsOneEventPerStep(t *testing.T) {
if code != 0 {
if code != 0 {
t.Fatalf("exit %d, out %s err %s", code, out, errOut)
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"} {
"First job: this machine", "system " + runtime.GOOS, "Reported this first run to OpenVibe.Bot.", "Next:", "docs/install.md"} {
if !strings.Contains(out, want) {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in output:\n%s", want, out)
t.Fatalf("missing %q in output:\n%s", want, out)
@@ -174,10 +174,10 @@ func TestParsePairingInput(t *testing.T) {
@@ -174,10 +174,10 @@ func TestParsePairingInput(t *testing.T) {
ok bool
ok bool
}{
}{
{"ABCD-1234", "ABCD-1234", "", "", "", true},
{"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",
{"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},
"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},
{"", "", "", "", "", false},
{"curl https://openvibe.bot/install | sh", "", "", "", "", false},
{"curl https://openvibe.bot/install | sh", "", "", "", "", false},
}
}
@@ -1,23 +1,23 @@
@@ -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)
// (--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)
// 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
// 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
package main
import (
import (
@@ -54,56 +54,56 @@ import (
@@ -54,56 +54,56 @@ import (
// version is set at build time: -ldflags "-X main.version=v0.1.0".
// version is set at build time: -ldflags "-X main.version=v0.1.0".
var version = "dev"
var version = "dev"
-const usage = `openvibe-node %s: connect this device to OpenVibe.
+const usage = `openvibe-link %s: connect this device to OpenVibe.
Usage:
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
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)
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,
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
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
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
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)
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 the memory files of the coding tools on this machine
(read-only, local, secrets removed)
(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
write the skills and rules you chose at openvibe.codes/skills
into each tool's own folders, inside one marked block
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
run a skill's code as a sandboxed job on this machine
([email protected]; off until "on")
([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
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
publish a QR of the millisecond clock for the headless latency viewer (the ticket comes from
OPENVIBE_STREAM_TICKET, never argv)
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
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):
Global flags (before or after the command):
--home DIR keep config and state in DIR (also OPENVIBE_NODE_HOME)
--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 {
@@ -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 {
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 len(args) > 0 && (args[0] == "-h" || args[0] == "--help" || args[0] == "help" || args[0] == "--version") {
if 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
return 0
}
}
fmt.Fprintf(stdout, usage, version)
fmt.Fprintf(stdout, usage, version)
return 0
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.
// 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], "-") {
if len(args) == 0 || args[0] == "first-run" || strings.HasPrefix(args[0], "-") {
g := &globals{}
g := &globals{}
@@ -197,7 +197,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -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)")
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)")
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)")
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.
// Accept flags anywhere: split positionals out.
var positional []string
var positional []string
@@ -226,7 +226,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -226,7 +226,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
positional, p.pairing = []string{p.pairing}, ""
positional, p.pairing = []string{p.pairing}, ""
}
}
if len(positional) != 1 {
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
return 2
}
}
p.code = positional[0]
p.code = positional[0]
@@ -237,7 +237,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -237,7 +237,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
}
}
case "credential":
case "credential":
if len(positional) != 1 || positional[0] != "import" {
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
return 2
}
}
err = cmdCredentialImport(g, stdin, stdout)
err = cmdCredentialImport(g, stdin, stdout)
@@ -275,7 +275,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -275,7 +275,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
err = cmdApprovals(g, *asJSON, stdout)
err = cmdApprovals(g, *asJSON, stdout)
case "approve", "deny":
case "approve", "deny":
if len(positional) != 1 {
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
return 2
}
}
err = cmdAnswer(g, cmd, positional[0], stdout)
err = cmdAnswer(g, cmd, positional[0], stdout)
@@ -285,7 +285,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -285,7 +285,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
err = cmdPanel(g, stdout)
err = cmdPanel(g, stdout)
case "profile":
case "profile":
if len(positional) != 2 || positional[0] != "verify" {
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
return 2
}
}
err = cmdProfileVerify(g, positional[1], *server, *keyID, stdout)
err = cmdProfileVerify(g, positional[1], *server, *keyID, stdout)
@@ -295,7 +295,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
@@ -295,7 +295,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
return 2
return 2
}
}
case "version", "--version":
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:
default:
fmt.Fprintf(stderr, "unknown command %q\n\n", cmd)
fmt.Fprintf(stderr, "unknown command %q\n\n", cmd)
fmt.Fprintf(stderr, usage, version)
fmt.Fprintf(stderr, usage, version)
@@ -356,7 +356,7 @@ func probeAll(cfg *config.Config, paths config.Paths, log *slog.Logger) (map[str
@@ -356,7 +356,7 @@ func probeAll(cfg *config.Config, paths config.Paths, log *slog.Logger) (map[str
return out, failed
return out, failed
}
}
-// pairArgs are the flags of `openvibe-node pair`.
+// pairArgs are the flags of `openvibe-link pair`.
type pairArgs struct {
type pairArgs struct {
code, robot, name, server, kind string
code, robot, name, server, kind string
network, pairing string
network, pairing string
@@ -471,14 +471,14 @@ func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {
@@ -471,14 +471,14 @@ func cmdPair(g *globals, a pairArgs, stdout io.Writer) error {
}
}
chownLikeDir(g.paths.CredentialFile(), g.paths.ConfigDir)
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.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)
reportCommunityProfile(ctx, cfg.Server, creds.Profile, stdout)
return finishPair(g, a, cfg, noConfig, 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
// 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
// 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 {
func cmdPairNetwork(g *globals, a pairArgs, stdout io.Writer) error {
code, err := link.NormalizeCode(a.code)
code, err := link.NormalizeCode(a.code)
if err != nil {
if err != nil {
@@ -523,7 +523,7 @@ func cmdPairNetwork(g *globals, a pairArgs, stdout io.Writer) error {
@@ -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.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)
return finishPair(g, a, cfg, noConfig, stdout)
}
}
@@ -595,7 +595,7 @@ func cmdCredentialImport(g *globals, stdin io.Reader, stdout io.Writer) error {
@@ -595,7 +595,7 @@ func cmdCredentialImport(g *globals, stdin io.Reader, stdout io.Writer) error {
}
}
old, err := credentials.Load(g.paths.CredentialFile(), nil)
old, err := credentials.Load(g.paths.CredentialFile(), nil)
if errors.Is(err, credentials.ErrNotPaired) {
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 {
if err != nil {
return err
return err
@@ -654,12 +654,12 @@ func cmdRun(g *globals, dryRun bool) error {
@@ -654,12 +654,12 @@ func cmdRun(g *globals, dryRun bool) error {
if err != nil {
if err != nil {
return err
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 {
return service.Run(serviceOptions(g, ""), func(ctx context.Context) error {
if err := n.Run(ctx); !errors.Is(err, node.ErrQuit) {
if err := n.Run(ctx); !errors.Is(err, node.ErrQuit) {
return err
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
return service.ErrQuit
})
})
}
}
@@ -700,14 +700,14 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
@@ -700,14 +700,14 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
if err := service.Control(opt, "start"); err != nil {
if err := service.Control(opt, "start"); err != nil {
return fmt.Errorf("installed, but could not start: %w", err)
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.
// 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) {
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.Upsert(firstrun.Step{Name: firstrun.StepInstalled, At: time.Now().UTC()})
_ = r.Save(g.paths.FirstRunFile())
_ = r.Save(g.paths.FirstRunFile())
}
}
if _, err := credentials.Load(g.paths.CredentialFile(), nil); err != nil {
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
return nil
}
}
@@ -715,7 +715,7 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
@@ -715,7 +715,7 @@ func cmdInstall(g *globals, user string, stdout io.Writer) error {
func cmdStop(g *globals, 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)
resp, err := localctl.Call(g.paths.SocketPath(), localctl.Request{Cmd: localctl.CmdStop}, 2*time.Second)
if err == nil {
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
return nil
}
}
if resp.Error != "" {
if resp.Error != "" {
@@ -731,7 +731,7 @@ func cmdStop(g *globals, stdout io.Writer) 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
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, "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
return nil
}
}
@@ -754,7 +754,7 @@ func cmdQuit(g *globals, stdout io.Writer) error {
@@ -754,7 +754,7 @@ func cmdQuit(g *globals, stdout io.Writer) error {
}
}
c.Close()
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 {
func cmdResume(g *globals, stdout io.Writer) error {
@@ -796,7 +796,7 @@ func cmdApprovals(g *globals, asJSON bool, 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.
// 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.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
return nil
}
}
@@ -863,11 +863,11 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
@@ -863,11 +863,11 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
if err := json.Unmarshal(resp.Data, &s); err != nil {
if err := json.Unmarshal(resp.Data, &s); err != nil {
return err
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 {
if s.Paired {
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(s.DeviceID, s.Principal, s.Server))
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(s.DeviceID, s.Principal, s.Server))
} else {
} else {
- fmt.Fprintln(stdout, "Device: not paired (openvibe-node pair <CODE>)")
+ fmt.Fprintln(stdout, "Device: not paired (openvibe-link pair <CODE>)")
}
}
link := "down"
link := "down"
if s.Link.Connected {
if s.Link.Connected {
@@ -893,7 +893,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
@@ -893,7 +893,7 @@ func cmdStatus(g *globals, asJSON bool, stdout io.Writer) error {
if s.Screen.LastError != "" && s.Screen.State != "live" {
if s.Screen.LastError != "" && s.Screen.State != "live" {
fmt.Fprintf(stdout, " (%s)", s.Screen.LastError)
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:")
fmt.Fprintln(stdout, "Plugins:")
for _, p := range s.Plugins {
for _, p := range s.Plugins {
@@ -932,13 +932,13 @@ func deviceLine(device, principal, server string) string {
@@ -932,13 +932,13 @@ func deviceLine(device, principal, server string) string {
func latchText(l safety.LatchState) string {
func latchText(l safety.LatchState) string {
if l.Computer {
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 {
switch {
case l.Local && l.Remote:
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:
case l.Local:
- return "STOPPED locally; `openvibe-node resume` releases it"
+ return "STOPPED locally; `openvibe-link resume` releases it"
case l.Remote:
case l.Remote:
return "E-STOPPED by the server (" + l.RemoteReason + "); the owner clears it on openvibe.bot"
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 {
@@ -966,11 +966,11 @@ func offlineStatus(g *globals, asJSON bool, stdout io.Writer) error {
if asJSON {
if asJSON {
return json.NewEncoder(stdout).Encode(o)
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 {
if o.Paired {
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(o.DeviceID, o.Principal, o.Server))
fmt.Fprintf(stdout, "Device: %s\n", deviceLine(o.DeviceID, o.Principal, o.Server))
} else {
} 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, "Stop: %s\n", latchText(o.Latch))
fmt.Fprintf(stdout, "Config: %s\n", o.Config)
fmt.Fprintf(stdout, "Config: %s\n", o.Config)
@@ -1064,7 +1064,7 @@ func reportCommunityProfile(ctx context.Context, server string, profile json.Raw
@@ -1064,7 +1064,7 @@ func reportCommunityProfile(ctx context.Context, server string, profile json.Raw
}
}
res, err := profilesig.Check(ctx, nil, server, p.ID, profile, "")
res, err := profilesig.Check(ctx, nil, server, p.ID, profile, "")
if err != nil {
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
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)
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)
@@ -47,7 +47,7 @@ func shortHome(t *testing.T) string {
@@ -47,7 +47,7 @@ func shortHome(t *testing.T) string {
}
}
func TestHelpAndVersion(t *testing.T) {
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)
t.Fatalf("%d %s", code, out)
}
}
if out, _, code := cli(t, "version"); code != 0 || !strings.Contains(out, runtime.GOOS) {
if out, _, code := cli(t, "version"); code != 0 || !strings.Contains(out, runtime.GOOS) {
@@ -11,13 +11,13 @@ import (
@@ -11,13 +11,13 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memory"
"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" +
"\n" +
" scan reads the memory files of the coding tools on this machine and\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" +
" prints what it found. It is read-only and local: nothing is written\n" +
@@ -14,8 +14,8 @@ import (
@@ -14,8 +14,8 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memorystore"
"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" +
"\n" +
" Writes your one portable memory file: a JSON document with every note, its\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" +
" 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
@@ -28,8 +28,8 @@ const memoryExportUsage = "openvibe-node memory export [--out FILE] [--scope SCO
" --endpoint URL the store (default " + memorystore.DefaultBaseURL + ")\n" +
" --endpoint URL the store (default " + memorystore.DefaultBaseURL + ")\n" +
" --token-env NAME the variable the access token is read from (default " + memorystore.DefaultTokenEnv + ")\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" +
"\n" +
" Reads a portable memory file back into your store. The store validates the\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" +
" format, removes secrets again and merges a note that says the same thing as\n" +
@@ -18,10 +18,10 @@ import (
@@ -18,10 +18,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memoryupload"
"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" +
"\n" +
" Writes your OpenVibe Memory profile and this project's rules into the\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" +
" places the coding tools read them: AGENTS.md (Codex and friends), a thin\n" +
@@ -16,8 +16,8 @@ import (
@@ -16,8 +16,8 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/memoryupload"
"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" +
"\n" +
" Reads the memory files of the coding tools on this machine and sends the\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" +
" 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
@@ -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" +
" --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"
" --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" +
"\n" +
" Shows what your own memory store holds: how many items, per source and per\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" +
" scope. The text of an item is only printed with --show.\n" +
@@ -4,7 +4,7 @@ package main
@@ -4,7 +4,7 @@ package main
import "errors"
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 userSessionSupported() error { return errUserSessionWindowsOnly }
func registerUserSession(string, string) error { return errUserSessionWindowsOnly }
func registerUserSession(string, string) error { return errUserSessionWindowsOnly }
@@ -17,7 +17,7 @@ import (
@@ -17,7 +17,7 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/link"
"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 {
type shareArgs struct {
key string
key string
port int
port int
@@ -26,15 +26,15 @@ type shareArgs struct {
@@ -26,15 +26,15 @@ type shareArgs struct {
}
}
const shareUsage = `usage:
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
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,
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
@@ -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
// 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) {
func shareByPort(ctx context.Context, g *globals, a shareArgs) (connect.Share, func(context.Context) (string, error), error) {
if a.port < 1 || a.port > 65535 {
if a.port < 1 || a.port > 65535 {
return connect.Share{}, nil, fmt.Errorf("port %d is not a port", a.port)
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
@@ -133,7 +133,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
if a.key == "" {
if a.key == "" {
a.key = os.Getenv("OV_CONNECT_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.
// 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 a.share == "" && a.key == "" && a.port == 0 && len(positional) == 1 {
if p, err := strconv.Atoi(positional[0]); err == nil && p > 0 && p < 65536 {
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
@@ -229,7 +229,7 @@ func cmdShare(g *globals, positional []string, a shareArgs, stdout, stderr io.Wr
return err
return err
}
}
if len(shares) == 0 {
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
return nil
}
}
for _, s := range shares {
for _, s := range shares {
@@ -10,10 +10,10 @@ import (
@@ -10,10 +10,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/skills"
"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" +
"\n" +
" Runs one tool of a declared skill as a sandboxed function job on this\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" +
" machine (artifact [email protected], docs/worker.md). The job's\n" +
@@ -106,7 +106,7 @@ func cmdSkillsRunner(args []string, stdout, stderr io.Writer) int {
@@ -106,7 +106,7 @@ func cmdSkillsRunner(args []string, stdout, stderr io.Writer) int {
if !s.On {
if !s.On {
fmt.Fprintln(stdout, "skill runner: off")
fmt.Fprintln(stdout, "skill runner: off")
fmt.Fprintf(stdout, " The job worker is %s.\n", onOff(s.WorkerEnabled))
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
break
}
}
fmt.Fprintf(stdout, "skill runner: on (%s@%s, %s)\n", s.Name, s.Version, s.Class)
fmt.Fprintf(stdout, "skill runner: on (%s@%s, %s)\n", s.Name, s.Version, s.Class)
@@ -17,10 +17,10 @@ import (
@@ -17,10 +17,10 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/skillsync"
"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" +
"\n" +
" Writes the skills and rules you chose in the OpenVibe skills hub\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" +
" (openvibe.codes/skills) into the folders each coding tool reads:\n" +
@@ -16,7 +16,7 @@ import (
@@ -16,7 +16,7 @@ import (
"github.com/OpenVibers/OpenVibe.Node/internal/stream"
"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.
// 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 {
func cmdStream(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("stream", flag.ContinueOnError)
fs := flag.NewFlagSet("stream", flag.ContinueOnError)
@@ -37,7 +37,7 @@ func cmdStream(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
@@ -37,7 +37,7 @@ func cmdStream(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 2
return 2
}
}
if !*probe {
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)")
fmt.Fprintln(stderr, "the publish ticket is read from OPENVIBE_STREAM_TICKET, or with --ticket-stdin from stdin (never argv)")
return 2
return 2
}
}
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
package main
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:
// 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
// - 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 {
@@ -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).
// 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 {
func viewOf(st *node.Status, list []node.Approval, err error) trayView {
if err != nil || st == nil {
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)}
v := trayView{Waiting: list, Panel: panelURL(st.Server)}
if lv, ok := st.Telemetry.Sensors["computer_level"].(string); ok {
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 {
@@ -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"))
v.Header = fmt.Sprintf("%d %s your answer", len(list), plural(len(list), "request waits for", "requests wait for"))
default:
default:
v.State = trayOK
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 != "" {
if v.Level != "" {
tip = append(tip, "computer level "+v.Level)
tip = append(tip, "computer level "+v.Level)
}
}
@@ -267,7 +267,7 @@ func (t *trayCore) answer(a node.Approval, approved bool) error {
@@ -267,7 +267,7 @@ func (t *trayCore) answer(a node.Approval, approved bool) error {
if approved && strings.Contains(err.Error(), "mouse or keyboard") {
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.")
t.note.Ask(a, "Not taken: the agent was using the mouse or keyboard. Answer again if it was you.")
} else {
} else {
- t.note.Info("OpenVibe Node", err.Error())
+ t.note.Info("OpenVibe Link", err.Error())
}
}
return err
return err
}
}
@@ -349,7 +349,7 @@ func trayDesktopEntry(exe, home string) string {
@@ -349,7 +349,7 @@ func trayDesktopEntry(exe, home string) string {
if home != "" {
if home != "" {
line += " --home " + shellQuote(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"
"Exec=" + line + "\nIcon=dialog-question\nTerminal=false\nX-GNOME-Autostart-enabled=true\nNoDisplay=false\n"
}
}
@@ -138,7 +138,7 @@ func (n *dbusNotifier) Ask(a node.Approval, again string) {
@@ -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")}
hints := map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(2)), "desktop-entry": dbus.MakeVariant("openvibe-node-tray")}
var nid uint32
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)
n.text(body), actions, hints, expire).Store(&nid)
if err != nil {
if err != nil {
n.log.Warn("tray: notification failed", "err", err)
n.log.Warn("tray: notification failed", "err", err)
@@ -165,7 +165,7 @@ func (n *dbusNotifier) Close(id string) {
@@ -165,7 +165,7 @@ func (n *dbusNotifier) Close(id string) {
func (n *dbusNotifier) Info(title, body string) {
func (n *dbusNotifier) Info(title, body string) {
hints := map[string]dbus.Variant{"urgency": dbus.MakeVariant(byte(1))}
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 }
func (n *dbusNotifier) Answers() <-chan trayAnswer { return n.answers }
@@ -43,7 +43,7 @@ type systrayUI struct {
@@ -43,7 +43,7 @@ type systrayUI struct {
func (u *systrayUI) build() {
func (u *systrayUI) build() {
u.link = systray.AddMenuItem("Open Link", "Link's own window: what runs here, what you allow, your shares")
u.link = systray.AddMenuItem("Open Link", "Link's own window: what runs here, what you allow, your shares")
systray.AddSeparator()
systray.AddSeparator()
- u.header = systray.AddMenuItem("OpenVibe Node", "")
+ u.header = systray.AddMenuItem("OpenVibe Link", "")
u.header.Disable()
u.header.Disable()
u.level = systray.AddMenuItem("", "")
u.level = systray.AddMenuItem("", "")
u.level.Disable()
u.level.Disable()
@@ -59,7 +59,7 @@ func (u *systrayUI) build() {
@@ -59,7 +59,7 @@ func (u *systrayUI) build() {
u.panel = systray.AddMenuItem("Open my machines page", "This machine on openvibe.bot")
u.panel = systray.AddMenuItem("Open my machines page", "This machine on openvibe.bot")
u.panel.Hide()
u.panel.Hide()
systray.AddSeparator()
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) {
func (u *systrayUI) SetIcon(state string) {
@@ -178,14 +178,14 @@ func (u *systrayUI) SetPanel(has bool) {
@@ -178,14 +178,14 @@ func (u *systrayUI) SetPanel(has bool) {
func runTray(g *globals) error {
func runTray(g *globals) error {
if runtime.GOOS == "linux" && os.Getenv("DBUS_SESSION_BUS_ADDRESS") == "" && os.Getenv("XDG_RUNTIME_DIR") == "" {
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{}
ui := &systrayUI{}
core := newTrayCore(g.paths.SocketPath(), ui, newDesktopNotifier(g.log))
core := newTrayCore(g.paths.SocketPath(), ui, newDesktopNotifier(g.log))
stopSig := make(chan os.Signal, 1)
stopSig := make(chan os.Signal, 1)
signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM)
signal.Notify(stopSig, os.Interrupt, syscall.SIGTERM)
systray.Run(func() {
systray.Run(func() {
- systray.SetTitle("OpenVibe Node")
+ systray.SetTitle("OpenVibe Link")
ui.build()
ui.build()
core.tick()
core.tick()
// The first time the tray runs, Link's window opens by itself: the person sees what the machine does.
// 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
@@ -218,16 +218,16 @@ func trayLoop(core *trayCore, ui *systrayUI, stopSig <-chan os.Signal, g *global
core.tick()
core.tick()
case <-ui.stop.ClickedCh:
case <-ui.stop.ClickedCh:
if err := core.stop(); err != nil {
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()
core.tick()
case <-ui.link.ClickedCh:
case <-ui.link.ClickedCh:
if err := core.openPanel(); err != nil {
if err := core.openPanel(); err != nil {
- core.note.Info("OpenVibe Node", err.Error())
+ core.note.Info("OpenVibe Link", err.Error())
}
}
case <-ui.resume.ClickedCh:
case <-ui.resume.ClickedCh:
if err := core.resume(); err != nil {
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()
core.tick()
case <-ui.panel.ClickedCh:
case <-ui.panel.ClickedCh:
@@ -293,9 +293,9 @@ func trayAutostart(on bool, exe, home string, stdout io.Writer) error {
@@ -293,9 +293,9 @@ func trayAutostart(on bool, exe, home string, stdout io.Writer) error {
var out []byte
var out []byte
var err error
var err error
if on {
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 {
} 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") {
if err != nil && strings.Contains(strings.ToLower(string(out)), "unable to find") {
err = nil
err = nil
}
}
@@ -26,7 +26,7 @@ func TestTrayViewStates(t *testing.T) {
@@ -26,7 +26,7 @@ func TestTrayViewStates(t *testing.T) {
st := &node.Status{Server: "https://openvibe.bot", Link: node.LinkStatus{Connected: true}}
st := &node.Status{Server: "https://openvibe.bot", Link: node.LinkStatus{Connected: true}}
st.Telemetry.Sensors = map[string]any{"computer_level": "ask"}
st.Telemetry.Sensors = map[string]any{"computer_level": "ask"}
v := viewOf(st, nil, nil)
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)
t.Fatalf("all well: %+v", v)
}
}
v = viewOf(st, []node.Approval{{ID: "a"}, {ID: "b"}}, nil)
v = viewOf(st, []node.Approval{{ID: "a"}, {ID: "b"}}, nil)
@@ -1,6 +1,6 @@
@@ -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
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
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.
servers and do not need it.
@@ -29,9 +29,9 @@ and a **Cozmo**.
@@ -29,9 +29,9 @@ and a **Cozmo**.
## The CLI
## 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
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`.
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`.
@@ -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
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.
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
- 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`
- 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.
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
- 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).
`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.
@@ -130,7 +130,7 @@ core. Say in `docs/` where it will attach.
## Done means
## 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
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
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.
is and how to install it.
@@ -2,7 +2,7 @@
@@ -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),
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
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.
request comes back over that connection.
## How it works
## How it works
@@ -43,7 +43,7 @@ With nothing else set, it uses the first of these that answers:
@@ -43,7 +43,7 @@ With nothing else set, it uses the first of these that answers:
| LM Studio | `http://127.0.0.1:1234` |
| LM Studio | `http://127.0.0.1:1234` |
| llama.cpp's server | `http://127.0.0.1:8080` |
| 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.
computer's models** for the machine on openvibe.bot/machines.
| key | default | meaning |
| key | default | meaning |
@@ -19,7 +19,7 @@ and adds the plugin with your home folder as its only folder. It cannot share a
@@ -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).
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
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
```json
{"name": "computer", "config": {"level": "ask", "roots": ["/home/you/projects"]}}
{"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
@@ -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:
(default 120 s) and never longer than the request's own deadline:
```sh
```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
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,
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
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
**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
reach: the folders are the plugin's own limit, not a sandbox around the program. Read what it would do before you
approve it.
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:
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
- 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
@@ -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.
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
**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.
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
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.
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
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
resume: the owner's remote clear (`estop` `latched:false`, or a config without `estop_latched`) releases the robot's remote
latch, never this computer.
latch, never this computer.
@@ -124,10 +124,10 @@ You can watch your machine's screen live on openvibe.bot (**Your machines** →
@@ -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
- **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
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.
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.
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
- **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`.
- **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;
- **Without Pillow** the capture tool must write JPEG itself: `screencapture`, `grim`, `import` and `scrot` do;
`gnome-screenshot` does not.
`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
@@ -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.
`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
- **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.
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.
handle, never by name.
- **Needs** Linux with `Xvfb` (Debian/Ubuntu: `apt install xvfb`), `xdotool` for input and ImageMagick `import` or `scrot`
- **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.
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):
@@ -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
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
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,
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
`desk_pids` processes and a `desk_disk_mb` private `/tmp`, where its profile and downloads live. It sees only the
@@ -12,12 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or ser
@@ -12,12 +12,12 @@ principal). On a share's page on connect.openvibe.website, pick it under "Or ser
then on the computer:
then on the computer:
```sh
```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
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
@@ -37,25 +37,25 @@ connect key once (`ovc_…`). Keep it like a password: it lets a computer serve
## Serve it now
## Serve it now
```sh
```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
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:
keeps it off the command line and out of your shell history:
```sh
```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
## Keep it up with the Node service
```sh
```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.
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
The saved shares are in the Node's config directory, `connect-shares.json`, readable only by the Node's account
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
# Adeept ADR036 4WD Smart Car — hardware bring-up (O30)
# 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
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`).
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`
@@ -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 +
`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
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.
`/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`.
`video.state` at `connecting` or `live`.
## 3. Panel checklist
## 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 |
| 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 | |
| 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 | |
| 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 | |
| Ultrasonic (`DistanceSensor(echo=24, trigger=23)`) | `sensors.distance_cm` changes with a hand | |
@@ -70,7 +70,7 @@ widget and a `display` command is nacked.
@@ -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
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
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,
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
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.
pattern. Report `video.state`, `video.last_error` and the journal lines above.
@@ -48,7 +48,7 @@ Then check the written config (`cat /etc/openvibe-node/config.json`): `device_ki
@@ -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
`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
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
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
`link.connected: true`, `sensors` (`head_deg`, `lift_mm`, `cliff`, `picked_up`, `cubes`) and `video.state` at
`connecting` or `live`.
`connecting` or `live`.
@@ -62,11 +62,11 @@ know a version works, allow it with `allow_firmware: [2381, <version>]` in the p
@@ -62,11 +62,11 @@ know a version works, allow it with `allow_firmware: [2381, <version>]` in the p
## 4. Panel checklist
## 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 |
| 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`) | |
| 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 | |
| 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) | |
| 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.
@@ -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
- **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.
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
- **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.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.
`"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
- **A command answers `nack`**: read the fault — `firmware_unsupported` is the firmware latch above; `cliff` and
@@ -1,4 +1,4 @@
@@ -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**
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
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
@@ -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
- **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
`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.
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
## Installing the driver
@@ -81,8 +81,8 @@ run the Node in your own session:
@@ -81,8 +81,8 @@ run the Node in your own session:
```bat
```bat
set OVHOME=%LOCALAPPDATA%\OpenVibe Node
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:
`config.json` in that directory:
@@ -99,9 +99,9 @@ openvibe-node install --user-session --home "%OVHOME%"
@@ -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
`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.
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
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.
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.
@@ -1,6 +1,6 @@
@@ -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.
ports, works behind any home router), and a local kill switch stops everything it drives.
## The one command (Raspberry Pi and other Linux robots)
## 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
@@ -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
many installs reach each step and how long they take (`/admin/first-run`), and the machine's page shows what the first
job found.
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.
--pairing pair_…` and `--home DIR` work as flags instead of the prompt.
## By hand
## By hand
```sh
```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:
# 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
Without root, everything goes to your user config directory (`~/.config/openvibe-node`), or to any directory with
`--home DIR` / `OPENVIBE_NODE_HOME`:
`--home DIR` / `OPENVIBE_NODE_HOME`:
```sh
```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`.
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
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).
--user-session` removes it. See [hardware-looi.md](hardware-looi.md).
## Commands
## Commands
| command | what it does |
| 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
`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.
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
@@ -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
`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
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.
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
`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
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
## Files
| Linux | macOS / Windows | contents |
| 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`.
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
@@ -270,22 +271,22 @@ internet (Ethernet, or a second Wi-Fi adapter). Install with `--driver cozmo`; t
## Troubleshooting
## 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
- 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
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`.
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).
- 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.
hardware.
## Uninstall
## Uninstall
```sh
```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
sudo userdel openvibe-node
```
```
@@ -21,12 +21,12 @@ memory comes later.
@@ -21,12 +21,12 @@ memory comes later.
## The commands
## The commands
```sh
```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 |
| Flag | What it does |
@@ -180,7 +180,7 @@ with no END line, and an `.env` block.
@@ -180,7 +180,7 @@ with no END line, and an `.env` block.
## The upload
## The upload
```sh
```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
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.
@@ -253,7 +253,7 @@ batch is sent again: nothing is dropped without being said.
## The status
## The status
```sh
```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
It shows what your own store holds: how many items there are, per source and
@@ -362,9 +362,9 @@ upload:
@@ -362,9 +362,9 @@ upload:
## The sync: back into the tools
## The sync: back into the tools
```sh
```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
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:
@@ -389,7 +389,7 @@ Every write is inside one block, and the text outside it is not touched:
```markdown
```markdown
<!-- openvibe-memory:start -->
<!-- openvibe-memory:start -->
-<!-- Managed by OpenVibe Node from your OpenVibe Memory. … -->
+<!-- Managed by OpenVibe Link from your OpenVibe Memory. … -->
## About you
## About you
…the cached profile…
…the cached profile…
@@ -441,8 +441,8 @@ the memory text.
@@ -441,8 +441,8 @@ the memory text.
## The portable file
## The portable file
```sh
```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`
`memory export` writes the store's one portable file, `openvibe.memory-export@1`
@@ -7,7 +7,7 @@ keeps, and what happened. The design and the JSON it reads are in
@@ -7,7 +7,7 @@ keeps, and what happened. The design and the JSON it reads are in
## Opening it
## Opening it
- The tray's **Open Link** (the first item) opens it. The first time the tray runs, it opens by itself.
- 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
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
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.
@@ -32,7 +32,7 @@ A restart makes a new port and a new token: an old link stops working.
| Call | What it does | Today |
| 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 |
| `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/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/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` |
| `POST /panel/api/capabilities/:id` `{ "on": false }` | Turns a capability off here | Real; persisted in `<state>/panel.json` |
@@ -23,7 +23,7 @@ hardware.
@@ -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
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
`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)
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.
computer plugin's shared desks) reads the same config as the Node that started it.
## Lifecycle
## Lifecycle
@@ -42,7 +42,7 @@ A plugin that exits is restarted with exponential backoff (1 s doubling to 30 s,
@@ -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`.
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.
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.
describes itself and waits for EOF without opening any hardware.
## Messages
## Messages
@@ -180,14 +180,14 @@ Asked by the Generic LOOI driver's authors (2026-10-27). Each answer is what Bot
@@ -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);
- a `halt` (a change of driver, with its `turn_epoch`, or an operator's halt);
- the link to Bot dropping;
- the link to Bot dropping;
- the core shutting down (then stdin closes);
- the core shutting down (then stdin closes);
- - `openvibe-node quit`;
+ - `openvibe-link quit`;
- a plugin missing its reply window.
- a plugin missing its reply window.
- `estop` stops every actuator **and latches**. The core refuses `drive`, `ptz` and `actuator` itself (`estopped`,
- `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:
`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 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.
- 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.
- 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.
- 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.
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
@@ -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` | `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` | `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 |
| `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` | `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 |
| `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 |
| `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
@@ -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 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
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.
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
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
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,
`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
@@ -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
`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
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
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
"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
`{"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.
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
@@ -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:
Before installing one, check that the profile is the one that was reviewed:
```sh
```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
The Node fetches the profile (`GET /api/v1/profiles/:id`, with the `signature` Bot made on approval) and Bot's key
@@ -45,7 +45,7 @@ RFC 9457 problems; the Node turns their `code` into a sentence: `bot.pairing_cod
@@ -45,7 +45,7 @@ RFC 9457 problems; the Node turns their `code` into a sentence: `bot.pairing_cod
(tests).
(tests).
The credential and publish key are stored in `credential.json` (mode 0600, with the device and robot ids) and never
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).
`[redacted]` in every format).
### Pairing through OpenVibe.Network
### Pairing through OpenVibe.Network
@@ -126,7 +126,7 @@ body into the Node to take the new pair without pairing again:
@@ -126,7 +126,7 @@ body into the Node to take the new pair without pairing again:
```sh
```sh
curl -s -X POST -H "Authorization: Bearer $TOKEN" https://openvibe.bot/api/v1/devices/dev_…/rotate \
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`
`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
@@ -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
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.
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
`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).
machine, whatever the server clears (docs/computer.md).
### Device → server
### Device → server
@@ -256,7 +256,7 @@ reply window (an owner's approval on the machine plus the program's own run): ab
@@ -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
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
(`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,
`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.
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.
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;
@@ -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 |
| motion command's deadline passes | the plugin (its own timer) | no |
| 2 missed heartbeats / link closed | the core → `stop` to every plugin | 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 shutdown | `stop`, then stdin closed | no |
| core killed / crashed | the plugin: stdin EOF, or 1 s without a heartbeat | 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 | — |
| plugin crash | restarted with backoff; starts stopped; `estop` re-sent if latched | — |
@@ -5,12 +5,12 @@ OpenVibe.Codes keeps skills, rules and MCP server records per person and per pro
@@ -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.
tools already read, on this machine, inside one marked block.
```sh
```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
`GET /api/v1/me/skills` on openvibe.codes, with `?project=<slug>` for a project, so it only ever sees what you chose
there.
there.
@@ -16,7 +16,7 @@ curtain, and measures the glass-to-glass latency with a QR probe. The design is
@@ -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
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
`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`.
`credentials.Secret`.
## The command
## The command
@@ -91,7 +91,7 @@ seconds, so a restarted plugin's first "clear" cannot uncover the desk.
@@ -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).
# 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
--whip-url https://whip.openvibe.live/whip/agent/ags_… --seconds 60
# The same ticket's view side, on a WHEP endpoint:
# The same ticket's view side, on a WHEP endpoint:
@@ -48,9 +48,9 @@ A skill that declares `runs: "sandbox"` has tools whose code runs as a **functio
@@ -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**:
skill runner. The runner is a declared function like the Codes runner, and it is **off until the owner turns it on**:
```sh
```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),
`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
@@ -247,7 +247,7 @@ exactly as with the worker off: there is no weaker mode. A job whose sandbox fai
its function having run.
its function having run.
Requirements: Linux 5.14 or later with cgroup v2; the Node's cgroup **delegated** to it (`Delegate=yes` on its
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`,
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
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
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.
@@ -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
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
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
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
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.
`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.
`state` `admitted`, `running` or `stopping`, and `started_ms` once running); the list is empty when the worker is off.
## Isolation model and its limits
## Isolation model and its limits
@@ -420,16 +420,16 @@ name the new file.
@@ -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
- **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.
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
- **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.
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
- **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.
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
- **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.
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
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:
[--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
- **It lives for hours and talks both ways.** Fds 3 and 4 pass through to the program (a browser's
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
#!/bin/sh
#!/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)
# (the same script is also at https://openvibe.bot/install, which keeps working)
#
#
@@ -126,13 +126,13 @@ usage() {
@@ -126,13 +126,13 @@ usage() {
echo " [--computer observe|ask|trusted|full] [--desk-host [--shared]] [--ai] [--ai-endpoint URL]"
echo " [--computer observe|ask|trusted|full] [--desk-host [--shared]] [--ai] [--ai-endpoint URL]"
echo " [--no-service] [--no-claim] [--setup osk_…]"
echo " [--no-service] [--no-claim] [--setup osk_…]"
echo " [--local DIR] [--version TAG]"
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 is the rob_… pairing target; it used to name the driver kind, which still works but is deprecated.
robot_arg() {
robot_arg() {
case "$1" in
case "$1" in
@@ -393,7 +393,7 @@ def call(method, path, body=None, headers=None):
@@ -393,7 +393,7 @@ def call(method, path, body=None, headers=None):
with urllib.request.urlopen(req, timeout=20) as r:
with urllib.request.urlopen(req, timeout=20) as r:
return json.load(r)
return json.load(r)
def die(msg):
def die(msg):
- print(f'\nopenvibe-node: error: {msg}', file=sys.stderr)
+ print(f'\nopenvibe-link: error: {msg}', file=sys.stderr)
sys.exit(1)
sys.exit(1)
setup_key = os.environ.get('OV_SETUP_KEY', '')
setup_key = os.environ.get('OV_SETUP_KEY', '')
body = {'hostname': host, 'hardware': hw}
body = {'hostname': host, 'hardware': hw}
@@ -859,9 +859,16 @@ if [ "$CLAIM" = 1 ] || [ "$DRIVER" = adeept ] || [ "$DRIVER" = adeept-mecanum ];
@@ -859,9 +859,16 @@ if [ "$CLAIM" = 1 ] || [ "$DRIVER" = adeept ] || [ "$DRIVER" = adeept-mecanum ];
# ---- download ----
# ---- download ----
TMP="$(mktemp -d)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
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
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"
cp "$LOCAL/openvibe-node-plugins.tar.gz" "$TMP/" || die "$LOCAL/openvibe-node-plugins.tar.gz not found"
else
else
if [ "$TAG" = latest ]; then
if [ "$TAG" = latest ]; then
@@ -875,7 +882,8 @@ else
@@ -875,7 +882,8 @@ else
else die "curl or wget is needed"; fi
else die "curl or wget is needed"; fi
}
}
say "downloading $BIN_NAME"
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/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"
fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "download failed: SHA256SUMS"
( cd "$TMP"
( cd "$TMP"
@@ -885,8 +893,10 @@ else
@@ -885,8 +893,10 @@ else
fi
fi
# ---- binary ----
# ---- 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 ----
# ---- 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
# 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
@@ -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.
# The tray starts when they sign in: what waits for approval with Approve and Deny, the stop and resume.
had_config=0
had_config=0
[ -d "$COMPUTER_HOME/.config" ] && had_config=1
[ -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
if [ "$(id -u)" = 0 ]; then
[ "$had_config" = 1 ] || chown "$SVC_USER": "$COMPUTER_HOME/.config" 2>/dev/null || true
[ "$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
chown "$SVC_USER": "$COMPUTER_HOME/.config/autostart" "$COMPUTER_HOME/.config/autostart/openvibe-node-tray.desktop" 2>/dev/null || true
fi
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
fi
fi
@@ -1194,19 +1204,19 @@ disable_stock_server
@@ -1194,19 +1204,19 @@ disable_stock_server
if [ "$SERVICE" = 1 ]; then
if [ "$SERVICE" = 1 ]; then
# An upgrade rewrites the service definition (this version's adds Delegate=yes, which the worker needs): the
# 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.
# 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
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
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
fi
say "service installed and started"
say "service installed and started"
fi
fi
# ---- the one command: the owner approves the code on openvibe.bot/link, then this machine pairs at once ----
# ---- the one command: the owner approves the code on openvibe.bot/link, then this machine pairs at once ----
if [ "$CLAIM" = 1 ]; then
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
CLAIM=0
else
else
claim_robot || die "not paired: nothing was added on openvibe.bot"
claim_robot || die "not paired: nothing was added on openvibe.bot"
@@ -1221,10 +1231,10 @@ fi
@@ -1221,10 +1231,10 @@ fi
# ---- pair ----
# ---- pair ----
if [ -n "$CODE" ]; then
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)"
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
else
- pair_with $SUDO "$BIN_DIR/openvibe-node"
+ pair_with $SUDO "$BIN_DIR/openvibe-link"
fi
fi
else
else
say "not paired yet: get a code on openvibe.bot and run: sudo openvibe-node pair <CODE>"
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
@@ -1235,7 +1245,7 @@ if [ "$CLAIM" = 1 ] && [ "$SERVICE" = 1 ]; then
say "checking the link, the driver and the camera (up to a minute)"
say "checking the link, the driver and the camera (up to a minute)"
_status="" _i=0
_status="" _i=0
while [ "$_i" -lt 30 ]; do
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
if printf '%s' "$_status" | grep -q '"connected":true' && printf '%s' "$_status" | grep -q '"state":"live"'; then break; fi
_i=$((_i + 1))
_i=$((_i + 1))
sleep 2
sleep 2
@@ -1258,5 +1268,5 @@ print(" camera %s%s" % (v.get("state") or "off", (" (" + v["last_error"] + ")
@@ -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"
[ "${LIVE:-0}" = 0 ] || say "its camera is on your OpenVibe.Live channel while it streams"
fi
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)"
@@ -119,7 +119,7 @@ func Body(content Content) string {
@@ -119,7 +119,7 @@ func Body(content Content) string {
if text == "" && len(rules) == 0 {
if text == "" && len(rules) == 0 {
return ""
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 text != "" {
if isProject(content.Scope) {
if isProject(content.Scope) {
b.WriteString("\n## About this project\n\n")
b.WriteString("\n## About this project\n\n")
@@ -15,8 +15,8 @@ import (
@@ -15,8 +15,8 @@ import (
const (
const (
Name = "openvibe-node"
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.
// Program is the body of the service: run until ctx ends.
@@ -105,7 +105,7 @@ func RulesBlock(rules []string) string {
@@ -105,7 +105,7 @@ func RulesBlock(rules []string) string {
return ""
return ""
}
}
var b strings.Builder
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 {
for _, rule := range clean {
b.WriteString("- " + rule + "\n")
b.WriteString("- " + rule + "\n")
}
}
@@ -1,4 +1,4 @@
@@ -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
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
their owner, for Vibe's answers or an agent's thinking, without the prompt leaving the machine's network and with no
@@ -1,4 +1,4 @@
@@ -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
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:
command can never raise it:
@@ -143,7 +143,7 @@ INPUT_GUARD_S = 2.0
@@ -143,7 +143,7 @@ INPUT_GUARD_S = 2.0
# macOS dialog); the owner answers it in full with `openvibe-node approvals`.
# macOS dialog); the owner answers it in full with `openvibe-node approvals`.
NOTIFY_SUMMARY_MAX = 300
NOTIFY_SUMMARY_MAX = 300
DIALOG_SUMMARY_MAX = 400
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
# 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.
# 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")
BIDI_CHARS = frozenset("\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069")
@@ -1383,23 +1383,23 @@ class Computer(Plugin):
@@ -1383,23 +1383,23 @@ class Computer(Plugin):
return
return
title = "OpenVibe: your agent asks"
title = "OpenVibe: your agent asks"
summary = _plain(summary)
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
parse = None
if self.platform.startswith("linux") and (self.env.get("DISPLAY") or self.env.get("WAYLAND_DISPLAY")) and self.which("notify-send"):
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
# 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.
# 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:
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",
"--expire-time=%d" % (self.approval_timeout_s * 1000), "--action=approve=Approve",
"--action=deny=Deny", "--", title, _markup(summary)]
"--action=deny=Deny", "--", title, _markup(summary)]
parse = _parse_notify_send
parse = _parse_notify_send
elif len(summary) > NOTIFY_SUMMARY_MAX:
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,
"--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"
_markup("This request is too long to show whole here. Run `%s` to read it and answer it.\n%s"
% (FULL_VIEW_HINT, hint))]
% (FULL_VIEW_HINT, hint))]
else:
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"):
elif self.platform == "darwin" and self.which("osascript"):
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
if len(summary) <= DIALOG_SUMMARY_MAX:
if len(summary) <= DIALOG_SUMMARY_MAX:
@@ -2653,10 +2653,10 @@ class Computer(Plugin):
@@ -2653,10 +2653,10 @@ class Computer(Plugin):
if not self.notify:
if not self.notify:
return
return
title = "Your screen is live on OpenVibe"
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
argv = None
if self.platform.startswith("linux") and (self.env.get("DISPLAY") or self.env.get("WAYLAND_DISPLAY")) and self.which("notify-send"):
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"):
elif self.platform == "darwin" and self.which("osascript"):
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
quote = lambda s: '"%s"' % s.replace("\\", "\\\\").replace('"', '\\"')
argv = ["osascript", "-e", "display notification %s with title %s" % (quote(body), quote(title))]
argv = ["osascript", "-e", "display notification %s with title %s" % (quote(body), quote(title))]
@@ -203,8 +203,8 @@ class ComputerTest(unittest.TestCase):
@@ -203,8 +203,8 @@ class ComputerTest(unittest.TestCase):
self.plugin.notify_actions = False # an older notify-send: no buttons, the command to answer with
self.plugin.notify_actions = False # an older notify-send: no buttons, the command to answer with
cid = self.cmd("exec", {"argv": ["echo", "hi"]})
cid = self.cmd("exec", {"argv": ["echo", "hi"]})
self.out.wait_for(lambda m: m.get("name") == "approval")
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.plugin.env = {}
self.cmd("exec", {"argv": ["echo", "again"]})
self.cmd("exec", {"argv": ["echo", "again"]})
self.assertEqual(len(calls), 1, "no graphical session: no notification")
self.assertEqual(len(calls), 1, "no graphical session: no notification")
@@ -728,7 +728,7 @@ class ComputerTest(unittest.TestCase):
@@ -728,7 +728,7 @@ class ComputerTest(unittest.TestCase):
self.out.wait_for(lambda m: m.get("name") == "approval" and m.get("id") == cid)
self.out.wait_for(lambda m: m.get("name") == "approval" and m.get("id") == cid)
argv = prompts[0].argv
argv = prompts[0].argv
self.assertNotIn("--action=approve=Approve", argv, "no approval button on cut text")
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):
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.
# F6: an answer from the prompt instance that is no longer the one shown is not the owner's answer.
@@ -1,4 +1,4 @@
@@ -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
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:
robot's topics into telemetry and camera frames:
@@ -1,4 +1,4 @@
@@ -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
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
object per line; logs go to stderr). This module implements the parts every driver must get right, so a driver only
@@ -1,4 +1,4 @@
@@ -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
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
says so (the core passes it on as the ``voice`` op). While it is on, this plugin listens on the machine itself, and
@@ -11,15 +11,19 @@ for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 wind
@@ -11,15 +11,19 @@ for target in linux/amd64 linux/arm64 linux/arm/7 darwin/amd64 darwin/arm64 wind
rest="${target#*/}"
rest="${target#*/}"
arch="${rest%%/*}"
arch="${rest%%/*}"
arm=""
arm=""
- name="openvibe-node-$os-$arch"
+ name="openvibe-link-$os-$arch"
if [ "$arch" = arm ]; then
if [ "$arch" = arm ]; then
arm="${rest#*/}"
arm="${rest#*/}"
- name="openvibe-node-$os-armv$arm"
+ name="openvibe-link-$os-armv$arm"
fi
fi
[ "$os" = windows ] && name="$name.exe"
[ "$os" = windows ] && name="$name.exe"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" GOARM="$arm" \
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
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
done
# The plugin bundle: the Python packages without their tests.
# The plugin bundle: the Python packages without their tests.
tar -czf dist/openvibe-node-plugins.tar.gz -C plugins \
tar -czf dist/openvibe-node-plugins.tar.gz -C plugins \