<!--
hoody-app Subskill (sdk)
Auto-generated by Hoody Skills Generator
Generated: 2026-06-20T00:06:19.974Z
Model: mimo-v2.5-pro + fixer:z-ai/glm-5.1
Mode: sdk


Tokens: 8737

DO NOT EDIT MANUALLY - Changes will be overwritten on next generation
-->

# hoody-app Subskill

## Overview

**Service**: hoody-app  
**Purpose**: Application search, resolution, and execution engine for the Hoody platform.  
**Base URL Pattern**: `https://{projectId}-{containerId}-app-{serviceId}.{node}.containers.hoody.com`

### What It Does

hoody-app is the central orchestration service for finding, configuring, and running applications within Hoody. It indexes package sources, ranks candidates by relevance, resolves selectors into concrete execution plans, and can optionally dispatch runs to hoody-terminal instances.

### When to Use

- **Search** for available applications across configured package sources.
- **Run** an application by resolving a selector into a shell command or terminal dispatch.
- **Manage** package sources (add, sync, diagnose).
- **Manage** user profiles (preference sets that override source behavior).
- **Manage** recipes (saved, reusable selector templates).
- **Monitor** async jobs (background search or sync operations).
- **Retrieve** runtime configuration or API specification.

### How It Fits Into Hoody Philosophy

hoody-app embodies the Hoody principle of **declarative execution**: you describe *what* you want to run (via a selector), and the service resolves *where* and *how* to run it. Sources act as the registry layer, profiles provide per-user customization, and recipes encode repeatable patterns. All interaction is API-first with full OpenAPI introspection.

### Quick Setup

```
import { HoodyClient } from '@hoody-ai/hoody-sdk'

// Token-based authentication via Hoody API
const client = new HoodyClient({
  baseURL: 'https://api.hoody.com',
  token: 'YOUR_TOKEN'
})
```

> **Note**: All `client.app.*` calls route through the Hoody Kit proxy to the hoody-app service. You do not set the hoody-app base URL directly in SDK initialization.

---

## Common Workflows

### Workflow 1: Health Check

Verify the service is operational before performing any work.

```
const health = await client.app.health.check()
console.log(health.status)
```

### Workflow 2: Search for Applications

Find candidates matching a query string across all enabled sources.

```
const results = await client.app.execution.searchCandidates({ app: 'node' })
console.log(results.candidates)
// candidates is an array ranked by relevance — pick by index for stable selection
```

With optional filters:

```
const results = await client.app.execution.searchCandidates('node', {
  os: 'linux',
  kind: 'cli',
  arch: 'x64',
  limit: 5
})
```

### Workflow 3: Paginated Search

For large result sets, use cursor-based pagination.

```
const page1 = await client.app.execution.searchCandidatesPaged({
  selector: { app: 'python' }
})
console.log(page1.candidates)
console.log(page1.cursor) // pass cursor to next request
```

Collect all pages automatically:

```
const allPages = await client.app.execution.searchCandidatesPagedAll({
  selector: { app: 'python' }
})
```

### Workflow 4: Preflight a Run

Resolve and validate a selector without executing. Useful for confirming resolution before committing to a terminal dispatch.

```
const plan = await client.app.execution.preflight({
  app: 'node',
  os: 'linux',
  channel: 'stable'
})
console.log(plan.resolved)
console.log(plan.command)
```

### Workflow 5: Run an Application (GET)

Resolve a selector via query parameters and receive the exact shell command.

```
const run = await client.app.execution.runAppGet('node', {
  os: 'linux',
  channel: 'lts',
  dry_run: true
})
console.log(run.command)
```

### Workflow 6: Run an Application (POST)

For programmatic clients with complex selectors, use the JSON body variant.

```
const run = await client.app.execution.runAppPost({
  app: 'ffmpeg',
  os: 'linux',
  version: '6.0',
  channel: 'stable',
  terminal_id: 1
})
console.log(run.command)
```

### Workflow 7: Path-Based Resolution

Use clean, bookmarkable URLs for quick resolution.

```
// Path: app only
const result = await client.app.execution.runPathBased({ rest: 'node' })
```

```
// Path: os/app
const result = await client.app.execution.runPathBased({ rest: 'linux/node' })
```

Terminal-anchored variant:

```
const result = await client.app.execution.runTerminalAnchored(1, 'node')
```

### Workflow 8: Async Search Job

For long-running searches, queue a background job and poll for completion.

```
const job = await client.app.jobs.createSearch({
  app: 'tensorflow'
})
console.log(job.job_id)

// Poll until done
const status = await client.app.jobs.getStatus(job.job_id, {
  wait: 'done',
  timeout_ms: 30000
})
console.log(status.result)
```

### Workflow 9: Batch Requests

Execute multiple search or run operations in a single request.

```
const batch = await client.app.execution.runBatch({
  items: [
    {
      request_id: 'req-1',
      mode: 'search',
      selector: { app: 'node' }
    },
    {
      request_id: 'req-2',
      mode: 'run',
      selector: { app: 'python' }
    }
  ]
})
console.log(batch.responses)
```

---

## Advanced Operations

### Managing Package Sources

#### List All Sources

```
const sources = await client.app.sources.list()
console.log(sources)
```

#### Add a New Source

```
const source = await client.app.sources.create({
  source_id: 'my-custom-source',
  enabled: true,
  priority: 10,
  provider: 'github',
  source_type: 'github_releases',
  pin: { url: 'https://github.com/org/repo' }
})
```

#### Update a Source

```
const updated = await client.app.sources.update('my-custom-source', {
  enabled: false,
  priority: 5
})
```

#### Delete a Source

```
await client.app.sources.delete({ source_id: 'my-custom-source' })
```

#### Sync a Single Source

