# Write a custom Handler

> Install the packaged SDK, write a trusted local TypeScript Handler, and configure Guv.

> **CAUTION:** Install only Handler code that you trust. It runs on your machine with all your credentials.

## Install the SDK

Custom TypeScript Handlers use Bun. Each Guv release includes a matching SDK. The npm registry does not include this SDK.

For Homebrew or Linuxbrew, use these commands:

```sh
mkdir -p ~/.config/guv/handlers/my-handler
cd ~/.config/guv/handlers/my-handler
bun add "@familiar/guv-handler-sdk@file:$(brew --prefix guv)/share/guv/sdk"
```

For a direct archive, use these commands:

```sh
mkdir -p ~/.config/guv/handlers/my-handler
cd ~/.config/guv/handlers/my-handler
bun add "@familiar/guv-handler-sdk@file:$HOME/.local/share/guv/sdk"
```

Keep the SDK and Guv binary on the same release.

## Write the Handler

Save this code as `handler.ts`:

```ts
import { serveHandler } from "@familiar/guv-handler-sdk/process";

await serveHandler((input) => ({
  kind: "outcome_produced",
  outcome: {
    kind: "text",
    summary: input.input.text,
    artifacts: [],
    effects: [],
  },
}));
```

## Handler lifecycle

The daemon starts the selected command and supervises the resident Handler. `serveHandler` accepts sequential Run connections through an owner-private Unix socket.

Each accepted Run has three bounded frames:

```text
Guv → HandlerInput
Handler → HandlerResult
Guv → receipt acknowledgement
```

The process protocol does not use stdin or stdout. Background Guv ignores Handler stdio.

Invalid framing, UTF-8, schema, size, or acknowledgement causes the current Run to fail. Guv restarts the Handler without replay.

A crash or timeout has the same result. A queued Job waits until the Handler is ready.

## Input

`HandlerInput.schema` is `com.familiar.handler.input.v1`. Guv accepts 256 KiB of input JSON. Job IDs contain 1–128 URL-safe characters.

A Job ID cannot be `.` or `..`.

Device context is optional. If input contains device context, it contains these two fields:

```json
{
  "text": "Set tea timer",
  "created_at": "2026-07-25T12:34:56-07:00",
  "time_zone": "America/Los_Angeles"
}
```

Supply RFC3339 `created_at` with `Z` or a numeric offset. Supply an IANA `time_zone`. Omit the two fields together.

## Results

Guv accepts 128 KiB of result JSON. An R1 Handler returns one Outcome or proposes one App-applied Effect.

Do not change external data before the Handler returns. Guv cannot first record a durable request for a non-App mutation.

Use `summary` for compact standalone text. Use nonblank `detail` for different expanded text. Guv preserves the two fields without model compression.

Use the SDK result builders and capability schemas:

```ts
import {
  appEffectResult,
  noteResult,
  textResult,
} from "@familiar/guv-handler-sdk";

textResult("Answered in one line.");
noteResult({ title: "Alice tea", body: input.input.text, tags: ["people"] });
appEffectResult({
  capability: "timer",
  summary: "Tea",
  params: { duration_seconds: 30 },
});
```

The App supports `reminder`, `timer`, `alarm`, and `calendar_event`. The SDK validates strict parameters before it returns the result.

Effect IDs contain 1–128 characters from `[A-Za-z0-9._~-]`. An Effect ID cannot be `.` or `..`. Generated `eff_<uuid>` IDs are valid.

## Configure Guv

Use this command from the Handler working directory:

```sh
guv handler set-command --cwd "$PWD" --timeout-ms 120000 -- bun handler.ts
```

For Homebrew or Linuxbrew, restart the service and examine status:

```sh
brew services restart guv
guv status
```

For a direct archive, restart foreground `guv run`. Then use `guv status` from another terminal.

The command has this syntax:

```text
guv handler set-command [--cwd <path>] [--timeout-ms <1..86400000>] -- <executable> [args...]
guv handler default
```

`--` separates Guv options from executable arguments. Guv resolves the executable to an absolute path. It does not invoke a shell.

The configuration contains a command, an optional working directory, and an optional timeout. Restart Guv after each configuration change.

The default timeout is 30 minutes. It limits one Invocation result, not the resident lifetime.

The complete API reference is at `share/guv/sdk/README.md` in the local Guv installation.
