CLI
The Qafka CLI bridges your dashboard project and your codebase. It scaffolds the chat screen, handler dispatcher, and tool-result components on first run, then keeps them in sync as you add or change tools in the dashboard.
Mental Model
Three commands cover the workflow. You authenticate once, scaffold once, and sync every time the dashboard changes:
| Command | When you run it | What it does |
|---|---|---|
qafka login | Once per machine | Authenticates against the backend; cached in ~/.qafka/auth.json |
qafka init | Once per app | Picks a project and scaffolds files (will prompt for login if you skipped it) |
qafka sync | Every time the dashboard changes | Pulls tools, wires missing handlers, patches <Qafka> props, generates components |
The dashboard is the source of truth for tool definitions; your handlers.ts and qafkaComponents/ are the source of truth for what’s wired locally. qafka sync reconciles the two — idempotently and non-destructively.
Installation
# Global install (recommended for repeat use)
npm install -g qafka
# Or run on demand without installing
npx qafka initQuick Start
# 1. Authenticate. Cached credential, no need to repeat per project.
qafka login
# 2. In your app's repo: pick a project and scaffold files.
qafka init
# 3. After making changes in the dashboard, sync them down.
qafka syncIf you skip step 1, qafka init will prompt for your email + password the first time it needs the backend.
What qafka init Creates
After init, your project has:
.qafka/
config.json # CLI cache (git-ignored): projectId, apiUrl, keys, paths
qafka.config.js # Public app config (commit-safe — keys are dashboard-restricted)
qafka.tools.json # Slim cache for drift detection (commit this)
src/qafkaComponents/
index.ts # Barrel (auto-managed by `qafka add component` and sync)
src/qafkaTools/
handlers.ts # Central dispatcher with @qafka:handlers-start/end markers
app/qafka.tsx # Chat screen mounting <Qafka>The screen path is auto-detected (Expo Router → app/qafka.tsx, react-navigation / plain RN → src/screens/qafka.tsx). Pass --screen-path to override.
Generated Screen Template
The scaffolded screen mounts <Qafka> with a baseline of host-app props. None are capability-driven — they’re the props every real-world app ends up using:
import React from 'react';
import { Qafka } from '@qafka/react-native';
import { handleToolSuggested } from '../src/qafkaTools/handlers';
// Public keys — commit-safe (bundle-ID restricted, attestation-validated).
const apiKey = __DEV__
? 'qafka_test_xxx' // inlined from your dashboard test key
: 'qafka_prod_xxx'; // inlined from your dashboard production key
const QafkaChatScreen = () => {
return (
<Qafka
apiKey={apiKey}
locale="en"
components={{}}
// User/session data passed to tool handlers and prompt resolution
// (e.g. { userId, firstName, locale }). Reference via {{user.field}} in tools.
context={{}}
// Set to true once your app has authenticated the user.
isAuthenticated={false}
onClose={() => {}}
onToolSuggested={(tools, addResponse) =>
handleToolSuggested(tools, addResponse, {})
}
/>
);
};
export default QafkaChatScreen;If qafka init finds both a TEST and PROD key in the dashboard, it inlines them with a __DEV__ switch. With one key only, it inlines that one and leaves a comment nudging you to create the other. With no keys, it emits 'YOUR_TEST_KEY' placeholders.
The SDK defaults apiUrl to https://api.qafka.com, so the prop is intentionally omitted — add it manually for self-hosted or staging environments.
What qafka sync Does
qafka sync runs four jobs in order, each idempotent:
1. Handler stubs
For every dashboard tool with no matching entry in handlers.ts, sync appends a typed handler stub. The stub respects the tool’s dashboard configuration:
const handleGetProducts: ToolHandler = (tool, addResponse, ctx) => {
// @qafka:tool tool_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
// TODO: implement getProducts.
const { category, limit } = tool.parameters || {};
// Dashboard binds dataPath="data.products" → SDK descends this path before rendering.
// Keep the nested shape below in sync (or nothing reaches the component).
addResponse({ data: { products: [/* TODO: items rendered by <ProductRow> */] } }, tool);
};What the generator does:
- Parameter destructure: pulls every parameter the tool declares so you don’t have to remember their names.
- Dashboard binding comment: reminds you that
dataPathand theaddResponseshape are coupled. - Nested payload skeleton: if the dashboard sets
dataPath: "data.products", sync emits{ data: { products: [/* TODO */] } }— array leaf for list/table, object leaf for card/detail. Mismatched shapes silently render nothing.
Re-running sync never overwrites a handler you’ve already implemented — appendHandlerToFile checks the registry block in handlers.ts for the key and skips if present. If you delete handlers.ts and re-run sync, every wired handler is regenerated automatically (self-healing).
2. Screen self-heal
If the screen file registered in .qafka/config.json is missing (you deleted it, or a new developer cloned a project without scaffolding), sync recreates it from the same template qafka init uses, inlining keys from the cached config. No need to re-run init.
3. Capability-driven <Qafka> props
Sync inspects each tool’s shape and patches missing <Qafka> JSX attributes:
| Capability detected by | Props added |
|---|---|
cardTemplateId set | onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardToolTrigger, onCardCTAClick |
fileInput set | onFileUploadRequest, onExtractionResult |
Any step has external_navigation action | onExternalSuggestion |
Any step has api_action | onApiActionsSuggested, onApiActionExecute |
Any step has email, webhook, or api_action | onActionResult |
| Tool has more than one step | onStepCompleted |
Inserted props are placeholders with TODO comments — edit them, move them, or rename them; sync’s idempotency check is AST-based, so it won’t re-insert a prop you’ve reshaped.
Navigation routing (onNavigationSuggest / onNavigationAction) is not capability-driven — wire it manually because the implementation depends on your router (Expo Router vs react-navigation).
4. Tool-result components
For every tool whose uiConfig.itemComponent names a non-default React component (e.g. ProductCard, ProductRow), sync:
- Creates
src/qafkaComponents/<Name>.tsxif missing - Adds a barrel re-export
- Inserts the component identifier into
<Qafka components={{ ... }}>
The generated component stub adapts its body to the dashboard’s response config. For a per-item list (default/horizontal/vertical layout), data is one item:
export const ProductCard = ({ data, onAction }: ToolComponentProps) => {
// Typical pattern: wrap your real list-item / card component and forward
// navigation through onAction so the host screen can route on it. Example:
//
// <Pressable onPress={() => onAction?.('navigate', data)}>
// <YourRealComponent item={data} />
// </Pressable>
return (
<Pressable
onPress={() => onAction?.('navigate', data)}
style={{ padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8 }}
>
<Text style={{ fontWeight: '600', marginBottom: 4 }}>ProductCard</Text>
<Text style={{ fontFamily: 'Courier', fontSize: 11 }}>
{JSON.stringify(data, null, 2)}
</Text>
</Pressable>
);
};For a custom-layout list (the SDK passes the full array to the component), sync emits a data.map(...) skeleton with a built-in empty-state guard:
export const ProductRow = ({ data, onAction }: ToolComponentProps) => {
// `data` is the FULL array (custom layout). Iterate it yourself.
if (!data?.length) return null;
return (
<View style={{ flex: 1, gap: 10 }}>
{data.map((item: any, idx: number) => (
<Pressable
key={item?.id ?? idx}
onPress={() => onAction?.('navigate', item)}
style={{ padding: 12, backgroundColor: '#f5f5f5', borderRadius: 8 }}
>
<Text style={{ fontFamily: 'Courier', fontSize: 11 }}>
{JSON.stringify(item, null, 2)}
</Text>
</Pressable>
))}
</View>
);
};The doc comment at the top of every generated component states the response shape, the dashboard dataPath, and (for per-item lists) the maxItems cap — so you don’t have to flip back to the dashboard to remember the binding.
Built-in Tool Filtering
System-controlled tools (qafka_response_status, qafka_add_followup, …) execute server-side and never need a host-app handler or component. The dashboard returns them in /tools-v2 for prompt construction; the CLI filters them out:
✔ Fetched 7 tool(s) (skipped 2 built-in)If your qafka.tools.json was created by an older CLI and still has built-in entries, sync silently prunes them — they won’t appear as orphans.
qafka.tools.json (Slim Metadata)
Each tool you have wired locally is recorded as:
{
"version": 3,
"tools": {
"get_products": {
"id": "tool_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"syncedRevision": "2026-04-28T16:15:13.554Z"
}
}
}Two fields, both load-bearing:
idanchors the tool against rename and lets sync match local entries to dashboard records by stable identifier.syncedRevisionis the dashboardupdatedAtat the last sync — sync compares it to the currentupdatedAtto flag drift (parameter shape changed since you generated the stub).
Older fields (handlerKey, handlersFile, syncedAt) were redundant with .qafka/config.json and handlers.ts and have been removed in v3. Files written by older CLIs are migrated silently on read.
Commit this file — it shares the “last known shape” snapshot with the rest of your team so drift detection is consistent across machines.
Drift and Orphans
When syncedRevision doesn’t match the dashboard’s current updatedAt, sync prints:
⚠ Drift detected (handler shape may need updating):
· get_products
Metadata was refreshed; review handler signatures against the dashboard.Drift is informational — the metadata is auto-refreshed, but your handler body is yours. Open the dashboard, check what changed, and adjust.
When a tool you have wired locally no longer exists on the dashboard:
⚠ Orphans (local tool not on dashboard):
· old_promo_lookup
Not removed automatically. Delete from handlers.ts when you no longer need them.Sync never deletes user code. Clean up handlers.ts by hand once you’re sure you don’t need the orphan.
Commands
qafka login
Authenticate against the Qafka backend. Required before any project-scoped command — and the first thing you should run on a fresh machine. Credentials are cached in ~/.qafka/auth.json so subsequent commands don’t ask again.
qafka loginOptions: -e, --email <email>, -p, --password <password>, --backend-url <url>.
qafka init
One-shot setup: login (if needed) → pick project → scaffold files.
qafka initOptions:
| Flag | Description |
|---|---|
--no-scaffold | Config-only; skip source generation |
-y, --yes | Accept all detected defaults (non-interactive) |
--screen-path <path> | Override the screen file path (e.g. app/(tabs)/qafkachat.tsx) |
--no-install | Skip installing @qafka/react-native |
--no-plugin | Skip registering the Qafka expo plugin in app.json |
--backend-url <url> | Self-hosted / staging API root |
qafka project
Pick or switch the active project without re-scaffolding files. Useful when working with multiple Qafka projects from one repo.
qafka projectqafka sync
Pull the latest tool definitions and reconcile them with handlers.ts, the screen, and qafkaComponents/. Idempotent.
qafka syncThe output is a single short report:
✔ Fetched 7 tool(s) (skipped 2 built-in)
✓ Added 1 new handler stub(s):
+ new_lookup
✓ Wired 2 <Qafka> prop(s) on app/qafka.tsx:
+ onCardDeepLink
+ onCardCTAClick
✓ Generated 1 tool UI component(s):
+ src/qafkaComponents/ProductRow.tsx
✓ Wired 1 component(s) into <Qafka components={{ ... }}>:
+ ProductRowqafka add component <Name>
Generate a tool-result component without running a full sync. Useful when you want a stub for a tool you’ll author later.
qafka add component PromoCardCreates src/qafkaComponents/PromoCard.tsx, updates the barrel, and patches every registered screen.
qafka add screen [path]
Add another chat screen (e.g. user-side vs merchant-side chat). The new path is appended to paths.screens in .qafka/config.json, so subsequent add component and sync runs patch every registered screen.
qafka add screen app/(merchant)/qafkachat.tsxqafka analyze
Scan the codebase for navigation screens and write a navigation schema.
qafka analyzeUseful flags:
| Flag | Description |
|---|---|
-p, --path <path> | Project path (defaults to current directory) |
-o, --output <file> | Output file (defaults to navigation-schema.json) |
--ai | Use the AI-powered analyzer (Expo Router, react-navigation, others) |
--auto | Try the traditional parser first, fall back to AI on failure |
--deep | Extract per-screen metadata via AI |
--incremental | With --deep, only re-analyze screens that changed since the last run |
qafka upload
Upload an analyzed navigation schema to the backend.
qafka uploadOptions: -f, --file <file> (defaults to navigation-schema.json), --project-id <id> (auto-detected from .qafka/config.json).
qafka test
Send a test message to your project and print the AI response — handy for poking at tools from the terminal.
qafka test --message "Show me the product catalog"Options: -m, --message <message> (defaults to "Hello!"), --api-key <key>.
qafka keys
Manage project API keys.
qafka keys list
qafka keys create --name "iOS Production" --type PRODUCTION
qafka keys revoke <keyId>
qafka keys enable <keyId>
qafka keys delete <keyId>revoke is reversible (apps using the key get 401 immediately). delete is permanent — reserve it for confirmed key compromise.
For most cases the dashboard (Dashboard › API Keys) is friendlier — full restriction surface (platforms, bundle IDs, rate limits) in one place, with the plain key value shown once and a copy button.
Self-Hosted / Staging
Every command accepts --backend-url <url> to point at a non-production API root. The URL is also persisted into .qafka/config.json after init, so subsequent sync calls don’t need the flag.
qafka init --backend-url https://qafka.staging.example.com