mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
feat(mini-apps): copilot builder — build-mini-app skill + install/data tools
Lets the copilot build a Mini App into ~/.rowboat/apps from chat. - build-mini-app skill: intent gate (confirm when ambiguous), verify Composio wiring by actually calling tools before building, writer branch (Code Mode via code-with-agents vs Copilot), agent-backed data pipeline, install - mini-app-install tool: write an app folder (manifest + dist/index.html) - mini-app-set-data tool: atomic deterministic data.json write (agent returns content; path/write handled in code) - serve canonical bridge shim at app://miniapp/__bridge__.js
This commit is contained in:
parent
924a93fa4a
commit
4a4dcb1fa0
6 changed files with 298 additions and 1 deletions
|
|
@ -0,0 +1,110 @@
|
|||
import { z } from 'zod';
|
||||
import { stringify as stringifyYaml } from 'yaml';
|
||||
import { MiniAppManifest } from '@x/shared/dist/mini-app.js';
|
||||
|
||||
const manifestSchema = stringifyYaml(z.toJSONSchema(MiniAppManifest)).trimEnd();
|
||||
|
||||
export const skill = String.raw`
|
||||
# Build a Mini App
|
||||
|
||||
A *Mini App* is a small app the user opens inside Rowboat — its own UI, powered by
|
||||
their integrations and (optionally) a background agent. Apps live on disk at
|
||||
\`~/.rowboat/apps/<id>/\` and are served at \`app://miniapp/<id>/\`:
|
||||
|
||||
\`\`\`
|
||||
~/.rowboat/apps/<id>/
|
||||
manifest.json # see schema below
|
||||
dist/index.html # the app UI (self-contained), served via app://miniapp/<id>/
|
||||
data.json # data the UI reads (produced by the app's agent; optional)
|
||||
\`\`\`
|
||||
|
||||
You do NOT hand-write these files with file tools. Use **\`mini-app-install\`** to
|
||||
write the folder, and **\`create-background-task\`** for the optional agent.
|
||||
|
||||
## 0. Should this even be an app? (intent gate)
|
||||
|
||||
- **Strong — build it:** "make/build/create an app · mini app · dashboard for …",
|
||||
"turn this into an app".
|
||||
- **Medium — CONFIRM FIRST:** the request could be a one-off answer OR a recurring
|
||||
app (e.g. "show me my open PRs", "track competitor launches"). Ask once:
|
||||
*"Want this as a Mini App you can reopen, or just a one-time answer?"* Build only
|
||||
if they say app. (Building installs a folder, maybe a background agent, and may
|
||||
prompt an OAuth connection — too heavy for a casual question.)
|
||||
- **Anti — don't build:** a clear one-off lookup/question → just answer it.
|
||||
|
||||
## 1. Scope the app
|
||||
|
||||
Decide: \`id\` (kebab slug), \`title\`, \`description\`, \`source\` (e.g. "GitHub"),
|
||||
the Composio \`scope\` (toolkits it may use), and whether it is:
|
||||
- **live** — calls Composio when the user interacts (no agent), or
|
||||
- **agent-backed** — a background agent refreshes \`data.json\` on a schedule and the
|
||||
UI just reads it (keeps tokens low; best for feeds/digests/dashboards).
|
||||
|
||||
## 2. Verify the wiring BEFORE building (required — do not speculate)
|
||||
|
||||
Load the \`composio-integration\` skill. Then for each toolkit in scope:
|
||||
1. Ensure it is connected (\`composio-connect-toolkit\` → user authorizes if needed).
|
||||
2. Actually call the tools you intend to use (\`composio-search-tools\` →
|
||||
\`composio-execute-tool\`) and **inspect the real returned JSON**.
|
||||
3. Derive the app's **data shape from those real responses** — never guess field
|
||||
names. This shape is the contract between the agent (or live calls) and the UI.
|
||||
|
||||
If a toolkit doesn't support managed OAuth2 (e.g. X/Twitter), tell the user it
|
||||
can't be connected this way and pick a different integration or a browser-based
|
||||
agent.
|
||||
|
||||
## 3. Write the UI (dist/index.html)
|
||||
|
||||
The app is a self-contained HTML document. It talks to Rowboat ONLY through the
|
||||
bridge: include the shim and code against \`window.rowboat\`:
|
||||
|
||||
\`\`\`html
|
||||
<script src="/__bridge__.js"></script>
|
||||
\`\`\`
|
||||
|
||||
\`window.rowboat\` API:
|
||||
- \`getData()\` / \`onData(cb)\` — the app's data (host serves \`data.json\`). Register
|
||||
\`onData\` then call \`ready()\`.
|
||||
- \`getState()\` / \`setState(patch)\` — per-app persistent UI state.
|
||||
- \`isConnected(scope)\` / \`connect(scope)\` — connection status / start OAuth.
|
||||
- \`searchTools(scope, query)\` → [{slug,name,description}], and
|
||||
\`callAction(scope, toolSlug, args)\` → tool result (rejects on error). Scope is
|
||||
enforced against the manifest, so only declared toolkits work.
|
||||
- \`ready()\` — call once after registering callbacks to receive initial data/state.
|
||||
|
||||
Keep it dependency-free (no remote CDNs unless truly needed; the app:// origin
|
||||
allows them but offline-safe is better). Render loading / empty / error states.
|
||||
|
||||
**Who writes this HTML:**
|
||||
- If a **coding agent is active** (user toggled the Code chip), delegate authoring
|
||||
via the \`code-with-agents\` skill — run it with \`cwd\` = the app's
|
||||
\`~/.rowboat/apps/<id>/\` folder so it can iterate and test, then install.
|
||||
- Otherwise, author the HTML yourself and install it with \`mini-app-install\`.
|
||||
|
||||
## 4. Data pipeline (agent-backed apps only)
|
||||
|
||||
Create a background task with \`create-background-task\` whose instructions:
|
||||
- fetch the data via Composio (or browse via the embedded browser for social
|
||||
feeds), and
|
||||
- call **\`mini-app-set-data\`** with \`{ appId: "<id>", data: <payload> }\`.
|
||||
|
||||
The agent only RETURNS the data; \`mini-app-set-data\` writes \`data.json\`
|
||||
atomically (deterministic path + write — the agent never touches files). Set the
|
||||
manifest's \`agent\` field to the task's slug. Give the task a sensible trigger
|
||||
(cron/window) from the background-task skill.
|
||||
|
||||
## 5. Finalize
|
||||
|
||||
Call \`mini-app-install\` with the manifest + html (+ optional seed data). Then
|
||||
confirm to the user, and ideally open it so they see it populate. For agent-backed
|
||||
apps, trigger the agent once (\`run-background-task-agent\`) so \`data.json\` exists
|
||||
immediately instead of waiting for the first scheduled run.
|
||||
|
||||
## Manifest schema (manifest.json)
|
||||
|
||||
\`\`\`yaml
|
||||
` + manifestSchema + `
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
export default skill;
|
||||
|
|
@ -16,6 +16,7 @@ import composioIntegrationSkill from "./composio-integration/skill.js";
|
|||
import liveNoteSkill from "./live-note/skill.js";
|
||||
import backgroundTaskSkill from "./background-task/skill.js";
|
||||
import notifyUserSkill from "./notify-user/skill.js";
|
||||
import buildMiniAppSkill from "./build-mini-app/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||
|
|
@ -102,6 +103,12 @@ const definitions: SkillDefinition[] = [
|
|||
summary: "Write code, build projects, create scripts, or fix bugs by delegating to Claude Code or Codex.",
|
||||
content: codeWithAgentsSkill,
|
||||
},
|
||||
{
|
||||
id: "build-mini-app",
|
||||
title: "Build a Mini App",
|
||||
summary: "Build a Mini App the user opens inside Rowboat — its own UI powered by their integrations and an optional background agent. Use when the user asks to make/build/create an app, mini app, or dashboard; for ambiguous 'show me X' requests, confirm whether they want an app first.",
|
||||
content: buildMiniAppSkill,
|
||||
},
|
||||
{
|
||||
id: "background-task",
|
||||
title: "Background Tasks",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { WorkDir } from "../../config/config.js";
|
|||
import { composioAccountsRepo } from "../../composio/repo.js";
|
||||
import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, searchTools as searchComposioTools } from "../../composio/client.js";
|
||||
import { CURATED_TOOLKITS, CURATED_TOOLKIT_SLUGS } from "@x/shared/dist/composio.js";
|
||||
import { MiniAppManifest } from "@x/shared/dist/mini-app.js";
|
||||
import { BrowserControlInputSchema, type BrowserControlInput } from "@x/shared/dist/browser-control.js";
|
||||
import { BackgroundTaskSchema, TriggersSchema } from "@x/shared/dist/background-task.js";
|
||||
import type { CodeModeManager } from "../../code-mode/acp/manager.js";
|
||||
|
|
@ -1495,6 +1496,51 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
},
|
||||
isAvailable: async () => isComposioConfigured(),
|
||||
},
|
||||
'mini-app-install': {
|
||||
description: "Install or update a Mini App on disk at ~/.rowboat/apps/<id>/. Writes manifest.json + dist/index.html (and optional initial data.json). Use this to materialize an app you built — do NOT hand-write these files with file tools. The HTML must be self-contained and include the bridge via <script src=\"/__bridge__.js\"></script>, coding against window.rowboat. After install, the app shows up in the Mini Apps gallery.",
|
||||
inputSchema: z.object({
|
||||
manifest: MiniAppManifest,
|
||||
html: z.string().describe('Full self-contained HTML for dist/index.html.'),
|
||||
data: z.unknown().optional().describe('Optional initial data.json content (only written if no data.json exists yet).'),
|
||||
}),
|
||||
execute: async ({ manifest, html, data }: { manifest: z.infer<typeof MiniAppManifest>; html: string; data?: unknown }) => {
|
||||
try {
|
||||
const m = MiniAppManifest.parse(manifest);
|
||||
const dir = path.join(WorkDir, 'apps', m.id);
|
||||
const dist = path.join(dir, 'dist');
|
||||
await fs.mkdir(dist, { recursive: true });
|
||||
await fs.writeFile(path.join(dir, 'manifest.json'), JSON.stringify(m, null, 2));
|
||||
await fs.writeFile(path.join(dist, 'index.html'), html);
|
||||
if (data !== undefined) {
|
||||
const dataPath = path.join(dir, 'data.json');
|
||||
try { await fs.access(dataPath); } catch { await fs.writeFile(dataPath, JSON.stringify(data, null, 2)); }
|
||||
}
|
||||
return { success: true, id: m.id, url: `app://miniapp/${m.id}/index.html` };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
},
|
||||
},
|
||||
'mini-app-set-data': {
|
||||
description: "Write a Mini App's data.json — the JSON its frontend reads via rowboat.getData/onData. The path is derived from appId and the write is atomic (temp→rename), so you only supply the content. This is how a background task refreshes an app's data; the agent returns the data, the write is deterministic.",
|
||||
inputSchema: z.object({
|
||||
appId: z.string().describe('The app id (folder name under ~/.rowboat/apps).'),
|
||||
data: z.unknown().describe('Full data payload to store as data.json (matching the app data schema).'),
|
||||
}),
|
||||
execute: async ({ appId, data }: { appId: string; data: unknown }) => {
|
||||
try {
|
||||
const dir = path.join(WorkDir, 'apps', appId);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
const dataPath = path.join(dir, 'data.json');
|
||||
const tmp = `${dataPath}.tmp`;
|
||||
await fs.writeFile(tmp, JSON.stringify(data, null, 2));
|
||||
await fs.rename(tmp, dataPath);
|
||||
return { success: true, appId };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
},
|
||||
},
|
||||
'run-live-note-agent': {
|
||||
description: "Manually trigger the live-note agent to run now on a note. Equivalent to the user clicking the Run button in the live-note sidebar, but you can pass extra `context` to bias what the agent does this run — most useful for backfills (e.g. seeding a newly-made-live note from existing synced emails) or focused refreshes. Returns the action taken, summary, and the new note body.",
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue