> ## Documentation Index
> Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> @altnautica/plugin-sdk reference. The GCS browser-half SDK every Mission Control plugin uses.

`@altnautica/plugin-sdk` (v1.0.0, GPL-3.0-or-later) is the **GCS
browser-half SDK**: the package a plugin's Mission Control side imports.
It is one of three SDKs an ADOS plugin can use. The other two are the
agent half, which runs on the companion: the [Python SDK](/developers/sdk-python)
(`ados.sdk`) and the [Rust SDK](/developers/sdk-rust) (the `ados-sdk`
crate). A plugin's GCS half always uses this one; its agent half picks
Python or Rust.

This SDK hides the postMessage envelope shape and gives you a typed
`PluginContext` instead.

## Install

The standalone npm publish lands with the hosted registry. Until
then the supported workflow is to develop your plugin inside the
[`altnautica/ADOSExtensions`](https://github.com/altnautica/ADOSExtensions)
monorepo as a pnpm workspace member; the SDK resolves via
`workspace:^` against the in-tree
[`packages/plugin-sdk`](https://github.com/altnautica/ADOSExtensions/tree/main/packages/plugin-sdk).
The [Quickstart](/developers/quickstart) walks through the clone and
scaffold flow.

The package is a peer of the host iframe runtime. It has no runtime
dependencies and ships ESM only.

## `definePlugin`

The single entry point:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { definePlugin } from "@altnautica/plugin-sdk";

definePlugin({
  id: "com.example.my-plugin",
  version: "1.0.0",
  locale: { "hello.title": "Hello" },
  async mount(ctx, info) {
    // wire up subscriptions, render the panel, etc.
  },
  async unmount(ctx, info) {
    // optional, runs when the host disposes the iframe
  },
});
```

`mount` is awaited; if it throws, the host receives a `handler_error`
envelope and surfaces it in the install dialog.

`info` is optional. Most plugins write `async mount(ctx)` and ignore
it; the second argument carries the host build version, the
plugin's resolved permission set, and the operator-edited config.
The same pattern applies to `unmount`.

## `PluginContext`

The argument the SDK hands to `mount`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
interface PluginContext {
  client: PluginClient;
  telemetry: {
    subscribe<T>(topic: string, handler: (args: T) => void): Promise<() => void>;
  };
  command: {
    send(command: string, args?: unknown): Promise<unknown>;
  };
  notifications: {
    publish(payload: NotificationPayload): Promise<unknown>;
  };
  recording: {
    mark(payload: RecordingMark): Promise<unknown>;
  };
  mission: {
    read(missionId: string): Promise<unknown>;
    write(update: MissionUpdate): Promise<unknown>;
  };
  config: {
    onChange<T>(handler: (next: T) => void): () => void;
  };
  events: {
    subscribe<T>(topic: string, handler: (args: T) => void): () => void;
  };
  theme: {
    onChange(handler: (vars: Record<string, string>) => void): () => void;
  };
  i18n: {
    t(key: string, params?: Record<string, string | number>): string;
  };
}
```

Each domain group is small on purpose. Plugins drop down to
`ctx.client.request(method, capability, args)` for any RPC the
helpers do not expose.

`definePlugin` builds this context for you. If you need one outside the
lifecycle (in a test, or to wire it by hand), construct it with
`createPluginContext`:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createPluginContext } from "@altnautica/plugin-sdk";

const ctx = createPluginContext({ locale: { "hello.title": "Hello" } });
await ctx.telemetry.subscribe("battery", (s) => store.ingest(s));
```

The `events` group subscribes to one-way host pushes that have no
request and response round-trip: video-overlay host props, an agent
plugin's state read-back, and any other topic the host forwards to the
iframe. `subscribe` returns an unsubscribe function.

## `PluginClient`

The lower-level RPC client:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { PluginClient } from "@altnautica/plugin-sdk";

const client = new PluginClient();
const result = await client.request<{ ok: boolean }>(
  "telemetry.subscribe",
  "telemetry.subscribe.battery",
  { topic: "battery" },
  { timeoutMs: 3000 },
);
```

Methods:

* `request(method, capability, args, options?)`: send a request and
  await the response. Default timeout 5s.
* `on(method, handler)`: subscribe to host-pushed events.
* `subscribeTelemetry(topic, handler)`: convenience that does both
  `request("telemetry.subscribe", ...)` and `on("telemetry.<topic>", ...)`.
* `dispose()`: tear down listeners and reject in-flight RPCs.

## `HostError`

Thrown by `client.request` when the host returns an error envelope:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { HostError } from "@altnautica/plugin-sdk";

try {
  await ctx.command.send("ARM");
} catch (err) {
  if (err instanceof HostError && err.code === "permission_denied") {
    // show a banner
  } else {
    throw err;
  }
}
```

The `code` field is the machine-readable error code from the
envelope. The `message` is human-readable; do not branch on it.

## Test harness

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createPluginHarness } from "@altnautica/plugin-sdk/harness";

const harness = createPluginHarness({
  grantedCapabilities: ["telemetry.subscribe.battery"],
  mount: async (ctx) => {
    await ctx.telemetry.subscribe("battery", (s) => store.ingest(s));
  },
});

await harness.start();
harness.pushTelemetry("battery", mockSample);
expect(harness.notifications).toEqual([...]);
await harness.teardown();
```

The harness:

* mounts the plugin against an in-memory transport (no real iframe);
* captures every RPC in `harness.calls`, grouped helpers in
  `harness.notifications` and `harness.recordingMarks`;
* accepts `pushTelemetry`, `pushEvent`, `pushConfig`, `pushTheme`;
* supports `failNext(method, code, message)` to simulate host
  failures;
* mirrors the real bridge's capability gate.

This means you can validate a plugin end-to-end inside Vitest without
running Mission Control or a real drone.

## Building the bundle

The SDK does not ship a build step. Plugin repos use esbuild,
Vite, or any other bundler that emits ESM. The `create-ados-plugin`
template uses esbuild for zero-config builds:

```sh theme={"theme":{"light":"github-light","dark":"github-dark"}}
esbuild src/plugin.ts --bundle --format=esm --target=es2022 --outfile=plugin.bundle.js --minify
```

Output goes to `gcs/plugin.bundle.js`. The manifest's `gcs.entrypoint`
field points at the same path. The bundle is not hashed in the
manifest; its integrity is bound by the canonical payload hash that
the signing step writes into the separate `SIGNATURE` file.

<Note>
  This SDK is the GCS browser half only. For the companion-side plugin
  half, see the [Python SDK](/developers/sdk-python) (`ados.sdk`) or the
  [Rust SDK](/developers/sdk-rust) (the `ados-sdk` crate). A hybrid
  plugin ships both halves from a single `manifest.yaml`.
</Note>
