fix: sync-example implementation

This commit is contained in:
tusharmagar 2025-11-21 10:32:24 +05:30
parent 55d9f80074
commit 532065dc0b
4 changed files with 197 additions and 150 deletions

View file

@ -1,8 +1,7 @@
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { app, modelConfig, updateState } from '../dist/app.js';
import { importExample, listAvailableExamples } from '../dist/application/examples/import-example.js';
import { app, modelConfig, updateState, importExample, listExamples } from '../dist/app.js';
yargs(hideBin(process.argv))
@ -26,41 +25,9 @@ yargs(hideBin(process.argv))
type: "boolean",
description: "Do not interact with the user",
default: false,
})
.option("sync-example", {
type: "string",
description: "Import an example workflow by name (use 'all' for every example) before running",
}),
async (argv) => {
let agent = argv.agent ?? "copilot";
if (argv["sync-example"]) {
const requested = String(argv["sync-example"]).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-example (or with --agent <name>) when you're ready to chat.");
return;
}
await app({
agent,
runId: argv.run_id,
@ -69,6 +36,69 @@ yargs(hideBin(process.argv))
});
}
)
.command(
"sync-example <example>",
"Import an example workflow by name",
(y) => y.positional("example", {
type: "string",
description: "The example to import",
}),
async (argv) => {
const exampleName = String(argv.example).trim();
try {
const imported = await importExample(exampleName);
// Build output message
const output = [
`✓ Imported example '${exampleName}'`,
` Agents: ${imported.importedAgents.join(", ")}`,
` Primary: ${imported.entryAgent}`,
];
if (imported.addedServers.length > 0) {
output.push(` MCP servers added: ${imported.addedServers.join(", ")}`);
}
if (imported.skippedServers.length > 0) {
output.push(` MCP servers skipped (already configured): ${imported.skippedServers.join(", ")}`);
}
console.log(output.join("\n"));
if (imported.postInstallInstructions) {
console.log("\n" + "=".repeat(60));
console.log("POST-INSTALL INSTRUCTIONS");
console.log("=".repeat(60));
console.log(imported.postInstallInstructions);
console.log("=".repeat(60) + "\n");
}
console.log(`\nRun: rowboatx --agent ${imported.entryAgent}`);
} catch (error) {
console.error("Error:", error?.message ?? error);
process.exit(1);
}
}
)
.command(
"list-example",
"List all available example workflows",
(y) => y,
async () => {
try {
const examples = await listExamples();
if (examples.length === 0) {
console.error("No packaged examples are available to list.");
return;
}
for (const example of examples) {
console.log(example);
}
} catch (error) {
console.error(error?.message ?? error);
process.exit(1);
}
}
)
.command(
"model-config",
"Select model",

View file

@ -1,5 +1,6 @@
{
"id": "twitter-podcast",
"post-import-instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
"description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
"entryAgent": "tweet-podcast",
"agents": [

View file

@ -2,13 +2,22 @@ import { AgentState, streamAgent } from "./application/lib/agent.js";
import { StreamRenderer } from "./application/lib/stream-renderer.js";
import { stdin as input, stdout as output } from "node:process";
import fs from "fs";
import { promises as fsp } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js";
import { RunEvent } from "./application/entities/run-events.js";
import { createInterface, Interface } from "node:readline/promises";
import { ToolCallPart } from "./application/entities/message.js";
import { Agent } from "./application/entities/agent.js";
import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js";
import { z } from "zod";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PackageRoot = path.resolve(__dirname, "..");
const ExamplesDir = path.join(PackageRoot, "examples");
export async function updateState(agent: string, runId: string) {
const state = new AgentState(agent, runId);
// If running in a TTY, read run events from stdin line-by-line
@ -386,4 +395,126 @@ function renderCurrentModel(provider: string, flavor: string, model: string) {
console.log(`- provider: ${provider}${flavor ? ` (${flavor})` : ""}`);
console.log(`- model: ${model}`);
console.log("");
}
}
const ExampleSchema = z.object({
id: z.string().min(1),
"post-install-instructions": z.string().optional(),
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 {
return await fsp.readFile(examplePath, "utf8");
} catch (error: any) {
if (error?.code === "ENOENT") {
const availableExamples = await listAvailableExamples();
const listMessage = availableExamples.length
? `Available examples: ${availableExamples.join(", ")}`
: "No packaged examples were found.";
throw new Error(`Unknown example '${exampleName}'. ${listMessage}`);
}
// Re-throw other errors (permission issues, etc.)
throw error;
}
}
async function listAvailableExamples(): Promise<string[]> {
try {
const entries = await fsp.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 fsp.mkdir(path.join(WorkDir, "agents"), { recursive: true });
await Promise.all(
agents.map(async (agent) => {
const agentPath = path.join(WorkDir, "agents", `${agent.name}.json`);
await fsp.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[] };
// Early return if no servers to process
if (!servers || Object.keys(servers).length === 0) {
return result;
}
const configPath = path.join(WorkDir, "config", "mcp.json");
// Read existing config
let currentConfig: z.infer<typeof McpServerConfig> = { mcpServers: {} };
try {
const contents = await fsp.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}`);
}
// File doesn't exist yet, use empty config
}
// Merge servers
for (const [name, definition] of Object.entries(servers)) {
if (currentConfig.mcpServers[name]) {
result.skipped.push(name);
} else {
currentConfig.mcpServers[name] = definition;
result.added.push(name);
}
}
// Only write if we added new servers
if (result.added.length > 0) {
await fsp.mkdir(path.dirname(configPath), { recursive: true });
await fsp.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.`);
}
const postInstallInstructions = parsed["post-install-instructions"];
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,
postInstallInstructions,
};
}
export async function listExamples() {
return listAvailableExamples();
}

View file

@ -1,115 +0,0 @@
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,
};
}