Merge cli branch into cli-to-dev for PR to dev

This commit is contained in:
tusharmagar 2025-11-20 17:24:46 +05:30
commit 5885773af4
7 changed files with 729 additions and 141 deletions

View file

@ -1,7 +1,8 @@
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { app, modelConfig } from '../dist/app.js';
import { app, modelConfig, updateState } from '../dist/app.js';
import { importExample, listAvailableExamples } from '../dist/application/examples/import-example.js';
yargs(hideBin(process.argv))
@ -11,8 +12,7 @@ yargs(hideBin(process.argv))
(y) => y
.option("agent", {
type: "string",
description: "The agent to run",
default: "copilot",
description: "The agent to run (defaults to copilot)",
})
.option("run_id", {
type: "string",
@ -26,10 +26,43 @@ yargs(hideBin(process.argv))
type: "boolean",
description: "Do not interact with the user",
default: false,
})
.option("sync-examples", {
type: "string",
description: "Import an example workflow by name (use 'all' for every example) before running",
}),
(argv) => {
app({
agent: argv.agent,
async (argv) => {
let agent = argv.agent ?? "copilot";
if (argv["sync-examples"]) {
const requested = String(argv["sync-examples"]).trim();
const isAll = requested.toLowerCase() === "all";
try {
const examplesToImport = isAll ? await listAvailableExamples() : [requested];
if (examplesToImport.length === 0) {
console.error("No packaged examples are available to import.");
process.exit(1);
}
for (const exampleName of examplesToImport) {
const imported = await importExample(exampleName);
const agentList = imported.importedAgents.join(", ");
console.error(`Imported example '${exampleName}' with agents: ${agentList}`);
console.error(`Primary agent: ${imported.entryAgent}`);
if (imported.addedServers.length > 0) {
console.error(`Configured new MCP servers: ${imported.addedServers.join(", ")}`);
}
if (imported.skippedServers.length > 0) {
console.error(`Skipped existing MCP servers (already configured): ${imported.skippedServers.join(", ")}`);
}
}
} catch (error) {
console.error(error?.message ?? error);
process.exit(1);
}
console.error("Examples imported. Re-run rowboatx without --sync-examples (or with --agent <name>) when you're ready to chat.");
return;
}
await app({
agent,
runId: argv.run_id,
input: argv.input,
noInteractive: argv.noInteractive,
@ -60,4 +93,4 @@ yargs(hideBin(process.argv))
updateState(argv.agent, argv.run_id);
}
)
.parse();
.parse();

View file

@ -1,128 +0,0 @@
{
"name": "podcast",
"description": "A workflow to create a podcast",
"steps": [
{
"type": "agent",
"id": "arxiv-feed-reader"
},
{
"type": "agent",
"id": "summarise-a-few"
},
{
"type": "agent",
"id": "podcast_transcript_agent"
},
{
"type": "agent",
"id": "elevenlabs_audio_gen"
}
]
}
{
"name": "summariser_workflow",
"description": "A workflow to summarise an arxiv paper",
"steps": [
{
"type": "agent",
"id": "summariser_agent"
}
]
}
{
"name": "summariser_agent",
"description": "An agent that will summarise an arxiv paper",
"model": "gpt-4.1",
"instructions": "Your job is to download and summarise an arxiv paper. Use a command like this to do it\n\n curl -L -o paper.pdf https://arxiv.org/pdf/2511.02997 (use the url that the user provides you). Important, just put out the GIST of the paper in two lines. Dont ask a human for inputs - do what you think is best.",
"tools": {
"bash": {
"type": "builtin",
"name": "executeCommand"
}
}
}
{
"name": "arxiv-feed-reader",
"description": "A feed reader for the arXiv",
"model": "gpt-4.1",
"instructions": "Your job is to extract the latest papers from the arXiv feed and summarise them. Use an example curl command like the following to get this done:\n\n! curl -s https://rss.arxiv.org/rss/cs.AI \\\n| yq -p=xml -o=json \\\n| jq -r '.rss.channel.item[] | select(.title | test(\"agent\"; \"i\")) | \"\\(.title)\\n\\(.link)\\n\\(.description)\\n\"' \n\nThis will give you a list of papers that contain the word \"agent\" in the title. You can then summarise these papers using the summariser agent.",
"tools": {
"bash": {
"type": "builtin",
"name": "executeCommand"
}
}
}
{
"name": "summarise-a-few",
"description": "An agent that will summarise a few arxiv papers",
"model": "gpt-4.1",
"instructions": "Your job is to pick 2 interesting papers and related papers on the same topic, and then summarise each of them inidivually using the right tool calls. Make sure to pass in the URL of the paper to the summaurse tool. Don't ask for human input.",
"tools": {
"summariser": {
"type": "agent",
"name": "summariser_workflow"
}
}
}
{
"name": "podcast_transcript_agent",
"description": "An agent that will generate a transcript of a podcast",
"model": "gpt-4.1",
"instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural."
}
{
"name": "elevenlabs_audio_gen",
"description": "An agent that will generate an audio file from a text",
"model": "gpt-4.1",
"instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the bash tool to look for the generated audio files and also combine the audio files into a single final podcast audio file. Use 'eleven_v3' for the model_id.",
"tools": {
"text_to_speech": {
"type": "mcp",
"name": "text_to_speech",
"description": "Generate an audio file from a text",
"mcpServerName": "elevenLabs",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The text to generate an audio file from"
},
"voice_name": {
"type": "string",
"description": "The voice name to use for the audio file"
},
"model_id": {
"type": "string",
"description": "The model id to use for the audio file"
}
}
}
},
"bash": {
"type": "builtin",
"name": "executeCommand"
}
}
}
{
"mcpServers": {
"elevenLabs": {
"command": "uvx",
"args": ["elevenlabs-mcp"],
"env": {
"ELEVENLABS_API_KEY": "sk_42ee2a0a19266552c18b0920b593e22f0185d4b1435b65ed"
}
}
}
}

View file

@ -0,0 +1,558 @@
{
"id": "twitter-podcast",
"description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
"entryAgent": "tweet-podcast",
"agents": [
{
"name": "tweet-podcast",
"description": "An agent that will produce a podcast from recent tweets",
"model": "gpt-5.1",
"instructions": "You are the orchestrator for producing a short podcast episode end-to-end. Follow these steps in order and only advance once each step succeeds:\n\n1. Tweets: call the tweets workflow to collect the latest tweets, .\n\n2.Transcript creation: Provide the resulting tweets to the podcast_transcript_agent tool so it can script a ~1 minute alternating dialogue between John and Chloe that references the tweets and a balanced conversation about AI bubble.\n\n4. Audio production: Send the transcript to the elevenlabs_audio_gen tool create an audio file.",
"tools": {
"tweets": {
"type": "agent",
"name": "tweets"
},
"podcast_transcript_agent": {
"type": "agent",
"name": "podcast_transcript_agent"
},
"elevenlabs_audio_gen": {
"type": "agent",
"name": "elevenlabs_audio_gen"
}
}
},
{
"name": "tweets",
"description": "Checks latest tweets",
"model": "gpt-4.1",
"instructions": "Pulls the recent 10 recent tweets each on OpenAI, Anthropic, Nvidia, Grok, Gemini",
"tools": {
"search_tweets": {
"type": "mcp",
"name": "TWITTER_RECENT_SEARCH",
"description": "Search recent Tweets from the last 7 days using X/Twitter's search syntax via Composio's Twitter MCP server.",
"mcpServerName": "twitter",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for matching Tweets. Use X search operators like from:username, -is:retweet, -is:reply, has:media, lang:en, etc. Limited to last 7 days."
},
"start_time": {
"type": "string",
"description": "Oldest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results, within the last 7 days."
},
"end_time": {
"type": "string",
"description": "Newest UTC timestamp (YYYY-MM-DDTHH:mm:ssZ) for results; exclusive."
},
"max_results": {
"type": "integer",
"description": "Number of Tweets to return (up to 2000 per call).",
"default": 10
},
"sort_order": {
"type": "string",
"enum": [
"recency",
"relevancy"
],
"description": "Order of results: 'recency' (most recent first) or 'relevancy'."
},
"tweet_fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"article",
"attachments",
"author_id",
"card_uri",
"context_annotations",
"conversation_id",
"created_at",
"edit_controls",
"edit_history_tweet_ids",
"entities",
"geo",
"id",
"in_reply_to_user_id",
"lang",
"non_public_metrics",
"note_tweet",
"organic_metrics",
"possibly_sensitive",
"promoted_metrics",
"public_metrics",
"referenced_tweets",
"reply_settings",
"scopes",
"source",
"text",
"withheld"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "Tweet fields to include in the response. Example: ['created_at','author_id','public_metrics']."
},
"expansions": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"article.cover_media",
"article.media_entities",
"attachments.media_keys",
"attachments.media_source_tweet",
"attachments.poll_ids",
"author_id",
"author_screen_name",
"edit_history_tweet_ids",
"entities.mentions.username",
"entities.note.mentions.username",
"geo.place_id",
"in_reply_to_user_id",
"referenced_tweets.id",
"referenced_tweets.id.author_id"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "Expansions to hydrate related objects like users, media, polls, and places."
},
"media_fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"alt_text",
"duration_ms",
"height",
"media_key",
"non_public_metrics",
"organic_metrics",
"preview_image_url",
"promoted_metrics",
"public_metrics",
"type",
"url",
"variants",
"width"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "Media fields to include when media keys are expanded."
},
"place_fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"contained_within",
"country",
"country_code",
"full_name",
"geo",
"id",
"name",
"place_type"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "Place fields to include when place IDs are expanded."
},
"poll_fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"duration_minutes",
"end_datetime",
"id",
"options",
"voting_status"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "Poll fields to include when poll IDs are expanded."
},
"user_fields": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string",
"enum": [
"affiliation",
"connection_status",
"created_at",
"description",
"entities",
"id",
"location",
"most_recent_tweet_id",
"name",
"pinned_tweet_id",
"profile_banner_url",
"profile_image_url",
"protected",
"public_metrics",
"receives_your_dm",
"subscription_type",
"url",
"verified",
"verified_type",
"withheld",
"username"
]
}
},
{
"type": "null"
}
],
"default": null,
"description": "User fields to include when user IDs are expanded. Username is always returned by default."
},
"since_id": {
"type": "string",
"description": "Return Tweets more recent than this ID (cannot be used with start_time)."
},
"until_id": {
"type": "string",
"description": "Return Tweets older than this ID (cannot be used with end_time)."
},
"next_token": {
"type": "string",
"description": "Pagination token from a previous response's meta.next_token."
},
"pagination_token": {
"type": "string",
"description": "Alternative pagination token from a previous meta.next_token; next_token is preferred."
}
},
"required": [
"query"
],
"additionalProperties": false
}
},
"bash": {
"type": "builtin",
"name": "executeCommand",
"description": "Execute bash commands to manipulate files like tweets.txt, e.g. writing search results to disk or appending logs.",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute, such as 'echo \"text\" >> tweets.txt' or 'cat tweets.txt'."
}
},
"required": [
"command"
],
"additionalProperties": false
}
}
}
},
{
"name": "podcast_transcript_agent",
"description": "An agent that will generate a transcript of a podcast",
"model": "gpt-4.1",
"instructions": "You job is to create a NotebookLM style 1 minute podcast between 2 speakers John and Chloe. Each line should be a new speaker. The podcast should be about the contents of the two papers (that were selected). You can use [sighs], [inhales then exhales], [chuckles], [laughs], [clears throat], [coughs], [sniffs], [pauses] etc. to make the podcast more natural."
},
{
"name": "elevenlabs_audio_gen",
"description": "An agent that will generate an audio file from a text",
"model": "gpt-4.1",
"instructions": "Your job is to take the mutli speaker transcript and generate an audio file from it. Use the elevenlabs text to speech tool to do this. For each speaker turn, you should generate an audio file and then combine them all into a single audio file. Use the voice_name 'Liam' for John and 'Cassidy' for Chloe. Make sure to remove the speaker names from the text before generating the audio files. Use the eleven_v3 model_id. In addition, you should use the compose_music tool to generate a short musical intro and outro for the podcast. The intro should be a small 5-10 second clip modeled after popular podcasts which fades and the podcast starts. The outro should be 10-15 seconds of a related sound. Save the intro and outro to files, and then use the bash tool to stitch them with the main podcast audio so that the final output audio file starts with the intro music, then the full conversation, and ends with the outro music. Place all generated audio on the Desktop by default unless otherwise instructed. Don't wait for confirmation - go ahead and produce the podcast.",
"tools": {
"text_to_speech": {
"type": "mcp",
"name": "text_to_speech",
"description": "Generate an audio file from a text",
"mcpServerName": "elevenLabs",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "The text to generate an audio file from"
},
"voice_name": {
"type": "string",
"description": "The voice name to use for the audio file"
},
"model_id": {
"type": "string",
"description": "The model id to use for the audio file"
}
}
}
},
"compose_music": {
"type": "mcp",
"name": "compose_music",
"description": "Generate intro and outro music for the podcast and save as audio files",
"mcpServerName": "elevenLabs",
"inputSchema": {
"type": "object",
"properties": {
"prompt": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Prompt"
},
"output_directory": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"title": "Output Directory"
},
"composition_plan": {
"anyOf": [
{
"$ref": "#/$defs/MusicPrompt"
},
{
"type": "null"
}
],
"default": null
},
"music_length_ms": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"title": "Music Length Ms"
}
},
"$defs": {
"MusicPrompt": {
"additionalProperties": true,
"properties": {
"positive_global_styles": {
"items": {
"type": "string"
},
"title": "Positive Global Styles",
"type": "array"
},
"negative_global_styles": {
"items": {
"type": "string"
},
"title": "Negative Global Styles",
"type": "array"
},
"sections": {
"items": {
"$ref": "#/$defs/SongSection"
},
"title": "Sections",
"type": "array"
}
},
"required": [
"positive_global_styles",
"negative_global_styles",
"sections"
],
"title": "MusicPrompt",
"type": "object"
},
"SectionSource": {
"additionalProperties": true,
"properties": {
"song_id": {
"title": "Song Id",
"type": "string"
},
"range": {
"$ref": "#/$defs/TimeRange"
},
"negative_ranges": {
"anyOf": [
{
"items": {
"$ref": "#/$defs/TimeRange"
},
"type": "array"
},
{
"type": "null"
}
],
"default": null,
"title": "Negative Ranges"
}
},
"required": [
"song_id",
"range"
],
"title": "SectionSource",
"type": "object"
},
"SongSection": {
"additionalProperties": true,
"properties": {
"section_name": {
"title": "Section Name",
"type": "string"
},
"positive_local_styles": {
"items": {
"type": "string"
},
"title": "Positive Local Styles",
"type": "array"
},
"negative_local_styles": {
"items": {
"type": "string"
},
"title": "Negative Local Styles",
"type": "array"
},
"duration_ms": {
"title": "Duration Ms",
"type": "integer"
},
"lines": {
"items": {
"type": "string"
},
"title": "Lines",
"type": "array"
},
"source_from": {
"anyOf": [
{
"$ref": "#/$defs/SectionSource"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"section_name",
"positive_local_styles",
"negative_local_styles",
"duration_ms",
"lines"
],
"title": "SongSection",
"type": "object"
},
"TimeRange": {
"additionalProperties": true,
"properties": {
"start_ms": {
"title": "Start Ms",
"type": "integer"
},
"end_ms": {
"title": "End Ms",
"type": "integer"
}
},
"required": [
"start_ms",
"end_ms"
],
"title": "TimeRange",
"type": "object"
}
},
"title": "compose_musicArguments"
}
},
"bash": {
"type": "builtin",
"name": "executeCommand"
}
}
}
],
"mcpServers": {
"elevenLabs": {
"command": "uvx",
"args": [
"elevenlabs-mcp"
],
"env": {
"ELEVENLABS_API_KEY": "<your-api-key>"
}
},
"calendar": {
"type": "http",
"url": "<composio-url>"
},
"twitter": {
"type": "http",
"url": "<composio-url>"
}
}
}

