Skip to content

Latest commit

 

History

History
418 lines (331 loc) · 16.5 KB

getting-started.md

File metadata and controls

418 lines (331 loc) · 16.5 KB

Getting Started with Kiro Crew Apps

Build, install, and run your first Kiro Crew app in 5 minutes.

Prerequisites

  • Kiro Crew installed and running (kirocrew gateway)
  • Node.js 22.12+ (24 LTS recommended) (for apps with UI)

1. Create an App Directory

Use the CLI scaffold so the manifest, opaque 512×512 placeholder icon, agent, skill, and UI build files agree with the current schema:

kirocrew app init my-dashboard --ui
my-dashboard/
├── app.json                 ← App manifest (required)
├── assets/icon.png          ← Replace the generated store icon before publishing
├── agents/
│   └── sample-agent.json    ← Agent definition
├── skills/
│   └── sample-skill/
│       └── SKILL.md         ← Skill knowledge file
├── ui/                      ← Frontend (if app has UI)
│   ├── package.json
│   ├── vite.config.ts
│   ├── src/App.tsx
│   └── .gitignore
└── README.md

2. Edit Your App

app.json — The Manifest

Every app needs an app.json. See Manifest Reference for all fields.

The scaffold writes no permissions block. Add the one below: the sample page calls api.get('/api/status') and listens for notification events, and the SDK rejects a path or event the manifest does not declare.

{
  "name": "my-dashboard",
  "version": "0.1.0",
  "displayName": "My Dashboard",
  "description": "A Kiro Crew app: My Dashboard",
  "author": "yourname",
  "iconPath": "assets/icon.png",
  "agents": ["agents/sample-agent.json"],
  "skills": ["skills/sample-skill"],
  "ui": {
    "entry": "dist/index.mjs",
    "pages": [
      {
        "route": "/apps/my-dashboard",
        "label": "My Dashboard",
        "icon": "Package"
      }
    ]
  },
  "permissions": {
    "api": ["/api/status"],
    "events": ["notification"]
  }
}

UI Page — React Component

Edit ui/src/App.tsx. Your app is a standard React component that uses @kirocrew/app-sdk hooks and @kirocrew/app-sdk/ui shared components.