```
const syncJob = await client.app.sources.sync({ source_id: 'my-custom-source' })
console.log(syncJob.job_id)
// Poll: await client.app.jobs.getStatus(syncJob.job_id, { wait: 'done', timeout_ms: 60000 })
```

#### Sync All Sources

```
const syncJob = await client.app.sources.syncAll()
console.log(syncJob.job_id)
```

#### Get Source Diagnostics

```
const diag = await client.app.sources.getDiagnostics({ source_id: 'my-custom-source' })
console.log(diag.recent_failures)
```

### Managing Profiles

#### List Profiles

```
const profiles = await client.app.profiles.list()
```

#### Create a Profile

```
const profile = await client.app.profiles.create({
  name: 'dev-profile',
  sources: [
    { source_id: 'npm-registry' },
    { source_id: 'github-oss' }
  ]
})
```

#### Update a Profile

```
const updated = await client.app.profiles.update('dev-profile', {
  description: 'Development environment preferences'
})
```

#### Select the Active Profile

```
const selected = await client.app.profiles.select({ profile: 'dev-profile' })
console.log(selected.active_profile)
```

#### Delete a Profile

```
await client.app.profiles.delete({ profile: 'dev-profile' })
```

### Managing Recipes

#### List Recipes

```
const recipes = await client.app.recipes.list()
```

#### Create a Recipe

```
const recipe = await client.app.recipes.create({
  name: 'video-encoder',
  selector: { app: 'ffmpeg', os: 'linux', channel: 'stable' }
})
```

#### Get a Recipe

```
const recipe = await client.app.recipes.get({ name: 'video-encoder' })
```

#### Update a Recipe

```
const updated = await client.app.recipes.update('video-encoder', {
  selector: { app: 'ffmpeg', os: 'linux', channel: 'stable', version: '6.0' }
})
```

#### Search Using a Recipe

```
const results = await client.app.recipes.search('video-encoder', {})
console.log(results.candidates)
```

#### Run Using a Recipe

```
const run = await client.app.recipes.run('video-encoder', {})
console.log(run.command)
```

#### Delete a Recipe

```
await client.app.recipes.delete({ name: 'video-encoder' })
```

### Configuration

#### Get Full Runtime Configuration

```
const config = await client.app.configuration.get()
console.log(config.sources)
console.log(config.profiles)
console.log(config.selected_profile)
```

### API Documentation

#### Retrieve OpenAPI Specification

```
const yamlSpec = await client.app.docs.getYaml()
const jsonSpec = await client.app.docs.getJson()
```

### Error Recovery Patterns

**Job timeout recovery** — If a job poll times out, retry with increased timeout:

```
async function pollJob(jobId: string, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const status = await client.app.jobs.getStatus(jobId, {
        wait: 'done',
        timeout_ms: 30000 * (i + 1)
      })
      return status
    } catch (err) {
      if (i === maxRetries - 1) throw err
    }
  }
}
```

**Source sync failure** — Check diagnostics before retrying:

```
const diag = await client.app.sources.getDiagnostics({ source_id: 'problematic-source' })
if (diag.recent_failures?.length > 0) {
  console.error('Recent failures:', diag.recent_failures)
  // Fix configuration, then re-sync
  await client.app.sources.sync({ source_id: 'problematic-source' })
}
```

### Performance Considerations

- **Use batch requests** when making multiple independent lookups.
- **Use async jobs** for searches across many sources to avoid request timeouts.
- **Set `limit`** on search queries to bound response size.
- **Use profiles** to pre-filter sources rather than passing `source` arrays on every call.
- **Use paged search** with `searchCandidatesPagedAll` for automated data collection instead of manual cursor management.

---

## Quick Reference

### Most Common Operations

| Operation | SDK Method | HTTP |
|-----------|-----------|------|
| Health check | `client.app.health.check()` | GET `/api/v1/run/health` |
| Search apps | `client.app.execution.searchCandidates(app)` | GET `/api/v1/run/search` |
| Run app (GET) | `client.app.execution.runAppGet(app)` | GET `/api/v1/run/run` |
| Run app (POST) | `client.app.execution.runAppPost({app})` | POST `/api/v1/run/run` |
| Preflight | `client.app.execution.preflight({app})` | POST `/api/v1/run/preflight` |
| Path-based run | `client.app.execution.runPathBased(rest)` | GET `/api/v1/run/go/{rest}` |
| List sources | `client.app.sources.list()` | GET `/api/v1/run/sources` |
| Sync all sources | `client.app.sources.syncAll()` | POST `/api/v1/run/sources/sync` |
| Get config | `client.app.configuration.get()` | GET `/api/v1/run/config` |
| List profiles | `client.app.profiles.list()` | GET `/api/v1/run/profiles` |
| List recipes | `client.app.recipes.list()` | GET `/api/v1/run/recipes` |
| Job status | `client.app.jobs.getStatus(jobId)` | GET `/api/v1/run/jobs/{job_id}` |

### Essential Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `app` | string | Primary name query (required for search/run) |
| `os` | string | Target operating system |
| `kind` | string | Application kind (cli, gui, etc.) |
| `arch` | string | Target architecture |
| `channel` | string | Release channel (stable, beta, nightly) |
| `version` | string | Version constraint |
| `terminal_id` | integer | Target terminal for dispatch |
| `profile` | string | Active profile name |
| `limit` | integer | Max candidates to return |

### Typical Response Shape

**Search response** returns a ranked `candidates` array. Each candidate includes a stable `candidate_id` for pick-by-index operations.

**Run response** returns a `command` string (the exact shell command) and optional metadata about the resolved candidate.

**Job response** returns a `job_id` and a `status` field (`pending`, `running`, `done`, `failed`). Use `wait=done` on GET to long-poll until completion.