Build a Free Agent

free as in freedom, cookies, and puppies

Let's wake up

Your brain needs this đź§ 

you rebuilt a worse version of OpenClaw...

Cool bro...

All for free*

*or... at least without paying more than my existing subscriptions

Three tools

  1. search - progressive disclosure
  2. execute - write and run code
  3. open_generated_ui - MCP Apps!

Three tools

  1. search - progressive disclosure
  2. execute - write and run code
  3. open_generated_ui - MCP Apps!

Search

{
  "query": "spotify weather current location playlist playback package connector",
  "limit": 10,
  "memoryContext": {
    "task": "Play weather-appropriate music on Spotify before AI Engineer Miami talk",
    "entities": ["Spotify", "weather", "current location", "playlist", "playback"],
    "constraints": ["prefer existing packages and connectors"]
  }
}
# Search results

For full detail on one hit, call `search` with `entity: "{id}:{type}"` (example: `kody_official_guide:capability`).

**How to run matches:**

- Built-in capabilities — `execute` with `import { codemode } from "kody:runtime"`
- Persisted values — `codemode.value_get({ name, scope })` or `codemode.value_list({ scope })`
- Saved connectors — `codemode.connector_get({ name })` or `codemode.connector_list({})`
- Saved packages — import from `kody:@package-id/export-name`, edit with `repo_*`, and open package apps with `open_generated_ui({ package_id })` when the package declares `kody.app`
- Secrets — placeholders in execute-time fetches or `codemode.secret_list` (never paste raw secrets in chat or embed `{{secret:...}}` literally into visible content such as comments, prompts, or issue bodies)

## Relevant memories

- **Kent's home timezone** (profile): Kent lives in Utah Valley, Utah, USA. His local timezone is America/Denver.
- **Kody execute sandbox: OAuth connector utilities** (workflow): `createAuthenticatedFetch(providerName)` can be used for OAuth-backed integrations like Spotify.
- **Kody integration-backed app workflow** (workflow): Confirm the connector exists, run one authenticated smoke test, then build or use the dependent package workflow.

## Package — @kentcdodds/spotify (`spotify`)

Package-first Spotify playback controls, queue helpers, device management, and a hosted remote app for Kent's Kody workflows.

**Why this matched:** The user asked to play music on Spotify. This package appears to provide the primary playback and device-control capabilities needed to fulfill that request.

**Tags:** `spotify`, `music`, `playback`, `queue`, `remote`
**Has app:** yes
**Hosted URL:** `https://heykody.dev/packages/spotify`

## Package — @kentcdodds/environment-lookups (`environment-lookups`)

Package-first real-world environment lookup helpers for weather, sun, air quality, and reusable location resolution.

**Why this matched:** The user asked for music appropriate to the weather "here." This package appears relevant for determining local weather conditions and resolving location context.

**Tags:** `weather`, `sun`, `air-quality`, `environment`, `lookup`
**Has app:** no

## Connector — `spotify`

OAuth connector config for spotify

**Why this matched:** Spotify playback likely requires authenticated access on the user's behalf.

**Flow:** `pkce`
**API base URL:** none

## Value — `spotify-client-id` (`user` scope)

spotify OAuth client ID

**Why this matched:** This suggests the Spotify integration is configured and may be usable by the Spotify package or connector.

**Entity:** `user:spotify-client-id:value`

## Capability — `kody_official_guide`

Load an official Kody guide from the kody GitHub repository (markdown). **For third-party integrations that will power a package, package app, or workflow, use `guide: "integration_bootstrap"` first.**

**Why this matched:** If the agent needs to verify integration readiness before execution, this capability provides the correct bootstrap path.

Available guides (order matters—start with `integration_bootstrap` for integration-dependent work):
- `integration_bootstrap`
- `oauth`
- `integration_backed_app`

> **Note:** This page does not include any matching user secret references. If you need credential metadata, use `codemode.secret_list` inside `execute` or save secrets via generated UI.

Execute

{
  "conversationId": "atpw5n5a47mk",
  "memoryContext": {
    "task": "Impress people Kent's presenting to at AIEMiami",
    "entities": [
      "Spotify",
      "playlist",
      "playback"
    ]
  },
  "code": "/* ... */"
}
import { createAuthenticatedFetch } from 'kody:runtime'
import weather from 'kody:@environment-lookups/weather'
import playbackController from 'kody:@spotify/playback-controller'
import transferPlayback from 'kody:@spotify/transfer-playback'