You do not npm install @kirocrew/app-sdk. The dashboard host provides it (and React, ReactDOM, lucide-react) at runtime through its import map: the bare @kirocrew/app-sdk specifier resolves to the host's vendored copy via window.__kirocrew_modules. This guarantees your app shares the host's exact React instance (so hooks work) and stays a small bundle. Mark these as externals in your build (don't bundle them).

import { useAppApi, useAppEvents } from '@kirocrew/app-sdk'
import { Card, CardTitle, PageHeader, StatCard } from '@kirocrew/app-sdk/ui'
import { useState, useEffect } from 'react'

export default function MyDashboard() {
  const api = useAppApi()
  const [data, setData] = useState(null)

  useEffect(() => {
    api.get('/api/status').then(setData)
  }, [])

  // Listen to real-time events
  useAppEvents('notification', (event) => {
    console.log('New notification:', event)
  })

  return (
    <>
      <PageHeader title="My Dashboard" subtitle="Custom app page" />
      <div className="px-6 pb-8 overflow-y-auto flex-1 min-h-0">
        <div className="grid gap-3.5 grid-cols-[repeat(auto-fit,minmax(150px,1fr))] mb-6">
          <StatCard label="Status" value={data ? 'Online' : '...'} accent />
        </div>
        <Card>
          <CardTitle>Content</CardTitle>
          <p className="text-sm text-muted">Your app content here.</p>
        </Card>
      </div>
    </>
  )
}

Agent — AI Configuration

Edit agents/sample-agent.json to customize your agent:

{
  "name": "my-agent",
  "model": "auto",
  "description": "Analyzes data and generates reports",
  "prompt": "You are a data analyst assistant.",
  "tools": ["@kirocrew-core"]
}

Skill — Domain Knowledge

Edit skills/sample-skill/SKILL.md to teach your agent domain knowledge.

3. Build the UI

cd my-dashboard/ui
npm install
npm run build

This produces dist/index.mjs — the ESM bundle loaded by the dashboard.

4. Install and Enable

Install and enable with the CLI:

kirocrew app install /absolute/path/to/my-dashboard

Before you enable it, trust the app. Enable refuses every app that is not built in, even one that ships only an agent and a skill, until the operator trusts it. Enabling it from the App Store asks you to trust this app alone (agent.apps_trusted). Settings → Security → Third-party apps lists and revokes those grants, and holds the switch that trusts all third-party apps (agent.apps_allow_third_party set to true). See Publishing Guide.

kirocrew app enable my-dashboard

The REST routes require dashboard or app authentication, so do not replace these commands with a bare curl. You can also open the Kiro Crew dashboard → App Store and install from a local path. The enabled app then appears in the sidebar.

5. Iterate

During development:

  1. Edit ui/src/App.tsx.
  2. Run cd ui && npm run build.
  3. Use the installed app's Update action in the authenticated App Store UI.
  4. Refresh the dashboard.

For UI live reload, follow App Dev Mode. Agent and skill changes take effect on the next agent invocation (no UI rebuild needed), but the installed copy still has to be updated unless the relevant source directory is linked through the documented dev-mode workflow.

App SDK Hooks

Available in @kirocrew/app-sdk:

Hook Purpose
useAppApi() Permission-scoped JSON-response HTTP client (request, GET/POST/PUT/PATCH/DELETE); request options and errors: API reference
useAppEvents(event, cb) Subscribe to real-time WebSocket events
useTheme() Reactive theme (mode, accent, colorTheme)
useAppInfo() App metadata (name, version, permissions, active, update)
useNavigate() Navigate to Kiro Crew routes
useNotify() Show toast notifications
useNavBadge() Update sidebar badge count
useChatLauncher() Open a new chat or target slotKey; set autoSend: false for an unsent draft

useAppInfo().active tells your app whether its host surface is the one the user is looking at. A routed ui.pages page is always the visible surface, so it reads true. A contributes.panelTabs side-panel tab is different: it stays MOUNTED while hidden — behind another tab, with the panel closed, or while another chat is active — so that switching back does not discard your component's state. Poll on an interval, hold a global hotkey, or run an animation loop and it keeps costing while nobody can see it.

const { active } = useAppInfo()
useEffect(() => {
  if (!active) return           // hidden: do not start the interval at all
  const id = setInterval(refresh, 5000)
  return () => clearInterval(id)
}, [active])

Treat undefined as true: the field is optional, so an app running on a host that predates it must still render rather than assume it is hidden.

Telling the user a new version is out

useAppInfo().update is how your app finds out that a newer version of itself has been published, so you can show your own "update available" row instead of waiting for the user to visit the store.

const { version, update } = useAppInfo()

if (!update) return null                 // nothing checked yet — say nothing
if (!update.available) return null       // checked, you are current

return <Banner>Version {update.latestVersion} is out (you have {version}).</Banner>

The three states are deliberately distinct, and the first two are easy to confuse:

update Means
absent The host has not consulted anything. You do not know you are current.
available: false The host checked and nothing newer is published.
available: true latestVersion is newer than your installed version.

Render an absent update as "nothing to say", never as "up to date" — the second claims something no one has verified.

checkedAt is when the host's cache was written, as an ISO-8601 instant, or null if it could not be read. The host answers from caches it already keeps and never fetches on your behalf, so the answer can be hours old; show checkedAt if the age matters to your user. A fresh answer arrives when the store's listing refreshes.

The field reads the same on every surface your app mounts on — a routed ui.pages page and a contributes.panelTabs side-panel tab both get it — so you write the banner once.

The field is advisory. It tells you what to display, not what to do: your app cannot update itself from it, and the gateway re-resolves the app's source when an update is actually applied.

Running something once after you are upgraded

There is no "you were just updated" event, on purpose. An update is applied from Discover or the app's detail page, where your app is usually not mounted, so a live signal would reach the code that needs it least. Compare instead, on mount: remember the last version your app ran, and act when useAppInfo().version differs from it.

const { version } = useAppInfo()
useEffect(() => {
  const last = localStorage.getItem('my-app:last-version')
  if (last !== version) {
    migrateIfNeeded(last, version)   // last is null on a first run
    localStorage.setItem('my-app:last-version', version)
  }
}, [version])

This runs in the NEW bundle, whenever it first mounts, however long after the update that is. Keep the stored version wherever the rest of your app's state lives — a backend can do the same comparison on start against its own data.

Chat Marker Protocol

Also in @kirocrew/app-sdk, for an app that renders agent messages itself. An agent puts follow-up choices and steer acknowledgements inline in its prose ([OPTIONS: a | b], [STEERING steer-<id>: …]); these parse them out so your UI can show buttons instead of raw syntax.

The functions and types are listed in api-reference.md, with worked examples. The module is React-free, so it also works in a worker or a test.

To render a transcript rather than parse one, ChatMessageList draws it and its row registry lets you add a row type or replace one without forking the list — including the roles the dashboard leaves undrawn. See api-reference.md.

Shared UI Components

Available in @kirocrew/app-sdk/ui:

Card, CardTitle, Btn, SendBtn, Input, SearchInput, Badge, SourceBadge, StatCard, Skeleton, ContentSkeleton, EmptyState, PageHeader, Toggle, InfoTip, SegmentedControl, MarkdownRenderer, Clickable, Modal, ErrorNotice, SettingsSection, SettingsCard, SettingsInput, SettingsToggle, SettingsSelect, Select, guideCaution

Shared interaction and locale helpers

The main SDK exports useImeGuard so app-owned inputs reuse the host's IME Enter protection. It also exports activeLocale, fmtNumber, fmtCurrency, fmtDate, fmtTime, fmtDateTime, fmtRelative, compareText and useLanguageGeneration. Subscribe with the hook when a memoized component needs to repaint on language changes. Apps own their translation catalogs; these helpers share locale selection and formatting, not permission to modify the dashboard's catalogs.

For chat handoff, openChat({ message, autoSend: false }) creates an unsent draft in a new session. Add slotKey to target an existing session; a draft launch appends to that session's unsent text. Omit autoSend to send automatically; without slotKey this starts a new session. agent applies only to new sessions. A target must activate successfully before its message is used; the SDK does not select another session on failure. memoryMode ('persistent', 'incognito' or 'temporary') and sessionFolder: false shape a new session and are ignored with slotKey. Either one opens a new draft session, like autoSend: false, so the message is staged for the user to send.

To tell an older host from one with these seams, read APP_SDK_LEVEL through a namespace import (import * as sdk from '@kirocrew/app-sdk'; an older host has no such export, so sdk.APP_SDK_LEVEL is undefined there). A named import of a name the host lacks fails the whole bundle at load time.

For Python app hooks with an existing cron grant, use await ctx.cron.set_enabled_async(job_id, False) to pause an owned job, or True to resume it without replacing its ID. Use set_enabled off-loop. update_job and update_job_async reject enabled and user_paused arguments; use the toggle methods instead. Foreign and missing job IDs are refused.

The add_job* methods accept approval_mode, timeout_secs and timeout at create time. Pass approval_mode="auto" for an unattended agent job: the default, "", is hook-based approval, so every tool call in the job's session waits on a prompt nobody is present to answer. Set it on the create call rather than in a follow-up update_job — the job is live from its first save, so a second write leaves a window in which a due-scan can run it under the wrong mode, and add_job_if_absent_async returns None once the name exists, which skips that follow-up on every registration after the first. timeout_secs is the per-wake execution budget (1..86400 seconds, default 1800) and timeout bounds only a script or command subprocess (default 30s for a script, 300s for a command). On a script or command job set the pair together: the create path refuses a wake budget that cannot cover the subprocess timeout plus cleanup, because a shorter budget cancels the executor future while the subprocess keeps running and the next wake launches a duplicate.

Shared React Query

Externalize @tanstack/react-query when bundling your app. The dashboard import map resolves that specifier to the host's module instance, so useQuery, useMutation and useQueryClient work with no provider of your own. Do not bundle a second copy of React Query. Use useAppApi() inside query and mutation functions to retain scoped transport and host session binding.

The module is shared; the cache is not. useQueryClient() returns a client the host creates for your app, so your keys, clear() and invalidateQueries() reach only your app's cache, and dashboard keys are neither readable nor writable from your bundle. That client is kept per app AND per host session for as long as the dashboard page lives, so returning to your page under the same host surface reuses the same cache rather than a fresh one. Your app mounted in two places bound to different sessions -- its own page and a chat side panel, or two panels -- gets one cache each, so a key you write in one never answers a read in the other. An individual query inside it still expires on React Query's own gcTime once nothing observes it, which is the default five minutes unless your query sets a longer one; the dashboard's thirty-minute app retention window is registered for builtin pages only. Dashboard data is available through useAppApi() under the paths your app.json declares.

Permissions

Declare what your app can access in app.json:

{
  "permissions": {
    "api": ["/api/crons", "/api/status"],
    "events": ["notification", "slots"],
    "mcpTools": ["cron_add", "cron_list"],
    "storage": true,
    "cron": true,
    "network": false
  }
}

The App SDK checks declared permissions before each request — accessing undeclared paths throws an error.

Next Steps

  • Backend communication: Your dashboard UI can call your app's backend through the gateway reverse proxy at /apps/{name}/api/* — no CORS issues. Verify requests in your backend with verify_proxy_request() from kiro_crew.apps.proxy_auth.
  • See App Manifest Reference for all app.json fields
  • See API Reference for TypeScript and Python client APIs
  • See Publishing Guide for publishing to the App Store registry

Python Client

Python apps, CLI tools and services use the source-only kirocrew-client package from a Kiro Crew checkout. Install steps, the constructor and the full method table are in API Reference.

Publishing Your App

Once your app works locally, publish it to the App Store registry so other Kiro Crew users can install it with one click. Your app's code stays in a dedicated GitHub repository listed in the official registry (kirodotdev/KiroCrewApps); Kiro Crew accepts no new built-in app, so an app is never added to the Kiro Crew package itself.

See Publishing Guide for the full workflow.