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
1 change: 1 addition & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment
## Examples

- [Basic Session](./examples/basic-session/README.md)
- [Google Docs Context](./examples/google-docs/README.md)
- [Advanced Session](./examples/advanced-session/README.md)
- [Agent Workflow](./examples/agent/README.md)
- [Webhook Integration](./examples/webhook/README.md)
Expand Down
Empty file modified packages/core/examples/custom-mcp-server/index.ts
100644 → 100755
Empty file.
58 changes: 58 additions & 0 deletions packages/core/examples/google-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Google Docs Context Example CLI

This example demonstrates how to extract text content from a Google Document 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 Docs 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 document ID and your prompt for the AI agent:

```bash
bun run index.ts \
--documentId "195j9e5Wezsq1Z-Jz3R8Q1R_1Z-Jz3R8Q1R_1Z-Jz3R" \
--prompt "Summarize the key points of this document."
```

### 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 --documentId "..." --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 the text content from the provided document ID.

It extracts the 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.
154 changes: 154 additions & 0 deletions packages/core/examples/google-docs/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { jules } from '@google/jules-sdk';
import { google } from 'googleapis';
import { RunSessionSpec, RunSessionInput, RunSessionResult } from './spec.js';

export class GoogleDocsSessionHandler 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/documents.readonly'],
});

const docs = google.docs({ version: 'v1', auth });

let response;
try {
response = await docs.documents.get({
documentId: input.documentId,
});
} 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 Docs API Error: ${authErr.message || String(authErr)}`,
recoverable: false,
},
};
}

const document = response.data;
if (!document || !document.body || !document.body.content) {
return {
success: false,
error: {
code: 'DOCUMENT_NOT_FOUND_OR_EMPTY',
message: 'No content found in the specified document.',
recoverable: true,
},
};
}

const extractText = (content: any[]): string => {
let text = '';
for (const element of content) {
if (element.paragraph && element.paragraph.elements) {
for (const pElement of element.paragraph.elements) {
if (pElement.textRun && pElement.textRun.content) {
text += pElement.textRun.content;
}
}
}
}
return text;
};

const docContext = extractText(document.body.content);
if (!docContext.trim()) {
return {
success: false,
error: {
code: 'DOCUMENT_NOT_FOUND_OR_EMPTY',
message: 'No text content found in the specified document.',
recoverable: true,
},
};
}

const finalPrompt = `${input.prompt}\n\n## Source Document Content\n${docContext}`;

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,
},
};
}
}
}
81 changes: 81 additions & 0 deletions packages/core/examples/google-docs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { defineCommand, runMain } from 'citty';
import { RunSessionInputSchema } from './spec.js';
import { GoogleDocsSessionHandler } from './handler.js';

const mainCommand = defineCommand({
meta: {
name: 'google-docs-context',
version: '1.0.0',
description: 'A CLI tool that reads from a Google Document and uses the content to start a Jules session.',
},
args: {
documentId: {
type: 'string',
description: 'The ID of the Google Document to read from',
required: true,
alias: 'd',
},
prompt: {
type: 'string',
description: 'The initial prompt to give to the Jules session, instructing it on what to do with the document content',
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 Document content and initializing session...');
}

const handler = new GoogleDocsSessionHandler();
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-docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "google-docs-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"
}
}
47 changes: 47 additions & 0 deletions packages/core/examples/google-docs/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { z } from 'zod';

// 1. INPUT (The Command)
export const RunSessionInputSchema = z.object({
documentId: z.string().min(1, 'Document ID 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',
'DOCUMENT_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