View file

@ -10,7 +10,8 @@
},
"files": [
"dist",
"bin"
"bin",
"examples"
],
"bin": {
"rowboatx": "bin/app.js"

View file

@ -27,6 +27,11 @@ Always consult this catalog first so you load the right skills before taking act
- Use relative paths (no \${BASE_DIR} prefixes) when running commands or referencing files.
- Keep user data safedouble-check before editing or deleting important resources.
## Workspace access & scope
- You have full read/write access inside \`${BASE_DIR}\` (this resolves to the user's \`~/.rowboat\` directory). Create folders, files, and agents there using builtin tools or allowed shell commands—don't wait for the user to do it manually.
- If a user mentions a different root (e.g., \`~/.rowboatx\` or another path), clarify whether they meant the Rowboat workspace and propose the equivalent path you can act on. Only refuse if they explicitly insist on an inaccessible location.
- Prefer builtin file tools (\`createFile\`, \`updateFile\`, \`deleteFile\`, \`exploreDirectory\`) for workspace changes. Reserve refusal or "you do it" responses for cases that are truly outside the Rowboat sandbox.
## Builtin Tools vs Shell Commands
**IMPORTANT**: Rowboat provides builtin tools that are internal and do NOT require security allowlist entries:

View file

@ -1,16 +1,20 @@
import z from "zod";
import { z } from "zod";
const StdioMcpServerConfig = z.object({
export const StdioMcpServerConfig = z.object({
type: z.literal("stdio").optional(),
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
});
const HttpMcpServerConfig = z.object({
export const HttpMcpServerConfig = z.object({
type: z.literal("http").optional(),
url: z.string(),
headers: z.record(z.string(), z.string()).optional(),
});
export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]);
export const McpServerConfig = z.object({
mcpServers: z.record(z.string(), z.union([StdioMcpServerConfig, HttpMcpServerConfig])),
});
mcpServers: z.record(z.string(), McpServerDefinition),
});

View file

@ -0,0 +1,115 @@
import path from "path";
import { fileURLToPath } from "url";
import { promises as fs } from "fs";
import { z } from "zod";
import { Agent } from "../entities/agent.js";
import { WorkDir } from "../config/config.js";
import { McpServerConfig, McpServerDefinition } from "../entities/mcp.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PackageRoot = path.resolve(__dirname, "../../..");
const ExamplesDir = path.join(PackageRoot, "examples");
const ExampleSchema = z.object({
id: z.string().min(1),
description: z.string().optional(),
entryAgent: z.string().optional(),
agents: z.array(Agent).min(1),
mcpServers: z.record(z.string(), McpServerDefinition).optional(),
}).refine(
(data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent),
{
message: "entryAgent must reference one of the defined agents",
path: ["entryAgent"],
},
);
async function readExampleFile(exampleName: string): Promise<string> {
const examplePath = path.join(ExamplesDir, `${exampleName}.json`);
try {
await fs.access(examplePath);
return await fs.readFile(examplePath, "utf8");
} catch (error) {
const availableExamples = await listAvailableExamples();
const listMessage = availableExamples.length
? `Available examples: ${availableExamples.join(", ")}`
: "No packaged examples were found.";
throw new Error(`Unknown example '${exampleName}'. ${listMessage}`);
}
}
export async function listAvailableExamples(): Promise<string[]> {
try {
const entries = await fs.readdir(ExamplesDir);
return entries
.filter((entry) => entry.endsWith(".json"))
.map((entry) => entry.replace(/\.json$/, ""))
.sort();
} catch {
return [];
}
}
async function writeAgents(agents: z.infer<typeof Agent>[]) {
await fs.mkdir(path.join(WorkDir, "agents"), { recursive: true });
await Promise.all(
agents.map(async (agent) => {
const agentPath = path.join(WorkDir, "agents", `${agent.name}.json`);
await fs.writeFile(agentPath, JSON.stringify(agent, null, 2), "utf8");
}),
);
}
async function mergeMcpServers(servers: Record<string, z.infer<typeof McpServerDefinition>>) {
const result = { added: [] as string[], skipped: [] as string[] };
if (!servers || Object.keys(servers).length === 0) {
return result;
}
const configPath = path.join(WorkDir, "config", "mcp.json");
let currentConfig: z.infer<typeof McpServerConfig> = { mcpServers: {} };
try {
const contents = await fs.readFile(configPath, "utf8");
currentConfig = McpServerConfig.parse(JSON.parse(contents));
} catch (error: any) {
if (error?.code !== "ENOENT") {
throw new Error(`Unable to read MCP config: ${error.message ?? error}`);
}
}
let modified = false;
for (const [name, definition] of Object.entries(servers)) {
if (currentConfig.mcpServers[name]) {
result.skipped.push(name);
continue;
}
currentConfig.mcpServers[name] = definition;
result.added.push(name);
modified = true;
}
await fs.mkdir(path.dirname(configPath), { recursive: true });
if (modified) {
await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), "utf8");
}
return result;
}
export async function importExample(exampleName: string) {
const raw = await readExampleFile(exampleName);
const parsed = ExampleSchema.parse(JSON.parse(raw));
const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name;
if (!entryAgentName) {
throw new Error(`Example '${exampleName}' does not define any agents to run.`);
}
await writeAgents(parsed.agents);
let serverMerge = { added: [] as string[], skipped: [] as string[] };
if (parsed.mcpServers) {
serverMerge = await mergeMcpServers(parsed.mcpServers);
}
return {
id: parsed.id,
entryAgent: entryAgentName,
importedAgents: parsed.agents.map((agent) => agent.name),
addedServers: serverMerge.added,
skippedServers: serverMerge.skipped,
};
}