function choosePlaylistQuery(weatherResult) {
  const condition = String(weatherResult?.daily?.condition ?? '').toLowerCase()
  const feelsLikeHigh = Number(
    weatherResult?.daily?.feelsLikeHigh ?? weatherResult?.daily?.high ?? 80
  )

  if (/(storm|thunder)/.test(condition)) return 'cinematic ambient storm playlist'
  if (/(rain|drizzle|showers)/.test(condition)) return 'rainy day focus playlist'
  if (/(overcast|cloud|fog)/.test(condition) && feelsLikeHigh >= 85) {
    return 'tropical chill playlist'
  }
  if (/(overcast|cloud|fog)/.test(condition)) return 'cloudy day chill playlist'
  if (/(clear|sunny)/.test(condition) && feelsLikeHigh >= 85) {
    return 'sunny tropical house playlist'
  }
  if (/(clear|sunny)/.test(condition)) return 'sunny upbeat playlist'
  return 'upbeat chill playlist'
}

function chooseBestPlaylist(items) {
  const scored = items.map(item => {
    const owner = String(item?.owner?.display_name ?? item?.owner?.id ?? '').toLowerCase()
    let score = 0

    if (/spotify/.test(owner)) score += 4
    if (/filtr/.test(owner)) score += 3
    if (/tropical|summer|vacation|ibiza|chill/.test(String(item?.name ?? '').toLowerCase())) {
      score += 2
    }

    return { item, score }
  })

  scored.sort((a, b) => b.score - a.score)
  return scored[0]?.item ?? null
}

async function readJsonOrText(response) {
  const text = await response.text()

  try {
    return JSON.parse(text)
  } catch {
    return text
  }
}

async function assertOk(response, label) {
  if (response.ok) return response

  const body = await readJsonOrText(response)
  throw new Error(
    `${label} failed (${response.status} ${response.statusText}): ${
      typeof body === 'string' ? body : JSON.stringify(body)
    }`
  )
}

export default async function run() {
  const fetchSpotify = await createAuthenticatedFetch('spotify')

  const weatherResult = await weather({
    location: 'Miami, FL',
    units: 'imperial',
  })

  const query = choosePlaylistQuery(weatherResult)

  const searchResponse = await assertOk(
    await fetchSpotify(
      `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=playlist&limit=10`
    ),
    'Spotify playlist search'
  )

  const searchBody = await searchResponse.json()
  const items = Array.isArray(searchBody?.playlists?.items)
    ? searchBody.playlists.items.filter(Boolean)
    : []
  const playlist = chooseBestPlaylist(items)

  if (!playlist?.uri) {
    throw new Error(`No Spotify playlist found for query: ${query}`)
  }

  const state = await playbackController({ action: 'status' })
  const device = Array.isArray(state?.devices)
    ? state.devices.find(d => d?.isActive) ?? state.devices[0]
    : null

  if (!device?.id) {
    throw new Error('No available Spotify playback device found.')
  }

  await transferPlayback({ deviceId: device.id, play: false })
  await new Promise(resolve => setTimeout(resolve, 1200))

  await assertOk(
    await fetchSpotify(
      `https://api.spotify.com/v1/me/player/play?device_id=${encodeURIComponent(device.id)}`,
      {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ context_uri: playlist.uri }),
      }
    ),
    'Spotify start playback'
  )

  const verify = await assertOk(
    await fetchSpotify('https://api.spotify.com/v1/me/player'),
    'Spotify verify playback'
  )

  return {
    weather: weatherResult.summary,
    playlistQuery: query,
    selectedPlaylist: {
      name: playlist.name,
      uri: playlist.uri,
      owner: playlist.owner?.display_name ?? playlist.owner?.id ?? null,
      url: playlist.external_urls?.spotify ?? null,
    },
    playbackDevice: {
      id: device.id,
      name: device.name,
      type: device.type,
    },
    verify: await verify.json(),
  }
}
{
  "conversationId": "atpw5n5a47mk",
  "relevantMemories": [
    {
      "title": "Kent's speaking",
      "kind": "profile",
      "summary": "Kent's speaking at AIE Miami on April 20th, 2026."
    },
  ],
  "result": {
    "ok": true,
    "weather": {
      "location": "Miami, Miami-Dade County, Florida, United States",
      "condition": "Overcast",
      "high": 83.9,
      "feelsLikeHigh": 93.5
    },
    "playlistQuery": "tropical chill playlist",
    "selectedPlaylist": {
      "name": "Tropical House Hits 2026",
      "uri": "spotify:playlist:2SRbIs0eBQwHeTP7kErjwo"
    },
    "playbackDevice": {
      "name": "Kent’s Jaguar",
      "type": "Computer"
    }
  }
}

Resources

Thank you!

𝕏 @kentcdodds

Build a Free Agent

By Kent C. Dodds

Build a Free Agent

Most AI assistants are still just chats with tool access. This talk shows a different approach: using MCP as a personal runtime. I'll walk through how Kody uses search, execute, and open_generated_ui to discover capabilities, run sandboxed workflows, manage and use memory, keep secrets out of prompts, and turn generated interfaces into reusable software. The goal isn't a better model. It's making AI assistants portable, secure, and actually useful across MCP hosts. In this talk, you'll learn that with the right primitives, you can create a highly capable assistant without paying an extra cent for inference. Free as in freedom Free as in cookies Free as in puppies

  • 8