Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
72 changes: 72 additions & 0 deletions packages/core/examples/custom-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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 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.

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

- 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: Cloud Compute Tasks

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, 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**: `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 run \
--input="./raw_data.csv" \
--instruction="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 run --help
```

### 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
bun run index.ts run --json='{"instruction": "Scrape the current temperature in Paris using a python script", "outputFile": "./temp.json"}' --output="json"
```

## Architecture

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, 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.
155 changes: 155 additions & 0 deletions packages/core/examples/custom-cli/commands/run/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { jules } from '@google/jules-sdk';
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 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 handleRunTaskRequest(input: unknown): Promise<RunTaskResponse> {
try {
// 1. Input Hardening
const validParams = runTaskRequestSchema.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 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 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 Objective
${validParams.instruction}
${fileContext}

## 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 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 objective 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 compute 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 compute session 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,
};
}
}
Loading
Loading