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
28 changes: 25 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)
- [Gitpatch Local](./examples/gitpatch-local/README.md)

## Send work to a Cloud based session

Expand Down
55 changes: 55 additions & 0 deletions packages/core/examples/gitpatch-local/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Gitpatch Local Example (CLI)

This example demonstrates how to use the Jules SDK to retrieve a `changeSet` artifact's GitPatch from a specific session and apply the generated code modifications locally on your machine using Git.

It is structured as a **CLI application** using `citty` and follows the **Typed Service Contract** pattern. It separates validation (`spec.ts`) and impure side effects (`handler.ts`), and provides agent-friendly `json` output flags to demonstrate CLI agent best practices.

It specifically showcases how to:
- Pass a `sessionId` as a positional CLI argument.
- Use `session.snapshot()` to retrieve the generated changes directly without relying on local cache queries.
- Download the resulting `GitPatch` (`unidiffPatch`) and write it to a `.patch` file.
- Use safely executed `execFileSync` to spin up a local git branch, `git apply` to patch the code, and commit the changes.

## Requirements

- Node.js >= 18 or Bun
- A Jules API Key (`JULES_API_KEY` environment variable)
- `git` installed and available in your `PATH`
- Must be executed inside a git repository (so `git checkout -b` and `git apply` work)
- A valid Jules Session ID that contains a `changeSet` artifact.

## Setup

1. Make sure you have installed the SDK dependencies in the project root.

2. Export your Jules API key:

```bash
export JULES_API_KEY="your-api-key-here"
```

3. Ensure example dependencies are installed:
```bash
bun install
```

## Running the Example

Using `bun`:

```bash
bun run index.ts <SESSION_ID>
```

**Options:**
- `--branch <name>`: Provide a custom name for the local git branch to be created (default is `jules-patch-test-<timestamp>`).
- `--json`: Output the result of the operation as a strict JSON blob (ideal for AI Agent consumption).

Example:
```bash
bun run index.ts jules:session:123456789 --branch test-patch-fix --json
```

## What it does

The CLI validates the input session ID using Zod. It then queries the Jules API for that session's snapshot data. It searches the snapshot for a `changeSet` artifact. It extracts the `unidiffPatch` from the artifact's `gitPatch` property, writes it to a `.patch` file locally, and uses standard `git apply` to patch the local git repository in the specified branch. Finally, it commits the applied patch. All side effects are encapsulated within a handler that returns a structured Result object (Success/Failure) rather than throwing raw exceptions.
48 changes: 48 additions & 0 deletions packages/core/examples/gitpatch-local/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: jules-gitpatch-local
version: 1.0.0
description: A CLI tool to securely download and apply a git patch from a Jules session.
---

# Jules GitPatch Local CLI

This CLI is frequently invoked by AI/LLM agents. It is designed with safety rails, strict validation, and structured output to prevent errors caused by hallucinated parameters.

Always assume your inputs can be adversarial or malformed.

## Agent Usage Guidelines

1. **Introspection First:** If you are unsure about the input or output schemas for this CLI, execute it with the `--describe` flag to dump the JSON schema.
```bash
bun run index.ts "" --describe
```

2. **Dry Run Safety Rails:** This tool performs mutating operations on the local file system and git repository (`git checkout`, `git apply`, `git commit`). When instructed to "test" or "verify" changes, always append the `--dry-run` flag to ensure the changes are safely fetched and simulated without modifying the host.
```bash
bun run index.ts <session-id> --dry-run
```

3. **Structured Outputs:** By default, this CLI prints human-friendly logs. As an agent, you must ALWAYS use the `--json` flag when invoking the command to receive a deterministic, machine-readable `Result` object.
```bash
bun run index.ts <session-id> --json
```

4. **Input Hardening:**
- The `<session-id>` parameter must not contain query parameters (`?`) or hash fragments (`#`).
- The `--branch` parameter must not contain directory traversal characters (`..`) or control characters.

## Result Schema
The `--json` output will always follow the `ApplyPatchResult` discriminated union pattern:
```typescript
{
success: true,
data: { branchName: string, commitMessage?: string }
}
```
or
```typescript
{
success: false,
error: { code: string, message: string, recoverable: boolean }
}
```
132 changes: 132 additions & 0 deletions packages/core/examples/gitpatch-local/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { jules } from '@google/jules-sdk';
import { execFileSync } from 'child_process';
import { writeFileSync, unlinkSync } from 'fs';
import { join } from 'path';
import { ApplyPatchSpec, ApplyPatchInput, ApplyPatchResult } from './spec.js';

export class ApplyPatchHandler implements ApplyPatchSpec {
async execute(input: ApplyPatchInput): Promise<ApplyPatchResult> {
const branchName = input.targetBranch || `jules-patch-test-${Date.now()}`;
let patchPath: string | null = null;
let commitMessage: string | undefined;

try {
// 1. Fetch Session Snapshot Data directly instead of querying cache
let snapshot;
try {
const session = jules.session(input.sessionId);
snapshot = await session.snapshot();
} catch (err) {
return {
success: false,
error: {
code: 'SESSION_NOT_FOUND',
message: `Could not fetch session snapshot: ${input.sessionId}`,
recoverable: false,
},
};
}

// 2. Extract the unidiff patch from the changeSet
const gitPatch = snapshot.changeSet()?.gitPatch;
if (!gitPatch || !gitPatch.unidiffPatch) {
return {
success: false,
error: {
code: 'NO_CHANGESET_FOUND',
message: `No ChangeSet artifact with gitPatch data found in session ${input.sessionId}.`,
recoverable: false,
},
};
}

commitMessage = gitPatch.suggestedCommitMessage || 'Applied changes from Jules';

// 3. Handle Dry Run Safety Rails
if (input.dryRun) {
return {
success: true,
data: {
branchName: `[DRY RUN] ${branchName}`,
commitMessage: `[DRY RUN] ${commitMessage}`,
},
};
}

// 4. Checkout a new branch to apply the changes
try {
execFileSync('git', ['checkout', '-b', branchName], { stdio: 'pipe' });
} catch (e: any) {
return {
success: false,
error: {
code: 'UNABLE_TO_CHECKOUT_BRANCH',
message: `Failed to checkout branch ${branchName}. Ensure you are in a git repository. ${e.message}`,
recoverable: true,
},
};
}

// 5. Save the patch to disk
patchPath = join(process.cwd(), 'jules_changes.patch');
writeFileSync(patchPath, gitPatch.unidiffPatch);

// 6. Apply the patch
try {
execFileSync('git', ['apply', patchPath], { stdio: 'pipe' });
} catch (e: any) {
return {
success: false,
error: {
code: 'UNABLE_TO_APPLY_PATCH',
message: `Failed to apply patch. It may conflict with your current local state. ${e.message}`,
recoverable: true,
},
};
}

// 7. Commit the applied changes
try {
execFileSync('git', ['add', '.'], { stdio: 'pipe' });
execFileSync('git', ['commit', '-m', commitMessage], { stdio: 'pipe' });
} catch (e: any) {
return {
success: false,
error: {
code: 'UNABLE_TO_COMMIT',
message: `Failed to commit the changes. ${e.message}`,
recoverable: true,
},
};
}

// 8. Success
return {
success: true,
data: {
branchName,
commitMessage,
},
};
} catch (error) {
// 8. Safety net for unknown exceptions
return {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error),
recoverable: false,
},
};
} finally {
// 9. Clean up the patch file if it exists
if (patchPath) {
try {
unlinkSync(patchPath);
} catch (e) {
// Ignore unlink errors
}
}
}
}
}
Loading
Loading