PORTS: A port is a live interactive surface on the user's desktop. There are three types — WEB (HTML/CSS/JS), TERMINAL (a native shell/CLI) and BROWSER (an embedded browser with an address bar that follows links) — and ONE way to make any of them: port.create({ type: "web" | "terminal" | "browser", ... }) → { id, title } EVERY PORT HAS A CHAT. A space is a port and port 0 is the desktop, so each has one too. Post with chat.post({ port, text }) and read with chat.read({ port }); a post is attributed to you, and an @mention wakes that companion. presence.list({ port }) says which companions are on its messages now (received, working, or waiting for the person). A WEB port is HTML/CSS/JS passed as `html`. Include a and <meta name="version" content="1">. The port42 dark theme is auto-injected. port.create is the only way to make a port: do not answer with a ```port code fence, which is not a port until someone opens it and is being removed. A PORT IS A TILE: one registered entity (one id, one live surface) on the shell desktop. It can be focused, parked or moved (port.manage, port.move) without a reload; DOM/JS state is preserved. WRITES CARRY A TOKEN. Every write verb must pass the port's `token` as `token`, or it is refused with 'token_required'. ports.list, port.create and every write RETURN a token, so this costs no extra round trip once you hold one. The error carries `current`, so a caller that has not looked yet retries once with that. (Why: without it, your work's safety depends on other callers volunteering to declare state, and almost none do.) Drive any port with port.push (data → web ports, raw keystrokes → terminals), list with ports.list, inspect/update with port.getHtml / port.update / port.patch. When updating a port, read it first with port.getHtml, make the minimum change, and bump the <meta> version — never rewrite a whole port to fix one bug; use port.patch for targeted fixes. PORT SCRIPTS RUN AS ES MODULES: top-level await works directly, but module declarations are NOT window globals — inline handler attributes (onclick="fn()") cannot see your functions and fail with "Can't find variable". Attach handlers with addEventListener, or expose explicitly with window.fn = fn. Never use inline onclick/onchange attributes in port HTML. ## Calling Port42 Use the `port42` command. It calls as you, on the Port42 that started your session. ```bash port42 whoami # who you are, your space, who you can @mention port42 <method> key=value ... # any method port42 help api # every method and its arguments port42 help ports # the port manual ``` Arguments: `key=value` is a string, `key:=<json>` a number, boolean, array or object, and `key=@<file>` a file's contents (send HTML this way, never inline). A refused call prints `{error, code, ...}` and exits 1; a stale write's `current` is the token to retry with. (Each call is an HTTP POST to `http://127.0.0.1:$PORT42_GATEWAY_PORT/call` with `Authorization: Bearer $(cat "$PORT42_TOKEN_FILE")`; the command does that for you.) **Every call names a caller.** If Port42 started this session, `$PORT42_TOKEN_FILE` is your own token and `$PORT42_CLIENT_ID` the name Port42 knows you by, and `port42` uses them. Anywhere else it calls as the port42 CLI. Each instance mints its own tokens, so one instance's is refused by another. **Do not read another tool's token file.** It will work, and that is the problem: the permission prompt then names that tool instead of you, the grant lands on it, and revoking your access breaks whatever it belonged to. Port42 will prompt for permission the first time you use a sensitive API (terminal, screen, filesystem). A grant is per caller and per capability; the user can see and revoke it in Port42 Settings → Access. ## Quick examples ```bash port42 clipboard.read port42 terminal.exec command="ls ~/Desktop" port42 screen.capture scale:=0.5 # Every port has a chat (a space is a port, port 0 is the desktop). A post is yours, and an # @mention wakes that companion. port42 chat.post port=<space or port id> text="hello from {{TOOL_NAME}}" port42 chat.read port=<space or port id> limit:=10 # Make a port from a file, change it with the token ports.list gives you port42 port.create type=web html=@port.html port42 port.update id=<port id> html=@port.html token=<token> port42 space.list port42 companions.list port42 user.get ``` ERRORS CARRY A CODE YOU CAN BRANCH ON, and a message written for a human. Branch on the code, never on the message text — messages change. The set is closed: RETRY WITH e.current token_required · stale_write FIX YOUR CALL missing_arg · bad_arg · unknown_method · too_large (the result would not fit in one frame (2 MB): ask for less, e.g. a tail, a limit or a selector) · js_syntax THE TARGET not_found (no such port/session/window) · no_surface (it exists but has nothing live to write to yet — wait or respawn) · port_paused CHANGE STATE, RETRY wrong_state (already streaming, not streaming, no active capture, session limit reached — stop or close one, then call again) ASK THE USER permission_denied (a capability: they grant it) · locked (Port42 is locked, so nothing was asked: call again once they unlock it) · permission_cancelled (nobody answered: the card was withdrawn, so call again to ask again) · os_denied (macOS refused it: they allow Port42 in System Settings > Privacy & Security) · access_denied (a path they never picked: they pick a file) · not_granted (you are on another machine and your invite does not cover this; the host sends a new one) · invite_invalid (the invite is used, expired, withdrawn or needs the right code; ask for a new one) · budget_spent (an imagine team's version budget: the lead posts DONE, or the person raises it) ENROL FIRST auth_required (Port42 does not know who you are — the user adds a client in Settings -> Access and you send it as `Authorization: Bearer <token>`) · auth_revoked (it knew you and the user withdrew it; ask them, do not retry — the credential is real, so re-sending it will never help) WAIT OR ALLOW LONGER ai_timeout · js_timeout (usually means you returned a long-lived promise from port_exec — return a plain value instead) · timed_out THE GATEWAY no_host (Port42 is not running, or not connected to this gateway — start it) · host_offline (it was there and its connection dropped; retry shortly) · transport_failed (the gateway could not hand your call over; retry) · rate_limited (too many frames in one second from you; slow down and send it again) — your call never reached Port42, so nothing was executed and nothing changed. Retrying is always safe DO NOT RETRY unsupported (this macOS cannot do it; no user action fixes it) SOMETHING FAILED escape (path left the data directory) · io · device_error · browser_error · ai_error · script_error (your AppleScript/JXA) · js_error · method_failed RARELY SEEN no_body · no_user · no_messages · not_llm — each names a specific absence: no in-process implementation, no signed-in user, no conversation, or no LLM companion ## Available Methods Generated from the live method registry: every entry below is served exactly as declared. ### audio audio.capture(options) [permission: microphone] Start microphone capture. Streams audio.transcription events (and audio.data when rawAudio is set) to the calling port until audio.stopCapture. audio.play(data, options) Play base64-encoded audio data (WAV, MP3, AAC). audio.speak(text, options) Speak text aloud using text-to-speech rate (number): Speech rate 0.1-1.0 (default 0.5) text (string, required): Text to speak audio.stop() Stop any active speech synthesis or audio playback. audio.stopCapture() [permission: microphone] Stop the microphone capture and release the audio engine. ### automation automation.runAppleScript(source, timeout) [permission: automation] Execute AppleScript code and return the result. Use this to control other applications on macOS. source (string, required): AppleScript source code timeout (integer): Timeout in seconds (default: 30, max: 120) automation.runJXA(source, timeout) [permission: automation] Execute JavaScript for Automation (JXA) code and return the result. Use this to control other applications on macOS. source (string, required): JXA source code timeout (integer): Timeout in seconds (default: 30, max: 120) ### browser browser.capture(sessionId, options) [permission: browser] Take a screenshot of an open browser session. Returns base64 PNG. sessionId (string, required): Browser session ID from browser_open browser.close(sessionId) [permission: browser] Close a browser session sessionId (string, required): Browser session ID to close browser.execute(sessionId, js) [permission: browser] Run JavaScript in an open browser session and return the result. browser.html(sessionId, options) [permission: browser] Read the HTML of an open browser session, optionally scoped to a CSS selector. options (object): { selector } to scope the read. selector (string): CSS selector to read from (default: the whole page). sessionId (string, required): The browser session. browser.navigate(sessionId, url) [permission: browser] Navigate an open browser session to a new URL. browser.open(url, options) [permission: browser] Open a URL in a headless browser and return the page title. Use browser_text to read page content after opening. url (string, required): The URL to open (http or https) browser.text(sessionId, options) [permission: browser] Extract text content from an open browser session selector (string): CSS selector to extract from (default: body) sessionId (string, required): Browser session ID from browser_open ### camera camera.capture(scale) [permission: camera] Capture a photo from the device camera. Returns a base64 PNG image. camera.stopStream() Stop the camera stream and release the capture session. camera.stream(options) [permission: camera] Start continuous camera streaming. Pushes camera.frame events to the calling port until camera.stopStream. ### chat chat.post(port, text) Post to a port's chat. Every port has one: pass port 0 for the desktop, a space id, or a port's id. The entry is attributed to you, the caller, and every subscriber of the port gets a `chat` event carrying it. port (string, required): Whose chat: `0` (the desktop), a space id, or a port id / udid / title. text (string, required): What to say. chat.read(port, after, limit) Read a port's chat, oldest first. Pass `after` (a seq you have seen) to get only what is newer. Returns { entries, last }, where `last` is the newest seq in the chat (0 when empty). after (integer): Only entries with a seq greater than this. limit (integer): At most this many, the newest ones (default 50, max 200). port (string, required): Whose chat: `0` (the desktop), a space id, or a port id / udid / title. ### clipboard clipboard.read() [permission: clipboard] Read the current clipboard contents. Returns text or base64 image data. clipboard.write(data) [permission: clipboard] Write text to the system clipboard data (string, required): The text to copy to clipboard ### companions companions.create(name, agent, args, runs, port, kinds, cwd, prompt, command, space_id) [permission: terminal] Make a companion, as the new-companion card does: an agent CLI (claude or codex) in a terminal port, or a custom command run headless. runs: "port" (default, on the desktop) or "hidden" (no place on the desktop; reach it through its chat). It joins the space and hears @mentions there; pass `port` to have it watch that port instead, woken by `kinds` (default ["port"], the port's own events) and replying in its chat. Needs the terminal permission, since it starts one. agent (string): The CLI (default claude). args (array): Arguments for the CLI or command. command (string): agent custom: the command to run. cwd (string): Working directory (default: the space's). kinds (array): With `port`: event kinds that wake it. name (string, required): Its name; @mention it by this. port (string): A port to watch instead of listening to the space. prompt (string): Its system prompt. runs (string): Where its terminal runs (default port). space_id (string): The space (default: yours, else the current one). companions.get(id) Get details about a specific companion by ID id (string, required): The companion's ID companions.list(space_id, port) List the companions in a space with their names, models, and trigger modes. Defaults to YOUR space — the companions you share this space with — because a companion acts within its space, not the whole instance. Pass space_id to target a different space, or space_id:"*" for the full global roster across every space in the Port42 instance (rarely what you want). From another machine: the companions of the shared port's own space, named by `port`. port (string): From another machine: the shared port whose space to list. space_id (string): Omit for your own space (the default). A space id targets that space. "*" returns the whole-instance roster. companions.unwatch(port, companion) Stop watching a port. Call it as the watcher, or pass `companion`. companion (string): Whose watch, by name or id (default: you). port (string, required): The watched port (id, udid or title). companions.watch(port, kinds, every, companion) Watch a port: an event on it of a kind you name wakes you for a turn, and your reply is posted in that port's chat. Default kinds: ["port"], the port's own published events (port.publish). Others: "console" (a log line or error), "state" (an edit), "chat" (every post in its chat), or one exact kind such as "port.alert". Events that arrive while you are in a turn are held and given to you together when it ends. Watching again changes the watch and resumes it if it paused (a watch pauses after 60 wakes in an hour, and says so in the port's chat). Call it as the companion that should wake, or pass `companion` to set a watch for another. companion (string): Whose watch, by name or id (default: you). every (integer): The least time between two wakes, in seconds (default: none). kinds (array): Event kinds that wake you (default ["port"]). port (string, required): The port to watch (id, udid or title). companions.watches(companion) List watches: yours, another companion's (`companion`), or every one (`companion`: "*"). Each is {companion, port, title, kinds, every, paused}. companion (string): A name or id, or "*" for all (default: you). ### fs fs.list(path) [permission: filesystem] List the contents of a directory in the Port42 data directory. Relative paths only (e.g. "scopes/strategy" or "scopes/strategy/decisions"). Returns a sorted list of filenames. path (string, required): Relative path to the directory (e.g. "scopes/strategy") fs.mkdir(path) [permission: filesystem] Create a directory (and any missing parent directories) in the Port42 data directory. Relative paths only (e.g. "scopes/strategy/decisions"). path (string, required): Relative path to create (e.g. "scopes/strategy/decisions") fs.pick(options) Open the native file picker. The chosen paths become readable and writable for the calling principal via fs.read / fs.write, with no further permission. fs.read(path, encoding) [permission: filesystem] Read a file. Use a relative path (e.g. "scopes/strategy/scope.md") to read from the Port42 data directory without a file picker. Use an absolute path for picker-approved files. encoding (string): utf8 (default) or base64 path (string, required): Relative path within Port42 data directory (e.g. "scopes/strategy/scope.md") or absolute path for picker-approved files. fs.write(path, data, encoding) [permission: filesystem] Write a file. Use a relative path (e.g. "scopes/strategy/facts.md") to write to the Port42 data directory — parent directories are created automatically. Use an absolute path for picker-approved files. data (string, required): Content to write encoding (string): utf8 (default) or base64 path (string, required): Relative path within Port42 data directory (e.g. "scopes/strategy/facts.md") or absolute path for picker-approved files. ### general help(topic) Return the Port42 API reference. Pass topic:"ports" for the port-authoring manual (read it BEFORE building or editing a port: sizing, module-scope, patterns, the design system). No topic returns the full method reference. topic (string): Optional. "ports" = the port-authoring manual. Omit for the API reference. presentation() The calling port's current presentation state { state, visible, w, h }: whether its surface is on screen right now and at what content size, so the port can pause its animation loop when not visible and scale fidelity to its size. The same value is delivered as the 'presentation' event on every change; this call returns the current snapshot for the initial read. whoami() Who you are to Port42: your name, your space and who is in it (the companions you can @mention), and, for a companion running in a Port42 terminal, that terminal's port id and chat. `elsewhere` lists companions on other machines met in the chat of a port shared with them, each with its mention and that port's chat: mention them there. Call it first. ### imagine imagine.budget(space, versions) Set the version budget of the imagine team in a space, for example to let it keep going after the budget is spent. The team's writes to its port past the budget are refused with budget_spent. The same as typing /imagine --versions N in that space's chat. space (string, required): The space the team was imagined in. versions (integer, required): The new budget, in versions of the port (at most 20). imagine.start(line, versions) [permission: terminal] Start an imagine team: from one line, a new space with its port (a placeholder until v1) and a lead and two engineers (their terminals on its desktop) who build that port, in at most `versions` versions (default 10), until the lead posts DONE. Returns the space, the port, the team's names, the port title and the budget. The same as ⌘I or typing /imagine in a chat. line (string, required): What to make, in the person's words. versions (integer): The version budget (default 10, at most 20). ### invite invite.accept(link, code, remoteWake) Accept an invite someone sent you: this instance joins their port, which opens here as a tile. Returns { address, title, rights, tile }. Then call methods on the port by its address or the tile's id. remoteWake (default true): a mention of one of your companions in that port's chat wakes it here, on your model; the tile's chrome can turn it off later. code (string): The six-digit code, if the invite needs one. link (string, required): The invite link (https://tele.port42.ai/#…). remoteWake (boolean): Let their chat wake your companions for this port (default true). invite.create(port, rights, expiresIn, requireCode) Make an invite link that lets one person on another machine open ONE port: in Port42 if they have it, otherwise in their browser. The link lets in two machines (say their browser, then their Port42) and is then used up. Returns { link, code?, id, expires, discloses }. rights: any of see, use, edit, wake_agents, fork (default see, use and wake_agents: remote wake, their companions may wake yours in this port's chat; fork lets them take a copy, which Port42 offers only when given). requireCode: a six-digit code they must type, sent to them another way. `discloses` lists what the port itself can do on this machine; whoever you let in can make it do so. Port 0 and spaces cannot be shared. expiresIn (integer): Seconds until the link stops working (default 7 days, at most 30). port (string, required): The port to share (id / udid / title). requireCode (boolean): Require a six-digit code, to send another way. rights (array): see, use, edit, wake_agents, fork. Default see, use and wake_agents. invite.list() The invites this instance has made: id, port, rights, expiry, whether a code is required, and whether each is open, used, expired or withdrawn. A link lets in two machines (a move, one), so it stays open after the first: usedBy names the first and usedAgainBy the second, when it has let them in. invite.revoke(id) Withdraw an invite that has not been used. To remove someone who already joined, remove them in Settings → Access. id (string, required) ### notify notify.send(title, body, options) [permission: notification] Send a macOS system notification body (string, required): Notification body text title (string, required): Notification title ### port port.close() Close the calling port. port.console(id, level, tail) Check a port for problems. level=count returns only how many errors and warnings it has logged, no text: the cheap check that a port you built works. The default (problems) adds the errors and warnings themselves (the most recent 20), to deal with them. level=all reads everything it printed, for debugging. A terminal's output has no levels, so for a terminal the default is its last 50 lines. `omitted` says how many lines were left out. id (string, required): The port's UDID (from ports_list), or a terminal's name. level (string): count: the numbers only. problems (default for a web port): errors and warnings. all: every line, for debugging. tail (integer): How many recent lines to return (default 20 for problems, 50 for all). port.create(options) Create a port and return its id. The uniform way to make any port. type:"web" needs html (a full port HTML body) and renders inline in chat. type:"terminal" needs command and opens a native terminal (runs in /bin/zsh; the command is typed in — claude/gemini get the Port42 hooks). type:"browser" needs url and opens an embedded browser tile with an address bar that follows links. type:"chat" reveals that space's chat port (idempotent — one chat per space; brings it back if parked, popped out or closed, and a DM is a space, so pass its space_id to open that conversation). For terminals you may also pass args, cwd, systemPrompt (companion personality), env, and initialInput (a line left waiting, unsent, in the CLI's input box). Drive the result with port_push (input to terminals, data to web ports) and list with ports_list. Pass space_id to target a space (default: current). args (array): type:"terminal" — arguments for the command. command (string): type:"terminal" — executable/CLI to run (e.g. "bash", "htop", "claude"). cwd (string): type:"terminal" — working directory (default: home). env (object): type:"terminal" — custom environment variables for the shell. html (string): type:"web" — full port HTML body (include a <title> and <meta name="version">). initialInput (string): type:"terminal" — a line typed into the CLI once it is up but NOT submitted: it waits in the input box for the user to press Enter. For handing someone a first prompt to run. Use port_push instead to actually send input. presentation (string): Where the port appears: "tiled" (default, a desktop tile), "parked" (a chip in the rail) or "hidden" (runs with no tile: a background job, a pipe stage, or an agent nobody needs to watch; show it with port.manage show). space_id (string): Space to create the port in (default: current space). systemPrompt (string): type:"terminal" — companion personality/role appended to the CLI's system prompt. title (string): Port title (default: derived from html <title>, or the command). type (string, required): The port type to create. url (string): type:"browser" — the page to open (e.g. "https://example.com"). port.delete(id) Delete a CLOSED port for good: its record, versions and chat. Close it first (port.manage close); an open port is refused, so nothing live is ever deleted in one step. id (string, required): The closed port's id. port.exec(id, js, token) Execute JavaScript on a live port. Use this to call functions, push data, or update state on an existing port without replacing its HTML. The JS runs in the port's webview context with access to window, document, and any globals the port defines. id (string, required): The port's UDID (from ports_list) js (string, required): JavaScript code to execute in the port's context. Return a value to get it back in the response, as {value, token}. A bare expression yields its value (multi-line is fine). A multi-statement body needs an explicit return: `foo(); 42` is a syntax error, `foo(); return 42;` works. token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. port.getDom(id, selector) Read a WEB or BROWSER port's LIVE DOM — what is on screen right now, including everything its JS has changed since load. Use this, not port_get_html, when you need current state: port_get_html returns the stored SOURCE, which does not reflect any port_exec or port_push that has run since. Returns {html, token}; pass that token as 'token' on your next write and it will be refused rather than clobber someone if the port moved in between. id (string, required): The port's UDID (from ports_list) selector (string): Optional CSS selector to read just one subtree. Omit for the whole document. port.getHtml(id, version) Read the HTML of a port. Omit 'version' to get the current HTML. Pass 'version' (from port_history) to read a specific historical snapshot. id (string, required): The port's UDID (from ports_list) version (integer): Optional version number (from port_history). Omit for current HTML. port.history(id) List all saved versions of a port by its UDID. Returns version number, createdBy, and createdAt for each snapshot. Use port_get_html with a version number to read a specific snapshot, or port_restore to roll back. id (string, required): The port's UDID (from ports_list) port.info() Return the calling port's own id, title, space, capabilities, and activity token. port.manage(id, action, token) Manage a port. Actions: focus (raise to the front of the desktop), close (archive it: it can be reopened with port.reopen), hide (off the desktop and out of the rail, still running, with its chat and subscriptions), show (bring a hidden port back onto its desktop), pin (keep it above the other ports in its space), pinEverywhere (show it in every space, above the other ports, at one position), unpin. Check the status field from ports_list: 'tiled' | 'parked' | 'hidden'. action (string, required): One of: focus, close, hide, show, pin, pinEverywhere, unpin (minimize, dock, restore and undock are older names for hide and show) id (string, required): The port's UDID or title token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. port.move(id, x, y, space_id, token) Move a port's tile to specific desktop coordinates. Use screen_info to get display bounds first. id (string, required): The port's UDID (from ports_list) space_id (string): Which desktop to move it on. A port kept from another space is a tile on BOTH, with a position on each. Defaults to the current space when the port is on it, else the port's home space. token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. x (number, required): Horizontal position in desktop points y (number, required): Vertical position in desktop points port.patch(id, search, replace, token) Make a targeted edit to a port's HTML — replace an exact string with new content. Much safer than port_update for small changes because only the specified text is replaced; everything else is preserved exactly. Use port_get_html first to read the current HTML, find the exact string to replace, then call port_patch. Errors if 'search' is not found in the current HTML, so the port is never silently mangled. Snapshots the result the same as port_update, and reaches the page the same way (reloading only when it has to; see port_update). id (string, required): The port's UDID (from ports_list) replace (string, required): The string to replace it with. search (string, required): The exact string to find in the current HTML. Must match exactly — copy it from port_get_html output. token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. port.position(id, space_id) Return a port's position and size on one desktop (a kept port has a position per desktop). port.publish(kind, payload) A port emits its OWN state or event on its own Notify topic, for consumers watching via port_subscribe. This is a port broadcasting AS itself — distinct from port_push, which is input sent INTO a port. Only meaningful from inside a port; the topic is the calling port's own id, so there is no target argument. Use this instead of having a consumer reach in with port_exec to read state: the port publishes, consumers subscribe. kind (string, required): Event kind, e.g. 'state', 'progress', 'error'. Namespaced on the way out: you publish 'state', subscribers see 'port.state', so a port cannot emit a system event like 'driver' or 'browser.load'. payload: Any JSON value (object/array/string/number) delivered as the Notify envelope's payload. port.push(id, data, token) Send input to a port — one verb, dispatched by the port's type. A WEB port receives the data as a 'port42:data' CustomEvent with the payload in event.detail. A TERMINAL port receives the data as raw keystrokes typed into the shell: end with a newline (e.g. "ls\n") to run the command, or omit it to leave the line waiting unsubmitted. Use the id from ports_list. Prefer this over port_exec for data transfer. data (required): For web ports: any JSON value (object/array/string/number) delivered as event.detail. For terminal ports: a string of raw keystrokes (include \n to execute). Required — omitting it is refused with missing_arg rather than sent as nothing, and a terminal refuses an explicit null because there is no keystroke for null. id (string, required): The port's UDID (from ports_list), or a terminal's name. token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. port.rename(id, title, token) Rename a port. Sets the port's display title (shown in the title bar). Works for tiled, parked, docked, and inline ports. Use the port's id from ports_list. id (string, required): The port's UDID (from ports_list) title (string, required): The new title for the port token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. port.reopen(id) Reopen a closed port with its id, content, position and chat. A terminal relaunches its command in its last working directory. Closed ports are listed by ports_list with include_closed. id (string, required): The closed port's id. port.restore(id, version, token) Restore a port to a specific earlier version. The port's live HTML is replaced with the snapshot and a new version entry is recorded. Use port_history to find available version numbers. id (string, required): The port's UDID (from ports_list) token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. version (integer, required): The version number to restore to (from port_history) port.setCapabilities(capabilities) Set the calling port's own capabilities list. port.setTitle(title) Set the calling port's own title. port.subscribe(id) [streaming] Subscribe to a port's live event stream. Yields Notify events { topic, kind, payload, token } as the port emits them (e.g. terminal.output). `token` is the port's state token AT THAT MOMENT, so you can write next without re-reading the port first. OVER THE GATEWAY THIS IS WEBSOCKET-ONLY: connect to /ws and send it as a `call` envelope, and events arrive as `stream` frames on the same call_id. On HTTP /call it is refused with `unsupported`, because the stream never ends and a request/response call could only hang. The stream stays open until cancelled. id (string, required): The port to observe (id / udid / title). port.update(id, html, token) Update an existing port's HTML content. The port can be identified by its UDID or title. Works whether the port is windowed or minimized. The page is reloaded only when it has to be: a change confined to <style> is applied in place, and any other change is first offered to the page as a cancelable 'port42:update' event (detail.html is the new HTML), which the page may apply itself by calling preventDefault(), keeping its state. Returns applied: unchanged, styles, handledByPage or reloaded. html (string, required): The new HTML content for the port (full HTML, not a diff) id (string, required): The port's UDID or title to identify which port to update token (string, required): REQUIRED. The port's `token`, as it was when you composed this write — from ports_list, port_create, or whatever your last write returned. Without it the write is refused with 'token_required'; if the port has changed since, with 'stale_write'. Both carry the current token, so retry once with that instead of clobbering whoever moved it. ### ports ports.list(capabilities, space_id, include_closed) List active ports. Each port has an id (UDID), title, capabilities array, status, spaceId, createdBy (an id) with createdByName (who that is, for display), and cwd (if it has a terminal). Terminal ports also report surfaceBound. Use capabilities: ["terminal"] to filter to terminal ports; pass space_id to list only that space's ports. Use the id field with port_push for reliable routing (raw keystrokes to terminals, data to web ports). Always show the id and capabilities fields when presenting results — they are required for follow-up tool calls. capabilities (array): Filter to ports that have all of these capabilities. Examples: "terminal", "claude-code", "browser". Omit to list all ports. include_closed (boolean): Also list closed (archived) ports, with status 'closed'. Reopen one with port.reopen. space_id (string): List only this space's ports. Omit to list every space you can see (a port or companion sees only its own space). ### presence presence.list(port) Who is on a chat's messages right now: each companion that has a message from this chat (`received`), is working on it (`working`), or is waiting for the person (`waiting`, with `why` when it said). `doing` says what it is doing right now ("editing ShellView.swift", "running swift test") when its CLI reports tools (Claude Code does); a caller on another machine is told only the kind ("editing a file"). Returns { presence: [{name, state, since, why?, doing?}] }, empty when nobody is. Subscribe to the port for the `presence` event to hear each change; the event carries only the kind of what each is doing. port (string, required): Whose chat: a space id, or a port id / udid / title. ### rest rest.call(url, options) [permission: rest] Make an HTTP request to an external API. Use the 'secret' parameter to inject authentication from the secrets store — you never see the raw credential. Supports GET, POST, PUT, PATCH, DELETE. JSON bodies are auto-serialized. Responses with JSON content-type are auto-parsed. body (string): Request body. Objects are JSON-serialized automatically. headers (object): Additional HTTP headers as key-value pairs. method (string): HTTP method: GET, POST, PUT, PATCH, DELETE. Default: GET. secret (string): Named secret from the secrets store. The runtime injects the auth header — you never see the raw key. timeout (integer): Timeout in milliseconds. Default: 30000, max: 120000. url (string, required): Full URL to call (https recommended) ### screen screen.capture(scale) [permission: screen] Capture a screenshot of the screen. Returns a base64 PNG image. scale (number): Image scale factor 0.1-2.0 (default 1.0) screen.displays() Get display information: size, position, and visible area (excluding dock/menubar) for all connected displays. No screen recording permission required. Use this to calculate port positions before calling port_move. screen.record(options) [permission: screen] Record an app surface for a fixed number of seconds and auto-stop (the convenience form). Pass options.seconds. Returns {path, width, height, seconds, fps, bytes}. options (object): Recording options: target ({window:"self"} | {window:<osId>} | {port:<udid>} | {ports:[<udid>...]} | {region:{x,y,w,h}} | {display:<id>}). window/port/ports are occlusion-proof (only that surface); region/display capture the raw display (may catch other apps). Also aspect (e.g. "16:9"), fit (cover|exact; contain is not yet supported), width, height, scale, fps, padding, cursor (bool; only on a display or region target — a window/port capture cannot include the cursor), audio (none|system|mic|both), format (mov|mp4), path, and for the convenience form seconds. screen.record.start(options) [permission: screen] Start recording an app surface (window:self / a port / multiple ports) to a video file with optional system or mic audio. Returns {recordingId, width, height, target}. Stop with screen.record.stop. options (object): Recording options: target ({window:"self"} | {window:<osId>} | {port:<udid>} | {ports:[<udid>...]} | {region:{x,y,w,h}} | {display:<id>}). window/port/ports are occlusion-proof (only that surface); region/display capture the raw display (may catch other apps). Also aspect (e.g. "16:9"), fit (cover|exact; contain is not yet supported), width, height, scale, fps, padding, cursor (bool; only on a display or region target — a window/port capture cannot include the cursor), audio (none|system|mic|both), format (mov|mp4), path, and for the convenience form seconds. screen.record.status(recordingId) Report whether a recording (or any recording) is active and its elapsed seconds. recordingId (string): Optional recording id; omit for the overall status screen.record.stop(recordingId) Stop a recording started with screen.record.start. Returns {path, width, height, seconds, fps, bytes}. recordingId (string, required): The id returned by screen.record.start screen.stopStream() Stop the screen stream and release the capture stream. screen.stream(options) [permission: screen] Start continuous screen streaming. Pushes screen.frame events to the calling port until screen.stopStream. screen.windows() [permission: screen] List all visible windows with their titles, apps, and positions ### sessions sessions.find() [permission: terminal] The Claude Code and Codex sessions running on this Mac that Port42 did not start, each with its project, branch, title, last activity and the app it runs in, grouped into a space per project. What the first-run import and ⌘K 'bring in running sessions' offer. sessions.import(sessions) [permission: terminal] Bring running sessions into Port42 as forks: each becomes a companion in its space whose terminal starts with a copy of the whole conversation; the original is not touched and should then be closed. Each item: {id, cli, cwd, space, name}. sessions (array, required): The sessions to bring in: {id, cli (claude|codex), cwd, space, name}. ### space space.create(name, switch) Create a space. Returns {id, name}. The name is lowercased with spaces as dashes. Pass switch: true to also make it the current space; by default the person stays where they are. name (string, required): The space's name. switch (boolean): Also switch to it (default false). space.current(space_id, port) Get a space's metadata and member list: { id, name, type, memberCount, members: [{ id, name, type, owner, qualifiedName }] }. Pass space_id to inspect a specific space (e.g. your own PORT42_SPACE_ID); omit it for the currently selected space. From another machine: the shared port's own space, named by `port`. port (string): From another machine: the shared port whose space to read. space_id (string): Optional space id to inspect. Defaults to the currently selected space. space.delete(space_id) Delete a space: its own ports and terminals close, then the space and its chat go. A port adopted into another space stays there. Cannot be undone. The same as Delete in the galaxy. space_id (string, required): The space to delete (from space_list). space.list() List all spaces the user belongs to space.setWorkingDirectory(space_id, path) [permission: filesystem] Set (or clear) a space's working directory. Command companions spawned in the space default their cwd here so they share one workspace; each still gets its own claude session. Clearing falls back to home, and is a deliberate act: send path as null (or an empty string). OMITTING path is an error, not a clear. Defaults to the current space. path (required): Absolute directory path. Send null or "" to clear it and fall back to home. Required: omitting it is refused with missing_arg, so a malformed call cannot silently clear the setting. space_id (string): Space id (default: current space). space.switchTo(space_id) Switch the app's current space by id. ### storage storage.delete(key, options) Delete a value from persistent storage key (string, required): The storage key to delete port (string): For a copy of a port shared from another machine: that port's id, to reach its own storage there. scope (string): "global" for storage shared across spaces; omit for this space's. shared (boolean): true for the space's shared bucket rather than the caller's own. storage.get(key, options) Get a value from persistent key-value storage. Private to the caller by default; options {shared:true} and {scope:'global'} widen it, and {scope:'global', shared:true} is a PUBLIC board every caller on this machine can read and overwrite, so treat what you read there as untrusted. key (string, required): The storage key port (string): For a copy of a port shared from another machine: that port's id, to reach its own storage there. scope (string): "global" for storage shared across spaces; omit for this space's. shared (boolean): true for the space's shared bucket rather than the caller's own. storage.list(options) List all keys in persistent storage port (string): For a copy of a port shared from another machine: that port's id, to reach its own storage there. scope (string): "global" for storage shared across spaces; omit for this space's. shared (boolean): true for the space's shared bucket rather than the caller's own. storage.set(key, value, options) Store a value in persistent key-value storage. Private to the caller by default. {scope:'global', shared:true} is a PUBLIC board: every port, companion and client on this machine can read and overwrite it, so never store secrets or personal data there. key (string, required): The storage key port (string): For a copy of a port shared from another machine: that port's id, to reach its own storage there. scope (string): "global" for storage shared across spaces; omit for this space's. shared (boolean): true for the space's shared bucket rather than the caller's own. value (string, required): The value to store ### terminal terminal.exec(command, options) [permission: terminal] Execute a shell command and return the output. Runs in /bin/zsh. command (string, required): The shell command to execute cwd (string): Working directory (default: home) timeout (integer): Timeout in seconds (default: 30, max: 120) ### user user.get() Get the current user's identity (id and display name) ### Aliases -h -> help files.pick -> fs.pick files.read -> fs.read files.write -> fs.write