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
129 changes: 126 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment
- [Basic Session](./examples/basic-session/README.md)
- [Advanced Session](./examples/advanced-session/README.md)
- [Agent Workflow](./examples/agent/README.md)
- [Google Sheets Context](./examples/google-sheets/README.md)
- [Webhook Integration](./examples/webhook/README.md)
- [GitHub Actions](./examples/github-actions/README.md)

Expand Down
59 changes: 59 additions & 0 deletions packages/core/examples/google-sheets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Google Sheets Context Example CLI

This example demonstrates how to extract tabular data from a Google Sheet using the `googleapis` library and pass it as context into an interactive Jules session prompt using a Command Line Interface (CLI).

It implements the [Typed Service Contract](https://raw.githubusercontent.com/davideast/stitch-mcp/refs/heads/main/.gemini/skills/typed-service-contract/skill.md) pattern and follows [Agent CLI Best Practices](https://justin.poehnelt.com/posts/rewrite-your-cli-for-ai-agents.md) with a dedicated `--json` output format.

## Requirements

- Node.js >= 18 or Bun
- A Jules API Key (`JULES_API_KEY` environment variable)
- Google Cloud Service Account Credentials configured for application default (`GOOGLE_APPLICATION_CREDENTIALS` environment variable)
- Enable the Google Sheets API in your Google Cloud Project.

## Setup

1. Make sure you have installed the SDK dependencies in the project root by running `bun install`.

2. Export your Jules API key:
```bash
export JULES_API_KEY="your-api-key-here"
```

3. Export your Google Cloud Credentials JSON file path:
```bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json"
```

## Running the CLI

Navigate to this directory and use `bun` to run the file. Use the `--help` flag to see available options:

```bash
bun run index.ts --help
```

### Basic Usage

Provide the spreadsheet ID, range, and your prompt for the AI agent:

```bash
bun run index.ts \
--spreadsheetId "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms" \
--range "Class Data!A2:E" \
--prompt "Analyze the following student data from a Google Sheet and provide a brief summary of the key demographics and trends."
```

### Agent / Machine-Readable Mode

To output the result as a strictly formatted JSON object suitable for agents and downstream parsing, use the `--json` flag:

```bash
bun run index.ts --spreadsheetId "..." --range "..." --prompt "..." --json
```

## What it does

The script uses `citty` to parse arguments and `zod` to validate input schemas (Spec and Handler pattern). It authenticates with Google Cloud using Application Default Credentials to retrieve rows from the provided spreadsheet ID and range.

It converts the rows into comma-separated text, appends it to your prompt, and starts a `jules.session()`. It waits for the agent to complete the task and displays the final analysis response or output files, returning structured errors on failure without throwing unhandled exceptions.
130 changes: 130 additions & 0 deletions packages/core/examples/google-sheets/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { jules } from '@google/jules-sdk';
import { google } from 'googleapis';
import { RunSessionSpec, RunSessionInput, RunSessionResult } from './spec.js';

export class GoogleSheetsSessionHandler implements RunSessionSpec {
async execute(input: RunSessionInput): Promise<RunSessionResult> {
try {
if (!process.env.JULES_API_KEY) {
return {
success: false,
error: {
code: 'MISSING_CREDENTIALS',
message: 'JULES_API_KEY environment variable is not set.',
recoverable: true,
},
};
}

const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
});

const sheets = google.sheets({ version: 'v4', auth });

let response;
try {
response = await sheets.spreadsheets.values.get({
spreadsheetId: input.spreadsheetId,
range: input.range,
});
} catch (authErr: any) {
if (authErr.message && authErr.message.includes('Could not load the default credentials')) {
return {
success: false,
error: {
code: 'MISSING_CREDENTIALS',
message: 'Google Application Default Credentials not found. Please set GOOGLE_APPLICATION_CREDENTIALS.',
recoverable: true,
},
};
}
return {
success: false,
error: {
code: 'API_ERROR',
message: `Google Sheets API Error: ${authErr.message || String(authErr)}`,
recoverable: false,
},
};
}

const rows = response.data.values;
if (!rows || rows.length === 0) {
return {
success: false,
error: {
code: 'SHEET_NOT_FOUND_OR_EMPTY',
message: 'No data found in the specified spreadsheet range.',
recoverable: true,
},
};
}

const sheetContext = rows.map((row) => row.join(', ')).join('\n');
const finalPrompt = `${input.prompt}\n\n## Source Data\n${sheetContext}`;

let session;
try {
session = await jules.session({ prompt: finalPrompt });
} catch (julesErr: any) {
return {
success: false,
error: {
code: 'JULES_ERROR',
message: `Jules Session Creation Error: ${julesErr.message || String(julesErr)}`,
recoverable: false,
},
};
}

const outcome = await session.result();

let agentMessage = undefined;
let generatedFiles: Record<string, string> = {};

if (outcome.state === 'completed') {
try {
const activities = await jules.select({
from: 'activities',
where: { type: 'agentMessaged', 'session.id': session.id },
order: 'desc',
limit: 1,
});

if (activities.length > 0) {
agentMessage = activities[0].message;
} else {
const files = outcome.generatedFiles();
if (files.size > 0) {
for (const [filename, content] of files.entries()) {
generatedFiles[filename] = content.content;
}
}
}
} catch (queryErr) {
console.error('Failed to query local cache for agent messages:', queryErr);
}
}

return {
success: true,
data: {
sessionId: session.id,
state: outcome.state,
agentMessage,
files: Object.keys(generatedFiles).length > 0 ? generatedFiles : undefined,
},
};
} catch (error) {
return {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error),
recoverable: false,
},
};
}
}
}
87 changes: 87 additions & 0 deletions packages/core/examples/google-sheets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { defineCommand, runMain } from 'citty';
import { RunSessionInputSchema } from './spec.js';
import { GoogleSheetsSessionHandler } from './handler.js';

