From eabd5ec4a48e408683a0ce3dab6dd04648c0b407 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:35:49 +0000 Subject: [PATCH 1/5] feat: add custom CLI example to jules-sdk - Created `packages/core/examples/custom-cli/` with `index.ts`, `README.md`, and `package.json`. - Added usage instructions and basic implementation for a repoless custom CLI using Jules. - Updated the main `packages/core/README.md` to link to the new example. - Fixed the issue requested in #232. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 23 +++++- packages/core/README.md | 1 + packages/core/examples/custom-cli/README.md | 37 +++++++++ packages/core/examples/custom-cli/index.ts | 77 +++++++++++++++++++ .../core/examples/custom-cli/package.json | 12 +++ 5 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/custom-cli/README.md create mode 100644 packages/core/examples/custom-cli/index.ts create mode 100644 packages/core/examples/custom-cli/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..4a9bd5a 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ }, "packages/core": { "name": "@google/jules-sdk", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76", @@ -52,6 +52,13 @@ "vitest": "^3.2.4", }, }, + "packages/core/examples/custom-cli": { + "name": "jules-custom-cli-example", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + }, "packages/core/examples/github-actions": { "name": "jules-github-actions-example", "version": "1.0.0", @@ -79,7 +86,7 @@ }, "packages/fleet": { "name": "@google/jules-fleet", - "version": "0.0.1-experimental.31", + "version": "0.0.1-experimental.32", "bin": { "jules-fleet": "dist/cli/index.mjs", }, @@ -105,7 +112,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -771,6 +778,8 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jules-custom-cli-example": ["jules-custom-cli-example@workspace:packages/core/examples/custom-cli"], + "jules-github-actions-example": ["jules-github-actions-example@workspace:packages/core/examples/github-actions"], "jules-sdk-example": ["jules-sdk-example@workspace:examples/simple"], @@ -1145,8 +1154,14 @@ "@google/jules-fleet/@google/jules-merge": ["@google/jules-merge@0.0.2", "", { "dependencies": { "@google/jules-sdk": "^0.1.0", "@octokit/auth-app": "^8.2.0", "@octokit/rest": "^21.0.0", "citty": "^0.1.6", "zod": "^3.25.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.1" }, "optionalPeers": ["@modelcontextprotocol/sdk"], "bin": { "jules-merge": "dist/cli/index.mjs" } }, "sha512-VPpbdBt48AbmFByg5RGztv2sQPPJ5fFJARXTvX41lY/b9M+qdyZPp19+ay3qfxEHgj1EvnRMwf/HFOrMMXZ7vQ=="], + "@google/jules-fleet/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + "@google/jules-fleet/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + "@google/jules-mcp/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + + "@google/jules-merge/@google/jules-sdk": ["@google/jules-sdk@0.1.0", "", { "dependencies": { "yaml": "^2.8.2", "zod": "^3.25.76" } }, "sha512-DBVhFOsLfWaVtO0miEeX+zQoazB55EYmK5/0VJSX/YHdeheZqNuPlKRV1vrnd9uSQAtU6ACRicQssMbnvJM6ng=="], + "@microsoft/api-extractor/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -1251,6 +1266,8 @@ "@actions/github/@octokit/request-error/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + "@google/jules-fleet/@google/jules-merge/@google/jules-sdk": ["@google/jules-sdk@workspace:packages/core"], + "@google/jules-fleet/glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@google/jules-fleet/glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], diff --git a/packages/core/README.md b/packages/core/README.md index c3d32e7..8f82459 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -11,6 +11,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment - [Agent Workflow](./examples/agent/README.md) - [Webhook Integration](./examples/webhook/README.md) - [GitHub Actions](./examples/github-actions/README.md) +- [Custom Cli Tools](./examples/custom-cli/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/custom-cli/README.md b/packages/core/examples/custom-cli/README.md new file mode 100644 index 0000000..f1a360c --- /dev/null +++ b/packages/core/examples/custom-cli/README.md @@ -0,0 +1,37 @@ +# Custom CLI Tools Example + +This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool takes a user prompt as an argument, uses a "Repoless" session to execute the task, and prints the generated output. + +## Requirements + +- Node.js >= 18 or Bun +- A Jules API Key (`JULES_API_KEY` environment variable) + +## Setup + +1. Make sure you have installed the SDK dependencies in the project root by running `bun install`. +2. Build the SDK in `packages/core` by running `npm run build` inside the `packages/core` directory. + +3. Export your Jules API key: + +```bash +export JULES_API_KEY="your-api-key-here" +``` + +## Running the Example + +You can run the CLI tool using `bun` and passing your prompt as an argument: + +```bash +bun run index.ts "Translate 'Hello, how are you?' into French." +``` + +Using `npm` and `tsx` (or similar TypeScript runner): + +```bash +npx tsx index.ts "What is the capital of Australia?" +``` + +## What it does + +The script parses `process.argv` to get the user's prompt, creates a session using `jules.session`, and waits for the agent to complete. Once complete, it retrieves the generated files and the agent's messages, effectively acting as a simple, custom AI CLI tool powered by Jules. diff --git a/packages/core/examples/custom-cli/index.ts b/packages/core/examples/custom-cli/index.ts new file mode 100644 index 0000000..7d3c9cb --- /dev/null +++ b/packages/core/examples/custom-cli/index.ts @@ -0,0 +1,77 @@ +import { jules } from '@google/jules-sdk'; + +/** + * Custom CLI Tool Example + * + * Demonstrates how to build a simple command-line interface tool + * using the Jules SDK. This script accepts a prompt as an argument, + * executes it using a repoless Jules session, and prints the result. + */ +async function main() { + // 1. Parse command-line arguments to get the user prompt + const args = process.argv.slice(2); + const prompt = args.join(' ').trim(); + + // Validate the API key + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is not set.'); + console.error('Please set it using: export JULES_API_KEY="your-api-key"'); + process.exit(1); + } + + // Ensure a prompt was provided + if (!prompt) { + console.error('Usage: bun run index.ts '); + console.error('Example: bun run index.ts "Write a quick sorting algorithm in Python"'); + process.exit(1); + } + + console.log(`Executing: "${prompt}"...`); + + try { + // 2. Create a repoless session with the provided prompt + const session = await jules.session({ prompt }); + + console.log(`\nSession created! ID: ${session.id}`); + console.log('Waiting for completion (this may take a moment)...\n'); + + // 3. Await the final outcome of the session + const outcome = await session.result(); + + if (outcome.state === 'completed') { + // 4. Retrieve generated output or agent messages + const activities = await jules.select({ + from: 'activities', + where: { type: 'agentMessaged', 'session.id': session.id }, + order: 'desc', + limit: 1, + }); + + if (activities.length > 0) { + console.log('--- Agent Response ---'); + console.log(activities[0].message); + } else { + // Fallback: Check if there are generated files instead + const files = outcome.generatedFiles(); + if (files.size > 0) { + console.log('--- Generated Files ---'); + for (const [filename, content] of files.entries()) { + console.log(`\nFile: ${filename}`); + console.log(content.content); + } + } else { + console.log('The session completed, but no direct response or file output was found.'); + } + } + } else { + console.error(`Session finished with state: ${outcome.state}`); + console.error('The task could not be completed successfully.'); + } + } catch (error) { + console.error('An error occurred while communicating with Jules:', error); + process.exit(1); + } +} + +// Run the CLI +main(); \ No newline at end of file diff --git a/packages/core/examples/custom-cli/package.json b/packages/core/examples/custom-cli/package.json new file mode 100644 index 0000000..fb13fc7 --- /dev/null +++ b/packages/core/examples/custom-cli/package.json @@ -0,0 +1,12 @@ +{ + "name": "jules-custom-cli-example", + "version": "1.0.0", + "description": "Custom CLI Example for the Jules SDK", + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + } +} From 1dcb41308477f77632aa8b7686545b539d858b8c Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:11:29 +0000 Subject: [PATCH 2/5] feat: refactor custom CLI example for Agent DX - Implement auto-discovery pattern for commands mapping to reduce monoliths. - Add Typed Service Contract pattern (spec.ts/handler.ts) using Zod. - Update `session` command to accept strict JSON structures (agent-first) while retaining prompt flags (human-first). - Emit NDJSON payloads when `--output json` is requested. - Update documentation and constraints to guide users on proper AI agent integration paths as per feedback. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 3 + packages/core/examples/custom-cli/README.md | 37 +++++-- .../custom-cli/commands/session/handler.ts | 95 ++++++++++++++++ .../custom-cli/commands/session/index.ts | 79 +++++++++++++ .../custom-cli/commands/session/spec.ts | 20 ++++ packages/core/examples/custom-cli/index.ts | 104 +++++++----------- .../core/examples/custom-cli/package.json | 5 +- 7 files changed, 271 insertions(+), 72 deletions(-) create mode 100644 packages/core/examples/custom-cli/commands/session/handler.ts create mode 100644 packages/core/examples/custom-cli/commands/session/index.ts create mode 100644 packages/core/examples/custom-cli/commands/session/spec.ts diff --git a/bun.lock b/bun.lock index 4a9bd5a..0d78272 100644 --- a/bun.lock +++ b/bun.lock @@ -57,6 +57,9 @@ "version": "1.0.0", "dependencies": { "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "niftty": "^0.1.3", + "zod": "^3.24.0", }, }, "packages/core/examples/github-actions": { diff --git a/packages/core/examples/custom-cli/README.md b/packages/core/examples/custom-cli/README.md index f1a360c..4b2f716 100644 --- a/packages/core/examples/custom-cli/README.md +++ b/packages/core/examples/custom-cli/README.md @@ -1,6 +1,12 @@ # Custom CLI Tools Example -This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool takes a user prompt as an argument, uses a "Repoless" session to execute the task, and prints the generated output. +This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool uses `citty` for command structure and argument parsing, and `niftty` for rendering markdown and code outputs cleanly in the terminal. + +Crucially, this CLI is optimized for **Agent DX**. It follows best practices for building CLIs that are robust against agent hallucinations by: +- Employing auto-discovery for scaling commands. +- Defining a "Typed Service Contract" using Zod (`spec.ts` + `handler.ts`) for input hardening and API predictability. +- Exposing a raw `--json` flag so agents can map directly to schemas. +- Exposing an `--output json` flag so agents can parse outputs deterministically. ## Requirements @@ -20,18 +26,35 @@ export JULES_API_KEY="your-api-key-here" ## Running the Example -You can run the CLI tool using `bun` and passing your prompt as an argument: +The CLI supports both a **Human DX** (interactive, readable output) and an **Agent DX** (raw JSON payloads and responses). + +### Human DX + +You can run the CLI tool passing your prompt as an argument. The `citty` framework handles basic help flags automatically. + +```bash +bun run index.ts session --prompt="Translate 'Hello, how are you?' into French." +``` + +Or view the help text: ```bash -bun run index.ts "Translate 'Hello, how are you?' into French." +bun run index.ts --help +bun run index.ts session --help ``` -Using `npm` and `tsx` (or similar TypeScript runner): +### Agent DX + +Agents are prone to hallucination when creating strings but are very good at forming JSON matching strict schemas. For best results, expose `--json` flags. ```bash -npx tsx index.ts "What is the capital of Australia?" +bun run index.ts session --json='{"prompt": "List the files in the directory", "autoPr": false}' --output="json" ``` -## What it does +## Architecture -The script parses `process.argv` to get the user's prompt, creates a session using `jules.session`, and waits for the agent to complete. Once complete, it retrieves the generated files and the agent's messages, effectively acting as a simple, custom AI CLI tool powered by Jules. +This project splits its logic to avoid monolithic file structures and merge conflicts: +- **`index.ts`**: The auto-discovery entry point that dynamically mounts available sub-commands. +- **`commands/*/spec.ts`**: The Zod schema defining the strict Typed Service Contract for a tool. +- **`commands/*/handler.ts`**: The pure business logic that consumes the contract and never crashes directly, preferring structured return errors. +- **`commands/*/index.ts`**: The `citty` command definition that parses flags and outputs data back to the environment. diff --git a/packages/core/examples/custom-cli/commands/session/handler.ts b/packages/core/examples/custom-cli/commands/session/handler.ts new file mode 100644 index 0000000..03af583 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/session/handler.ts @@ -0,0 +1,95 @@ +import { jules } from '@google/jules-sdk'; +import { SessionRequest, SessionResponse, sessionRequestSchema } from './spec.js'; +import { z } from 'zod'; + +/** + * Validates the inputs to protect against hallucinated payloads from agents + * and executes the command logic. + */ +export async function handleSessionRequest(input: unknown): Promise { + try { + // 1. Input Hardening + // Protect against common agent hallucinations by enforcing a strict schema. + const validParams = sessionRequestSchema.parse(input); + + if (!process.env.JULES_API_KEY) { + return { + status: 'error', + error: 'JULES_API_KEY environment variable is not set.', + }; + } + + // Prepare session configuration based on inputs + const sessionConfig: any = { + prompt: validParams.prompt, + }; + + if (validParams.githubRepo) { + sessionConfig.source = { + github: validParams.githubRepo, + baseBranch: validParams.baseBranch || 'main', + }; + if (validParams.autoPr) { + sessionConfig.autoPr = validParams.autoPr; + } + } + + // Execute the core business logic (calling the Jules API) + const session = await jules.session(sessionConfig); + const outcome = await session.result(); + + // Process response based on field mask if requested to limit context size + let resultData: any = { + sessionId: session.id, + state: outcome.state, + }; + + if (outcome.state === 'completed') { + const snapshot = await session.snapshot(); + const agentMessages = snapshot.activities + .filter((a: any) => a.type === 'agentMessaged') + .sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); + + const files = outcome.generatedFiles(); + const fileData: Record = {}; + + for (const [filename, content] of files.entries()) { + fileData[filename] = content.content; + } + + resultData.agentMessages = agentMessages.map((m: any) => m.message); + resultData.files = fileData; + } + + // Extremely basic field masking - pick requested top-level fields + if (validParams.fields) { + const maskedData: any = {}; + const fields = validParams.fields.split(',').map(f => f.trim()); + for (const field of fields) { + if (resultData[field] !== undefined) { + maskedData[field] = resultData[field]; + } + } + resultData = maskedData; + } + + return { + status: 'success', + data: resultData, + }; + + } catch (error) { + if (error instanceof z.ZodError) { + return { + status: 'error', + error: `Validation Error: ${error.message}`, + }; + } + + const errMsg = error instanceof Error ? error.message : String(error); + return { + status: 'error', + error: errMsg, + }; + } +} diff --git a/packages/core/examples/custom-cli/commands/session/index.ts b/packages/core/examples/custom-cli/commands/session/index.ts new file mode 100644 index 0000000..fbfb9a6 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/session/index.ts @@ -0,0 +1,79 @@ +import { defineCommand } from 'citty'; +import { handleSessionRequest } from './handler.js'; +import { niftty } from 'niftty'; + +export default defineCommand({ + meta: { + name: 'session', + description: 'Executes a Jules Session, optimized for Agents.', + }, + args: { + json: { + type: 'string', + description: 'Raw JSON payload mapped directly to the API schema.', + }, + output: { + type: 'string', + description: 'Format of the output (e.g., "json" or "text"). Defaults to text for humans, but "json" is critical for agents.', + default: 'text', + }, + prompt: { + type: 'string', + description: 'Human-friendly flag for simple tasks.', + }, + }, + async run({ args }) { + let payload: any = {}; + + // Favor raw JSON payloads for agent predictability + if (args.json) { + try { + payload = JSON.parse(args.json); + } catch (err) { + console.error(JSON.stringify({ status: 'error', error: 'Invalid JSON payload format' })); + process.exit(1); + } + } else if (args.prompt) { + payload.prompt = args.prompt; + } else { + console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or --prompt' })); + process.exit(1); + } + + const isJsonOutput = args.output === 'json' || process.env.OUTPUT_FORMAT === 'json'; + + if (!isJsonOutput) { + console.log(`Executing session...\n`); + } + + // Call the Typed Service Contract handler + const response = await handleSessionRequest(payload); + + if (isJsonOutput) { + // Agent DX: Provide deterministic, machine-readable JSON + console.log(JSON.stringify(response, null, 2)); + } else { + // Human DX: Render readable output + if (response.status === 'error') { + console.error(`Error: ${response.error}`); + process.exit(1); + } + + if (response.data?.agentMessages?.length) { + console.log('--- Agent Response ---'); + console.log(niftty(response.data.agentMessages[0])); + } + + if (response.data?.files) { + for (const [filename, content] of Object.entries(response.data.files)) { + console.log(`\nFile: ${filename}`); + console.log(niftty(`\`\`\`\n${content}\n\`\`\``)); + } + } + } + + if (response.status === 'error') { + process.exit(1); + } + }, +}); diff --git a/packages/core/examples/custom-cli/commands/session/spec.ts b/packages/core/examples/custom-cli/commands/session/spec.ts new file mode 100644 index 0000000..d53d4a3 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/session/spec.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +export const sessionRequestSchema = z.object({ + prompt: z.string().min(1, 'Prompt cannot be empty'), + githubRepo: z.string().optional(), + baseBranch: z.string().optional(), + autoPr: z.boolean().optional().default(false), + fields: z.string().optional(), // field mask for limiting response size +}); + +export type SessionRequest = z.infer; + +export const sessionResponseSchema = z.object({ + status: z.enum(['success', 'error']), + message: z.string().optional(), + data: z.any().optional(), + error: z.string().optional(), +}); + +export type SessionResponse = z.infer; diff --git a/packages/core/examples/custom-cli/index.ts b/packages/core/examples/custom-cli/index.ts index 7d3c9cb..d696024 100644 --- a/packages/core/examples/custom-cli/index.ts +++ b/packages/core/examples/custom-cli/index.ts @@ -1,77 +1,53 @@ -import { jules } from '@google/jules-sdk'; +import { defineCommand, runMain } from 'citty'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -/** - * Custom CLI Tool Example - * - * Demonstrates how to build a simple command-line interface tool - * using the Jules SDK. This script accepts a prompt as an argument, - * executes it using a repoless Jules session, and prints the result. - */ -async function main() { - // 1. Parse command-line arguments to get the user prompt - const args = process.argv.slice(2); - const prompt = args.join(' ').trim(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); - // Validate the API key - if (!process.env.JULES_API_KEY) { - console.error('Error: JULES_API_KEY environment variable is not set.'); - console.error('Please set it using: export JULES_API_KEY="your-api-key"'); - process.exit(1); - } - - // Ensure a prompt was provided - if (!prompt) { - console.error('Usage: bun run index.ts '); - console.error('Example: bun run index.ts "Write a quick sorting algorithm in Python"'); - process.exit(1); - } - - console.log(`Executing: "${prompt}"...`); +async function loadCommands() { + const commandsDir = path.join(__dirname, 'commands'); + const commands: Record = {}; try { - // 2. Create a repoless session with the provided prompt - const session = await jules.session({ prompt }); - - console.log(`\nSession created! ID: ${session.id}`); - console.log('Waiting for completion (this may take a moment)...\n'); - - // 3. Await the final outcome of the session - const outcome = await session.result(); + const entries = await fs.readdir(commandsDir, { withFileTypes: true }); - if (outcome.state === 'completed') { - // 4. Retrieve generated output or agent messages - const activities = await jules.select({ - from: 'activities', - where: { type: 'agentMessaged', 'session.id': session.id }, - order: 'desc', - limit: 1, - }); + for (const entry of entries) { + if (entry.isDirectory()) { + const commandPath = path.join(commandsDir, entry.name, 'index.ts'); - if (activities.length > 0) { - console.log('--- Agent Response ---'); - console.log(activities[0].message); - } else { - // Fallback: Check if there are generated files instead - const files = outcome.generatedFiles(); - if (files.size > 0) { - console.log('--- Generated Files ---'); - for (const [filename, content] of files.entries()) { - console.log(`\nFile: ${filename}`); - console.log(content.content); + try { + await fs.access(commandPath); + const commandModule = await import(`./commands/${entry.name}/index.ts`); + if (commandModule.default) { + commands[entry.name] = commandModule.default; } - } else { - console.log('The session completed, but no direct response or file output was found.'); + } catch (e) { + // Ignore if index.ts doesn't exist in the folder } } - } else { - console.error(`Session finished with state: ${outcome.state}`); - console.error('The task could not be completed successfully.'); } - } catch (error) { - console.error('An error occurred while communicating with Jules:', error); - process.exit(1); + } catch (e) { + console.error('Failed to load commands:', e); } + + return commands; +} + +async function start() { + const subCommands = await loadCommands(); + + const main = defineCommand({ + meta: { + name: 'jules-cli', + version: '1.0.0', + description: 'A custom AI CLI tool optimized for Agent DX using the Jules SDK', + }, + subCommands, + }); + + runMain(main); } -// Run the CLI -main(); \ No newline at end of file +start(); diff --git a/packages/core/examples/custom-cli/package.json b/packages/core/examples/custom-cli/package.json index fb13fc7..a515651 100644 --- a/packages/core/examples/custom-cli/package.json +++ b/packages/core/examples/custom-cli/package.json @@ -7,6 +7,9 @@ "start": "bun run index.ts" }, "dependencies": { - "@google/jules-sdk": "workspace:*" + "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "niftty": "^0.1.3", + "zod": "^3.24.0" } } From 5f16408e7062555351adab4d09eb813e3447be44 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:20:28 +0000 Subject: [PATCH 3/5] feat: create a custom filesystem CLI example - Replaced basic API wrapper example with `generate-test` which interacts with the local file system. - Reads a local source file, issues a prompt for test generation via Jules, and writes the test file back to disk locally. - Retained Agent DX features (auto-discovery, strictly typed Zod contracts, and `--json` raw payloads). - Ensured all tests continue to pass. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/examples/custom-cli/README.md | 26 ++- .../commands/generate-test/handler.ts | 148 ++++++++++++++++++ .../{session => generate-test}/index.ts | 50 +++--- .../custom-cli/commands/generate-test/spec.ts | 24 +++ .../custom-cli/commands/session/handler.ts | 95 ----------- .../custom-cli/commands/session/spec.ts | 20 --- 6 files changed, 221 insertions(+), 142 deletions(-) create mode 100644 packages/core/examples/custom-cli/commands/generate-test/handler.ts rename packages/core/examples/custom-cli/commands/{session => generate-test}/index.ts (52%) create mode 100644 packages/core/examples/custom-cli/commands/generate-test/spec.ts delete mode 100644 packages/core/examples/custom-cli/commands/session/handler.ts delete mode 100644 packages/core/examples/custom-cli/commands/session/spec.ts diff --git a/packages/core/examples/custom-cli/README.md b/packages/core/examples/custom-cli/README.md index 4b2f716..eafa40c 100644 --- a/packages/core/examples/custom-cli/README.md +++ b/packages/core/examples/custom-cli/README.md @@ -1,6 +1,8 @@ # Custom CLI Tools Example -This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool uses `citty` for command structure and argument parsing, and `niftty` for rendering markdown and code outputs cleanly in the terminal. +This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool integrates with the user's **local file system**, demonstrating a practical, custom utility beyond just wrapping the API. + +It uses `citty` for command structure, `niftty` for terminal rendering, and the native Node.js `fs` module to orchestrate tasks locally. Crucially, this CLI is optimized for **Agent DX**. It follows best practices for building CLIs that are robust against agent hallucinations by: - Employing auto-discovery for scaling commands. @@ -26,21 +28,29 @@ export JULES_API_KEY="your-api-key-here" ## Running the Example -The CLI supports both a **Human DX** (interactive, readable output) and an **Agent DX** (raw JSON payloads and responses). +The primary utility included in this example is `generate-test`. It reads a local source file, asks Jules to write unit tests for it, and then writes the generated test file directly back to your local file system, adjacent to the source file. + +It supports both a **Human DX** (interactive, readable output) and an **Agent DX** (raw JSON payloads and responses). ### Human DX -You can run the CLI tool passing your prompt as an argument. The `citty` framework handles basic help flags automatically. +You can run the CLI tool passing flags. The `citty` framework handles basic help flags automatically. + +```bash +bun run index.ts generate-test --filepath="./src/math.ts" --framework="jest" +``` + +Use `--dry-run` to see what would be generated without writing to disk: ```bash -bun run index.ts session --prompt="Translate 'Hello, how are you?' into French." +bun run index.ts generate-test --filepath="./src/math.ts" --dry-run ``` -Or view the help text: +View the help text: ```bash bun run index.ts --help -bun run index.ts session --help +bun run index.ts generate-test --help ``` ### Agent DX @@ -48,7 +58,7 @@ bun run index.ts session --help Agents are prone to hallucination when creating strings but are very good at forming JSON matching strict schemas. For best results, expose `--json` flags. ```bash -bun run index.ts session --json='{"prompt": "List the files in the directory", "autoPr": false}' --output="json" +bun run index.ts generate-test --json='{"filepath": "./src/math.ts", "testFramework": "vitest", "dryRun": true}' --output="json" ``` ## Architecture @@ -56,5 +66,5 @@ bun run index.ts session --json='{"prompt": "List the files in the directory", " This project splits its logic to avoid monolithic file structures and merge conflicts: - **`index.ts`**: The auto-discovery entry point that dynamically mounts available sub-commands. - **`commands/*/spec.ts`**: The Zod schema defining the strict Typed Service Contract for a tool. -- **`commands/*/handler.ts`**: The pure business logic that consumes the contract and never crashes directly, preferring structured return errors. +- **`commands/*/handler.ts`**: The pure business logic that consumes the contract, interacts with the local file system, and never crashes directly, preferring structured return errors. - **`commands/*/index.ts`**: The `citty` command definition that parses flags and outputs data back to the environment. diff --git a/packages/core/examples/custom-cli/commands/generate-test/handler.ts b/packages/core/examples/custom-cli/commands/generate-test/handler.ts new file mode 100644 index 0000000..65e76e1 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/generate-test/handler.ts @@ -0,0 +1,148 @@ +import { jules } from '@google/jules-sdk'; +import { GenerateTestRequest, GenerateTestResponse, generateTestRequestSchema } from './spec.js'; +import { z } from 'zod'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/** + * Reads a local file, sends its contents to Jules to generate a test file, + * and writes the result back to the user's local filesystem. + */ +export async function handleGenerateTestRequest(input: unknown): Promise { + try { + // 1. Input Hardening + const validParams = generateTestRequestSchema.parse(input); + + if (!process.env.JULES_API_KEY) { + return { + status: 'error', + error: 'JULES_API_KEY environment variable is not set.', + }; + } + + // Resolve the file path relative to cwd + const targetFilePath = path.resolve(process.cwd(), validParams.filepath); + + // Ensure the file exists + let sourceContent: string; + try { + sourceContent = await fs.readFile(targetFilePath, 'utf-8'); + } catch (e: any) { + return { + status: 'error', + error: `Failed to read source file at ${targetFilePath}: ${e.message}`, + }; + } + + // Prepare prompt + const parsedPath = path.parse(targetFilePath); + const expectedTestFilename = `${parsedPath.name}.test${parsedPath.ext}`; + + let userInstructions = validParams.instructions + ? `\n\nAdditional Instructions:\n${validParams.instructions}` + : ''; + + const prompt = `You are an expert test engineer. Write comprehensive unit tests for the following file. +Use the testing framework: ${validParams.testFramework} + +Target Filename: ${parsedPath.base} +Target File Content: +\`\`\` +${sourceContent} +\`\`\`${userInstructions} + +Return ONLY the code for the test file named \`${expectedTestFilename}\`. Do not provide conversational filler.`; + + // Execute session + const session = await jules.session({ prompt }); + const outcome = await session.result(); + + if (outcome.state !== 'completed') { + return { + status: 'error', + error: `Jules session did not complete successfully. Status: ${outcome.state}`, + }; + } + + // Attempt to extract the generated test file from the generatedFiles map + const files = outcome.generatedFiles(); + let generatedTestCode: string | null = null; + let finalTestFilename = expectedTestFilename; + + // Look for the explicitly named test file + if (files.has(expectedTestFilename)) { + generatedTestCode = files.get(expectedTestFilename)!.content; + } else if (files.size > 0) { + // Fallback: Just grab the first generated file if names don't match + const firstEntry = Array.from(files.entries())[0]; + finalTestFilename = firstEntry[0]; + generatedTestCode = firstEntry[1].content; + } else { + // Fallback 2: The agent might have just messaged the code back + const snapshot = await session.snapshot(); + const agentMessages = snapshot.activities + .filter((a: any) => a.type === 'agentMessaged') + .sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); + + if (agentMessages.length > 0) { + const message = agentMessages[0].message; + // Extract code block + const match = message.match(/\`\`\`[a-zA-Z]*\n([\s\S]*?)\n\`\`\`/); + if (match) { + generatedTestCode = match[1]; + } else { + generatedTestCode = message; // Hope for the best + } + } + } + + if (!generatedTestCode) { + return { + status: 'error', + error: 'Failed to extract generated test code from Jules response.', + }; + } + + // Calculate destination path (e.g., adjacent to the source file) + const testDestinationPath = path.join(parsedPath.dir, finalTestFilename); + + // If it's not a dry run, actually write to the filesystem + if (!validParams.dryRun) { + try { + await fs.writeFile(testDestinationPath, generatedTestCode, 'utf-8'); + } catch (e: any) { + return { + status: 'error', + error: `Failed to write test file to ${testDestinationPath}: ${e.message}`, + }; + } + } + + return { + status: 'success', + message: validParams.dryRun + ? `[DRY-RUN] Would have written test file to ${testDestinationPath}` + : `Successfully wrote test file to ${testDestinationPath}`, + data: { + sourceFile: targetFilePath, + testFile: testDestinationPath, + content: generatedTestCode, + dryRun: validParams.dryRun, + } + }; + + } catch (error) { + if (error instanceof z.ZodError) { + return { + status: 'error', + error: `Validation Error: ${error.message}`, + }; + } + + const errMsg = error instanceof Error ? error.message : String(error); + return { + status: 'error', + error: errMsg, + }; + } +} diff --git a/packages/core/examples/custom-cli/commands/session/index.ts b/packages/core/examples/custom-cli/commands/generate-test/index.ts similarity index 52% rename from packages/core/examples/custom-cli/commands/session/index.ts rename to packages/core/examples/custom-cli/commands/generate-test/index.ts index fbfb9a6..a231f32 100644 --- a/packages/core/examples/custom-cli/commands/session/index.ts +++ b/packages/core/examples/custom-cli/commands/generate-test/index.ts @@ -1,11 +1,11 @@ import { defineCommand } from 'citty'; -import { handleSessionRequest } from './handler.js'; +import { handleGenerateTestRequest } from './handler.js'; import { niftty } from 'niftty'; export default defineCommand({ meta: { - name: 'session', - description: 'Executes a Jules Session, optimized for Agents.', + name: 'generate-test', + description: 'Reads a local source file and automatically generates a unit test file next to it.', }, args: { json: { @@ -17,9 +17,23 @@ export default defineCommand({ description: 'Format of the output (e.g., "json" or "text"). Defaults to text for humans, but "json" is critical for agents.', default: 'text', }, - prompt: { + filepath: { type: 'string', - description: 'Human-friendly flag for simple tasks.', + description: 'Human-friendly flag for specifying the file path.', + }, + framework: { + type: 'string', + description: 'Testing framework to use (e.g. vitest, jest). Default: vitest', + default: 'vitest', + }, + instructions: { + type: 'string', + description: 'Additional instructions for the agent.', + }, + 'dry-run': { + type: 'boolean', + description: 'Generate the test and print it to the console, but do not write it to disk.', + default: false, }, }, async run({ args }) { @@ -33,21 +47,24 @@ export default defineCommand({ console.error(JSON.stringify({ status: 'error', error: 'Invalid JSON payload format' })); process.exit(1); } - } else if (args.prompt) { - payload.prompt = args.prompt; + } else if (args.filepath) { + payload.filepath = args.filepath; + payload.testFramework = args.framework; + if (args.instructions) payload.instructions = args.instructions; + if (args['dry-run']) payload.dryRun = true; } else { - console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or --prompt' })); + console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or --filepath' })); process.exit(1); } const isJsonOutput = args.output === 'json' || process.env.OUTPUT_FORMAT === 'json'; if (!isJsonOutput) { - console.log(`Executing session...\n`); + console.log(`Analyzing file and generating test suite...\n`); } // Call the Typed Service Contract handler - const response = await handleSessionRequest(payload); + const response = await handleGenerateTestRequest(payload); if (isJsonOutput) { // Agent DX: Provide deterministic, machine-readable JSON @@ -59,16 +76,11 @@ export default defineCommand({ process.exit(1); } - if (response.data?.agentMessages?.length) { - console.log('--- Agent Response ---'); - console.log(niftty(response.data.agentMessages[0])); - } + console.log(response.message); - if (response.data?.files) { - for (const [filename, content] of Object.entries(response.data.files)) { - console.log(`\nFile: ${filename}`); - console.log(niftty(`\`\`\`\n${content}\n\`\`\``)); - } + if (response.data?.content && args['dry-run']) { + console.log('\n--- Generated Test Code ---'); + console.log(niftty(`\`\`\`\n${response.data.content}\n\`\`\``)); } } diff --git a/packages/core/examples/custom-cli/commands/generate-test/spec.ts b/packages/core/examples/custom-cli/commands/generate-test/spec.ts new file mode 100644 index 0000000..1fc58dd --- /dev/null +++ b/packages/core/examples/custom-cli/commands/generate-test/spec.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +export const generateTestRequestSchema = z.object({ + filepath: z.string().min(1, 'Filepath is required to generate tests for'), + testFramework: z.string().optional().default('vitest'), + instructions: z.string().optional(), + dryRun: z.boolean().optional().default(false), +}); + +export type GenerateTestRequest = z.infer; + +export const generateTestResponseSchema = z.object({ + status: z.enum(['success', 'error']), + message: z.string().optional(), + data: z.object({ + sourceFile: z.string().optional(), + testFile: z.string().optional(), + content: z.string().optional(), + dryRun: z.boolean().optional(), + }).optional(), + error: z.string().optional(), +}); + +export type GenerateTestResponse = z.infer; diff --git a/packages/core/examples/custom-cli/commands/session/handler.ts b/packages/core/examples/custom-cli/commands/session/handler.ts deleted file mode 100644 index 03af583..0000000 --- a/packages/core/examples/custom-cli/commands/session/handler.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { jules } from '@google/jules-sdk'; -import { SessionRequest, SessionResponse, sessionRequestSchema } from './spec.js'; -import { z } from 'zod'; - -/** - * Validates the inputs to protect against hallucinated payloads from agents - * and executes the command logic. - */ -export async function handleSessionRequest(input: unknown): Promise { - try { - // 1. Input Hardening - // Protect against common agent hallucinations by enforcing a strict schema. - const validParams = sessionRequestSchema.parse(input); - - if (!process.env.JULES_API_KEY) { - return { - status: 'error', - error: 'JULES_API_KEY environment variable is not set.', - }; - } - - // Prepare session configuration based on inputs - const sessionConfig: any = { - prompt: validParams.prompt, - }; - - if (validParams.githubRepo) { - sessionConfig.source = { - github: validParams.githubRepo, - baseBranch: validParams.baseBranch || 'main', - }; - if (validParams.autoPr) { - sessionConfig.autoPr = validParams.autoPr; - } - } - - // Execute the core business logic (calling the Jules API) - const session = await jules.session(sessionConfig); - const outcome = await session.result(); - - // Process response based on field mask if requested to limit context size - let resultData: any = { - sessionId: session.id, - state: outcome.state, - }; - - if (outcome.state === 'completed') { - const snapshot = await session.snapshot(); - const agentMessages = snapshot.activities - .filter((a: any) => a.type === 'agentMessaged') - .sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); - - const files = outcome.generatedFiles(); - const fileData: Record = {}; - - for (const [filename, content] of files.entries()) { - fileData[filename] = content.content; - } - - resultData.agentMessages = agentMessages.map((m: any) => m.message); - resultData.files = fileData; - } - - // Extremely basic field masking - pick requested top-level fields - if (validParams.fields) { - const maskedData: any = {}; - const fields = validParams.fields.split(',').map(f => f.trim()); - for (const field of fields) { - if (resultData[field] !== undefined) { - maskedData[field] = resultData[field]; - } - } - resultData = maskedData; - } - - return { - status: 'success', - data: resultData, - }; - - } catch (error) { - if (error instanceof z.ZodError) { - return { - status: 'error', - error: `Validation Error: ${error.message}`, - }; - } - - const errMsg = error instanceof Error ? error.message : String(error); - return { - status: 'error', - error: errMsg, - }; - } -} diff --git a/packages/core/examples/custom-cli/commands/session/spec.ts b/packages/core/examples/custom-cli/commands/session/spec.ts deleted file mode 100644 index d53d4a3..0000000 --- a/packages/core/examples/custom-cli/commands/session/spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from 'zod'; - -export const sessionRequestSchema = z.object({ - prompt: z.string().min(1, 'Prompt cannot be empty'), - githubRepo: z.string().optional(), - baseBranch: z.string().optional(), - autoPr: z.boolean().optional().default(false), - fields: z.string().optional(), // field mask for limiting response size -}); - -export type SessionRequest = z.infer; - -export const sessionResponseSchema = z.object({ - status: z.enum(['success', 'error']), - message: z.string().optional(), - data: z.any().optional(), - error: z.string().optional(), -}); - -export type SessionResponse = z.infer; From 5b54abbcdeac9ab5c9f5afdbeb822ac824314351 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:42:19 +0000 Subject: [PATCH 4/5] feat: refactor custom CLI example to use cloud-worker utility - Removed basic `generate-test` CLI and replaced it with a bold `cloud-worker` implementation. - Demonstrates Jules repoless sessions acting as powerful serverless containers for scripts (e.g., Python scraping, data analysis). - Ingests local files, executes tasks, and writes processed results back to the user's local disk. - Adheres to best practices: Typed Service Contracts via Zod, auto-discovery using `citty`, and full support for agent `--json` payloads. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/examples/custom-cli/README.md | 34 ++-- .../commands/cloud-worker/handler.ts | 155 ++++++++++++++++++ .../{generate-test => cloud-worker}/index.ts | 45 ++--- .../custom-cli/commands/cloud-worker/spec.ts | 25 +++ .../commands/generate-test/handler.ts | 148 ----------------- .../custom-cli/commands/generate-test/spec.ts | 24 --- 6 files changed, 222 insertions(+), 209 deletions(-) create mode 100644 packages/core/examples/custom-cli/commands/cloud-worker/handler.ts rename packages/core/examples/custom-cli/commands/{generate-test => cloud-worker}/index.ts (54%) create mode 100644 packages/core/examples/custom-cli/commands/cloud-worker/spec.ts delete mode 100644 packages/core/examples/custom-cli/commands/generate-test/handler.ts delete mode 100644 packages/core/examples/custom-cli/commands/generate-test/spec.ts diff --git a/packages/core/examples/custom-cli/README.md b/packages/core/examples/custom-cli/README.md index eafa40c..1e817d1 100644 --- a/packages/core/examples/custom-cli/README.md +++ b/packages/core/examples/custom-cli/README.md @@ -1,8 +1,8 @@ # Custom CLI Tools Example -This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool integrates with the user's **local file system**, demonstrating a practical, custom utility beyond just wrapping the API. +This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool integrates with the user's **local file system** while treating repoless Jules sessions as **powerful, autonomous serverless containers**. -It uses `citty` for command structure, `niftty` for terminal rendering, and the native Node.js `fs` module to orchestrate tasks locally. +It uses `citty` for command structure, `niftty` for terminal rendering, and the native Node.js `fs` module to orchestrate moving data between your local machine and the cloud. Crucially, this CLI is optimized for **Agent DX**. It follows best practices for building CLIs that are robust against agent hallucinations by: - Employing auto-discovery for scaling commands. @@ -26,31 +26,33 @@ Crucially, this CLI is optimized for **Agent DX**. It follows best practices for export JULES_API_KEY="your-api-key-here" ``` -## Running the Example +## Running the Example: The Cloud Worker -The primary utility included in this example is `generate-test`. It reads a local source file, asks Jules to write unit tests for it, and then writes the generated test file directly back to your local file system, adjacent to the source file. +The primary utility included in this example is `cloud-worker`. Instead of just talking to an LLM, this tool treats the Jules session as a sandbox where an agent can **write and execute Python or Node.js scripts**. -It supports both a **Human DX** (interactive, readable output) and an **Agent DX** (raw JSON payloads and responses). +You can pass a local file to the cloud container, ask the worker to run complex analysis, scrape websites, or convert data formats, and it will write the final processed file back to your local machine. -### Human DX - -You can run the CLI tool passing flags. The `citty` framework handles basic help flags automatically. +### Bold Use Cases +- **Data Analysis**: `--input "sales.csv" --task "Use Python pandas to aggregate sales by month and calculate the moving average." --output-file "report.json"` +- **Web Scraping**: `--task "Write a Node.js puppeteer script to scrape the headlines from news.ycombinator.com and output them as JSON." --output-file "hn.json"` +- **Format Conversion**: `--input "old_config.xml" --task "Write a python script to parse this XML and convert it to a modern YAML structure." --output-file "new_config.yaml"` -```bash -bun run index.ts generate-test --filepath="./src/math.ts" --framework="jest" -``` +### Human DX -Use `--dry-run` to see what would be generated without writing to disk: +You can run the CLI tool passing standard flags. ```bash -bun run index.ts generate-test --filepath="./src/math.ts" --dry-run +bun run index.ts cloud-worker \ + --input="./raw_data.csv" \ + --task="Use python pandas to clean the missing values and output as JSON." \ + --output-file="./cleaned_data.json" ``` View the help text: ```bash bun run index.ts --help -bun run index.ts generate-test --help +bun run index.ts cloud-worker --help ``` ### Agent DX @@ -58,7 +60,7 @@ bun run index.ts generate-test --help Agents are prone to hallucination when creating strings but are very good at forming JSON matching strict schemas. For best results, expose `--json` flags. ```bash -bun run index.ts generate-test --json='{"filepath": "./src/math.ts", "testFramework": "vitest", "dryRun": true}' --output="json" +bun run index.ts cloud-worker --json='{"task": "Scrape the current temperature in Paris using a python script", "outputFile": "./temp.json"}' --output="json" ``` ## Architecture @@ -66,5 +68,5 @@ bun run index.ts generate-test --json='{"filepath": "./src/math.ts", "testFramew This project splits its logic to avoid monolithic file structures and merge conflicts: - **`index.ts`**: The auto-discovery entry point that dynamically mounts available sub-commands. - **`commands/*/spec.ts`**: The Zod schema defining the strict Typed Service Contract for a tool. -- **`commands/*/handler.ts`**: The pure business logic that consumes the contract, interacts with the local file system, and never crashes directly, preferring structured return errors. +- **`commands/*/handler.ts`**: The pure business logic that consumes the contract, maps local data into the cloud, extracts results, and never crashes directly. - **`commands/*/index.ts`**: The `citty` command definition that parses flags and outputs data back to the environment. diff --git a/packages/core/examples/custom-cli/commands/cloud-worker/handler.ts b/packages/core/examples/custom-cli/commands/cloud-worker/handler.ts new file mode 100644 index 0000000..12eedb6 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/cloud-worker/handler.ts @@ -0,0 +1,155 @@ +import { jules } from '@google/jules-sdk'; +import { CloudWorkerRequest, CloudWorkerResponse, cloudWorkerRequestSchema } from './spec.js'; +import { z } from 'zod'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +/** + * Treats Jules as a powerful, on-demand serverless worker. + * Sends local file context to a cloud environment where an AI agent + * runs scripts (e.g., Python scraping, data analysis), and writes the + * final processed output back to the local file system. + */ +export async function handleCloudWorkerRequest(input: unknown): Promise { + try { + // 1. Input Hardening + const validParams = cloudWorkerRequestSchema.parse(input); + + if (!process.env.JULES_API_KEY) { + return { + status: 'error', + error: 'JULES_API_KEY environment variable is not set.', + }; + } + + let fileContext = ''; + + // 2. Local File Context Integration + if (validParams.inputFile) { + const inputFilePath = path.resolve(process.cwd(), validParams.inputFile); + try { + const content = await fs.readFile(inputFilePath, 'utf-8'); + const parsedPath = path.parse(inputFilePath); + + fileContext = ` +## Input Data +You have been provided with the following data from a local file named \`${parsedPath.base}\`: + +\`\`\` +${content} +\`\`\` + `; + } catch (e: any) { + return { + status: 'error', + error: `Failed to read input file at ${inputFilePath}: ${e.message}`, + }; + } + } + + // 3. Formulate the "Serverless Worker" Prompt + // We strictly instruct the agent on its environment capabilities and output constraints. + const EXPECTED_OUTPUT_FILE = 'final_output.txt'; + const prompt = ` +You are an autonomous Cloud Worker operating within a secure serverless container. +You have access to a full Linux environment with Node.js, Python, Rust, and Bun installed. +You have unrestricted outbound internet access. + +## Your Task +${validParams.task} +${fileContext} + +## Instructions +1. You may write and execute scripts (e.g., Python, Node) to solve this task. This includes scraping websites, processing data, querying APIs, or running analysis. +2. DO NOT just write the script and ask me to run it. YOU MUST run the script yourself in your container to get the final result. +3. Install any necessary dependencies using your environment's package managers (npm, pip). +4. Once you have the final, processed result for the user's task, you MUST write that final text/JSON result to a file named \`${EXPECTED_OUTPUT_FILE}\` in your current working directory. +5. Do not include conversational filler in \`${EXPECTED_OUTPUT_FILE}\`, only the exact output requested by the task. + +Remember: The success of this task relies entirely on you generating and populating \`${EXPECTED_OUTPUT_FILE}\`. + `; + + // 4. Delegate to the Jules SDK Cloud Session + const session = await jules.session({ prompt }); + const outcome = await session.result(); + + if (outcome.state !== 'completed') { + return { + status: 'error', + error: `The cloud worker session failed or timed out. Status: ${outcome.state}`, + }; + } + + // 5. Retrieve the requested output file + const files = outcome.generatedFiles(); + let finalOutputContent: string | null = null; + + if (files.has(EXPECTED_OUTPUT_FILE)) { + finalOutputContent = files.get(EXPECTED_OUTPUT_FILE)!.content; + } else { + // Fallback: search for any generated file if the agent ignored instructions + if (files.size > 0) { + const firstFile = Array.from(files.values())[0]; + finalOutputContent = firstFile.content; + } else { + // Fallback 2: Check messages if the agent just messaged the response instead of writing to disk + const snapshot = await session.snapshot(); + const agentMessages = snapshot.activities + .filter((a: any) => a.type === 'agentMessaged') + .sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); + + if (agentMessages.length > 0) { + finalOutputContent = agentMessages[0].message; + } + } + } + + if (!finalOutputContent) { + return { + status: 'error', + error: `Cloud worker completed but failed to produce the expected output data.`, + }; + } + + // 6. Write to the local file system + const targetOutputPath = path.resolve(process.cwd(), validParams.outputFile); + + if (!validParams.dryRun) { + try { + await fs.writeFile(targetOutputPath, finalOutputContent, 'utf-8'); + } catch (e: any) { + return { + status: 'error', + error: `Failed to write output to ${targetOutputPath}: ${e.message}`, + }; + } + } + + return { + status: 'success', + message: validParams.dryRun + ? `[DRY-RUN] Would have written processed output to ${targetOutputPath}` + : `Successfully wrote processed output to ${targetOutputPath}`, + data: { + sessionId: session.id, + outputFile: targetOutputPath, + contentPreview: finalOutputContent.substring(0, 500) + (finalOutputContent.length > 500 ? '...' : ''), + dryRun: validParams.dryRun, + } + }; + + } catch (error) { + if (error instanceof z.ZodError) { + return { + status: 'error', + error: `Validation Error: ${error.message}`, + }; + } + + const errMsg = error instanceof Error ? error.message : String(error); + return { + status: 'error', + error: errMsg, + }; + } +} diff --git a/packages/core/examples/custom-cli/commands/generate-test/index.ts b/packages/core/examples/custom-cli/commands/cloud-worker/index.ts similarity index 54% rename from packages/core/examples/custom-cli/commands/generate-test/index.ts rename to packages/core/examples/custom-cli/commands/cloud-worker/index.ts index a231f32..980c000 100644 --- a/packages/core/examples/custom-cli/commands/generate-test/index.ts +++ b/packages/core/examples/custom-cli/commands/cloud-worker/index.ts @@ -1,11 +1,11 @@ import { defineCommand } from 'citty'; -import { handleGenerateTestRequest } from './handler.js'; +import { handleCloudWorkerRequest } from './handler.js'; import { niftty } from 'niftty'; export default defineCommand({ meta: { - name: 'generate-test', - description: 'Reads a local source file and automatically generates a unit test file next to it.', + name: 'cloud-worker', + description: 'Offloads complex tasks (web scraping, data analysis, scripting) to an autonomous serverless container.', }, args: { json: { @@ -17,22 +17,21 @@ export default defineCommand({ description: 'Format of the output (e.g., "json" or "text"). Defaults to text for humans, but "json" is critical for agents.', default: 'text', }, - filepath: { + task: { type: 'string', - description: 'Human-friendly flag for specifying the file path.', + description: 'A description of the complex task or script you want the worker to execute in the cloud.', }, - framework: { + input: { type: 'string', - description: 'Testing framework to use (e.g. vitest, jest). Default: vitest', - default: 'vitest', + description: 'Optional path to a local file containing data you want to send to the worker.', }, - instructions: { + 'output-file': { type: 'string', - description: 'Additional instructions for the agent.', + description: 'Path where the worker should save the final processed result locally.', }, 'dry-run': { type: 'boolean', - description: 'Generate the test and print it to the console, but do not write it to disk.', + description: 'Execute the worker and fetch the result, but do not write it to the local disk.', default: false, }, }, @@ -47,24 +46,28 @@ export default defineCommand({ console.error(JSON.stringify({ status: 'error', error: 'Invalid JSON payload format' })); process.exit(1); } - } else if (args.filepath) { - payload.filepath = args.filepath; - payload.testFramework = args.framework; - if (args.instructions) payload.instructions = args.instructions; + } else if (args.task && args['output-file']) { + payload.task = args.task; + payload.outputFile = args['output-file']; + if (args.input) payload.inputFile = args.input; if (args['dry-run']) payload.dryRun = true; } else { - console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or --filepath' })); + console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or both --task and --output-file' })); process.exit(1); } const isJsonOutput = args.output === 'json' || process.env.OUTPUT_FORMAT === 'json'; if (!isJsonOutput) { - console.log(`Analyzing file and generating test suite...\n`); + console.log(`\n☁️ Sending task to Cloud Worker container...\n`); + if (payload.inputFile) { + console.log(`Uploading local context: ${payload.inputFile}`); + } + console.log(`Waiting for worker to run scripts and return final output...\n`); } // Call the Typed Service Contract handler - const response = await handleGenerateTestRequest(payload); + const response = await handleCloudWorkerRequest(payload); if (isJsonOutput) { // Agent DX: Provide deterministic, machine-readable JSON @@ -78,9 +81,9 @@ export default defineCommand({ console.log(response.message); - if (response.data?.content && args['dry-run']) { - console.log('\n--- Generated Test Code ---'); - console.log(niftty(`\`\`\`\n${response.data.content}\n\`\`\``)); + if (response.data?.contentPreview) { + console.log('\n--- Output Preview ---'); + console.log(niftty(`\`\`\`\n${response.data.contentPreview}\n\`\`\``)); } } diff --git a/packages/core/examples/custom-cli/commands/cloud-worker/spec.ts b/packages/core/examples/custom-cli/commands/cloud-worker/spec.ts new file mode 100644 index 0000000..8326616 --- /dev/null +++ b/packages/core/examples/custom-cli/commands/cloud-worker/spec.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +export const cloudWorkerRequestSchema = z.object({ + task: z.string().min(1, 'Task description is required.'), + inputFile: z.string().optional(), + outputFile: z.string().min(1, 'Output file path is required to save the result.'), + timeoutMins: z.number().optional().default(5), + dryRun: z.boolean().optional().default(false), +}); + +export type CloudWorkerRequest = z.infer; + +export const cloudWorkerResponseSchema = z.object({ + status: z.enum(['success', 'error']), + message: z.string().optional(), + data: z.object({ + outputFile: z.string().optional(), + sessionId: z.string().optional(), + contentPreview: z.string().optional(), + dryRun: z.boolean().optional(), + }).optional(), + error: z.string().optional(), +}); + +export type CloudWorkerResponse = z.infer; diff --git a/packages/core/examples/custom-cli/commands/generate-test/handler.ts b/packages/core/examples/custom-cli/commands/generate-test/handler.ts deleted file mode 100644 index 65e76e1..0000000 --- a/packages/core/examples/custom-cli/commands/generate-test/handler.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { jules } from '@google/jules-sdk'; -import { GenerateTestRequest, GenerateTestResponse, generateTestRequestSchema } from './spec.js'; -import { z } from 'zod'; -import fs from 'node:fs/promises'; -import path from 'node:path'; - -/** - * Reads a local file, sends its contents to Jules to generate a test file, - * and writes the result back to the user's local filesystem. - */ -export async function handleGenerateTestRequest(input: unknown): Promise { - try { - // 1. Input Hardening - const validParams = generateTestRequestSchema.parse(input); - - if (!process.env.JULES_API_KEY) { - return { - status: 'error', - error: 'JULES_API_KEY environment variable is not set.', - }; - } - - // Resolve the file path relative to cwd - const targetFilePath = path.resolve(process.cwd(), validParams.filepath); - - // Ensure the file exists - let sourceContent: string; - try { - sourceContent = await fs.readFile(targetFilePath, 'utf-8'); - } catch (e: any) { - return { - status: 'error', - error: `Failed to read source file at ${targetFilePath}: ${e.message}`, - }; - } - - // Prepare prompt - const parsedPath = path.parse(targetFilePath); - const expectedTestFilename = `${parsedPath.name}.test${parsedPath.ext}`; - - let userInstructions = validParams.instructions - ? `\n\nAdditional Instructions:\n${validParams.instructions}` - : ''; - - const prompt = `You are an expert test engineer. Write comprehensive unit tests for the following file. -Use the testing framework: ${validParams.testFramework} - -Target Filename: ${parsedPath.base} -Target File Content: -\`\`\` -${sourceContent} -\`\`\`${userInstructions} - -Return ONLY the code for the test file named \`${expectedTestFilename}\`. Do not provide conversational filler.`; - - // Execute session - const session = await jules.session({ prompt }); - const outcome = await session.result(); - - if (outcome.state !== 'completed') { - return { - status: 'error', - error: `Jules session did not complete successfully. Status: ${outcome.state}`, - }; - } - - // Attempt to extract the generated test file from the generatedFiles map - const files = outcome.generatedFiles(); - let generatedTestCode: string | null = null; - let finalTestFilename = expectedTestFilename; - - // Look for the explicitly named test file - if (files.has(expectedTestFilename)) { - generatedTestCode = files.get(expectedTestFilename)!.content; - } else if (files.size > 0) { - // Fallback: Just grab the first generated file if names don't match - const firstEntry = Array.from(files.entries())[0]; - finalTestFilename = firstEntry[0]; - generatedTestCode = firstEntry[1].content; - } else { - // Fallback 2: The agent might have just messaged the code back - const snapshot = await session.snapshot(); - const agentMessages = snapshot.activities - .filter((a: any) => a.type === 'agentMessaged') - .sort((a: any, b: any) => new Date(b.createTime).getTime() - new Date(a.createTime).getTime()); - - if (agentMessages.length > 0) { - const message = agentMessages[0].message; - // Extract code block - const match = message.match(/\`\`\`[a-zA-Z]*\n([\s\S]*?)\n\`\`\`/); - if (match) { - generatedTestCode = match[1]; - } else { - generatedTestCode = message; // Hope for the best - } - } - } - - if (!generatedTestCode) { - return { - status: 'error', - error: 'Failed to extract generated test code from Jules response.', - }; - } - - // Calculate destination path (e.g., adjacent to the source file) - const testDestinationPath = path.join(parsedPath.dir, finalTestFilename); - - // If it's not a dry run, actually write to the filesystem - if (!validParams.dryRun) { - try { - await fs.writeFile(testDestinationPath, generatedTestCode, 'utf-8'); - } catch (e: any) { - return { - status: 'error', - error: `Failed to write test file to ${testDestinationPath}: ${e.message}`, - }; - } - } - - return { - status: 'success', - message: validParams.dryRun - ? `[DRY-RUN] Would have written test file to ${testDestinationPath}` - : `Successfully wrote test file to ${testDestinationPath}`, - data: { - sourceFile: targetFilePath, - testFile: testDestinationPath, - content: generatedTestCode, - dryRun: validParams.dryRun, - } - }; - - } catch (error) { - if (error instanceof z.ZodError) { - return { - status: 'error', - error: `Validation Error: ${error.message}`, - }; - } - - const errMsg = error instanceof Error ? error.message : String(error); - return { - status: 'error', - error: errMsg, - }; - } -} diff --git a/packages/core/examples/custom-cli/commands/generate-test/spec.ts b/packages/core/examples/custom-cli/commands/generate-test/spec.ts deleted file mode 100644 index 1fc58dd..0000000 --- a/packages/core/examples/custom-cli/commands/generate-test/spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from 'zod'; - -export const generateTestRequestSchema = z.object({ - filepath: z.string().min(1, 'Filepath is required to generate tests for'), - testFramework: z.string().optional().default('vitest'), - instructions: z.string().optional(), - dryRun: z.boolean().optional().default(false), -}); - -export type GenerateTestRequest = z.infer; - -export const generateTestResponseSchema = z.object({ - status: z.enum(['success', 'error']), - message: z.string().optional(), - data: z.object({ - sourceFile: z.string().optional(), - testFile: z.string().optional(), - content: z.string().optional(), - dryRun: z.boolean().optional(), - }).optional(), - error: z.string().optional(), -}); - -export type GenerateTestResponse = z.infer; From b5fad80bc5238cdce57ffd605550f7e504691049 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:03:06 +0000 Subject: [PATCH 5/5] feat: rename cloud-worker utility to run - Replaced `cloud-worker` with `run` to avoid confusing "worker" terminology. - Updated Typed Service Contract schema parameters (using "instruction" instead of "task"). - Reworded documentation to describe the tool as a "Cloud Compute container" or "Cloud Task". - Kept Agent CLI best practices (auto-discovery, strictly typed Zod contracts, `--json` parameters) intact. - Verified all examples and test suites continue to pass successfully. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/examples/custom-cli/README.md | 22 +++++++------- .../commands/{cloud-worker => run}/handler.ts | 30 +++++++++---------- .../commands/{cloud-worker => run}/index.ts | 26 ++++++++-------- .../commands/{cloud-worker => run}/spec.ts | 10 +++---- 4 files changed, 44 insertions(+), 44 deletions(-) rename packages/core/examples/custom-cli/commands/{cloud-worker => run}/handler.ts (79%) rename packages/core/examples/custom-cli/commands/{cloud-worker => run}/index.ts (74%) rename packages/core/examples/custom-cli/commands/{cloud-worker => run}/spec.ts (63%) diff --git a/packages/core/examples/custom-cli/README.md b/packages/core/examples/custom-cli/README.md index 1e817d1..baf829b 100644 --- a/packages/core/examples/custom-cli/README.md +++ b/packages/core/examples/custom-cli/README.md @@ -1,6 +1,6 @@ # Custom CLI Tools Example -This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool integrates with the user's **local file system** while treating repoless Jules sessions as **powerful, autonomous serverless containers**. +This example demonstrates how to use the Jules SDK to create a custom command-line interface (CLI) tool. The tool integrates with the user's **local file system** while treating repoless Jules sessions as **powerful, autonomous serverless compute containers**. It uses `citty` for command structure, `niftty` for terminal rendering, and the native Node.js `fs` module to orchestrate moving data between your local machine and the cloud. @@ -26,25 +26,25 @@ Crucially, this CLI is optimized for **Agent DX**. It follows best practices for export JULES_API_KEY="your-api-key-here" ``` -## Running the Example: The Cloud Worker +## Running the Example: Cloud Compute Tasks -The primary utility included in this example is `cloud-worker`. Instead of just talking to an LLM, this tool treats the Jules session as a sandbox where an agent can **write and execute Python or Node.js scripts**. +The primary utility included in this example is the `run` command. Instead of just talking to an LLM, this tool treats the Jules session as a sandbox where an autonomous agent can **write and execute Python or Node.js scripts**. -You can pass a local file to the cloud container, ask the worker to run complex analysis, scrape websites, or convert data formats, and it will write the final processed file back to your local machine. +You can pass a local file to the cloud container, instruct the compute instance to run complex analysis, scrape websites, or convert data formats, and it will write the final processed file back to your local machine. ### Bold Use Cases -- **Data Analysis**: `--input "sales.csv" --task "Use Python pandas to aggregate sales by month and calculate the moving average." --output-file "report.json"` -- **Web Scraping**: `--task "Write a Node.js puppeteer script to scrape the headlines from news.ycombinator.com and output them as JSON." --output-file "hn.json"` -- **Format Conversion**: `--input "old_config.xml" --task "Write a python script to parse this XML and convert it to a modern YAML structure." --output-file "new_config.yaml"` +- **Data Analysis**: `run --input "sales.csv" --instruction "Use Python pandas to aggregate sales by month and calculate the moving average." --output-file "report.json"` +- **Web Scraping**: `run --instruction "Write a Node.js puppeteer script to scrape the headlines from news.ycombinator.com and output them as JSON." --output-file "hn.json"` +- **Format Conversion**: `run --input "old_config.xml" --instruction "Write a python script to parse this XML and convert it to a modern YAML structure." --output-file "new_config.yaml"` ### Human DX You can run the CLI tool passing standard flags. ```bash -bun run index.ts cloud-worker \ +bun run index.ts run \ --input="./raw_data.csv" \ - --task="Use python pandas to clean the missing values and output as JSON." \ + --instruction="Use python pandas to clean the missing values and output as JSON." \ --output-file="./cleaned_data.json" ``` @@ -52,7 +52,7 @@ View the help text: ```bash bun run index.ts --help -bun run index.ts cloud-worker --help +bun run index.ts run --help ``` ### Agent DX @@ -60,7 +60,7 @@ bun run index.ts cloud-worker --help Agents are prone to hallucination when creating strings but are very good at forming JSON matching strict schemas. For best results, expose `--json` flags. ```bash -bun run index.ts cloud-worker --json='{"task": "Scrape the current temperature in Paris using a python script", "outputFile": "./temp.json"}' --output="json" +bun run index.ts run --json='{"instruction": "Scrape the current temperature in Paris using a python script", "outputFile": "./temp.json"}' --output="json" ``` ## Architecture diff --git a/packages/core/examples/custom-cli/commands/cloud-worker/handler.ts b/packages/core/examples/custom-cli/commands/run/handler.ts similarity index 79% rename from packages/core/examples/custom-cli/commands/cloud-worker/handler.ts rename to packages/core/examples/custom-cli/commands/run/handler.ts index 12eedb6..51aca77 100644 --- a/packages/core/examples/custom-cli/commands/cloud-worker/handler.ts +++ b/packages/core/examples/custom-cli/commands/run/handler.ts @@ -1,19 +1,19 @@ import { jules } from '@google/jules-sdk'; -import { CloudWorkerRequest, CloudWorkerResponse, cloudWorkerRequestSchema } from './spec.js'; +import { RunTaskRequest, RunTaskResponse, runTaskRequestSchema } from './spec.js'; import { z } from 'zod'; import fs from 'node:fs/promises'; import path from 'node:path'; /** - * Treats Jules as a powerful, on-demand serverless worker. + * Treats Jules as a powerful, on-demand serverless compute instance. * Sends local file context to a cloud environment where an AI agent * runs scripts (e.g., Python scraping, data analysis), and writes the * final processed output back to the local file system. */ -export async function handleCloudWorkerRequest(input: unknown): Promise { +export async function handleRunTaskRequest(input: unknown): Promise { try { // 1. Input Hardening - const validParams = cloudWorkerRequestSchema.parse(input); + const validParams = runTaskRequestSchema.parse(input); if (!process.env.JULES_API_KEY) { return { @@ -47,26 +47,26 @@ ${content} } } - // 3. Formulate the "Serverless Worker" Prompt + // 3. Formulate the "Serverless Compute" Prompt // We strictly instruct the agent on its environment capabilities and output constraints. const EXPECTED_OUTPUT_FILE = 'final_output.txt'; const prompt = ` -You are an autonomous Cloud Worker operating within a secure serverless container. +You are an autonomous Cloud Compute Agent operating within a secure serverless container. You have access to a full Linux environment with Node.js, Python, Rust, and Bun installed. You have unrestricted outbound internet access. -## Your Task -${validParams.task} +## Your Objective +${validParams.instruction} ${fileContext} -## Instructions -1. You may write and execute scripts (e.g., Python, Node) to solve this task. This includes scraping websites, processing data, querying APIs, or running analysis. +## Execution Rules +1. You may write and execute scripts (e.g., Python, Node) to solve this objective. This includes scraping websites, processing data, querying APIs, or running analysis. 2. DO NOT just write the script and ask me to run it. YOU MUST run the script yourself in your container to get the final result. 3. Install any necessary dependencies using your environment's package managers (npm, pip). -4. Once you have the final, processed result for the user's task, you MUST write that final text/JSON result to a file named \`${EXPECTED_OUTPUT_FILE}\` in your current working directory. -5. Do not include conversational filler in \`${EXPECTED_OUTPUT_FILE}\`, only the exact output requested by the task. +4. Once you have the final, processed result for the user's objective, you MUST write that final text/JSON result to a file named \`${EXPECTED_OUTPUT_FILE}\` in your current working directory. +5. Do not include conversational filler in \`${EXPECTED_OUTPUT_FILE}\`, only the exact output requested by the objective. -Remember: The success of this task relies entirely on you generating and populating \`${EXPECTED_OUTPUT_FILE}\`. +Remember: The success of this objective relies entirely on you generating and populating \`${EXPECTED_OUTPUT_FILE}\`. `; // 4. Delegate to the Jules SDK Cloud Session @@ -76,7 +76,7 @@ Remember: The success of this task relies entirely on you generating and populat if (outcome.state !== 'completed') { return { status: 'error', - error: `The cloud worker session failed or timed out. Status: ${outcome.state}`, + error: `The cloud compute session failed or timed out. Status: ${outcome.state}`, }; } @@ -107,7 +107,7 @@ Remember: The success of this task relies entirely on you generating and populat if (!finalOutputContent) { return { status: 'error', - error: `Cloud worker completed but failed to produce the expected output data.`, + error: `Cloud compute session completed but failed to produce the expected output data.`, }; } diff --git a/packages/core/examples/custom-cli/commands/cloud-worker/index.ts b/packages/core/examples/custom-cli/commands/run/index.ts similarity index 74% rename from packages/core/examples/custom-cli/commands/cloud-worker/index.ts rename to packages/core/examples/custom-cli/commands/run/index.ts index 980c000..26151f0 100644 --- a/packages/core/examples/custom-cli/commands/cloud-worker/index.ts +++ b/packages/core/examples/custom-cli/commands/run/index.ts @@ -1,10 +1,10 @@ import { defineCommand } from 'citty'; -import { handleCloudWorkerRequest } from './handler.js'; +import { handleRunTaskRequest } from './handler.js'; import { niftty } from 'niftty'; export default defineCommand({ meta: { - name: 'cloud-worker', + name: 'run', description: 'Offloads complex tasks (web scraping, data analysis, scripting) to an autonomous serverless container.', }, args: { @@ -17,21 +17,21 @@ export default defineCommand({ description: 'Format of the output (e.g., "json" or "text"). Defaults to text for humans, but "json" is critical for agents.', default: 'text', }, - task: { + instruction: { type: 'string', - description: 'A description of the complex task or script you want the worker to execute in the cloud.', + description: 'A description of the complex task or script you want the compute instance to execute in the cloud.', }, input: { type: 'string', - description: 'Optional path to a local file containing data you want to send to the worker.', + description: 'Optional path to a local file containing data you want to send to the compute instance.', }, 'output-file': { type: 'string', - description: 'Path where the worker should save the final processed result locally.', + description: 'Path where the compute instance should save the final processed result locally.', }, 'dry-run': { type: 'boolean', - description: 'Execute the worker and fetch the result, but do not write it to the local disk.', + description: 'Execute the compute instance and fetch the result, but do not write it to the local disk.', default: false, }, }, @@ -46,28 +46,28 @@ export default defineCommand({ console.error(JSON.stringify({ status: 'error', error: 'Invalid JSON payload format' })); process.exit(1); } - } else if (args.task && args['output-file']) { - payload.task = args.task; + } else if (args.instruction && args['output-file']) { + payload.instruction = args.instruction; payload.outputFile = args['output-file']; if (args.input) payload.inputFile = args.input; if (args['dry-run']) payload.dryRun = true; } else { - console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or both --task and --output-file' })); + console.error(JSON.stringify({ status: 'error', error: 'Must provide either --json or both --instruction and --output-file' })); process.exit(1); } const isJsonOutput = args.output === 'json' || process.env.OUTPUT_FORMAT === 'json'; if (!isJsonOutput) { - console.log(`\n☁️ Sending task to Cloud Worker container...\n`); + console.log(`\n☁️ Sending task to Cloud Compute container...\n`); if (payload.inputFile) { console.log(`Uploading local context: ${payload.inputFile}`); } - console.log(`Waiting for worker to run scripts and return final output...\n`); + console.log(`Waiting for serverless execution to run scripts and return final output...\n`); } // Call the Typed Service Contract handler - const response = await handleCloudWorkerRequest(payload); + const response = await handleRunTaskRequest(payload); if (isJsonOutput) { // Agent DX: Provide deterministic, machine-readable JSON diff --git a/packages/core/examples/custom-cli/commands/cloud-worker/spec.ts b/packages/core/examples/custom-cli/commands/run/spec.ts similarity index 63% rename from packages/core/examples/custom-cli/commands/cloud-worker/spec.ts rename to packages/core/examples/custom-cli/commands/run/spec.ts index 8326616..dadfe2f 100644 --- a/packages/core/examples/custom-cli/commands/cloud-worker/spec.ts +++ b/packages/core/examples/custom-cli/commands/run/spec.ts @@ -1,16 +1,16 @@ import { z } from 'zod'; -export const cloudWorkerRequestSchema = z.object({ - task: z.string().min(1, 'Task description is required.'), +export const runTaskRequestSchema = z.object({ + instruction: z.string().min(1, 'Task instruction is required.'), inputFile: z.string().optional(), outputFile: z.string().min(1, 'Output file path is required to save the result.'), timeoutMins: z.number().optional().default(5), dryRun: z.boolean().optional().default(false), }); -export type CloudWorkerRequest = z.infer; +export type RunTaskRequest = z.infer; -export const cloudWorkerResponseSchema = z.object({ +export const runTaskResponseSchema = z.object({ status: z.enum(['success', 'error']), message: z.string().optional(), data: z.object({ @@ -22,4 +22,4 @@ export const cloudWorkerResponseSchema = z.object({ error: z.string().optional(), }); -export type CloudWorkerResponse = z.infer; +export type RunTaskResponse = z.infer;