summaryrefslogtreecommitdiff
path: root/doc/external-editor-json-rpc.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/external-editor-json-rpc.md')
-rw-r--r--doc/external-editor-json-rpc.md1889
1 files changed, 1889 insertions, 0 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md
new file mode 100644
index 0000000000..80cc135522
--- /dev/null
+++ b/doc/external-editor-json-rpc.md
@@ -0,0 +1,1889 @@
+# Viewer to External Editor JSON-RPC<br>Message Interfaces Documentation
+
+> **This file is a mirror.** The canonical source is `doc/Message_Interfaces.md` in the
+> object_publish extension repository. Do not edit this copy — edit the canonical file and
+> re-copy it here.
+
+This document describes all the message interfaces defined for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension.
+
+## Table of Contents
+
+- [Usage Flow](#usage-flow)
+- [VS Code Launch URI](#vs-code-launch-uri)
+- [JSON-RPC Method Summary](#json-rpc-method-summary)
+- [Error Handling](#error-handling)
+- [Session Management Interfaces](#session-management-interfaces)
+ - [SessionHandshake](#sessionhandshake)
+ - [SessionHandshakeResponse](#sessionhandshakeresponse)
+ - [Session OK](#session-ok)
+ - [SessionDisconnect](#sessiondisconnect)
+ - [SystemPing](#systemping)
+ - [SystemVersion](#systemversion)
+ - [SystemStatus](#systemstatus)
+ - [SystemListMethods](#systemlistmethods)
+- [Language and Syntax Interfaces](#language-and-syntax-interfaces)
+ - [SyntaxChange](#syntaxchange)
+ - [Language Syntax ID Request](#language-syntax-id-request)
+ - [Language Syntax Request](#language-syntax-request)
+ - [Language Syntax Cache List](#language-syntax-cache-list)
+ - [Language Syntax Cache Get](#language-syntax-cache-get)
+- [Script Subscription Interfaces](#script-subscription-interfaces)
+ - [ScriptSubscribe](#scriptsubscribe)
+ - [ScriptSubscribeResponse](#scriptsubscriberesponse)
+ - [ScriptUnsubscribe](#scriptunsubscribe)
+ - [ScriptList](#scriptlist)
+- [Compilation Interfaces](#compilation-interfaces)
+ - [Diagnostic](#diagnostic)
+ - [CompilationResult](#compilationresult)
+- [Runtime Event Interfaces](#runtime-event-interfaces)
+ - [RuntimeDebug](#runtimedebug)
+ - [RuntimeError](#runtimeerror)
+- [Handler and Configuration Interfaces](#handler-and-configuration-interfaces)
+ - [WebSocketHandlers](#websockethandlers)
+ - [ClientInfo](#clientinfo)
+- [Object Explorer Interfaces](#object-explorer-interfaces)
+ - [Core Data Types](#core-data-types)
+ - [Common preconditions](#common-preconditions)
+ - [ObjectPublish](#objectpublish)
+ - [ObjectUnpublish](#objectunpublish)
+ - [ObjectUpdate](#objectupdate)
+ - [ObjectContentGet](#objectcontentget)
+ - [ObjectContentSave](#objectcontentsave)
+ - [ObjectItemCreate](#objectitemcreate)
+ - [ObjectItemDelete](#objectitemdelete)
+ - [ObjectScriptSetRunning](#objectscriptsetrunning)
+ - [ObjectScriptReset](#objectscriptreset)
+ - [ObjectRequest](#objectrequest)
+ - [ObjectList](#objectlist)
+ - [ObjectModify](#objectmodify)
+ - [ObjectItemModify](#objectitemmodify)
+- [Command Interfaces](#command-interfaces)
+ - [CommandExecute](#commandexecute)
+ - [CommandList](#commandlist)
+
+## Usage Flow
+
+1. **Connection Establishment:**
+
+ - The viewer refuses a WebSocket upgrade request that carries an `Origin` header (HTTP 403):
+ browsers send one with every socket a page opens, and a program connecting need not
+ - Viewer sends `session.handshake` call with `SessionHandshake` data
+ - Extension responds with `SessionHandshakeResponse`, answering the challenge
+ - Viewer confirms with `session.ok` notification
+ - Until `session.ok`, every request the extension makes is refused with `-32002`
+ (Authentication required) and the viewer sends it no notifications. A wrong challenge
+ response, an error in place of one, or no response within 30 seconds closes the connection.
+
+2. **Language Information Exchange:**
+
+ - Extension makes `language.syntax.id` call to get current syntax version
+ - Extension makes `language.syntax` calls with different `kind` parameters to get specific language data
+ - Viewer responds with a `LanguageInfo` object containing the requested definitions
+
+3. **Script Subscription Management:**
+
+ - Extension makes `script.subscribe` call with `ScriptSubscribe` data to request live synchronization for a script
+ - Viewer responds with `ScriptSubscribeResponse` indicating success or failure
+ - When subscription needs to be terminated, viewer sends `script.unsubscribe` notification with `ScriptUnsubscribe` data
+ - Extension handles unsubscription by cleaning up local script tracking
+
+4. **Object Explorer:**
+
+ - Viewer sends `object.publish` notification when an in-world object's contents are made available for editing (user clicks "Explore in IDE")
+ - Viewer sends `object.unpublish` notification when an object is removed or the user stops exploring
+ - Viewer sends `object.update` notification when object inventory changes (full replacement or delta)
+ - Extension calls `object.content.get` to fetch an item's content on demand
+ - Extension calls `object.content.save` to write modified content back to the viewer
+ - Extension calls `object.item.create` / `object.item.delete` to manage inventory items
+ - Extension calls `object.script.set_running` to start or stop a script
+
+5. **Runtime Events:**
+
+ - Viewer sends `language.syntax.change` notification with `SyntaxChange` when language changes
+ - Viewer sends `script.compiled` notification with `CompilationResult` after script compilation
+ - Viewer sends `runtime.debug` notification with `RuntimeDebug` for debug messages during script execution
+ - Viewer sends `runtime.error` notification with `RuntimeError` when runtime errors occur
+
+6. **Connection Termination:**
+ - Either side can send `session.disconnect` notification with `SessionDisconnect` data
+ - Connection is closed gracefully
+
+## VS Code Launch URI
+
+The viewer can launch VS Code and trigger an automatic WebSocket connection by opening a `vscode://` URI via the operating system's default URI handler. The extension registers a URI handler for this scheme; VS Code will launch itself if not already running and deliver the URI to the extension.
+
+### URI Format
+
+```
+vscode://lindenlab.sl-vscode-plugin/connect[?port=<port>][&object=<uuid>][&script=<uuid>]
+```
+
+### Parameters
+
+| Parameter | Required | Description |
+| --------- | -------- | ----------- |
+| `port` | No | Port number the viewer's WebSocket server is listening on. Overrides the user's configured port for this session. Defaults to the configured `slVscodeEdit.network.websocketPort` (default `9020`) if absent. Must be in range 1024–65535. |
+| `object` | No | UUID of a root prim. After the handshake completes the extension calls `object.request` to ask the viewer to start exploring this object. The viewer then sends an `object.publish` notification and the object appears as a workspace folder in the Explorer. |
+| `script` | No | UUID of a script. After the handshake completes the extension locates the corresponding temp file via `script.list` and opens it, triggering the normal `script.subscribe` + live-sync flow. |
+
+`object` and `script` are mutually exclusive in typical use but both may be supplied; the extension will process both.
+
+### Examples
+
+```
+# Open VS Code and connect on default port
+vscode://lindenlab.sl-vscode-plugin/connect
+
+# Connect on a custom port
+vscode://lindenlab.sl-vscode-plugin/connect?port=9021
+
+# Connect and immediately explore a specific object
+vscode://lindenlab.sl-vscode-plugin/connect?port=9020&object=550e8400-e29b-41d4-a716-446655440000
+
+# Connect and open a specific script for editing
+vscode://lindenlab.sl-vscode-plugin/connect?port=9020&script=6ba7b810-9dad-11d1-80b4-00c04fd430c8
+```
+
+### Post-connection sequence
+
+When the URI contains an `object` or `script` parameter the extension acts only **after** the handshake is fully complete (`session.ok` received):
+
+```
+URI received by extension
+ │
+ ▼
+WebSocket connects → session.handshake → session.ok
+ │
+ ├─ object=<uuid> → object.request({ object_id }) call
+ │ │
+ │ ▼ (async, when viewer is ready)
+ │ object.publish notification
+ │
+ └─ script=<uuid> → script.list call → open temp file
+ │
+ ▼
+ script.subscribe + live-sync
+```
+
+## JSON-RPC Method Summary
+
+| Method | Direction | Type | Interface/Parameters |
+| ------------------------------- | ------------------ | ------------ | -------------------------- |
+| `session.handshake` | Viewer → Extension | Call | `SessionHandshake` |
+| `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` |
+| `session.ok` | Viewer → Extension | Notification | _(no interface)_ |
+| `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` |
+| `system.ping` | Bidirectional | Call | `SystemPing` |
+| `system.ping` (response) | Bidirectional | Response | `SystemPingResponse` |
+| `system.getVersion` | Bidirectional | Call | _(no parameters)_ |
+| `system.getVersion` (response) | Bidirectional | Response | `SystemVersionResponse` |
+| `system.status` | Bidirectional | Call | _(no parameters)_ |
+| `system.status` (response) | Bidirectional | Response | `SystemStatusResponse` |
+| `system.listMethods` | Bidirectional | Call | _(no parameters)_ |
+| `system.listMethods` (response) | Bidirectional | Response | `SystemListMethodsResponse`|
+| `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` |
+| `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` |
+| `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` |
+| `script.unsubscribe` | Extension → Viewer | Call | `ScriptUnsubscribeParams` |
+| `script.unsubscribe` (response) | Viewer → Extension | Response | `null` |
+| `script.list` | Extension → Viewer | Call | _(no parameters)_ |
+| `script.list` (response) | Viewer → Extension | Response | `ScriptList` |
+| `language.syntax.id` | Extension → Viewer | Call | _(no parameters)_ |
+| `language.syntax.id` (response) | Viewer → Extension | Response | `{ id: string }` |
+| `language.syntax` | Extension → Viewer | Call | `{ kind: string }` |
+| `language.syntax` (response) | Viewer → Extension | Response | `LanguageInfo` |
+| `language.syntax.cache` | Extension → Viewer | Call | _(no parameters)_ |
+| `language.syntax.cache` (response) | Viewer → Extension | Response | `SyntaxCacheList` |
+| `language.syntax.get` | Extension → Viewer | Call | `{ filename: string, as_json?: boolean }` |
+| `language.syntax.get` (response) | Viewer → Extension | Response | `SyntaxCacheFile` |
+| `language.syntax.change` | Viewer → Extension | Notification | `SyntaxChange` |
+| `script.compiled` | Viewer → Extension | Notification | `CompilationResult` |
+| `runtime.debug` | Viewer → Extension | Notification | `RuntimeDebug` |
+| `runtime.error` | Viewer → Extension | Notification | `RuntimeError` |
+| `object.publish` | Viewer → Extension | Notification | `ObjectPublishMessage` |
+| `object.unpublish` | Viewer → Extension | Notification | `ObjectUnpublishMessage` |
+| `object.unpublish` | Extension → Viewer | Call | `ObjectUnpublishParams` |
+| `object.unpublish` (response) | Viewer → Extension | Response | `ObjectUnpublishResponse` |
+| `object.update` | Viewer → Extension | Notification | `ObjectUpdateMessage` |
+| `object.content.get` | Extension → Viewer | Call | `ObjectContentGetParams` |
+| `object.content.get` (response) | Viewer → Extension | Response | `ObjectContentGetResponse` |
+| `object.content.save` | Extension → Viewer | Call | `ObjectContentSaveParams` |
+| `object.content.save` (response)| Viewer → Extension | Response | `ObjectContentSaveResponse`|
+| `object.item.create` | Extension → Viewer | Call | `ObjectItemCreateParams` |
+| `object.item.create` (response) | Viewer → Extension | Response | `ObjectItemCreateResponse` |
+| `object.item.delete` | Extension → Viewer | Call | `ObjectItemDeleteParams` |
+| `object.item.delete` (response) | Viewer → Extension | Response | `ObjectItemDeleteResponse` |
+| `object.script.set_running` | Extension → Viewer | Call | `ObjectScriptSetRunningParams` |
+| `object.script.set_running` (response) | Viewer → Extension | Response | `ObjectScriptSetRunningResponse` |
+| `object.script.reset` | Extension → Viewer | Call | `ObjectScriptResetParams` |
+| `object.script.reset` (response)| Viewer → Extension | Response | `ObjectScriptResetResponse` |
+| `object.request` | Extension → Viewer | Call | `ObjectRequestParams` |
+| `object.request` (response) | Viewer → Extension | Response | `ObjectRequestResponse` |
+| `object.list` | Extension → Viewer | Call | `{}` (no params) |
+| `object.list` (response) | Viewer → Extension | Response | `ObjectListResponse` |
+| `object.modify` | Extension → Viewer | Call | `ObjectModifyParams` |
+| `object.modify` (response) | Viewer → Extension | Response | `ObjectModifyResponse` |
+| `object.item.modify` | Extension → Viewer | Call | `ObjectItemModifyParams` |
+| `object.item.modify` (response) | Viewer → Extension | Response | `ObjectItemModifyResponse` |
+| `command.execute` | Bidirectional | Call | `CommandExecuteParams` |
+| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` |
+| `command.list` | Bidirectional | Call | _(no params)_ |
+| `command.list` (response) | Bidirectional | Response | `CommandListResponse` |
+
+## Error Handling
+
+A call that fails returns a JSON-RPC 2.0 `error` object rather than a result. There is no partial
+success: a response carries either `result` or `error`, never both.
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 12,
+ "error": {
+ "code": -32602,
+ "message": "Invalid params: No syntax category specified"
+ }
+}
+```
+
+**Standard JSON-RPC codes:**
+
+| Code | Meaning |
+| ---- | ------- |
+| `-32700` | Parse error — invalid JSON was received |
+| `-32600` | Invalid Request — the JSON sent is not a valid Request object |
+| `-32601` | Method not found |
+| `-32602` | Invalid params |
+| `-32603` | Internal error |
+
+**Server-specific codes.** The range `-32000` to `-32099` is reserved for server errors. The
+transport defines the following; `-32001` and `-32003` are the ones handlers commonly raise.
+
+| Code | Meaning |
+| ---- | ------- |
+| `-32000` | Connection closed unexpectedly |
+| `-32001` | Request timed out |
+| `-32002` | Authentication required |
+| `-32003` | Access denied |
+| `-32004` | Too many requests |
+| `-32005` | Service temporarily unavailable |
+| `-32006` | Message exceeds maximum size |
+| `-32007` | Session expired or invalid |
+
+**Message format.** Standard errors prefix the handler's detail text with a fixed label, so
+`error.message` reads `"Invalid params: <detail>"`, `"Internal error: <detail>"`,
+`"Method not found: <method>"`, and so on. Server-specific errors carry the detail text alone
+(e.g. `"Access denied"`). Clients should branch on `error.code`, not on `error.message`.
+
+**`success` is not an error channel.** Several results carry a `success` field. Where a method
+reports failure through a JSON-RPC error, that field is `true` on every response it ever
+appears in, and a failed call produces no result to inspect. Do not treat a missing or false
+`success` as the failure signal unless the method's own section documents it as one.
+
+## Session Management Interfaces
+
+### SessionHandshake
+
+**JSON-RPC Method:** `session.handshake` (call from viewer)
+
+The initial handshake call sent by the viewer to establish a session.
+
+```typescript
+interface SessionHandshake {
+ server_version: "1.0.0";
+ protocol_version: "1.0";
+ viewer_name: string;
+ viewer_version: string;
+ agent_id: string;
+ agent_name: string;
+ challenge: string;
+ languages: string[];
+ syntax_id: string;
+ features: { [feature: string]: boolean };
+}
+```
+
+**Fields:**
+
+- `server_version`: Fixed version "1.0.0" indicating the server API version
+- `protocol_version`: Fixed version "1.0" for the communication protocol
+- `viewer_name`: Name of the Second Life viewer application
+- `viewer_version`: Version string of the viewer
+- `agent_id`: Unique identifier for the user/agent
+- `agent_name`: Human-readable name of the agent
+- `challenge`: Path to a temporary file on the local filesystem containing a UUID, and nothing else. The client must read this file and return its contents as `challenge_response` to authenticate the connection. The file is named at random, readable only by the user running the viewer, and deleted once the handshake is answered or abandoned, so only a client running as that user can answer.
+- `languages`: Array of supported scripting languages (e.g., `["lsl", "luau"]`)
+- `syntax_id`: Current active syntax identifier as a UUID string
+- `features`: Dictionary of feature flags indicating viewer capabilities. Known flags:
+ - `live_sync`: Viewer supports live script synchronisation with the external editor
+ - `compilation`: Viewer will forward compilation results via `script.compiled`
+ - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files
+ - `commands`: Both sides support `command.execute` and `command.list`
+ - `unified_diagnostics`: Viewer supports the unified diagnostic reporting format
+
+### SessionHandshakeResponse
+
+**JSON-RPC Method:** Response to `session.handshake`
+
+The response sent by the VS Code extension to complete the handshake.
+
+```typescript
+interface SessionHandshakeResponse {
+ client_name: string;
+ client_version: "1.0";
+ protocol_version: string;
+ challenge_response: string;
+ languages: string[];
+ features: { [feature: string]: boolean };
+ script_name?: string;
+ script_language?: string;
+}
+```
+
+**Fields:**
+
+- `client_name`: Name of the client (VS Code extension)
+- `client_version`: Fixed version "1.0" of the client
+- `protocol_version`: Protocol version the client supports
+- `challenge_response`: The UUID read from the temporary file identified by the `challenge` field in the handshake. Without it, or with any other value, the connection is closed.
+- `languages`: Array of languages supported by the client
+- `features`: Dictionary of features supported by the client. Known flags:
+ - `live_sync`: Client supports live script synchronisation
+ - `error_reporting`: Client accepts `runtime.error` notifications
+ - `unified_diagnostics`: Client supports the unified diagnostic reporting format
+ - `object_publish`: Client supports the object explorer methods and notifications
+ - `commands`: Both sides support `command.execute` and `command.list`
+ - `debugging`: Advertised as `false`. Reserved; no debugging support.
+ - `breakpoints`: Advertised as `false`. Reserved; no breakpoint support.
+- `script_name` (optional): Name of the script currently open in the editor
+- `script_language` (optional): Language of the script currently open in the editor (e.g. `"lsl"`, `"luau"`)
+
+### Session OK
+
+**JSON-RPC Method:** `session.ok` (notification from viewer)
+
+Confirmation notification sent by the viewer after successful handshake completion. No parameters are sent with this notification.
+
+### SessionDisconnect
+
+**JSON-RPC Method:** `session.disconnect` (notification, bidirectional)
+
+Message sent when terminating the connection.
+
+```typescript
+interface SessionDisconnect {
+ reason: number;
+ message: string;
+}
+```
+
+**Fields:**
+
+- `reason`: Numeric code indicating the reason for disconnection:
+ - `0`: Normal closure
+ - `1`: Editor closed
+ - `2`: Protocol error
+ - `3`: Connection timeout
+ - `4`: Internal server error
+- `message`: Human-readable description of the disconnect reason
+
+### SystemPing
+
+**JSON-RPC Method:** `system.ping` (call, bidirectional)
+
+Ping call used to verify that the connection is alive and measure latency. Either side can
+initiate a ping.
+
+The generic JSON-RPC server responds with the simple result `"pong"`. The editor server extends
+that response with the original timestamp and its current server time. The extension uses the
+extended response for its periodic connection-health check.
+
+```typescript
+interface SystemPing {
+ timestamp: number;
+}
+```
+
+**Fields:**
+
+- `timestamp`: Unix timestamp in milliseconds when the ping was sent
+
+**Response:**
+
+```typescript
+interface SystemPingResponse {
+ pong: string;
+ timestamp: number;
+ server_time: number;
+}
+```
+
+**Response Fields:**
+
+- `pong`: Acknowledgement that the ping was received.
+- `timestamp`: The original timestamp from the request, echoed by the editor server.
+- `server_time`: Unix timestamp in milliseconds when the response was generated
+
+The extension sends a `system.ping` request every 30 seconds and tears the connection down
+after two consecutive failures.
+
+**Example Request:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "system.ping",
+ "id": 42,
+ "params": {
+ "timestamp": 1721145600000
+ }
+}
+```
+
+### SystemVersion
+
+**JSON-RPC Method:** `system.getVersion` (call, bidirectional)
+
+Requests the identity and version of the peer. The response uses the same field names in both
+directions:
+
+```typescript
+interface SystemVersionResponse {
+ client_name: string;
+ client_version: string;
+}
+```
+
+The viewer returns its viewer channel as `client_name` and its full viewer version as
+`client_version`. The extension returns its package name as `client_name` and its package version
+as `client_version`.
+
+**Example viewer response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 43,
+ "result": {
+ "client_name": "Second Life",
+ "client_version": "7.1.0.123456"
+ }
+}
+```
+
+**Example extension response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 43,
+ "result": {
+ "client_name": "sl-vscode-plugin",
+ "client_version": "1.0.6"
+ }
+}
+```
+
+**Example Response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 42,
+ "result": {
+ "pong": "pong",
+ "timestamp": 1721145600000,
+ "server_time": 1721145600015
+ }
+}
+```
+
+### SystemStatus
+
+**JSON-RPC Method:** `system.status` (call, bidirectional)
+
+Requests the current status of the peer. The default response is:
+
+```typescript
+interface SystemStatusResponse {
+ status: "OK";
+}
+```
+
+Both the viewer and the extension currently return `status: "OK"`. The response may be extended
+with additional status information in the future.
+
+**Example response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 44,
+ "result": {
+ "status": "OK"
+ }
+}
+```
+
+### SystemListMethods
+
+**JSON-RPC Method:** `system.listMethods` (call, bidirectional)
+
+Requests the names of the methods available on the receiving peer. The response contains an
+alphabetically ordered list of unique method names. Sync versus async dispatch is an internal
+implementation detail and is not exposed by this interface.
+
+```typescript
+type SystemListMethodsResponse = string[];
+```
+
+The viewer returns all methods registered on the connection, including methods registered by the
+base JSON-RPC server and the editor server. The extension returns its built-in system methods
+together with dynamically registered handlers.
+
+**Example response:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 45,
+ "result": [
+ "command.execute",
+ "command.list",
+ "system.getVersion",
+ "system.listMethods",
+ "system.ping",
+ "system.status"
+ ]
+}
+```
+
+## Language and Syntax Interfaces
+
+### SyntaxChange
+
+**JSON-RPC Method:** `language.syntax.change` (notification from viewer)
+
+Notification sent when the active language syntax changes in the viewer.
+
+```typescript
+interface SyntaxChange {
+ id: string;
+}
+```
+
+**Fields:**
+
+- `id`: UUID string identifying the new syntax version
+
+### Language Syntax ID Request
+
+**JSON-RPC Method:** `language.syntax.id` (call from extension to viewer)
+
+Requests the current active language syntax identifier from the viewer. This method takes no parameters.
+
+**Response:** Returns `{ id: string }` where `id` is the current syntax version as a UUID string.
+
+### Language Syntax Request
+
+**JSON-RPC Method:** `language.syntax` (call from extension to viewer)
+
+Requests the in-memory keyword definitions for a specific language. These definitions are the deserialized, viewer-processed form of the syntax data for the current region.
+
+**Parameters:**
+
+```typescript
+{
+ kind: string; // The language whose definitions to retrieve
+}
+```
+
+**Valid `kind` values:**
+
+| Value | Description |
+| ----------- | ----------------------------------------- |
+| `"defs.lsl"` | Returns the LSL keyword definitions |
+| `"defs.lua"` | Returns the Luau keyword definitions |
+
+**Response:**
+
+```typescript
+interface LanguageInfo {
+ id: string;
+ defs: object;
+ success: boolean;
+}
+```
+
+**Response Fields:**
+
+- `id`: The current syntax version identifier
+- `defs`: The keyword definitions object. Structure varies by language.
+- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| No `kind` parameter supplied | `-32602` | `Invalid params: No syntax category specified` |
+| Unknown `kind` value | `-32602` | `Invalid params: Unknown syntax category requested` |
+| Definitions unavailable for a valid `kind` | `-32603` | `Internal error: Syntax definitions are unavailable` |
+
+### Language Syntax Cache List
+
+**JSON-RPC Method:** `language.syntax.cache` (call from extension to viewer)
+
+Requests the list of file names currently held in the `LLSyntaxDefCache`. This provides the extension with the available syntax definition file names that can subsequently be retrieved with `language.syntax.get`. This method takes no parameters.
+
+**Response:**
+
+```typescript
+interface SyntaxCacheList {
+ files: string[]; // Array of file names (e.g. ["lsl_keywords.xml", "slua_definitions.yaml"])
+ success: boolean;
+}
+```
+
+**Response Fields:**
+
+- `files`: Array of file name strings, each of which can be passed as the `filename` parameter to `language.syntax.get`
+- `success`: Whether the request was handled successfully
+
+**Known cache files:**
+
+| File name | Description |
+| -------------------------------- | ---------------------------------------------------- |
+| `builtins.txt` | LSL built-in keyword list in plain text format |
+| `lsl_definitions.yaml` | LSL language definitions in YAML format |
+| `lsl_keywords.xml` | LSL keyword definitions in LLSD XML format. Used by the viewer's script editor |
+| `lsl_keywords_pretty.xml` | LSL keyword definitions in formatted LLSD XML format |
+| `secondlife.d.luau` | Luau type definition file. Used by luau-lsp |
+| `secondlife.docs.json` | Luau documentation data in JSON format. Used by luau-lsp |
+| `slua_definitions.yaml` | Luau language definitions in YAML format |
+| `lua_keywords.xml` | Luau keyword definitions in LLSD XML format. Used by the viewer's script editor |
+| `lua_keywords_pretty.xml` | Luau keyword definitions in formatted LLSD XML format |
+| `secondlife_selene.yml` | Luau Selene linter configuration in YAML format |
+
+Not all files may be present in every cache — the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request.
+
+### Language Syntax Cache Get
+
+**JSON-RPC Method:** `language.syntax.get` (call from extension to viewer)
+
+Requests the content of a specific file from the syntax definition cache. The file name must be one of the names returned by a prior `language.syntax.cache` call. Content is returned either as a raw text string or as a parsed JSON/LLSD object depending on the `as_json` parameter.
+
+**Parameters:**
+
+```typescript
+{
+ filename: string; // The file name to retrieve, as returned by language.syntax.cache
+ as_json?: boolean; // Optional. If true, content is returned as a parsed object rather than raw text
+}
+```
+
+**Fields:**
+
+- `filename`: The file name to retrieve (e.g. `"lsl_keywords.xml"`, `"slua_definitions.yaml"`)
+- `as_json` (optional): When `true`, the file is deserialized and returned as a structured object in `content`. When omitted or `false`, `content` is the raw text of the file.
+
+**Response:**
+
+```typescript
+interface SyntaxCacheFile {
+ content: string | object; // String if as_json is false/omitted, object if as_json is true
+ success: boolean;
+}
+```
+
+**Response Fields:**
+
+- `content`: The file content. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`.
+- `success`: Always `true`. Failures are returned as JSON-RPC errors — see below.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| No `filename` parameter supplied | `-32602` | `Invalid params: No filename specified` |
+| Name not found in cache | `-32602` | `Invalid params: Requested syntax cache file not found` |
+| File could not be loaded (`as_json` omitted or `false`) | `-32603` | `Internal error: Failed to load syntax cache file` |
+| File could not be loaded or parsed (`as_json` is `true`) | `-32603` | `Internal error: Failed to load and format syntax cache file.` |
+
+## Script Subscription Interfaces
+
+### ScriptSubscribe
+
+**JSON-RPC Method:** `script.subscribe` (call from extension to viewer)
+
+Requests subscription to a script for live synchronization between the editor and viewer.
+
+```typescript
+interface ScriptSubscribe {
+ script_id: string;
+ script_name: string;
+ script_language: string;
+}
+```
+
+**Fields:**
+
+- `script_id`: Unique identifier for the script to subscribe to
+- `script_name`: Display name of the script file
+- `script_language`: Programming language of the script (e.g., "lsl", "luau")
+
+### ScriptSubscribeResponse
+
+**JSON-RPC Method:** Response to `script.subscribe`
+
+Response from the viewer indicating whether script subscription was successful.
+
+```typescript
+interface ScriptSubscribeResponse {
+ script_id: string;
+ success: boolean;
+ status: number;
+ message: string;
+ object_id?: string; // Success only
+ root_id?: string; // Success only
+ item_id?: string; // Success only
+}
+```
+
+**Fields:**
+
+- `script_id`: The script identifier that was subscribed to
+- `success`: Whether the subscription was successful
+- `status`: Numeric status code indicating the result:
+ - `0`: Success
+ - `1`: Invalid editor — the script editor panel is no longer open
+ - `2`: Invalid subscription — no subscription found for the given `script_id`
+ - `3`: Already subscribed — another connection is already subscribed to this script
+ - `4`: Internal server error
+- `message`: Always present. Fixed text matching `status`:
+
+| `status` | `message` |
+| -------- | --------- |
+| `0` | `OK` |
+| `1` | `Invalid editor handle` |
+| `2` | `No subscription found for script` |
+| `3` | `Script already subscribed` |
+| `4` | `Internal server error` |
+
+- `object_id`: UUID of the **prim** that owns the script. For a script in a child prim this is the child's UUID, not the linkset root's. Present only when `success` is `true`.
+- `root_id`: UUID of the root prim of the linkset containing the script. Present only when `success` is `true`. If the prim cannot be resolved, `root_id` is set equal to `object_id`, so equality does not by itself mean the script lives in the root prim.
+- `item_id`: The inventory item UUID of the script within the prim. Present only when `success` is `true`.
+
+### ScriptUnsubscribe
+
+**JSON-RPC Method:** `script.unsubscribe` (notification from viewer)
+
+Notification sent by the viewer when a script subscription should be terminated. It is delivered
+only to the connection holding the subscription, not to all connections.
+
+```typescript
+interface ScriptUnsubscribe {
+ script_id: string;
+}
+```
+
+**Fields:**
+
+- `script_id`: Unique identifier for the script to unsubscribe from
+
+**JSON-RPC Method:** `script.unsubscribe` (call from extension to viewer)
+
+The extension may also call `script.unsubscribe` to end a subscription it holds.
+
+```typescript
+interface ScriptUnsubscribeParams {
+ script_id: string; // Script whose subscription should be dropped
+}
+```
+
+The result is `null`.
+
+The viewer drops the subscription only when the calling connection owns it. The call is
+idempotent: an unknown `script_id`, or one held by a different connection, is ignored and still
+returns a successful `null` result. A client therefore cannot use the response to detect that it
+targeted the wrong script.
+
+### ScriptList
+
+**JSON-RPC Method:** `script.list` (call from extension to viewer)
+
+Requests the list of all scripts currently open and tracked by the viewer, along with the viewer's temp directory. This is intended for use by a file watcher tool that needs to discover which script temp files are active without going through the full `script.subscribe` flow. This method takes no parameters.
+
+**Response:**
+
+```typescript
+interface ScriptList {
+ temp_dir: string;
+ script_ids: string[];
+ success: boolean;
+}
+```
+
+**Response Fields:**
+
+- `temp_dir`: The absolute path to the viewer's temp directory where live-sync script files are written. Combined with a `script_id`, the caller can locate the corresponding temp file on disk.
+- `script_ids`: Array of script ID strings for all currently subscribed scripts, across all active connections.
+- `success`: Always `true`.
+
+## Compilation Interfaces
+
+### Diagnostic
+
+Individual compilation error record.
+
+```typescript
+interface Diagnostic {
+ row: number;
+ column: number;
+ level: string;
+ message: string;
+ format?: "lsl"; // Present only for LSL compilation errors
+}
+```
+
+**Fields:**
+
+- `row`: Line number where the error occurred (1-based for both LSL and Luau)
+- `column`: Column position of the error (1-based for LSL; always `0` for Luau as the compiler does not provide column information)
+- `level`: Compiler severity string (e.g. `"ERROR"`, `"WARNING"`)
+- `message`: Error description
+- `format` (optional): Present and set to `"lsl"` for LSL compilation errors; absent for Luau errors
+
+### CompilationResult
+
+**JSON-RPC Method:** `script.compiled` (notification from viewer)
+
+Result of a compilation operation in the viewer.
+
+```typescript
+interface CompilationResult {
+ script_id: string;
+ success: boolean;
+ running: boolean;
+ diagnostics?: Diagnostic[];
+}
+```
+
+**Fields:**
+
+- `script_id`: Unique identifier for the script that was compiled
+- `success`: Whether the compilation was successful
+- `running`: Whether the compiled script is currently running
+- `diagnostics` (optional): Array of `Diagnostic` records if any occurred
+
+**Delivery:** routed only to the connection subscribed to that script. This differs from
+`runtime.debug` and `runtime.error`, which are broadcast to every connection.
+
+**Two compile-feedback paths.** Compilation results reach a client by one of two routes,
+depending on how the save was made:
+
+| Save route | Feedback |
+| ---------- | -------- |
+| `object.content.save` (object explorer) | `compiled` and `diagnostics` returned inline in the response |
+| Live-sync editing of a subscribed script | `script.compiled` notification |
+
+A client using only the object explorer never receives `script.compiled`; a client waiting for
+`script.compiled` after an `object.content.save` will wait indefinitely. Consolidating these two
+paths is tracked separately.
+
+**Known limitation.** `script.compiled` is produced only while the viewer's script editor for that
+script is open. If that editor has closed, compilation results are dropped without notice — no
+result, no error, and not necessarily a preceding `script.unsubscribe`. Tracked separately.
+
+## Runtime Event Interfaces
+
+### RuntimeDebug
+
+**JSON-RPC Method:** `runtime.debug` (notification from viewer)
+
+Debug message notification sent by the viewer during script execution.
+
+```typescript
+interface RuntimeDebug {
+ script_id?: string; // Present when the viewer can resolve a script subscription id
+ object_id: string;
+ prim_id: string;
+ item_id: string;
+ object_name: string;
+ message: string;
+ channel: "debug" | "owner_say";
+ item: ItemRef;
+}
+```
+
+**Fields:**
+
+- `script_id`: Identifier for the script generating the debug message
+- `object_id`: UUID of the root prim of the object containing the script
+- `prim_id`: UUID of the prim that owns the script
+- `item_id`: Inventory item UUID of the script
+- `object_name`: Human-readable name of the object
+- `message`: The debug message content
+- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat.
+- `item`: Reference identifying the originating script. See `ItemRef` under `RuntimeError`.
+
+### RuntimeError
+
+**JSON-RPC Method:** `runtime.error` (notification from viewer)
+
+Runtime error notification sent by the viewer when a script encounters an error during execution.
+
+```typescript
+interface RuntimeError {
+ script_id?: string; // Present when the viewer can resolve a script subscription id
+ object_id: string;
+ prim_id: string;
+ item_id: string;
+ object_name: string;
+ message: string;
+ error: string;
+ line: number;
+ column: number;
+ stack: string[];
+ channel: "debug" | "owner_say";
+ item: ItemRef;
+}
+
+interface ItemRef {
+ root_id: string;
+ prim_id: string;
+ item_id: string;
+ name: string;
+ language: "lsl" | "luau";
+}
+```
+
+**Fields:**
+
+- `script_id`: Identifier for the script that encountered the error
+- `object_id`: UUID of the root prim of the object containing the script
+- `prim_id`: UUID of the prim that owns the script
+- `item_id`: Inventory item UUID of the script
+- `object_name`: Human-readable name of the object
+- `message`: The full raw chat text of the runtime error message as received from the simulator
+- `error`: Extracted runtime error description. This remains a top-level compatibility field while the protocol stays on version `1.0`.
+- `line`: Line number where the error occurred when the runtime format can be parsed; otherwise `0`.
+- `column`: Column position where the error occurred when the runtime format can be parsed; otherwise `0`.
+- `stack`: Stack trace lines extracted from the error message. Always present; an empty array when no trace could be extracted, so test its length rather than its presence.
+- `channel`: Source of the text. `"debug"` for script debug output, `"owner_say"` for owner-directed chat.
+- `item`: Reference identifying the originating script.
+ - `root_id`: UUID of the root prim of the linkset.
+ - `prim_id`: UUID of the prim that owns the script.
+ - `item_id`: Inventory item UUID of the script.
+ - `name`: Script name as it appears in the prim's inventory.
+ - `language`: The script's source language. Independent of the compile target; the VM is not carried in runtime messages.
+
+**Note on `script_id`:** this field is optional. It is included when the viewer can resolve the
+originating script inventory item and construct a subscription id from `prim_id` + `item_id`.
+When that resolution is not possible, the field is omitted.
+
+**Delivery:** `runtime.debug` and `runtime.error` are broadcast to all connections. An event is
+emitted when the originating object is published or its script is subscribed.
+
+## Handler and Configuration Interfaces
+
+### WebSocketHandlers
+
+Event handler interface for WebSocket events.
+
+```typescript
+interface WebSocketHandlers {
+ onHandshake?: (message: SessionHandshake) => SessionHandshakeResponse;
+ onHandshakeOk?: () => void;
+ onDisconnect?: (message: SessionDisconnect) => void;
+ onSubscribe?: (message: ScriptSubscribe) => ScriptSubscribeResponse;
+ onUnsubscribe?: (message: ScriptUnsubscribe) => void;
+ onSyntaxChange?: (message: SyntaxChange) => void;
+ onConnectionClosed?: () => void;
+ onCompilationResult?: (message: CompilationResult) => void;
+ onRuntimeDebug?: (message: RuntimeDebug) => void;
+ onRuntimeError?: (message: RuntimeError) => void;
+}
+```
+
+**Methods:**
+
+- `onHandshake`: Handler for initial handshake message, returns handshake response
+- `onHandshakeOk`: Handler called when handshake is successfully completed
+- `onDisconnect`: Handler for disconnect notifications
+- `onSubscribe`: Handler called when the extension sends a `script.subscribe` request, returns subscription response
+- `onUnsubscribe`: Handler for script unsubscription notifications from viewer
+- `onSyntaxChange`: Handler for syntax change notifications
+- `onConnectionClosed`: Handler called when connection is closed
+- `onCompilationResult`: Handler for compilation result notifications
+- `onRuntimeDebug`: Handler for runtime debug message notifications
+- `onRuntimeError`: Handler for runtime error notifications
+
+### ClientInfo
+
+Client information used in handshake responses.
+
+```typescript
+interface ClientInfo {
+ scriptName: string;
+ scriptId: string;
+ extension: string;
+}
+```
+
+**Fields:**
+
+- `scriptName`: Name of the script being edited
+- `scriptId`: Unique identifier for the script
+- `extension`: File extension or script type
+
+---
+
+## Object Explorer Interfaces
+
+These interfaces support exploring in-world object inventories (scripts and notecards) from the external editor as a browseable virtual filesystem. The extension exposes explored objects under the `sl://objects/` URI scheme.
+
+Explored objects are shared across all connections rather than owned by the connection that
+requested them. `object.publish`, `object.update` and `object.unpublish` are broadcast to every
+connected client, so a client will receive notifications for objects it never requested, and must
+drop an object from its own state when an `object.unpublish` for it arrives.
+
+### Core Data Types
+
+```typescript
+type InventoryItemType = "script" | "notecard";
+
+type ScriptVM = "lsl2" | "mono" | "luau";
+
+/** Permission mask fields. Only owner and next_owner are transmitted. */
+interface ItemPermissions {
+ owner: number; // e.g. PERM_MODIFY=0x4000, PERM_COPY=0x8000, PERM_TRANSFER=0x2000
+ next_owner: number;
+}
+
+/**
+ * Inventory item within an object or linked prim.
+ * asset_id is intentionally never transmitted.
+ */
+interface ObjectInventoryItem {
+ item_id: string; // Inventory item UUID
+ name: string; // Display name (no file extension)
+ description?: string;
+ type: InventoryItemType;
+ subtype?: number; // Scripts only: language from II_FLAGS_SUBTYPE_MASK (0=LSL, 1=Luau)
+ vm?: ScriptVM; // Scripts only: which VM the script targets
+ running?: boolean; // Scripts only: whether the script is running
+ faulted?: boolean; // Scripts only: whether the script has a runtime fault
+ permissions?: ItemPermissions;
+ creator_id?: string;
+}
+
+/** A linked (child) prim within a linkset */
+interface LinkedObject {
+ link_id: string; // UUID of the linked prim
+ link_number: number; // Link number (root=1, children≥2)
+ link_name: string;
+ link_description?: string;
+ permissions?: ObjectPermissions; // Actual permissions for this linked prim
+ inventory: ObjectInventoryItem[];
+}
+
+interface ObjectPermissions {
+ owner: number;
+ next_owner?: number; // Absent until the viewer receives ObjectProperties for the prim
+}
+
+/** Root of a linkset, as published to the extension */
+interface PublishedObject {
+ object_id: string; // UUID of the root prim
+ object_name: string;
+ object_description?: string;
+ region?: string;
+ owner_id?: string;
+ permissions?: ObjectPermissions; // Actual permissions for this root prim
+ can_save_back?: boolean; // Whether Save Back to Contents is currently available for this object
+ inventory: ObjectInventoryItem[]; // Root prim's scripts and notecards
+ linked_objects?: LinkedObject[]; // Child prims
+}
+```
+
+**Display names.** The extension synthesises a file extension for scripts when presenting items in
+the virtual filesystem:
+
+| Item | Extension |
+| ---- | --------- |
+| Script, `subtype` `0` (LSL) | `.lsl` |
+| Script, `subtype` `1` (Luau) | `.luau` |
+| Notecard | _(none)_ |
+
+Notecards receive no synthetic extension; the inventory name is used verbatim, so an extension the
+user gave the notecard is preserved and none is added.
+
+These extensions exist only for display and are never part of the item's inventory name. Names sent
+to and received from the viewer — including in `object.item.create` and `object.item.modify` — are
+always the pure inventory name, without an extension.
+
+### Common preconditions
+
+Every method that addresses an item by `prim_id` + `item_id` — `object.content.get`,
+`object.content.save`, `object.item.delete` and `object.item.modify` — runs the same validation
+before doing any work, in this order:
+
+1. Both `prim_id` and `item_id` are present.
+2. The prim exists.
+3. The object containing the prim is currently published.
+4. The item exists in that prim's inventory.
+5. The item is a script or a notecard.
+6. The caller holds the permissions that method requires.
+
+An object must be published before any of its items can be addressed. Learning an object's id from
+`object.list` is not sufficient on its own — the object must be published, which `object.list`
+reports and `object.request` initiates.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` |
+| Prim not found | `-32602` | `Invalid params: Prim not found` |
+| Object is not published | `-32003` | `Object is not published` |
+| Item not in the prim's inventory | `-32602` | `Invalid params: Item not found in prim inventory` |
+| Item is not a script or notecard | `-32602` | `Invalid params: Item is not a script or notecard` |
+| Required item permission denied | `-32003` | `Insufficient permissions` |
+| Modify denied on the containing prim | `-32003` | `No modify permission on object` |
+
+Writes require modify permission on both the item **and** the prim that contains it. A no-modify
+object can be published and read, but its contents cannot be changed.
+
+---
+
+### ObjectPublish
+
+**JSON-RPC Method:** `object.publish` (notification from viewer)
+
+Sent when the viewer publishes an in-world object's inventory for external editing. Triggers creation of a virtual filesystem workspace folder in the extension.
+
+```typescript
+interface ObjectPublishMessage {
+ object: PublishedObject;
+}
+```
+
+**Fields:**
+
+- `object`: The full object tree being explored, including root prim inventory and all linked prim inventories.
+ - `can_save_back` (optional): Capability hint for UI actions. When `true`, the object currently supports the `viewer.object.save_back_to_contents` command.
+
+---
+
+### ObjectUnpublish
+
+**JSON-RPC Method:** `object.unpublish` (notification from viewer)
+
+Sent when the viewer stops exploring a previously explored object — for example when the user stops exploring it from the viewer UI, the extension calls `object.unpublish`, or the object is deleted or linked into another object.
+
+```typescript
+interface ObjectUnpublishMessage {
+ object_id: string;
+ reason?: string;
+}
+```
+
+**Fields:**
+
+- `object_id`: UUID of the root prim that is no longer being explored
+- `reason` (optional): Machine-readable token identifying why exploring stopped. One of:
+
+| Value | Meaning |
+| ----- | ------- |
+| `manual` | The extension called `object.unpublish` for this object. |
+| `republish` | The object is being re-published; a fresh `object.publish` follows. |
+| `user` | The user stopped exploring the object from the viewer UI. |
+| `deleted` | The object was deleted. |
+| `linked` | The object became a child prim of a linkset and is no longer a root. |
+
+The field is omitted only when no reason was supplied; in practice every unpublish carries one
+of the values above.
+
+**JSON-RPC Method:** `object.unpublish` (call from extension to viewer)
+
+The extension may also call `object.unpublish` to manually stop exploring an object. The viewer will stop and broadcast a corresponding `object.unpublish` notification to all connections.
+
+```typescript
+interface ObjectUnpublishParams {
+ object_id: string; // UUID of the root prim to unpublish
+}
+
+interface ObjectUnpublishResponse {
+ success: boolean;
+ object_id?: string;
+}
+```
+
+**Fields:**
+
+- `object_id`: UUID of the root prim to stop exploring.
+- `success`: `true` if the object was being explored and has been removed.
+
+**Note:** The viewer also broadcasts an `object.unpublish` notification immediately after responding. The caller receives it too, so extensions should handle that notification idempotently.
+
+---
+
+### ObjectUpdate
+
+**JSON-RPC Method:** `object.update` (notification from viewer)
+
+Sent when an explored object's inventory, properties, or linkset membership change.
+
+**Shapes currently emitted.** The viewer sends exactly the following five payloads:
+
+| Trigger | Payload |
+| ------- | ------- |
+| Root prim inventory changed | `object_id`, `inventory` (complete replacement array) |
+| Child prim inventory changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` and `inventory` (complete replacement array for that child) |
+| Root prim properties changed | `object_id`, plus any changed subset of `object_name`, `object_description`, `owner_id`, `permissions`, `can_save_back` |
+| Child prim properties changed | `object_id`, `changes.linked_objects.modified[]` with `link_id` plus any changed subset of `link_name`, `link_description`, `permissions` |
+| Linkset membership changed | `object_id`, `linked_objects` (complete replacement array covering every child prim) |
+
+`inventory` and `linked_objects` are always complete replacements of the prior state, never
+increments. Linkset membership changes are coalesced behind a short flush delay, so several
+rapid link or unlink operations may arrive as a single update.
+
+**Deferred permission fields.** `object.publish` is gated on inventory loading, which can finish
+before the `ObjectProperties` reply arrives. Until it does the viewer derives `permissions.owner`
+from the object's update flags, which describe the current agent's rights, and omits `next_owner`.
+`owner_id` may be absent or the null UUID, and `can_save_back` may be `false` because it depends on
+selection state. Once properties arrive the viewer sends an `object.update` carrying the full
+`owner`/`next_owner` block and the real `owner_id`, so clients must merge these fields on update
+rather than assuming the publish payload is final.
+
+```typescript
+interface ObjectUpdateMessage {
+ object_id: string;
+ object_name?: string;
+ object_description?: string;
+ owner_id?: string;
+ permissions?: ObjectPermissions;
+ can_save_back?: boolean;
+ // Full replacement
+ inventory?: ObjectInventoryItem[];
+ linked_objects?: LinkedObject[];
+ changes?: {
+ inventory?: InventoryChanges; // Not implemented — see below
+ linked_objects?: LinkedObjectChanges;
+ };
+}
+
+interface LinkedObjectChanges {
+ added?: LinkedObject[]; // Not implemented — see below
+ removed?: string[]; // Not implemented — see below
+ modified?: {
+ link_id: string;
+ link_name?: string;
+ link_description?: string;
+ permissions?: ObjectPermissions;
+ inventory?: ObjectInventoryItem[]; // Complete replacement array, not a delta
+ }[];
+}
+```
+
+**Note:** `changes.linked_objects.modified` is emitted, as shown in the table above.
+`changes.linked_objects.added` and `changes.linked_objects.removed` are not.
+
+#### Not implemented
+
+The delta sub-protocol below is specified but is **not currently emitted by the viewer**.
+Implementation is tracked separately. Clients must rely on the full-replacement shapes listed
+above; code written to consume these types will never run against the current viewer.
+
+```typescript
+interface InventoryChanges {
+ added?: ObjectInventoryItem[];
+ removed?: string[]; // item_ids removed
+ modified?: ObjectInventoryItem[]; // metadata-only changes
+ content_changed?: string[]; // item_ids whose content changed (invalidates cache)
+ running_changed?: { item_id: string; running: boolean }[]; // running state toggled
+}
+```
+
+Also not implemented: `LinkedObjectChanges.added` and `LinkedObjectChanges.removed`. Linkset
+membership changes are sent as a complete `linked_objects` replacement array instead.
+
+---
+
+### ObjectContentGet
+
+**JSON-RPC Method:** `object.content.get` (call from extension to viewer)
+
+Requests the text content of a script or notecard. The extension calls this lazily when the user opens a file in the virtual filesystem.
+
+```typescript
+interface ObjectContentGetParams {
+ prim_id: string; // UUID of any prim (root or child) — no object_id + link_id needed
+ item_id: string;
+}
+
+interface ObjectContentGetResponse {
+ success: boolean;
+ prim_id: string;
+ item_id: string;
+ content: string; // Raw text content (UTF-8). Notecard envelope is unwrapped automatically.
+}
+```
+
+**Fields:**
+
+- `prim_id`: UUID of the prim that owns the item. Child prims are addressable directly by UUID without knowing the root object_id.
+- `item_id`: Inventory item UUID.
+- `success`: `true` on success.
+- `content`: The raw text content of the item. For notecards, the `Linden text version 2` envelope is stripped — only the body text is returned.
+
+**Permissions.** Scripts require both `PERM_COPY` and `PERM_MODIFY` on the item: the source of a
+no-copy or no-modify script is never exposed. Notecards require no permission at all, so that
+no-modify notecards remain readable in the external editor. See
+[Common preconditions](#common-preconditions) for the shared checks and errors.
+
+**Timeout.** 30 seconds to fetch the asset, after which the call fails with `-32001`
+(`Asset fetch timed out`).
+
+---
+
+### ObjectContentSave
+
+**JSON-RPC Method:** `object.content.save` (call from extension to viewer)
+
+Writes modified content back to the viewer. For scripts, the viewer will attempt to compile the updated source.
+
+```typescript
+interface ObjectContentSaveParams {
+ prim_id: string;
+ item_id: string;
+ content: string;
+ vm?: "mono" | "lsl2" | "luau";
+ running?: boolean; // Scripts only: run state applied after compilation when supplied.
+}
+
+interface ObjectContentSaveResponse {
+ success: boolean;
+ prim_id?: string;
+ item_id?: string;
+ compiled?: boolean;
+ diagnostics?: Diagnostic[];
+}
+```
+
+**Fields:**
+
+- `prim_id`: UUID of the prim that owns the saved item.
+- `item_id`: UUID of the saved inventory item.
+- `content`: Raw script/notecard source text to store.
+- `vm` (optional): Scripts only compile target. Accepted values are `"mono"`, `"lsl2"`, `"luau"`. When `"luau"` is specified for an LSL script (as opposed to a native Luau script), the viewer automatically selects the correct LSL-on-Luau compile path. If omitted, inferred from item metadata or content analysis.
+- `running` (optional): Scripts only. When provided, the viewer applies that run state after upload and compilation. When omitted, the viewer preserves the script's current run state and does not force it off.
+- `success`: Whether the upload/save operation succeeded.
+- `compiled` (optional): Scripts only. `true` when compilation succeeded, `false` when source saved but compile failed.
+- `diagnostics` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`.
+
+> **Note:** Omitting `running` leaves the script's existing run state unchanged. Only an explicit `true` or `false` changes the post-save state.
+
+**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim.
+See [Common preconditions](#common-preconditions) for the shared checks and errors.
+
+**Timeouts.** Scripts allow 60 seconds for upload and compilation; notecards allow 30 seconds. A
+client's own timeout must exceed the longer of the two. Exceeding either fails the call with
+`-32001`.
+
+---
+
+### ObjectItemCreate
+
+**JSON-RPC Method:** `object.item.create` (call from extension to viewer)
+
+Creates a new script or notecard in a prim's inventory. The call is asynchronous and returns the
+created item's details once the simulator confirms the item exists. The simulator may rename the
+item if a duplicate name exists.
+
+```typescript
+interface ObjectItemCreateParams {
+ prim_id: string; // UUID of the prim to create the item in
+ name: string; // Pure SL inventory name — no file extension
+ type: InventoryItemType; // "script" | "notecard"
+ vm?: ScriptVM; // Scripts only (required): "luau" | "mono" | "lsl2"
+ text?: string; // Notecards only (optional): initial body text
+}
+
+// On success, returns an ObjectInventoryItem with prim_id:
+interface ObjectItemCreateResponse extends ObjectInventoryItem {
+ prim_id: string; // Echoed prim UUID
+}
+```
+
+**Fields:**
+
+- `prim_id`: UUID of the prim to create the item in. Child prims are addressable directly by UUID.
+- `name`: Pure SL inventory name, without a file extension.
+- `type`: `"script"` or `"notecard"`.
+- `vm`: Scripts only. Required when `type` is `"script"`; accepted values are `"luau"`, `"mono"` and `"lsl2"`. Ignored for notecards.
+- `text` (optional): Notecards only. Initial body text for the new notecard. Ignored for scripts.
+
+**Notes:**
+- The response matches the `ObjectInventoryItem` structure (same fields as items in
+ `object.publish` and `object.update` notifications).
+- The `name` in the response may differ from the request if the simulator renamed it.
+- An `object.update` notification will also fire for the prim (since inventory changed).
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `type` is not `"script"` or `"notecard"` | `-32602` | `Invalid params: Unsupported item type: <type>` |
+| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` |
+| Prim not found | `-32602` | `Invalid params: Prim not found` |
+| `name` missing | `-32602` | `Invalid params: name is required` |
+| `vm` missing or invalid for a script | `-32602` | `Invalid params: vm must be 'luau', 'mono', or 'lsl2'` |
+| Object is not published | `-32003` | `Object is not published` |
+| Another `object.item.create` is already in flight for this prim | `-32600` | `Invalid Request: An item.create is already in flight for this prim` |
+| Simulator did not respond within 30 seconds | `-32001` | `Timed out waiting for item creation` |
+
+---
+
+### ObjectItemDelete
+
+**JSON-RPC Method:** `object.item.delete` (call from extension to viewer)
+
+Deletes a script or notecard from a prim's inventory. Requires `PERM_MODIFY` on the item.
+
+```typescript
+interface ObjectItemDeleteParams {
+ prim_id: string;
+ item_id: string;
+}
+
+interface ObjectItemDeleteResponse {
+ success: boolean;
+ prim_id: string; // Echoed back from request
+ item_id: string; // Echoed back from request
+}
+```
+
+**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim.
+See [Common preconditions](#common-preconditions) for the shared checks and errors.
+
+---
+
+### ObjectScriptSetRunning
+
+**JSON-RPC Method:** `object.script.set_running` (call from extension to viewer)
+
+Starts or stops a script within a prim.
+
+```typescript
+interface ObjectScriptSetRunningParams {
+ prim_id: string;
+ item_id: string;
+ running: boolean; // true = start, false = stop
+}
+
+interface ObjectScriptSetRunningResponse {
+ success: boolean;
+}
+```
+
+**`success` means dispatched, not applied.** The viewer sends a message to the simulator and
+returns immediately. `success: true` confirms the message was sent — it does not confirm the
+simulator started or stopped the script. Confirmation, if it arrives, comes later as an
+`object.update` notification. Treat the response as an acknowledgement and wait for the update
+before reporting the new run state to the user.
+
+**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the
+shared item validator and accepts scripts only.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `prim_id` or `item_id` missing | `-32602` | `Invalid params: prim_id and item_id are required` |
+| Prim not found | `-32602` | `Invalid params: Prim not found` |
+| Object is not published | `-32003` | `Object is not published` |
+| Script not in the prim's inventory | `-32602` | `Invalid params: Script not found in prim inventory` |
+| Item is not a script | `-32602` | `Invalid params: Item is not a script` |
+| No modify permission on the script | `-32003` | `No modify permission on script` |
+
+---
+
+### ObjectScriptReset
+
+**JSON-RPC Method:** `object.script.reset` (call from extension to viewer)
+
+Resets a script within a prim, clearing its state and restarting from the default state entry.
+
+```typescript
+interface ObjectScriptResetParams {
+ prim_id: string;
+ item_id: string;
+}
+
+interface ObjectScriptResetResponse {
+ success: boolean;
+}
+```
+
+**`success` means dispatched, not applied.** As with `object.script.set_running`, the viewer sends
+a message to the simulator and returns immediately. `success: true` does not confirm the script
+was reset.
+
+**Permissions.** Requires `PERM_MODIFY` on the script. This method validates independently of the
+shared item validator and accepts scripts only.
+
+**Errors:** identical to `object.script.set_running` above.
+
+---
+
+### ObjectRequest
+
+**JSON-RPC Method:** `object.request` (call from extension to viewer)
+
+Requests the viewer to publish a specific in-world object. The viewer responds synchronously to confirm the request was accepted, then asynchronously sends an `object.publish` notification with the full object tree.
+
+This is typically called immediately after the handshake completes when the extension was launched by the viewer with an `object=<uuid>` URI parameter.
+
+```typescript
+interface ObjectRequestParams {
+ object_id: string; // UUID of the root prim to request exploring
+}
+
+interface ObjectRequestResponse {
+ success: boolean;
+}
+```
+
+**Fields:**
+
+- `object_id`: UUID of the root prim of the linkset to explore.
+- `success`: Whether the viewer accepted the request. A `true` response does not mean `object.publish` has been sent yet — it means the viewer will send it. Failures are returned as JSON-RPC errors — see below.
+
+**Permissions.** Requires modify permission on the object.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `object_id` missing | `-32602` | `Invalid params: No object_id specified` |
+| Object not found | `-32602` | `Invalid params: Object not found` |
+| No modify permission on the object | `-32003` | `Permission denied` |
+| Publish could not be started | `-32603` | `Internal error: Failed to initiate publish` |
+
+**Sequence:**
+1. Extension calls `object.request`
+2. Viewer responds with `{ success: true }` (or error)
+3. Viewer sends `object.publish` notification (asynchronously, when ready)
+
+---
+
+### ObjectList
+
+**JSON-RPC Method:** `object.list` (call from extension to viewer)
+
+Requests the complete list of currently explored objects. Called by the extension immediately after the handshake completes (`session.ok`) to restore state for any objects the viewer already has open for exploration.
+
+The viewer responds synchronously with all explored objects in the same format as `object.publish` notifications. No follow-up notifications are sent.
+
+```typescript
+// No request parameters
+
+interface ObjectListResponse {
+ objects: PublishedObject[]; // All currently explored objects; empty array if none
+}
+```
+
+**Fields:**
+
+- `objects`: Array of `PublishedObject` records (same shape as the `object` field in `object.publish`). Empty array when no objects are currently being explored.
+
+**Sequence:**
+1. Viewer sends `session.ok`
+2. Extension calls `object.list` (no params)
+3. Viewer responds with `{ objects: [...] }` synchronously
+
+---
+
+### ObjectModify
+
+**JSON-RPC Method:** `object.modify` (call from extension to viewer)
+
+Modifies properties of a prim (root or linked) such as name, description, or permissions. Only specified fields are modified; omitted fields remain unchanged. Requires `PERM_MODIFY` on the object.
+
+```typescript
+interface ObjectModifyParams {
+ prim_id: string; // UUID of any prim (root or child)
+ name?: string; // New display name
+ description?: string; // New description
+ permissions?: {
+ next_owner?: number; // Permission mask applied on transfer
+ };
+}
+
+interface ObjectModifyResponse {
+ success: boolean;
+ prim_id: string; // Echoed back from request
+}
+```
+
+**Fields:**
+
+- `prim_id`: UUID of the prim to modify. Child prims are addressable directly by UUID.
+- `name` (optional): New display name for the prim. If omitted, name remains unchanged.
+- `description` (optional): New description for the prim. If omitted, description remains unchanged.
+- `permissions` (optional): Permission changes.
+ - `next_owner`: Permission mask applied when the object is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`).
+- `success`: Whether the property messages were dispatched.
+
+**`success` means dispatched, not applied.** Each supplied property is sent to the simulator as a
+separate message and the viewer returns immediately. `success: true` confirms the messages were
+sent, not that any of them took effect — and because they travel independently, one may be applied
+while another is not. Confirmation arrives later as an `object.update` notification.
+
+**Permissions.** Requires modify permission on the prim.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `prim_id` missing | `-32602` | `Invalid params: prim_id is required` |
+| No property supplied | `-32602` | `Invalid params: At least one property (name, description, or permissions) must be specified` |
+| Prim not found | `-32602` | `Invalid params: Prim not found` |
+| Object is not published | `-32003` | `Object is not published` |
+| No modify permission on the prim | `-32003` | `No modify permission on object` |
+
+**Notes:**
+- At least one property field (`name`, `description`, or `permissions`) must be specified.
+- An `object.update` notification will fire after successful modification.
+- Owner permissions cannot be modified directly — only `next_owner` can be changed.
+
+---
+
+### ObjectItemModify
+
+**JSON-RPC Method:** `object.item.modify` (call from extension to viewer)
+
+Modifies properties of an inventory item such as name, description, or permissions. Only specified fields are modified; omitted fields remain unchanged. Requires `PERM_MODIFY` on the item.
+
+```typescript
+interface ObjectItemModifyParams {
+ prim_id: string; // UUID of any prim (root or child)
+ item_id: string; // Inventory item UUID
+ name?: string; // New display name (no file extension)
+ description?: string; // New description
+ permissions?: {
+ next_owner?: number; // Permission mask applied on transfer
+ };
+}
+
+interface ObjectItemModifyResponse {
+ success: boolean;
+ prim_id: string; // Echoed back from request
+ item_id: string; // Echoed back from request
+}
+```
+
+**Fields:**
+
+- `prim_id`: UUID of the prim that owns the item. Child prims are addressable directly by UUID.
+- `item_id`: Inventory item UUID.
+- `name` (optional): New display name for the item. Should not include file extension (e.g., `.lsl`, `.luau`). If omitted, name remains unchanged.
+- `description` (optional): New description for the item. If omitted, description remains unchanged.
+- `permissions` (optional): Permission changes.
+ - `next_owner`: Permission mask applied when the item is transferred. Uses same bit flags as `ItemPermissions` (e.g., `PERM_MODIFY=0x4000`, `PERM_COPY=0x8000`, `PERM_TRANSFER=0x2000`).
+- `success`: Whether the property messages were dispatched.
+
+**`success` means dispatched, not applied.** The viewer sends the change to the simulator and
+returns immediately; confirmation arrives later as an `object.update` notification.
+
+**Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim.
+See [Common preconditions](#common-preconditions) for the shared checks and errors.
+
+**Errors:** as listed under [Common preconditions](#common-preconditions), plus `-32602` when no
+property field is supplied.
+
+**Notes:**
+- At least one property field (`name`, `description`, or `permissions`) must be specified.
+- An `object.update` notification will fire after successful modification.
+- Owner permissions cannot be modified directly — only `next_owner` can be changed.
+- If the item is renamed, the virtual filesystem path will change and the extension must handle the rename appropriately.
+
+---
+
+## Command Interfaces
+
+These interfaces provide a general-purpose, bidirectional command channel. Either side may invoke a named command on the other side and receive a structured result. The feature is optional and must be negotiated via the `commands` flag in the session handshake.
+
+### CommandExecute
+
+**JSON-RPC Method:** `command.execute` (call, bidirectional)
+
+Invokes a named command on the receiving side. Commands are identified by a namespaced string and carry an optional freeform parameter map.
+
+```typescript
+interface CommandExecuteParams {
+ command: string; // namespaced command id, e.g. "viewer.teleport"
+ params?: Record<string, unknown>; // command-specific arguments
+}
+
+interface CommandExecuteResponse {
+ success: boolean;
+ result?: unknown; // optional command-specific return value
+}
+```
+
+**Fields:**
+
+- `command`: Namespaced command identifier. The prefix before the first `.` identifies the side that owns and executes the command:
+ - `viewer.*` — commands executed by the viewer (e.g. `viewer.teleport`, `viewer.camera.focus`)
+ - `editor.*` — commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`)
+- `params` (optional): Command-specific argument map. Structure varies by command.
+- `success`: `true` when the command executed. Failures are returned as JSON-RPC errors — see below.
+- `result` (optional): Command-specific return value. Only present when the command produces output.
+
+**Errors:**
+
+| Condition | Code | `error.message` |
+| --------- | ---- | --------------- |
+| `command` missing | `-32602` | `Invalid params: command is required` |
+| Command not registered | `-32602` | `Invalid params: Unknown command: <command>` |
+
+The invoked command's own handler may raise further errors — `-32602` for bad arguments,
+`-32003` when the action is not permitted, `-32603` on internal failure. Clients must handle any
+error code, not only the two above.
+
+**Capability gate (directional):**
+
+- A sender MUST NOT call `command.execute` on a peer that did not advertise `commands: true`.
+- A receiver MAY accept `command.execute` whenever it advertised `commands: true`, regardless of
+ whether the sender advertised `commands`.
+- In practice this means:
+ - Extension → Viewer calls are allowed when the viewer advertised `commands: true`.
+ - Viewer → Extension calls are allowed only when the extension advertised `commands: true`.
+
+This treats `commands` as a receiver capability per direction, not as a symmetric "both sides or
+nothing" toggle.
+
+**Example — extension asks viewer to teleport:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "command.execute",
+ "id": 7,
+ "params": {
+ "command": "viewer.teleport",
+ "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" }
+ }
+}
+```
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 7,
+ "result": { "success": true }
+}
+```
+
+**Example — extension asks viewer to save object back to contents:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "command.execute",
+ "id": 9,
+ "params": {
+ "command": "viewer.object.save_back_to_contents",
+ "params": { "object_id": "550e8400-e29b-41d4-a716-446655440000" }
+ }
+}
+```
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 9,
+ "result": { "success": true, "result": { "object_id": "550e8400-e29b-41d4-a716-446655440000" } }
+}
+```
+
+**Example — viewer asks extension to show a message:**
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "command.execute",
+ "id": 8,
+ "params": {
+ "command": "editor.show_message",
+ "params": { "message": "Script reset complete", "level": "info" }
+ }
+}
+```
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 8,
+ "result": { "success": true }
+}
+```
+
+The extension implements `editor.show_message`. It accepts `level` values of `"info"`,
+`"warn"`, or `"error"`; when omitted, `level` defaults to `"info"`.
+
+---
+
+### CommandList
+
+**JSON-RPC Method:** `command.list` (call, bidirectional)
+
+Requests the list of commands the receiving side supports. Intended for tooling and autocomplete; implementations may omit this method and return an error response if discovery is not needed.
+
+This method takes no parameters.
+
+**Response:**
+
+```typescript
+interface CommandListResponse {
+ commands: CommandInfo[];
+}
+
+interface CommandInfo {
+ command: string;
+ description?: string;
+ params?: Record<string, CommandParamInfo>;
+}
+
+interface CommandParamInfo {
+ type: "string" | "number" | "boolean" | "object" | "array";
+ required?: boolean;
+ description?: string;
+}
+```
+
+**Response Fields:**
+
+- `commands`: Array of commands the responder supports. Each entry describes one command.
+ - `command`: The namespaced command identifier.
+ - `description` (optional): Human-readable description of what the command does.
+ - `params` (optional): Map of parameter names to their type descriptors. The viewer populates
+ this field for its registered commands.
+
+**Known viewer commands:**
+
+| Command | Required params | Description |
+|---------|----------------|-------------|
+| `viewer.teleport` | `object_id: string` | Teleport agent to an in-world object. |
+| `viewer.camera.focus` | `object_id: string` | Zoom camera to an in-world object (same behavior as context menu Zoom In). |
+| `viewer.object.save_back_to_contents` | `object_id: string` | Save an in-world object back to source object contents. |
+| `viewer.script.reset_all` | `object_id: string` | Open the viewer's reset queue and reset all scripts in an in-world object. |
+| `viewer.script.recompile_all` | `object_id: string`, `target: "luau" \| "lsl2" \| "mono" \| "auto"` | Open the viewer's compile queue and recompile scripts in an in-world object using the selected target. `luau` automatically selects Luau for native Luau scripts and LSL-Luau for LSL scripts. `auto` uses each script's previously registered VM. |
+
+**Known extension commands:**
+
+| Command | Required params | Description |
+|---------|----------------|-------------|
+| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" \| "warn" \| "error"`. |