const mainCommand = defineCommand({
meta: {
name: 'google-sheets-context',
version: '1.0.0',
description: 'A CLI tool that reads from a Google Sheet and uses the data to start a Jules session.',
},
args: {
spreadsheetId: {
type: 'string',
description: 'The ID of the Google Spreadsheet to read from',
required: true,
alias: 's',
},
range: {
type: 'string',
description: 'The A1 notation of the values to retrieve (e.g. "Class Data!A2:E")',
required: true,
alias: 'r',
},
prompt: {
type: 'string',
description: 'The initial prompt to give to the Jules session, instructing it on what to do with the data',
required: true,
alias: 'p',
},
json: {
type: 'boolean',
description: 'Output response as JSON',
default: false,
},
},
async run({ args }) {
// Input validation
const inputResult = RunSessionInputSchema.safeParse(args);

if (!inputResult.success) {
if (args.json) {
console.error(JSON.stringify({ error: inputResult.error.errors }));
} else {
console.error('Validation Error:', inputResult.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '));
}
process.exit(1);
}

if (!args.json) {
console.log('Fetching Google Sheet data and initializing session...');
}

const handler = new GoogleSheetsSessionHandler();
const result = await handler.execute(inputResult.data);

if (!result.success) {
if (args.json) {
console.error(JSON.stringify(result.error));
} else {
console.error(`Error (${result.error.code}): ${result.error.message}`);
if (result.error.recoverable) {
console.log('Suggestion: Check your credentials and input values.');
}
}
process.exit(1);
}

if (args.json) {
console.log(JSON.stringify(result.data, null, 2));
} else {
console.log(`\nSession Completed!`);
console.log(`Session ID: ${result.data.sessionId}`);
console.log(`State: ${result.data.state}`);

if (result.data.agentMessage) {
console.log(`\nAgent Analysis:\n${result.data.agentMessage}`);
} else if (result.data.files) {
console.log('\nGenerated Files:');
for (const [filename, content] of Object.entries(result.data.files)) {
console.log(`\nFile: ${filename}\n${content}`);
}
}
}
},
});

runMain(mainCommand);
13 changes: 13 additions & 0 deletions packages/core/examples/google-sheets/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "google-sheets-context-example",
"type": "module",
"scripts": {
"start": "bun run index.ts"
},
"dependencies": {
"@google/jules-sdk": "workspace:*",
"citty": "^0.2.1",
"googleapis": "^171.4.0",
"zod": "^4.3.6"
}
}
48 changes: 48 additions & 0 deletions packages/core/examples/google-sheets/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from 'zod';

// 1. INPUT (The Command)
export const RunSessionInputSchema = z.object({
spreadsheetId: z.string().min(1, 'Spreadsheet ID is required'),
range: z.string().min(1, 'Range is required'),
prompt: z.string().min(1, 'Prompt is required'),
json: z.boolean().default(false).optional(),
});
export type RunSessionInput = z.infer<typeof RunSessionInputSchema>;

// 2. ERROR CODES
export const RunSessionErrorCode = z.enum([
'MISSING_CREDENTIALS',
'SHEET_NOT_FOUND_OR_EMPTY',
'API_ERROR',
'JULES_ERROR',
'UNKNOWN_ERROR'
]);

// 3. RESULT
export const RunSessionSuccess = z.object({
success: z.literal(true),
data: z.object({
sessionId: z.string(),
state: z.string(),
agentMessage: z.string().optional(),
files: z.record(z.string(), z.string()).optional(),
}),
});

export const RunSessionFailure = z.object({
success: z.literal(false),
error: z.object({
code: RunSessionErrorCode,
message: z.string(),
recoverable: z.boolean(),
})
});

export type RunSessionResult =
| z.infer<typeof RunSessionSuccess>
| z.infer<typeof RunSessionFailure>;

// 4. INTERFACE
export interface RunSessionSpec {
execute(input: RunSessionInput): Promise<RunSessionResult>;
}
Loading