From 17a54a3a81e7f35bf899e5e971ccb2868ae74bf9 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:34:35 +0000 Subject: [PATCH 01/28] docs: add ai-sdk integration example Adds a practical example demonstrating how to integrate the Jules TypeScript SDK with the Vercel AI SDK. Includes a basic application showing how an AI model can delegate a coding task to a Jules session using the Tool API. Fixes #227 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 58 +++++++++- packages/core/README.md | 1 + packages/core/examples/ai-sdk/README.md | 44 ++++++++ packages/core/examples/ai-sdk/index.ts | 124 +++++++++++++++++++++ packages/core/examples/ai-sdk/package.json | 15 +++ 5 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/ai-sdk/README.md create mode 100644 packages/core/examples/ai-sdk/index.ts create mode 100644 packages/core/examples/ai-sdk/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..718b3b4 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,16 @@ "vitest": "^3.2.4", }, }, + "packages/core/examples/ai-sdk": { + "name": "ai-sdk-example", + "version": "1.0.0", + "dependencies": { + "@ai-sdk/openai": "^1.1.13", + "@google/jules-sdk": "workspace:*", + "ai": "^4.1.45", + "zod": "^3.24.2", + }, + }, "packages/core/examples/github-actions": { "name": "jules-github-actions-example", "version": "1.0.0", @@ -79,7 +89,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 +115,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -171,6 +181,16 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], + "@ai-sdk/openai": ["@ai-sdk/openai@1.3.24", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="], + + "@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="], + + "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="], "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.6", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.4" } }, "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg=="], @@ -377,6 +397,8 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], @@ -463,6 +485,8 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], @@ -517,6 +541,10 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ai": ["ai@4.3.19", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q=="], + + "ai-sdk-example": ["ai-sdk-example@workspace:packages/core/examples/ai-sdk"], + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -625,6 +653,8 @@ "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -763,12 +793,16 @@ "jsdom": ["jsdom@27.4.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.6.0", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], "json-with-bigint": ["json-with-bigint@3.5.3", "", {}, "sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A=="], + "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "jules-github-actions-example": ["jules-github-actions-example@workspace:packages/core/examples/github-actions"], @@ -901,6 +935,8 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -925,6 +961,8 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -987,10 +1025,14 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], @@ -1073,6 +1115,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -1145,8 +1189,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 +1301,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..3c9be4d 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) +- [Ai Sdk Integration](./examples/ai-sdk/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/ai-sdk/README.md b/packages/core/examples/ai-sdk/README.md new file mode 100644 index 0000000..688c348 --- /dev/null +++ b/packages/core/examples/ai-sdk/README.md @@ -0,0 +1,44 @@ +# Vercel AI SDK Integration Example + +This example demonstrates how to integrate the Jules SDK with the Vercel AI SDK to provide AI-powered coding capabilities within an AI application. + +It uses the `generateText` function from the `ai` package and an OpenAI model. The AI is given a custom tool called `executeCodingTask` which internally uses the Jules SDK to spin up a cloud environment and perform a complex coding task. + +## Prerequisites + +- Node.js or Bun installed. +- A Jules API Key. Set it using: + ```bash + export JULES_API_KEY= + ``` +- An OpenAI API Key. Set it using: + ```bash + export OPENAI_API_KEY= + ``` + +## Running the Example + +You can run this example using `bun`, `tsx`, or `ts-node`: + +### Using Bun + +```bash +bun run index.ts +``` + +### Using Node.js and TSX + +If you don't have `bun` installed, you can run the example using `tsx`: + +```bash +npm install -g tsx +tsx index.ts +``` + +## Example Overview + +1. The script initializes an OpenAI model. +2. It calls `generateText` with a user prompt asking for a coding fix. +3. The AI model determines it needs to call the `executeCodingTask` tool. +4. The tool executes, creating a Jules session (`jules.session`) and waits for the result. +5. The outcome is returned to the AI model, which then summarizes the action to the user. diff --git a/packages/core/examples/ai-sdk/index.ts b/packages/core/examples/ai-sdk/index.ts new file mode 100644 index 0000000..2b333f9 --- /dev/null +++ b/packages/core/examples/ai-sdk/index.ts @@ -0,0 +1,124 @@ +import { jules } from '@google/jules-sdk'; +import { generateText, tool } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import { z } from 'zod'; + +/** + * AI SDK Integration Example + * + * This example demonstrates how to integrate the Vercel AI SDK + * with the Jules SDK by creating an AI-powered application that + * can delegate coding tasks to Jules using an AI tool. + */ +async function main() { + console.log('Starting AI SDK Integration Example...'); + + // The task we want the general AI model to handle, which involves coding + const userRequest = + 'Please fix the visibility issues in the repository "your-org/your-repo" on branch "main". The backgrounds are too light and there is low contrast on button hovers.'; + + console.log(`User Request: "${userRequest}"`); + + // We use Vercel AI SDK to generate a response, providing it with + // a tool that can execute coding tasks via Jules. + const { text, toolCalls, toolResults } = await generateText({ + model: openai('gpt-4o'), // Or your preferred OpenAI model + prompt: userRequest, + tools: { + executeCodingTask: tool({ + description: + 'Executes a complex coding task in an ephemeral cloud environment and returns the result (like a PR URL).', + parameters: z.object({ + prompt: z + .string() + .describe( + 'Detailed instructions for the coding task, including what needs to be changed.', + ), + githubRepo: z + .string() + .describe( + 'The GitHub repository in the format "owner/repo".', + ), + baseBranch: z + .string() + .describe('The base branch to make the changes against.'), + }), + execute: async ({ prompt, githubRepo, baseBranch }) => { + console.log(`\nTool 'executeCodingTask' invoked!`); + console.log(` Repo: ${githubRepo} (${baseBranch})`); + console.log(` Prompt: ${prompt}\n`); + + try { + // Note: In a real scenario, you'd use a real repository you have access to. + // For this example, if the githubRepo is "your-org/your-repo", we'll run a repoless session + // to simulate the behavior, or use a known public repo. + const useRepoless = githubRepo === 'your-org/your-repo'; + + const sessionOptions: any = { + prompt, + }; + + if (!useRepoless) { + sessionOptions.source = { + github: githubRepo, + baseBranch: baseBranch, + }; + sessionOptions.autoPr = true; + } else { + // Simulate a basic repoless task if dummy repo provided + sessionOptions.prompt = `Simulate a fix for: ${prompt}`; + } + + // Create and start the Jules session + const session = await jules.session(sessionOptions); + console.log(` Jules session created: ${session.id}`); + console.log(` Waiting for session to complete...`); + + // Wait for the session to complete + const outcome = await session.result(); + console.log(` Session finished with state: ${outcome.state}`); + + if (outcome.pullRequest) { + return `Successfully completed the task. A Pull Request has been created: ${outcome.pullRequest.url}`; + } else if (outcome.state === 'succeeded') { + const files = outcome.generatedFiles(); + return `Successfully completed the task. Generated ${files.size} files in a repoless environment.`; + } else { + return `Failed to complete the task. Session state: ${outcome.state}`; + } + } catch (error: any) { + console.error(' Error during Jules session:', error); + return `Failed to execute coding task due to an error: ${error.message}`; + } + }, + }), + }, + maxSteps: 2, // Allow the model to call the tool and then respond to the user + }); + + console.log('\n--- Final Response from AI ---'); + console.log(text); + console.log('------------------------------'); + + if (toolCalls && toolCalls.length > 0) { + console.log('\nTool Calls Made:'); + for (const call of toolCalls) { + console.log(`- ${call.toolName} with args:`, call.args); + } + } +} + +// Ensure required environment variables are set +if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is missing.'); + console.log('Please set it using: export JULES_API_KEY='); + process.exit(1); +} + +if (!process.env.OPENAI_API_KEY) { + console.error('Error: OPENAI_API_KEY environment variable is missing.'); + console.log('Please set it using: export OPENAI_API_KEY='); + process.exit(1); +} + +main().catch(console.error); diff --git a/packages/core/examples/ai-sdk/package.json b/packages/core/examples/ai-sdk/package.json new file mode 100644 index 0000000..95ea55c --- /dev/null +++ b/packages/core/examples/ai-sdk/package.json @@ -0,0 +1,15 @@ +{ + "name": "ai-sdk-example", + "version": "1.0.0", + "description": "An example integrating Vercel AI SDK with Jules SDK", + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@ai-sdk/openai": "^1.1.13", + "@google/jules-sdk": "workspace:*", + "ai": "^4.1.45", + "zod": "^3.24.2" + } +} From ba6d6e5b0b77315b34db5d888839aa0a9de6524e Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:34:43 +0000 Subject: [PATCH 02/28] feat: Add Next.js integration example to Jules SDK Fixes #226 Adds a practical Next.js example demonstrating how to integrate the Jules SDK into App Router API handlers and Server Actions. Also updates the main SDK README to link to the new example and includes a local test runner. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 94 +++++++++++++- packages/core/README.md | 1 + packages/core/examples/nextjs/README.md | 69 ++++++++++ packages/core/examples/nextjs/index.ts | 142 +++++++++++++++++++++ packages/core/examples/nextjs/package.json | 22 ++++ 5 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 packages/core/examples/nextjs/README.md create mode 100644 packages/core/examples/nextjs/index.ts create mode 100644 packages/core/examples/nextjs/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..37f0f35 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", @@ -65,6 +65,22 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/nextjs": { + "name": "jules-nextjs-example", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + "next": "^14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.0.0", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +95,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 +121,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -317,6 +333,26 @@ "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], + "@next/env": ["@next/env@14.2.35", "", {}, "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.33", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.33", "", { "os": "darwin", "cpu": "x64" }, "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.33", "", { "os": "win32", "cpu": "arm64" }, "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ=="], + + "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.33", "", { "os": "win32", "cpu": "x64" }, "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg=="], + "@octokit/app": ["@octokit/app@15.1.6", "", { "dependencies": { "@octokit/auth-app": "^7.2.1", "@octokit/auth-unauthenticated": "^6.1.3", "@octokit/core": "^6.1.5", "@octokit/oauth-app": "^7.1.6", "@octokit/plugin-paginate-rest": "^12.0.0", "@octokit/types": "^14.0.0", "@octokit/webhooks": "^13.6.1" } }, "sha512-WELCamoCJo9SN0lf3SWZccf68CF0sBNPQuLYmZ/n87p5qvBJDe9aBtr5dHkh7T9nxWZ608pizwsUbypSzZAiUw=="], "@octokit/auth-app": ["@octokit/auth-app@8.2.0", "", { "dependencies": { "@octokit/auth-oauth-app": "^9.0.3", "@octokit/auth-oauth-user": "^6.0.2", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "toad-cache": "^3.7.0", "universal-github-app-jwt": "^2.2.0", "universal-user-agent": "^7.0.0" } }, "sha512-vVjdtQQwomrZ4V46B9LaCsxsySxGoHsyw6IYBov/TqJVROrlYdyNgw5q6tQbB7KZt53v1l1W53RiqTvpzL907g=="], @@ -453,6 +489,10 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.160", "", {}, "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA=="], @@ -475,6 +515,12 @@ "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -547,6 +593,8 @@ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], @@ -555,6 +603,8 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], @@ -573,6 +623,8 @@ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -605,6 +657,8 @@ "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], @@ -757,7 +811,7 @@ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], - "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -773,6 +827,8 @@ "jules-github-actions-example": ["jules-github-actions-example@workspace:packages/core/examples/github-actions"], + "jules-nextjs-example": ["jules-nextjs-example@workspace:packages/core/examples/nextjs"], + "jules-sdk-example": ["jules-sdk-example@workspace:examples/simple"], "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], @@ -787,6 +843,8 @@ "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], "lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="], @@ -835,6 +893,8 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "next": ["next@14.2.35", "", { "dependencies": { "@next/env": "14.2.35", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.33", "@next/swc-darwin-x64": "14.2.33", "@next/swc-linux-arm64-gnu": "14.2.33", "@next/swc-linux-arm64-musl": "14.2.33", "@next/swc-linux-x64-gnu": "14.2.33", "@next/swc-linux-x64-musl": "14.2.33", "@next/swc-win32-arm64-msvc": "14.2.33", "@next/swc-win32-ia32-msvc": "14.2.33", "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig=="], + "niftty": ["niftty@0.1.3", "", { "dependencies": { "chalk": "^5.6.2", "shiki": "^3.12.2", "string-length": "^6.0.0", "tinycolor2": "^1.6.0" } }, "sha512-wy8Kysxzh/R3hBq0BDlBbnzxDU/b/3PUtWfWVm1KwOestaVF3423U4iHD7TthPMF/RTHPXGenxh6YNERaD8M2g=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -901,6 +961,10 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -925,6 +989,8 @@ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -967,6 +1033,8 @@ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -983,6 +1051,8 @@ "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], @@ -1019,6 +1089,8 @@ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], @@ -1145,8 +1217,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=="], @@ -1215,8 +1293,12 @@ "jules-github-actions-example/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "jules-nextjs-example/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], + "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "octokit/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="], "octokit/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], @@ -1227,6 +1309,8 @@ "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], @@ -1251,6 +1335,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..0dcefd9 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -7,6 +7,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment ## Examples - [Basic Session](./examples/basic-session/README.md) +- [Nextjs Integration](./examples/nextjs/README.md) - [Advanced Session](./examples/advanced-session/README.md) - [Agent Workflow](./examples/agent/README.md) - [Webhook Integration](./examples/webhook/README.md) diff --git a/packages/core/examples/nextjs/README.md b/packages/core/examples/nextjs/README.md new file mode 100644 index 0000000..c207d95 --- /dev/null +++ b/packages/core/examples/nextjs/README.md @@ -0,0 +1,69 @@ +# Next.js Integration Example + +This example demonstrates how to integrate the Jules SDK into a Next.js application using the App Router. + +It covers two common patterns for starting Jules sessions from a Next.js application: +1. **Server Actions:** Ideal for form submissions and simple mutations. +2. **API Route Handlers:** Ideal for webhooks, external client integrations, or more complex APIs. + +## Prerequisites + +- A Jules API Key (`JULES_API_KEY` environment variable). + +## How to use in your Next.js app + +### 1. Server Actions + +You can use the `triggerJulesTask` pattern from `index.ts` directly in your Next.js application. + +Create a file like `app/actions.ts` and add `'use server'` at the top: + +```typescript +'use server' + +import { jules } from '@google/jules-sdk'; + +export async function triggerJulesTask(prompt: string) { + const session = await jules.session({ prompt }); + return { success: true, sessionId: session.id }; +} +``` + +Then, import and call this action from your Client Components or Server Components. + +### 2. API Route Handlers + +You can copy the `POST` function pattern to a file like `app/api/jules/route.ts` to create a REST endpoint for your Next.js application. + +```typescript +import { jules } from '@google/jules-sdk'; + +export async function POST(req: Request) { + const body = await req.json(); + const session = await jules.session({ + prompt: body.taskDescription, + source: { github: body.githubUrl }, + }); + + return new Response(JSON.stringify({ sessionId: session.id }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); +} +``` + +## Running the Example Locally + +The `index.ts` file includes a runnable test script to verify that both the Server Action function and the API Route function work as expected. + +Ensure you have your API key set: + +```bash +export JULES_API_KEY="your-api-key-here" +``` + +Then, you can run the file using `bun` (or another runner like `tsx`): + +```bash +bun run index.ts +``` diff --git a/packages/core/examples/nextjs/index.ts b/packages/core/examples/nextjs/index.ts new file mode 100644 index 0000000..b54f192 --- /dev/null +++ b/packages/core/examples/nextjs/index.ts @@ -0,0 +1,142 @@ +import { jules } from '@google/jules-sdk'; + +/** + * Next.js Integration Example + * + * This file demonstrates how to integrate the Jules SDK into a Next.js application. + * It provides two common patterns: + * 1. A Server Action (App Router) + * 2. An API Route Handler (App Router) + */ + +// ============================================================================ +// Pattern 1: Next.js Server Action +// ============================================================================ +// Typically placed in a file like `app/actions.ts` or inline with components. +// +// 'use server' // Uncomment this in a real Next.js Server Action file + +/** + * A server action to trigger a Jules session from a form submission or button click. + */ +export async function triggerJulesTask(prompt: string) { + try { + // Check if the API key is configured (Next.js automatically loads .env files) + if (!process.env.JULES_API_KEY) { + throw new Error('JULES_API_KEY is not configured'); + } + + // Example: Create a repoless session for a simple task + const session = await jules.session({ + prompt: `Review the following text and provide a summary: \n\n${prompt}`, + }); + + console.log(`[Server Action] Session created: ${session.id}`); + + // In a real application, you might save the session.id to your database here + // so the client can poll or subscribe to updates. + + return { success: true, sessionId: session.id }; + } catch (error) { + console.error('[Server Action] Failed to start Jules task:', error); + return { success: false, error: 'Failed to start task' }; + } +} + +// ============================================================================ +// Pattern 2: Next.js API Route Handler +// ============================================================================ +// Typically placed in a file like `app/api/jules/route.ts` + +/** + * A Next.js App Router API Handler (POST method) + * + * @param req NextRequest + */ +export async function POST(req: Request) { + try { + if (!process.env.JULES_API_KEY) { + return new Response(JSON.stringify({ error: 'JULES_API_KEY missing' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const body = await req.json(); + const { githubUrl, taskDescription } = body; + + if (!githubUrl || !taskDescription) { + return new Response(JSON.stringify({ error: 'Missing parameters' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Example: Create a session linked to a GitHub repository + const session = await jules.session({ + prompt: taskDescription, + source: { github: githubUrl }, + }); + + console.log(`[API Route] Session created: ${session.id}`); + + // Return the session ID to the client + return new Response(JSON.stringify({ sessionId: session.id }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error('[API Route] Error:', error); + return new Response(JSON.stringify({ error: 'Internal Server Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// ============================================================================ +// Local Test Runner +// ============================================================================ +// This allows you to run this example file directly to verify it works. + +async function main() { + 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); + } + + console.log('Testing Server Action example...'); + const result = await triggerJulesTask('Next.js Server Actions are great for mutations.'); + + if (result.success) { + console.log(`Server Action successfully created session: ${result.sessionId}`); + } else { + console.error('Server Action failed:', result.error); + } + + console.log('\\nTesting API Route example...'); + // Simulate a Next.js Request object + const mockRequest = new Request('http://localhost:3000/api/jules', { + method: 'POST', + body: JSON.stringify({ + githubUrl: 'google-labs-code/jules-sdk', + taskDescription: 'Look for any console.log statements and remove them.' + }), + headers: { 'Content-Type': 'application/json' } + }); + + const response = await POST(mockRequest); + const responseData = await response.json(); + + if (response.ok) { + console.log(`API Route successfully created session: ${responseData.sessionId}`); + } else { + console.error('API Route failed:', responseData.error); + } +} + +// Run the main function if executed directly (e.g. via `bun run index.ts`) +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch(console.error); +} diff --git a/packages/core/examples/nextjs/package.json b/packages/core/examples/nextjs/package.json new file mode 100644 index 0000000..50d36f7 --- /dev/null +++ b/packages/core/examples/nextjs/package.json @@ -0,0 +1,22 @@ +{ + "name": "jules-nextjs-example", + "version": "1.0.0", + "description": "An example of integrating the Jules SDK in a Next.js application", + "type": "module", + "scripts": { + "build": "tsc --noEmit", + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*", + "next": "^14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "typescript": "^5.0.0" + } +} From e63db5239d037d8ce3aa41b31763ab6d111814d1 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:34:53 +0000 Subject: [PATCH 03/28] Add practical example of creating a Custom MCP Server using the Jules TypeScript SDK Fixes #231 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 24 ++- packages/core/README.md | 1 + .../core/examples/custom-mcp-server/README.md | 38 +++++ .../core/examples/custom-mcp-server/index.ts | 138 ++++++++++++++++++ .../examples/custom-mcp-server/package.json | 14 ++ 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/custom-mcp-server/README.md create mode 100644 packages/core/examples/custom-mcp-server/index.ts create mode 100644 packages/core/examples/custom-mcp-server/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..04a20cd 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,14 @@ "vitest": "^3.2.4", }, }, + "packages/core/examples/custom-mcp-server": { + "name": "custom-mcp-server-example", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + "@modelcontextprotocol/sdk": "^1.2.0", + }, + }, "packages/core/examples/github-actions": { "name": "jules-github-actions-example", "version": "1.0.0", @@ -79,7 +87,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 +113,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -605,6 +613,8 @@ "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "custom-mcp-server-example": ["custom-mcp-server-example@workspace:packages/core/examples/custom-mcp-server"], + "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], @@ -1145,8 +1155,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 +1267,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..ea0d943 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 Mcp Server](./examples/custom-mcp-server/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/custom-mcp-server/README.md b/packages/core/examples/custom-mcp-server/README.md new file mode 100644 index 0000000..af2f8d2 --- /dev/null +++ b/packages/core/examples/custom-mcp-server/README.md @@ -0,0 +1,38 @@ +# Custom MCP Server Example + +This example demonstrates how to create a custom MCP (Model Context Protocol) server that integrates with the Jules TypeScript SDK. + +The custom MCP server provides tools to other AI assistants, allowing them to: +1. Orchestrate Jules sessions locally or in the cloud. +2. Query data, summarize files, or prepare plans to pass as context. + +## Setup and Running + +1. Ensure your `JULES_API_KEY` is set in your environment: + ```bash + export JULES_API_KEY="your-jules-api-key" + ``` + +2. Run the example using Bun (or another TypeScript runner like `tsx`): + ```bash + bun run index.ts + ``` + +## Integration + +You can add this server to your local MCP client configuration (e.g., Claude Desktop, Zed, or a VS Code MCP extension) to allow it to utilize Jules' agentic capabilities directly from your IDE or chat client. + +Example Claude Desktop Config: +```json +{ + "mcpServers": { + "jules-custom": { + "command": "bun", + "args": ["run", "/absolute/path/to/this/example/index.ts"], + "env": { + "JULES_API_KEY": "your-api-key" + } + } + } +} +``` diff --git a/packages/core/examples/custom-mcp-server/index.ts b/packages/core/examples/custom-mcp-server/index.ts new file mode 100644 index 0000000..48ada15 --- /dev/null +++ b/packages/core/examples/custom-mcp-server/index.ts @@ -0,0 +1,138 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { jules } from '@google/jules-sdk'; + +/** + * Custom MCP Server Example + * + * This example demonstrates how to create a custom Model Context Protocol (MCP) + * server that wraps the Jules SDK. This allows other AI assistants (like Claude, + * Zed, etc.) to use Jules to orchestrate complex coding tasks or queries. + */ + +// 1. Initialize the MCP Server +const server = new Server( + { + name: 'jules-custom-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + }, +); + +// 2. Define the tools available to the MCP client +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'run_jules_task', + description: 'Run a Repoless Jules session to answer a question or complete a standalone task', + inputSchema: { + type: 'object', + properties: { + prompt: { + type: 'string', + description: 'The instructions or query for the Jules agent', + }, + }, + required: ['prompt'], + }, + }, + { + name: 'get_jules_sessions', + description: 'Query the local cache for recent Jules sessions', + inputSchema: { + type: 'object', + properties: { + limit: { + type: 'number', + description: 'Maximum number of sessions to return', + }, + }, + }, + }, + ], + }; +}); + +// 3. Handle tool execution requests +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + if (name === 'run_jules_task') { + const prompt = String(args?.prompt || ''); + if (!prompt) { + throw new Error('Prompt is required'); + } + + // Use the Jules SDK to run a session + const session = await jules.session({ prompt }); + const result = await session.result(); + + // Attempt to extract the primary generated markdown file or the final state + let answer = `Session finished with state: ${result.state}`; + const files = result.generatedFiles(); + if (files.size > 0) { + // Just return the first file's content for simplicity + for (const [_, content] of files.entries()) { + answer = content.content; + break; + } + } + + return { + content: [{ type: 'text', text: answer }], + }; + } + + if (name === 'get_jules_sessions') { + const limit = Number(args?.limit) || 5; + + // Query recent sessions using Jules SDK + const sessions = await jules.select({ + from: 'sessions', + limit, + }); + + const summary = sessions.map( + (s) => `ID: ${s.id} | Status: ${s.state} | URL: ${s.url}` + ).join('\n'); + + return { + content: [{ type: 'text', text: summary || 'No sessions found.' }], + }; + } + + throw new Error(`Unknown tool: ${name}`); + } catch (error) { + return { + isError: true, + content: [{ type: 'text', text: String(error) }], + }; + } +}); + +// 4. Start the server using stdio transport +async function main() { + if (!process.env.JULES_API_KEY) { + console.error('Warning: JULES_API_KEY environment variable is missing.'); + console.error('The tools will fail if they require SDK calls to the backend.'); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Custom Jules MCP Server is running on stdio'); +} + +main().catch((error) => { + console.error('Fatal error in main():', error); + process.exit(1); +}); diff --git a/packages/core/examples/custom-mcp-server/package.json b/packages/core/examples/custom-mcp-server/package.json new file mode 100644 index 0000000..03dffba --- /dev/null +++ b/packages/core/examples/custom-mcp-server/package.json @@ -0,0 +1,14 @@ +{ + "name": "custom-mcp-server-example", + "version": "1.0.0", + "description": "Example demonstrating how to create custom MCP servers with the Jules SDK", + "private": true, + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*", + "@modelcontextprotocol/sdk": "^1.2.0" + } +} 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 04/28] 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 d7855cd6ff5c78d1e39ef5abf42731747a10c286 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:35:51 +0000 Subject: [PATCH 05/28] Fixes #236: Added GitHub Action AgentSkills Example Added an example demonstrating how to use the Jules SDK on a scheduled GitHub Action cron job to analyze a repository for Agent Skills. Included full Action config and updated READMEs. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/README.md | 1 + .../github-action-agentskills/README.md | 55 ++++++++++++ .../github-action-agentskills/action.yml | 5 ++ .../github-action-agentskills/index.ts | 89 +++++++++++++++++++ .../github-action-agentskills/package.json | 18 ++++ .../github-action-agentskills/tsconfig.json | 13 +++ 6 files changed, 181 insertions(+) create mode 100644 packages/core/examples/github-action-agentskills/README.md create mode 100644 packages/core/examples/github-action-agentskills/action.yml create mode 100644 packages/core/examples/github-action-agentskills/index.ts create mode 100644 packages/core/examples/github-action-agentskills/package.json create mode 100644 packages/core/examples/github-action-agentskills/tsconfig.json diff --git a/packages/core/README.md b/packages/core/README.md index c3d32e7..9596ac9 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) +- [Github Action Agentskills](./examples/github-action-agentskills/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/github-action-agentskills/README.md b/packages/core/examples/github-action-agentskills/README.md new file mode 100644 index 0000000..06b8a66 --- /dev/null +++ b/packages/core/examples/github-action-agentskills/README.md @@ -0,0 +1,55 @@ +# GitHub Action Agent Skills Example + +This example demonstrates how to use the Jules SDK on a scheduled GitHub Action cron job to analyze a repository and generate suggestions for Agent Skills that improve automation. + +## Overview + +The action reads the Agent Skills specification from [https://agentskills.io/specification.md](https://agentskills.io/specification.md) and instructs the Jules agent to: +1. Review the repository structure and workflows. +2. Identify areas where an Agent Skill could be beneficial. +3. Create corresponding Agent Skills configuration files. +4. Generate a pull request with the suggested changes. + +## Files + +- `index.ts`: The main action logic using the Jules SDK. +- `package.json`: Dependencies for the action (`@actions/core`, `@actions/github`, `@google/jules-sdk`). + +## Usage + +You can use this action in your own GitHub workflows by referencing its path or publishing it. + +Here is an example workflow file (`.github/workflows/jules-agentskills.yml`) that triggers the agent on a weekly schedule: + +```yaml +name: Jules Agent Skills Generator + +on: + schedule: + # Run at 00:00 every Monday + - cron: '0 0 * * 1' + +jobs: + run-jules-agentskills: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Jules Agent Skills Analyzer + uses: ./packages/core/examples/github-action-agentskills + env: + JULES_API_KEY: ${{ secrets.JULES_API_KEY }} +``` + +## Running the Example Locally + +To compile the TypeScript action: + +```bash +cd packages/core/examples/github-action-agentskills +npm install +npm run build +``` + +The compiled JavaScript will be placed in `dist/index.js`, which is what GitHub Actions will execute. diff --git a/packages/core/examples/github-action-agentskills/action.yml b/packages/core/examples/github-action-agentskills/action.yml new file mode 100644 index 0000000..a94e918 --- /dev/null +++ b/packages/core/examples/github-action-agentskills/action.yml @@ -0,0 +1,5 @@ +name: 'Jules Agent Skills Generator' +description: 'Analyze repository for Agent Skills using a scheduled GitHub Action cron job' +runs: + using: 'node20' + main: 'dist/index.js' diff --git a/packages/core/examples/github-action-agentskills/index.ts b/packages/core/examples/github-action-agentskills/index.ts new file mode 100644 index 0000000..a455ce0 --- /dev/null +++ b/packages/core/examples/github-action-agentskills/index.ts @@ -0,0 +1,89 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { jules } from '@google/jules-sdk'; + +async function run() { + try { + if (!process.env.JULES_API_KEY) { + throw new Error('JULES_API_KEY environment variable is missing.'); + } + + // 1. Get context about the current repository from the GitHub Action payload + const context = github.context; + // In a cron job, context.repo will still point to the repository where the workflow is running + const owner = context.repo.owner || process.env.GITHUB_REPOSITORY?.split('/')[0] || 'unknown'; + const repo = context.repo.repo || process.env.GITHUB_REPOSITORY?.split('/')[1] || 'unknown'; + const ref = context.ref || process.env.GITHUB_REF || 'refs/heads/main'; + + let baseBranch = 'main'; + if (ref.startsWith('refs/heads/')) { + baseBranch = ref.replace('refs/heads/', ''); + } + + core.info(`Starting Jules session for ${owner}/${repo} on branch ${baseBranch}`); + + // 2. Construct the prompt for generating Agent Skills + const prompt = `Analyze this repository and suggest Agent Skills to improve automation of common or complex tasks. + +Use the Agent Skills specification located at https://agentskills.io/specification.md as a reference for formatting and structuring the skills. + +Tasks: +1. Review the repository structure, code, and existing workflows. +2. Identify 1 to 3 areas where an Agent Skill could be beneficial (e.g., code review, automated testing, boilerplate generation, or specific formatting rules). +3. Create the corresponding Agent Skills configuration files (e.g., in a \`.jules/skills\` directory or similar, as per the specification). +4. Provide a brief explanation of what each skill does and why it is useful for this repository.`; + + core.info(`Prompt: ${prompt}`); + + // 3. Create a new Jules session + const session = await jules.session({ + prompt: prompt, + source: { + github: `${owner}/${repo}`, + baseBranch: baseBranch, + }, + // Automatically create a PR when the session is complete + autoPr: true, + }); + + core.info(`Session created successfully. ID: ${session.id}`); + + // 4. Monitor the progress + for await (const activity of session.stream()) { + switch (activity.type) { + case 'planGenerated': + core.info(`[Plan Generated] ${activity.plan.steps.length} steps.`); + break; + case 'progressUpdated': + core.info(`[Progress Updated] ${activity.title}`); + break; + case 'sessionCompleted': + core.info(`[Session Completed]`); + break; + } + } + + // 5. Wait for the final outcome + const outcome = await session.result(); + + if (outcome.state === 'failed') { + core.setFailed(`Session failed.`); + return; + } + + core.info(`Session finished successfully.`); + + if (outcome.pullRequest) { + core.info(`Pull Request created: ${outcome.pullRequest.url}`); + core.setOutput('pr-url', outcome.pullRequest.url); + } + } catch (error) { + if (error instanceof Error) { + core.setFailed(`Action failed with error: ${error.message}`); + } else { + core.setFailed(`Action failed with an unknown error.`); + } + } +} + +run(); diff --git a/packages/core/examples/github-action-agentskills/package.json b/packages/core/examples/github-action-agentskills/package.json new file mode 100644 index 0000000..395dea4 --- /dev/null +++ b/packages/core/examples/github-action-agentskills/package.json @@ -0,0 +1,18 @@ +{ + "name": "jules-github-action-agentskills-example", + "version": "1.0.0", + "description": "Analyze repository for Agent Skills using a scheduled GitHub Action cron job", + "main": "dist/index.js", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/core/examples/github-action-agentskills/tsconfig.json b/packages/core/examples/github-action-agentskills/tsconfig.json new file mode 100644 index 0000000..487a581 --- /dev/null +++ b/packages/core/examples/github-action-agentskills/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist" + }, + "include": ["index.ts"] +} From 53897acf5d79ba35507e621c50bb834b3ce67519 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:36:11 +0000 Subject: [PATCH 06/28] feat(sdk-examples): add gitpatch improve example Fixes #235 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 27 +++++- packages/core/README.md | 1 + .../core/examples/gitpatch-improve/README.md | 22 +++++ .../core/examples/gitpatch-improve/index.ts | 96 +++++++++++++++++++ .../examples/gitpatch-improve/package.json | 17 ++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/gitpatch-improve/README.md create mode 100644 packages/core/examples/gitpatch-improve/index.ts create mode 100644 packages/core/examples/gitpatch-improve/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..a6cd1df 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", @@ -65,6 +65,17 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/gitpatch-improve": { + "name": "gitpatch-improve", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.7.3", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +90,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 +116,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -697,6 +708,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "gitpatch-improve": ["gitpatch-improve@workspace:packages/core/examples/gitpatch-improve"], + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1145,8 +1158,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 +1270,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..953e80f 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) +- [Gitpatch Improve](./examples/gitpatch-improve/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/gitpatch-improve/README.md b/packages/core/examples/gitpatch-improve/README.md new file mode 100644 index 0000000..5be628c --- /dev/null +++ b/packages/core/examples/gitpatch-improve/README.md @@ -0,0 +1,22 @@ +# GitPatch Improve Example + +This example demonstrates how to use the Jules SDK to create a session that analyzes code using GitPatch data to identify areas for improvement or automation in a repository. + +## Setup + +1. Make sure you have installed the SDK dependencies in the project root. +2. Ensure you have your `JULES_API_KEY` set. + +```sh +export JULES_API_KEY="your-api-key-here" +``` + +## Running + +You can run the script via: + +```sh +bun run index.ts +``` + +This will create a session targeting a specific repository and prompt the agent to analyze the code using GitPatch data to identify areas for improvement and print out the analysis. diff --git a/packages/core/examples/gitpatch-improve/index.ts b/packages/core/examples/gitpatch-improve/index.ts new file mode 100644 index 0000000..da86d74 --- /dev/null +++ b/packages/core/examples/gitpatch-improve/index.ts @@ -0,0 +1,96 @@ +import { jules, JulesError, ChangeSetArtifact } from '@google/jules-sdk'; + +/** + * GitPatch Improve Example + * + * Demonstrates how to: + * - Query recent sessions to find one with code changes (ChangeSet artifacts) + * - Retrieve the GitPatch data from that session + * - Use the GitPatch data in a new repoless session to analyze for improvements + */ +async function runGitPatchImproveSession() { + try { + console.log('Finding a recent session with a GitPatch...'); + + // We query the local cache for activities that have a changeSet artifact. + // This gives us activities containing code modifications. + const activitiesWithChanges = await jules.select({ + from: 'activities', + where: { artifactCount: { gt: 0 } }, + order: 'desc', + limit: 10, + }); + + let gitPatchData: string | null = null; + let sourceSessionId: string | null = null; + + // Find the first activity with a changeSet artifact + for (const activity of activitiesWithChanges) { + if (activity.artifacts) { + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + const changeSet = artifact as ChangeSetArtifact; + if (changeSet.gitPatch?.unidiffPatch) { + gitPatchData = changeSet.gitPatch.unidiffPatch; + sourceSessionId = activity.session?.id || 'unknown'; + break; + } + } + } + } + if (gitPatchData) break; + } + + if (!gitPatchData) { + console.log('No recent GitPatch data found in local cache. Please run another session that generates code changes first.'); + return; + } + + console.log(`Found GitPatch data from session ${sourceSessionId}.`); + console.log(`Starting analysis session...`); + + // Create a new repoless session to analyze the GitPatch data. + const session = await jules.session({ + prompt: `You are an expert code reviewer. Analyze the following GitPatch data. + Identify any potential bugs, areas for optimization, or coding standard violations. + Write your analysis to a file named 'analysis.md'. + + ## GitPatch Data + \`\`\`diff + ${gitPatchData} + \`\`\` + `, + }); + + console.log(`Session created: ${session.id}`); + + // Wait for the session to complete + console.log('Waiting for the agent to complete the analysis...'); + const outcome = await session.result(); + + console.log(`Session finished with state: ${outcome.state}`); + + // Retrieve the analysis file + const files = outcome.generatedFiles(); + const analysisFile = files.get('analysis.md'); + + if (analysisFile) { + console.log('\n--- Analysis Report ---'); + console.log(analysisFile.content); + console.log('-----------------------\n'); + } else { + console.log('Analysis file was not generated.'); + } + + } catch (error) { + if (error instanceof JulesError) { + console.error(`SDK error: ${error.message}`); + } else { + console.error('Unknown error:', error); + } + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + runGitPatchImproveSession(); +} diff --git a/packages/core/examples/gitpatch-improve/package.json b/packages/core/examples/gitpatch-improve/package.json new file mode 100644 index 0000000..386538b --- /dev/null +++ b/packages/core/examples/gitpatch-improve/package.json @@ -0,0 +1,17 @@ +{ + "name": "gitpatch-improve", + "version": "1.0.0", + "private": true, + "description": "Example using Jules' GitPatch data to analyze how to improve or automate code in a repository.", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} From 536d2e37bc44aed6e162cbae8d29bed12c5bf7db Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:36:41 +0000 Subject: [PATCH 07/28] feat: add google sheets context example to sdk Adds a runnable example demonstrating how to read tabular data from a Google Sheet using the googleapis library and provide it as context to an interactive Jules session prompt. Fixes #229 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 123 +++++++++++++++++- packages/core/README.md | 1 + .../core/examples/google-sheets/README.md | 47 +++++++ packages/core/examples/google-sheets/index.ts | 101 ++++++++++++++ .../core/examples/google-sheets/package.json | 11 ++ 5 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/google-sheets/README.md create mode 100644 packages/core/examples/google-sheets/index.ts create mode 100644 packages/core/examples/google-sheets/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..42b0cfa 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", @@ -65,6 +65,13 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/google-sheets": { + "name": "google-sheets-context-example", + "dependencies": { + "@google/jules-sdk": "workspace:*", + "googleapis": "^171.4.0", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "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", }, @@ -303,6 +310,8 @@ "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@microsoft/api-extractor": ["@microsoft/api-extractor@7.55.2", "", { "dependencies": { "@microsoft/api-extractor-model": "7.32.2", "@microsoft/tsdoc": "~0.16.0", "@microsoft/tsdoc-config": "~0.18.0", "@rushstack/node-core-library": "5.19.1", "@rushstack/rig-package": "0.6.0", "@rushstack/terminal": "0.19.5", "@rushstack/ts-command-line": "5.1.5", "diff": "~8.0.2", "lodash": "~4.17.15", "minimatch": "10.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-1jlWO4qmgqYoVUcyh+oXYRztZde/pAi7cSVzBz/rc+S7CoVzDasy8QE13dx6sLG4VRo8SfkkLbFORR6tBw4uGQ=="], @@ -377,6 +386,8 @@ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], @@ -535,16 +546,22 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], "brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -605,6 +622,8 @@ "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="], "de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], @@ -627,6 +646,10 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -665,6 +688,8 @@ "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fake-indexeddb": ["fake-indexeddb@6.2.5", "", {}, "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="], "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], @@ -675,10 +700,16 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -689,6 +720,10 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -699,6 +734,16 @@ "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], + "google-auth-library": ["google-auth-library@10.6.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "7.1.3", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "google-sheets-context-example": ["google-sheets-context-example@workspace:packages/core/examples/google-sheets"], + + "googleapis": ["googleapis@171.4.0", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-xybFL2SmmUgIifgsbsRQYRdNrSAYwxWZDmkZTGjUIaRnX5jPqR8el/cEvo6rCqh7iaZx6MfEPS/lrDgZ0bymkg=="], + + "googleapis-common": ["googleapis-common@8.0.1", "", { "dependencies": { "extend": "^3.0.2", "gaxios": "^7.0.0-rc.4", "google-auth-library": "^10.1.0", "qs": "^6.7.0", "url-template": "^2.0.8" } }, "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -753,6 +798,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], @@ -763,6 +810,8 @@ "jsdom": ["jsdom@27.4.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.6.0", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], @@ -775,6 +824,10 @@ "jules-sdk-example": ["jules-sdk-example@workspace:examples/simple"], + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], "libsodium": ["libsodium@0.8.2", "", {}, "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw=="], @@ -837,6 +890,10 @@ "niftty": ["niftty@0.1.3", "", { "dependencies": { "chalk": "^5.6.2", "shiki": "^3.12.2", "string-length": "^6.0.0", "tinycolor2": "^1.6.0" } }, "sha512-wy8Kysxzh/R3hBq0BDlBbnzxDU/b/3PUtWfWVm1KwOestaVF3423U4iHD7TthPMF/RTHPXGenxh6YNERaD8M2g=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -857,6 +914,8 @@ "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -917,10 +976,14 @@ "rettime": ["rettime@0.7.0", "", {}, "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw=="], + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], @@ -975,10 +1038,14 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], @@ -1073,6 +1140,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-template": ["url-template@2.0.8", "", {}, "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -1091,6 +1160,8 @@ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "webhook": ["webhook@workspace:packages/core/examples/webhook"], "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], @@ -1105,6 +1176,8 @@ "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -1145,8 +1218,18 @@ "@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=="], + + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + "@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=="], @@ -1213,6 +1296,10 @@ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "gcp-metadata/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + + "googleapis-common/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + "jules-github-actions-example/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], @@ -1221,14 +1308,22 @@ "octokit/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], "semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@actions/github/@octokit/core/@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="], "@actions/github/@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="], @@ -1251,12 +1346,18 @@ "@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=="], "@google/jules-fleet/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app": ["@octokit/auth-oauth-app@8.1.4", "", { "dependencies": { "@octokit/auth-oauth-device": "^7.1.5", "@octokit/auth-oauth-user": "^5.1.4", "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-71iBa5SflSXcclk/OL3lJzdt4iFs56OJdpBGEBl1wULp7C58uiswZLV6TdRaiAzHP1LT8ezpbHlKuxADb+4NkQ=="], "@octokit/app/@octokit/auth-app/@octokit/auth-oauth-user": ["@octokit/auth-oauth-user@5.1.6", "", { "dependencies": { "@octokit/auth-oauth-device": "^7.1.5", "@octokit/oauth-methods": "^5.1.5", "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-/R8vgeoulp7rJs+wfJ2LtXEVC7pjQTIqDab7wPKwVG6+2v/lUnCOub6vaHmysQBbb45FknM3tbHW8TOVqYHxCw=="], @@ -1323,8 +1424,18 @@ "octokit/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="], + "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "rimraf/glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@actions/github/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], @@ -1377,6 +1488,12 @@ "@vue/language-core/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@octokit/app/@octokit/auth-app/@octokit/auth-oauth-app/@octokit/auth-oauth-device/@octokit/oauth-methods": ["@octokit/oauth-methods@5.1.5", "", { "dependencies": { "@octokit/oauth-authorization-url": "^7.0.0", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0" } }, "sha512-Ev7K8bkYrYLhoOSZGVAGsLEscZQyq7XQONCBBAl2JdMg7IT3PQn/y8P0KjloPoYpI5UylqYrLeUcScaYWXwDvw=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/packages/core/README.md b/packages/core/README.md index c3d32e7..cb37b7c 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -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) diff --git a/packages/core/examples/google-sheets/README.md b/packages/core/examples/google-sheets/README.md new file mode 100644 index 0000000..ffe8280 --- /dev/null +++ b/packages/core/examples/google-sheets/README.md @@ -0,0 +1,47 @@ +# Google Sheets Context Example + +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. + +## 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" + ``` + +4. (Optional) Provide your own spreadsheet ID. By default, it uses a public sample sheet: + ```bash + export SPREADSHEET_ID="your-spreadsheet-id" + ``` + +## Running the Example + +Navigate to this directory and use `bun` to run the file: + +```bash +bun run index.ts +``` + +Using `npm` and `tsx` (or similar TypeScript runner): + +```bash +npx tsx index.ts +``` + +## What it does + +The script authenticates with Google Cloud using Application Default Credentials to retrieve rows from the provided spreadsheet ID using the range `Class Data!A2:E`. It extracts this information, converts the rows into comma-separated text grouped by newlines, and appends it to a prompt given to an AI agent in `jules.session`. It waits for the agent to complete the task and displays the final analysis response. diff --git a/packages/core/examples/google-sheets/index.ts b/packages/core/examples/google-sheets/index.ts new file mode 100644 index 0000000..76c746c --- /dev/null +++ b/packages/core/examples/google-sheets/index.ts @@ -0,0 +1,101 @@ +import { jules } from '@google/jules-sdk'; +import { google } from 'googleapis'; + +/** + * Google Sheets Context Example + * + * Demonstrates reading data from a Google Sheet and using it as context + * to create a Jules session. + */ +async function main() { + 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); + } + + // Authentication Setup for Google API + // Uses Application Default Credentials by default (GOOGLE_APPLICATION_CREDENTIALS) + // or checks running environment (e.g., GCE, GKE) + const auth = new google.auth.GoogleAuth({ + scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'], + }); + + const sheets = google.sheets({ version: 'v4', auth }); + + // Example public Google Sheet: "Class Data" from Google Sheets API examples + const spreadsheetId = process.env.SPREADSHEET_ID || '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'; + const range = 'Class Data!A2:E'; + + console.log('Fetching data from Google Sheets...'); + try { + const response = await sheets.spreadsheets.values.get({ + spreadsheetId, + range, + }); + + const rows = response.data.values; + if (!rows || rows.length === 0) { + console.log('No data found in the spreadsheet.'); + return; + } + + console.log(`Found ${rows.length} rows of data. Formatting as context...`); + + // Format data as context. Here we create a simple text representation. + // Each row is joined by a comma, and rows are separated by newlines. + const sheetContext = rows.map(row => row.join(', ')).join('\n'); + + console.log('\nCreating a Jules session with the Sheet context...'); + + // Create a repoless session passing the spreadsheet data in the prompt + const session = await jules.session({ + prompt: `Analyze the following student data from a Google Sheet and provide a brief summary of the key demographics and trends (e.g., major distribution, class year distribution):\n\n${sheetContext}`, + }); + + console.log(`Session created! ID: ${session.id}`); + console.log('Waiting for the agent to analyze the data...'); + + const outcome = await session.result(); + console.log(`\n--- Session Result ---`); + console.log(`State: ${outcome.state}`); + + if (outcome.state === 'completed') { + // Retrieve the final agent message + const activities = await jules.select({ + from: 'activities', + where: { type: 'agentMessaged', 'session.id': session.id }, + order: 'desc', + limit: 1, + }); + + if (activities.length > 0) { + console.log('\nAgent Analysis:'); + console.log(activities[0].message); + } else { + console.log('\nThe agent did not leave a final message.'); + + // Check for any generated files instead + const files = outcome.generatedFiles(); + if (files.size > 0) { + console.log('\nGenerated Files:'); + for (const [filename, content] of files.entries()) { + console.log(`\nFile: ${filename}`); + console.log(content.content); + } + } + } + } else { + console.error('The session did not complete successfully.'); + } + } catch (error) { + console.error('An error occurred:', error); + if (error instanceof Error && error.message.includes('Could not load the default credentials')) { + console.error('\nMake sure to set your GOOGLE_APPLICATION_CREDENTIALS environment variable'); + console.error('to the path of your Google Cloud service account key file.'); + } + } +} + +// Run the example +main(); diff --git a/packages/core/examples/google-sheets/package.json b/packages/core/examples/google-sheets/package.json new file mode 100644 index 0000000..49ec727 --- /dev/null +++ b/packages/core/examples/google-sheets/package.json @@ -0,0 +1,11 @@ +{ + "name": "google-sheets-context-example", + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*", + "googleapis": "^171.4.0" + } +} From d608cd25e1454d13a6a72f3a2c147823921630c9 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:49:37 +0000 Subject: [PATCH 08/28] Add Cloudflare Workers integration example to SDK examples - Created `packages/core/examples/cloudflare-workers/index.ts` demonstrating edge-based event processing using the Jules SDK within a mocked Cloudflare Worker `fetch` handler. - Added corresponding `package.json` for the workspace with a `--target=node` bun build script. - Documented usage and setup instructions in `README.md`. - Updated `packages/core/README.md` to link to the new example. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 26 +++++++- packages/core/README.md | 1 + .../examples/cloudflare-workers/README.md | 34 ++++++++++ .../core/examples/cloudflare-workers/index.ts | 64 +++++++++++++++++++ .../examples/cloudflare-workers/package.json | 16 +++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/cloudflare-workers/README.md create mode 100644 packages/core/examples/cloudflare-workers/index.ts create mode 100644 packages/core/examples/cloudflare-workers/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..5bedb3f 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,16 @@ "vitest": "^3.2.4", }, }, + "packages/core/examples/cloudflare-workers": { + "name": "cloudflare-workers", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + "devDependencies": { + "bun-types": "^1.1.8", + }, + }, "packages/core/examples/github-actions": { "name": "jules-github-actions-example", "version": "1.0.0", @@ -79,7 +89,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 +115,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -575,6 +585,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cloudflare-workers": ["cloudflare-workers@workspace:packages/core/examples/cloudflare-workers"], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -1145,8 +1157,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 +1269,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..c41cc58 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -7,6 +7,7 @@ Orchestrate complex, long-running coding tasks to an ephemeral cloud environment ## Examples - [Basic Session](./examples/basic-session/README.md) +- [Cloudflare Workers](./examples/cloudflare-workers/README.md) - [Advanced Session](./examples/advanced-session/README.md) - [Agent Workflow](./examples/agent/README.md) - [Webhook Integration](./examples/webhook/README.md) diff --git a/packages/core/examples/cloudflare-workers/README.md b/packages/core/examples/cloudflare-workers/README.md new file mode 100644 index 0000000..c98c10c --- /dev/null +++ b/packages/core/examples/cloudflare-workers/README.md @@ -0,0 +1,34 @@ +# Cloudflare Workers Example + +This example demonstrates how to use the Jules SDK within a Cloudflare Worker environment. By intercepting incoming HTTP `POST` requests, the worker can automatically trigger and orchestrate a new coding session through the `@google/jules-sdk`. + +This is especially useful for edge-based event processing (e.g., handling incoming webhooks from external services like Stripe or GitHub directly at the edge) and kicking off agent workflows globally without dedicated infrastructure. + +## Prerequisites + +- Ensure you have [Bun](https://bun.sh/) installed, or another compatible runtime like Node.js. +- Ensure your `JULES_API_KEY` is set as an environment variable in your local shell or your Cloudflare environment bindings. + +## Running the Example Locally + +The example uses a mocked entry point for the Cloudflare worker module via `index.ts`. To ensure it builds and can run basic checks in the monorepo context: + +1. Build the module: + + ```bash + bun run build + ``` + +2. To run the handler script as a standard Bun process (noting it simulates the Worker `fetch` function structure): + + ```bash + bun run start + ``` + +*(Note: While `bun run start` simply executes `index.ts`, a true worker testing environment typically requires `wrangler` and a test server setup. This simple repository example demonstrates the SDK's structural integration.)* + +## Notes + +- In a real-world Cloudflare deployment, the `JULES_API_KEY` should be set via Cloudflare secrets using `wrangler secret put JULES_API_KEY`. It would be available on the `env` object inside the `fetch` handler. +- If your environment provides the API key via `env.JULES_API_KEY` rather than the global `process.env`, you can customize the instantiation using `jules.with({ apiKey: env.JULES_API_KEY })`. +- Make sure to modify the target `source` in `index.ts` to match the specific GitHub repository or branching strategy your worker intends to automate. diff --git a/packages/core/examples/cloudflare-workers/index.ts b/packages/core/examples/cloudflare-workers/index.ts new file mode 100644 index 0000000..fae8cd8 --- /dev/null +++ b/packages/core/examples/cloudflare-workers/index.ts @@ -0,0 +1,64 @@ +import { jules } from '@google/jules-sdk'; + +/** + * Cloudflare Worker Example + * + * This example demonstrates how to use the Jules SDK within a Cloudflare Worker environment. + * The worker intercepts incoming HTTP POST requests (e.g., a webhook or custom event trigger) + * and starts a new Jules coding session. + */ +export default { + async fetch(request: Request, env: any, ctx: any): Promise { + // Only accept POST requests for this example + if (request.method !== 'POST') { + return new Response('Method Not Allowed. Please send a POST request.', { status: 405 }); + } + + try { + // Parse the incoming JSON payload + const payload = await request.json().catch(() => ({})); + console.log('Received payload:', payload); + + // We can use the global `jules` object since JULES_API_KEY should be passed as an environment variable + // or configured globally in the environment (e.g. via .env in local dev or Cloudflare bindings). + // Note: If JULES_API_KEY is not available globally, you can construct a custom instance + // of Jules SDK using `jules.with({ apiKey: env.JULES_API_KEY })`. + + // Construct a prompt dynamically from the payload + const promptText = `Process this event triggered from a Cloudflare Worker: ${JSON.stringify(payload)}`; + + // Start a Jules session + const session = await jules.session({ + prompt: promptText, + // Define a target source context (replace with your repository/branch as needed) + source: { github: 'davideast/dataprompt', baseBranch: 'main' }, + }); + + console.log(`Successfully created Jules session: ${session.id}`); + + // Return a successful JSON response with the created session ID + return new Response(JSON.stringify({ + success: true, + message: 'Cloudflare Worker processed event and created a session.', + sessionId: session.id, + }), { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }); + + } catch (error) { + console.error('Error creating session:', error); + + return new Response(JSON.stringify({ + success: false, + message: 'Internal Server Error while creating Jules session', + error: error instanceof Error ? error.message : String(error) + }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } + }, +}; diff --git a/packages/core/examples/cloudflare-workers/package.json b/packages/core/examples/cloudflare-workers/package.json new file mode 100644 index 0000000..b0bd7ca --- /dev/null +++ b/packages/core/examples/cloudflare-workers/package.json @@ -0,0 +1,16 @@ +{ + "name": "cloudflare-workers", + "version": "1.0.0", + "description": "Example demonstrating how to use the Jules SDK within a Cloudflare Worker.", + "main": "index.ts", + "scripts": { + "build": "bun build index.ts --target=node", + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "bun-types": "^1.1.8" + } +} \ No newline at end of file From f5383527f1413e18a70d27bc1234c372ef2d03e4 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:51:19 +0000 Subject: [PATCH 09/28] Fixes #234: [Fleet Execution] [GitPatch Goals Review Example] Added a new practical example to the Jules TypeScript SDK demonstrating how to use the session GitPatch data. The example shows how to use a Repoless session to generate code, extract the resulting GitPatch (either via the changeSet artifact or manually from generated files), and then pass that patch to a second session for reviewing against the original goals and coding standards. - Created `packages/core/examples/gitpatch-goals/index.ts` containing the example logic. - Created `packages/core/examples/gitpatch-goals/README.md` explaining the example. - Created `packages/core/examples/gitpatch-goals/package.json` to configure the workspace module. - Updated `packages/core/README.md` to include a link to the new example. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 26 ++- packages/core/README.md | 1 + .../core/examples/gitpatch-goals/README.md | 39 +++++ .../core/examples/gitpatch-goals/index.ts | 148 ++++++++++++++++++ .../core/examples/gitpatch-goals/package.json | 15 ++ 5 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/gitpatch-goals/README.md create mode 100644 packages/core/examples/gitpatch-goals/index.ts create mode 100644 packages/core/examples/gitpatch-goals/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..2d61f94 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", @@ -65,6 +65,16 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/gitpatch-goals": { + "name": "gitpatch-goals", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + "devDependencies": { + "bun-types": "^1.1.8", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +89,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 +115,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -697,6 +707,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "gitpatch-goals": ["gitpatch-goals@workspace:packages/core/examples/gitpatch-goals"], + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1145,8 +1157,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 +1269,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..2f0b32c 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) +- [Gitpatch Goals](./examples/gitpatch-goals/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/gitpatch-goals/README.md b/packages/core/examples/gitpatch-goals/README.md new file mode 100644 index 0000000..e156910 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/README.md @@ -0,0 +1,39 @@ +# GitPatch Goals Review Example + +This example demonstrates how to use the Jules SDK to evaluate the code generated by an AI agent. It uses a "Repoless" session to simulate code generation, then extracts the GitPatch (the diff of the generated code) and uses a second session to review the patch against the original prompt's goals and generic coding standards. + +## 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. + +2. Export your Jules API key: + +```bash +export JULES_API_KEY="your-api-key-here" +``` + +## Running the Example + +Using `bun`: + +```bash +bun run index.ts +``` + +Using `npm` and `tsx` (or similar TypeScript runner): + +```bash +npx tsx index.ts +``` + +## What it does + +1. **Generation Session**: Creates a session asking the agent to write a simple Node.js HTTP server. +2. **Patch Extraction**: Waits for the session to complete and extracts the generated code as a GitPatch. +3. **Review Session**: Creates a second session, providing the original prompt, the generated GitPatch, and instructions to evaluate if the code meets the goals and adheres to best practices. +4. **Result**: Outputs the review findings from the second agent. diff --git a/packages/core/examples/gitpatch-goals/index.ts b/packages/core/examples/gitpatch-goals/index.ts new file mode 100644 index 0000000..6d1288b --- /dev/null +++ b/packages/core/examples/gitpatch-goals/index.ts @@ -0,0 +1,148 @@ +import { jules } from '@google/jules-sdk'; + +/** + * GitPatch Goals Review Example + * + * This example demonstrates how to evaluate an AI agent's output against + * its original goals and coding standards by extracting the generated code + * diff (GitPatch) and passing it to a second review session. + */ +async function main() { + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is missing.'); + process.exit(1); + } + + console.log('--- Step 1: Initiating Code Generation Session ---'); + + const originalPrompt = ` + Create a simple Node.js HTTP server. + Requirements: + - Listen on port 8080. + - Serve a "Hello, World!" plain text response for the root path (/). + - Return a 404 Not Found for all other paths. + - Use the built-in 'http' module. + `; + + // 1. Create the generation session + const genSession = await jules.session({ + prompt: originalPrompt, + }); + + console.log(`Generation Session created! ID: ${genSession.id}`); + console.log('Waiting for the agent to generate code...'); + + const genOutcome = await genSession.result(); + + if (genOutcome.state !== 'completed') { + console.error(`Generation session failed with state: ${genOutcome.state}`); + process.exit(1); + } + + // 2. Extract the GitPatch (diff) of the generated code + let gitPatch = ''; + + // Get activities for the session + const activities = await jules.select({ + from: 'activities', + where: { 'session.id': genSession.id }, + order: 'asc', + }); + + for (const activity of activities) { + if (activity.type === 'progressUpdated' && activity.artifacts) { + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + // The changeSet artifact has a raw content property representing the git diff + if (artifact.content) { + gitPatch += artifact.content + '\n'; + } + } + } + } + } + + if (!gitPatch) { + console.log('No GitPatch data found in the generation session.'); + // Fallback to getting generated files + const files = genOutcome.generatedFiles(); + if (files.size > 0) { + for (const [path, file] of files.entries()) { + gitPatch += `--- a/${path}\n+++ b/${path}\n@@ -0,0 +1,${file.content.split('\n').length} @@\n`; + gitPatch += + file.content + .split('\n') + .map((l) => '+' + l) + .join('\n') + '\n'; + } + } else { + console.error('No generated files found either. Exiting.'); + process.exit(1); + } + } + + console.log('\n--- Step 2: Extracted GitPatch ---'); + console.log(gitPatch.substring(0, 500) + '...\n(truncated for brevity)'); + + console.log('\n--- Step 3: Initiating Review Session ---'); + + const reviewPrompt = ` + You are an expert code reviewer. Review the following GitPatch generated by an AI agent. + + ### Original Goals and Requirements + ${originalPrompt} + + ### GitPatch to Review + \`\`\`diff + ${gitPatch} + \`\`\` + + ### Task + 1. Determine if the generated code successfully meets ALL the Original Goals and Requirements. + 2. Determine if the code adheres to general Node.js coding standards and best practices. + 3. Provide a clear, structured markdown response with the following sections: + - **Goal Satisfaction**: Yes/No and why. + - **Code Quality**: Feedback on best practices. + - **Final Verdict**: Pass or Fail. + `; + + // 3. Create the review session + const reviewSession = await jules.session({ + prompt: reviewPrompt, + }); + + console.log(`Review Session created! ID: ${reviewSession.id}`); + console.log('Waiting for the agent to review the code...'); + + const reviewOutcome = await reviewSession.result(); + + console.log('\n--- Step 4: Review Results ---'); + + if (reviewOutcome.state === 'completed') { + // 4. Output the review results + const reviewActivities = await jules.select({ + from: 'activities', + where: { type: 'agentMessaged', 'session.id': reviewSession.id }, + order: 'desc', + limit: 1, + }); + + if (reviewActivities.length > 0) { + console.log(reviewActivities[0].message); + } else { + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + for (const [filename, content] of files.entries()) { + console.log(`\nReview File: ${filename}`); + console.log(content.content); + } + } else { + console.log('Review agent did not leave a final message or file.'); + } + } + } else { + console.error(`Review session failed with state: ${reviewOutcome.state}`); + } +} + +main().catch(console.error); diff --git a/packages/core/examples/gitpatch-goals/package.json b/packages/core/examples/gitpatch-goals/package.json new file mode 100644 index 0000000..1ad9348 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/package.json @@ -0,0 +1,15 @@ +{ + "name": "gitpatch-goals", + "version": "1.0.0", + "description": "Example demonstrating how to evaluate generated code using a GitPatch.", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "bun-types": "^1.1.8" + } +} From 4b4b8f5037cd24c48104ead5d107a0b69e1337a3 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:53:49 +0000 Subject: [PATCH 10/28] feat(sdk): Add GitPatch Review example Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 26 ++- packages/core/README.md | 1 + .../core/examples/gitpatch-review/README.md | 71 ++++++++ .../core/examples/gitpatch-review/index.ts | 164 ++++++++++++++++++ .../examples/gitpatch-review/package.json | 15 ++ 5 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/README.md create mode 100644 packages/core/examples/gitpatch-review/index.ts create mode 100644 packages/core/examples/gitpatch-review/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..cd8fd04 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", @@ -65,6 +65,16 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/gitpatch-review": { + "name": "jules-gitpatch-review-example", + "version": "1.0.0", + "dependencies": { + "@google/jules-sdk": "workspace:*", + }, + "devDependencies": { + "bun-types": "^1.1.8", + }, + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +89,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 +115,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -773,6 +783,8 @@ "jules-github-actions-example": ["jules-github-actions-example@workspace:packages/core/examples/github-actions"], + "jules-gitpatch-review-example": ["jules-gitpatch-review-example@workspace:packages/core/examples/gitpatch-review"], + "jules-sdk-example": ["jules-sdk-example@workspace:examples/simple"], "kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="], @@ -1145,8 +1157,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 +1269,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..46c9b47 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) +- [Gitpatch Review](./examples/gitpatch-review/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/gitpatch-review/README.md b/packages/core/examples/gitpatch-review/README.md new file mode 100644 index 0000000..b1cee00 --- /dev/null +++ b/packages/core/examples/gitpatch-review/README.md @@ -0,0 +1,71 @@ +# GitPatch Review Example + +This example demonstrates how to use Jules' session GitPatch to review and analyze code generated by a Jules coding agent against the context of a GitHub repository. + +## Overview + +The example runs two sequential sessions using the Jules SDK: + +1. **Generation Session**: Instructs an agent to intentionally write poor code, producing a Git patch with terrible practices. +2. **Review Session**: Takes the resulting Git patch generated from the first session and creates a *new* session, passing the patch string as context to instruct the agent to review and analyze the code against standard coding practices. + +This demonstrates common workflows such as: +- Checking if code generated by Jules sticks to original prompt goals. +- Determining if generated code adheres to repository standards. +- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures from the SDK's abstraction layer. + +## Prerequisites + +- Node.js or Bun installed. +- A Jules API Key. Set it using: + ```bash + export JULES_API_KEY= + ``` + +## Running the Example + +You can run this example using `bun`, `tsx`, or `ts-node`: + +### Using Bun + +```bash +bun run index.ts +``` + +### Using Node.js and TSX + +If you don't have `bun` installed, you can run the example using `tsx`: + +```bash +npm install -g tsx +tsx index.ts +``` + +## Example Output + +```text +1. Starting a new session to generate code... +Code Generation Session ID: jules:session:12345 +Waiting for the code generation to complete... + +2. Retrieving GitPatch data from the session... +Found ChangeSet on session snapshot. + +--- Extracted Patch Content --- +File: bad_math.ts (Additions: 4, Deletions: 0) +------------------------------- + +3. Starting a new session to review the GitPatch... +Review Session ID: jules:session:67890 +Waiting for the review to complete... + +--- Review Agent Analysis --- +This code is terrible! Here are the issues... +1. The function is named 'a' and takes parameters 'x' and 'y', making it unreadable. +2. It lacks basic TypeScript type annotations. +3. The spacing is inconsistent. + +Recommendation: +Rename the function to 'addNumbers', add type hints, and fix formatting. +----------------------------- +``` diff --git a/packages/core/examples/gitpatch-review/index.ts b/packages/core/examples/gitpatch-review/index.ts new file mode 100644 index 0000000..aa7286e --- /dev/null +++ b/packages/core/examples/gitpatch-review/index.ts @@ -0,0 +1,164 @@ +import { jules } from '@google/jules-sdk'; + +/** + * GitPatch Review Example + * + * Demonstrates how to use the Jules SDK to generate a code change + * and then review its GitPatch content. This is useful for analyzing + * generated code before applying it, checking it against coding standards, + * or using it to drive further automation. + */ +async function main() { + 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); + } + + // Define the target repository and base branch + const source = { github: 'davideast/dataprompt', baseBranch: 'main' }; + + console.log('1. Starting a new session to generate code...'); + + try { + // Start a session to generate some changes. We instruct the agent to + // intentionally write bad code so we have something obvious to review. + const codeGenSession = await jules.session({ + prompt: `Create a new file called 'bad_math.ts' with a poorly implemented +function that adds two numbers together. Include no comments and terrible variable names.`, + source, + }); + + console.log(`Code Generation Session ID: ${codeGenSession.id}`); + console.log('Waiting for the code generation to complete...'); + + // Await the completion of the session + const genOutcome = await codeGenSession.result(); + + if (genOutcome.state !== 'completed' && genOutcome.state !== 'succeeded') { + console.error(`Code generation session failed or did not complete. State: ${genOutcome.state}`); + return; + } + + // Retrieve the activities to find the changeset artifact + console.log('\n2. Retrieving GitPatch data from the session...'); + const activities = await jules.select({ + from: 'activities', + where: { 'session.id': codeGenSession.id }, + order: 'desc', + }); + + let gitPatchStr = ''; + + // Iterate through activities to find a ChangeSet artifact + for (const activity of activities) { + if (activity.artifacts) { + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + // In the SDK, the underlying structure for ChangeSet includes the gitPatch. + // We can extract the unidiffPatch from the raw artifact data. + const parsed = artifact.parsed(); + // Often, you might want the raw patch string to send to another agent or tool. + // We'll reconstruct a simple patch string from the parsed diff for this example. + + for(const file of parsed.files) { + gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; + for(const chunk of file.chunks) { + gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + for(const change of chunk.changes) { + if(change.type === 'add') gitPatchStr += `+${change.content}\n`; + if(change.type === 'del') gitPatchStr += `-${change.content}\n`; + if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; + } + } + } + } + } + } + } + + if (!gitPatchStr) { + console.log('No GitPatch found. The agent might not have written any code.'); + // If no gitpatch was found in activities, sometimes the final state has it on the outcome snapshot. + const snapshot = codeGenSession.snapshot(); + const changeSet = snapshot?.changeSet(); + if(changeSet) { + console.log("Found ChangeSet on session snapshot."); + // Note: in a real scenario you would access the raw gitpatch string if the API exposes it directly, + // but for the SDK's abstraction, parsing the diff is the recommended way. + const parsed = changeSet.parsed(); + for(const file of parsed.files) { + gitPatchStr += `File: ${file.path} (Additions: ${file.additions}, Deletions: ${file.deletions})\n`; + } + } + } + + if(!gitPatchStr) { + console.log("Could not find any changes. Exiting."); + return; + } + + console.log('\n--- Extracted Patch Content ---'); + console.log(gitPatchStr); + console.log('-------------------------------\n'); + + console.log('3. Starting a new session to review the GitPatch...'); + + // Now, create a second session to review the patch generated by the first session. + // This demonstrates using Jules to evaluate code against coding standards. + const reviewSession = await jules.session({ + prompt: `Review the following code change patch and determine if it adheres to clean coding standards. +Provide a short summary of the issues found and how they should be fixed. + +## Git Patch +\`\`\`diff +${gitPatchStr} +\`\`\` +`, + // We can optionally attach the same source repo context if the review needs to know about other files + source + }); + + console.log(`Review Session ID: ${reviewSession.id}`); + console.log('Waiting for the review to complete...'); + + const reviewOutcome = await reviewSession.result(); + + if (reviewOutcome.state === 'completed' || reviewOutcome.state === 'succeeded') { + // Find the agent's review message + const reviewActivities = await jules.select({ + from: 'activities', + where: { type: 'agentMessaged', 'session.id': reviewSession.id }, + order: 'desc', + limit: 1, + }); + + if (reviewActivities.length > 0) { + console.log('\n--- Review Agent Analysis ---'); + console.log(reviewActivities[0].message); + console.log('-----------------------------'); + } else { + // Fallback if the agent wrote the review to a file instead of a message + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + console.log('\n--- Review Agent Analysis (Files) ---'); + for (const [filename, content] of files.entries()) { + console.log(`\nFile: ${filename}`); + console.log(content.content); + } + console.log('-------------------------------------'); + } else { + console.log("The review agent completed but didn't leave a message or file."); + } + } + } else { + console.error(`Review session failed or did not complete. State: ${reviewOutcome.state}`); + } + + } catch (error) { + console.error('An error occurred during the process:', error); + } +} + +// Run the example +main(); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json new file mode 100644 index 0000000..f2863f2 --- /dev/null +++ b/packages/core/examples/gitpatch-review/package.json @@ -0,0 +1,15 @@ +{ + "name": "jules-gitpatch-review-example", + "version": "1.0.0", + "description": "Example demonstrating how to use Jules' session GitPatch to review and analyze generated code", + "main": "index.ts", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@google/jules-sdk": "workspace:*" + }, + "devDependencies": { + "bun-types": "^1.1.8" + } +} From 90bee5e4edc6e00088c753c7d5d74d0a7e20c48d Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:54:41 +0000 Subject: [PATCH 11/28] feat: refactor google sheets example to an agent cli Refactors the previously added Google Sheets example into a robust CLI application. Adheres to Agent SDK best practices and the Typed Service Contract pattern. Changes include: - Implementing CLI parsing with `citty`. - Defining input schemas, error codes, and Result objects in `spec.ts` using `zod`. - Extracting business logic to a class in `handler.ts` that implements the spec. - Adding a `--json` flag to provide deterministic, machine-readable output. Fixes #229 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 6 + .../core/examples/google-sheets/README.md | 36 ++-- .../core/examples/google-sheets/handler.ts | 130 ++++++++++++++ packages/core/examples/google-sheets/index.ts | 168 ++++++++---------- .../core/examples/google-sheets/package.json | 4 +- packages/core/examples/google-sheets/spec.ts | 48 +++++ 6 files changed, 288 insertions(+), 104 deletions(-) create mode 100644 packages/core/examples/google-sheets/handler.ts create mode 100644 packages/core/examples/google-sheets/spec.ts diff --git a/bun.lock b/bun.lock index 42b0cfa..2f2f933 100644 --- a/bun.lock +++ b/bun.lock @@ -69,7 +69,9 @@ "name": "google-sheets-context-example", "dependencies": { "@google/jules-sdk": "workspace:*", + "citty": "^0.2.1", "googleapis": "^171.4.0", + "zod": "^4.3.6", }, }, "packages/core/examples/webhook": { @@ -1298,6 +1300,10 @@ "gcp-metadata/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], + "google-sheets-context-example/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + + "google-sheets-context-example/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "googleapis-common/gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], "jules-github-actions-example/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], diff --git a/packages/core/examples/google-sheets/README.md b/packages/core/examples/google-sheets/README.md index ffe8280..7a442ac 100644 --- a/packages/core/examples/google-sheets/README.md +++ b/packages/core/examples/google-sheets/README.md @@ -1,6 +1,8 @@ -# Google Sheets Context Example +# 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. +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 @@ -23,25 +25,35 @@ This example demonstrates how to extract tabular data from a Google Sheet using export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/service-account-file.json" ``` -4. (Optional) Provide your own spreadsheet ID. By default, it uses a public sample sheet: - ```bash - export SPREADSHEET_ID="your-spreadsheet-id" - ``` +## Running the CLI -## Running the Example +Navigate to this directory and use `bun` to run the file. Use the `--help` flag to see available options: -Navigate to this directory and use `bun` to run the file: +```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 +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." ``` -Using `npm` and `tsx` (or similar TypeScript runner): +### 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 -npx tsx index.ts +bun run index.ts --spreadsheetId "..." --range "..." --prompt "..." --json ``` ## What it does -The script authenticates with Google Cloud using Application Default Credentials to retrieve rows from the provided spreadsheet ID using the range `Class Data!A2:E`. It extracts this information, converts the rows into comma-separated text grouped by newlines, and appends it to a prompt given to an AI agent in `jules.session`. It waits for the agent to complete the task and displays the final analysis response. +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. diff --git a/packages/core/examples/google-sheets/handler.ts b/packages/core/examples/google-sheets/handler.ts new file mode 100644 index 0000000..9bb7a8e --- /dev/null +++ b/packages/core/examples/google-sheets/handler.ts @@ -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 { + 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 = {}; + + 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, + }, + }; + } + } +} diff --git a/packages/core/examples/google-sheets/index.ts b/packages/core/examples/google-sheets/index.ts index 76c746c..093ee17 100644 --- a/packages/core/examples/google-sheets/index.ts +++ b/packages/core/examples/google-sheets/index.ts @@ -1,101 +1,87 @@ -import { jules } from '@google/jules-sdk'; -import { google } from 'googleapis'; - -/** - * Google Sheets Context Example - * - * Demonstrates reading data from a Google Sheet and using it as context - * to create a Jules session. - */ -async function main() { - 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); - } - - // Authentication Setup for Google API - // Uses Application Default Credentials by default (GOOGLE_APPLICATION_CREDENTIALS) - // or checks running environment (e.g., GCE, GKE) - const auth = new google.auth.GoogleAuth({ - scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'], - }); - - const sheets = google.sheets({ version: 'v4', auth }); - - // Example public Google Sheet: "Class Data" from Google Sheets API examples - const spreadsheetId = process.env.SPREADSHEET_ID || '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms'; - const range = 'Class Data!A2:E'; - - console.log('Fetching data from Google Sheets...'); - try { - const response = await sheets.spreadsheets.values.get({ - spreadsheetId, - range, - }); - - const rows = response.data.values; - if (!rows || rows.length === 0) { - console.log('No data found in the spreadsheet.'); - return; +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); } - console.log(`Found ${rows.length} rows of data. Formatting as context...`); - - // Format data as context. Here we create a simple text representation. - // Each row is joined by a comma, and rows are separated by newlines. - const sheetContext = rows.map(row => row.join(', ')).join('\n'); - - console.log('\nCreating a Jules session with the Sheet context...'); - - // Create a repoless session passing the spreadsheet data in the prompt - const session = await jules.session({ - prompt: `Analyze the following student data from a Google Sheet and provide a brief summary of the key demographics and trends (e.g., major distribution, class year distribution):\n\n${sheetContext}`, - }); - - console.log(`Session created! ID: ${session.id}`); - console.log('Waiting for the agent to analyze the data...'); - - const outcome = await session.result(); - console.log(`\n--- Session Result ---`); - console.log(`State: ${outcome.state}`); + if (!args.json) { + console.log('Fetching Google Sheet data and initializing session...'); + } - if (outcome.state === 'completed') { - // Retrieve the final agent message - const activities = await jules.select({ - from: 'activities', - where: { type: 'agentMessaged', 'session.id': session.id }, - order: 'desc', - limit: 1, - }); + const handler = new GoogleSheetsSessionHandler(); + const result = await handler.execute(inputResult.data); - if (activities.length > 0) { - console.log('\nAgent Analysis:'); - console.log(activities[0].message); + if (!result.success) { + if (args.json) { + console.error(JSON.stringify(result.error)); } else { - console.log('\nThe agent did not leave a final message.'); - - // Check for any generated files instead - const files = outcome.generatedFiles(); - if (files.size > 0) { - console.log('\nGenerated Files:'); - for (const [filename, content] of files.entries()) { - console.log(`\nFile: ${filename}`); - console.log(content.content); - } + console.error(`Error (${result.error.code}): ${result.error.message}`); + if (result.error.recoverable) { + console.log('Suggestion: Check your credentials and input values.'); } } - } else { - console.error('The session did not complete successfully.'); + process.exit(1); } - } catch (error) { - console.error('An error occurred:', error); - if (error instanceof Error && error.message.includes('Could not load the default credentials')) { - console.error('\nMake sure to set your GOOGLE_APPLICATION_CREDENTIALS environment variable'); - console.error('to the path of your Google Cloud service account key file.'); + + 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}`); + } + } } - } -} + }, +}); -// Run the example -main(); +runMain(mainCommand); diff --git a/packages/core/examples/google-sheets/package.json b/packages/core/examples/google-sheets/package.json index 49ec727..f118ea0 100644 --- a/packages/core/examples/google-sheets/package.json +++ b/packages/core/examples/google-sheets/package.json @@ -6,6 +6,8 @@ }, "dependencies": { "@google/jules-sdk": "workspace:*", - "googleapis": "^171.4.0" + "citty": "^0.2.1", + "googleapis": "^171.4.0", + "zod": "^4.3.6" } } diff --git a/packages/core/examples/google-sheets/spec.ts b/packages/core/examples/google-sheets/spec.ts new file mode 100644 index 0000000..a8d2490 --- /dev/null +++ b/packages/core/examples/google-sheets/spec.ts @@ -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; + +// 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 + | z.infer; + +// 4. INTERFACE +export interface RunSessionSpec { + execute(input: RunSessionInput): Promise; +} From a62fac40d0a7f34ee71c25631b62a71f15de6769 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:55:02 +0000 Subject: [PATCH 12/28] Add gitpatch-local example to the Jules SDK This commit introduces a new example demonstrating how to retrieve a `changeSet` artifact's `gitPatch` from a Jules session and apply it locally. It covers creating the branch, extracting the `unidiffPatch`, writing it to a patch file, and safely applying it to the local system using Git. The SDK's core README has also been updated to link to this new example. Fixes #237 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 19 ++- packages/core/README.md | 1 + .../core/examples/gitpatch-local/README.md | 45 +++++++ .../core/examples/gitpatch-local/index.ts | 123 ++++++++++++++++++ .../core/examples/gitpatch-local/package.json | 5 + 5 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 packages/core/examples/gitpatch-local/README.md create mode 100644 packages/core/examples/gitpatch-local/index.ts create mode 100644 packages/core/examples/gitpatch-local/package.json diff --git a/bun.lock b/bun.lock index 72b46b8..6f09192 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", @@ -65,6 +65,9 @@ "typescript": "^5.0.0", }, }, + "packages/core/examples/gitpatch-local": { + "name": "gitpatch-local-example", + }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -79,7 +82,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 +108,7 @@ }, "packages/mcp": { "name": "@google/jules-mcp", - "version": "0.1.0", + "version": "0.2.0", "bin": { "jules-mcp": "./dist/cli.mjs", }, @@ -697,6 +700,8 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "gitpatch-local-example": ["gitpatch-local-example@workspace:packages/core/examples/gitpatch-local"], + "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -1145,8 +1150,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 +1262,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..a959842 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) +- [Gitpatch Local](./examples/gitpatch-local/README.md) ## Send work to a Cloud based session diff --git a/packages/core/examples/gitpatch-local/README.md b/packages/core/examples/gitpatch-local/README.md new file mode 100644 index 0000000..c9d1619 --- /dev/null +++ b/packages/core/examples/gitpatch-local/README.md @@ -0,0 +1,45 @@ +# Gitpatch Local Example + +This example demonstrates how to use the Jules SDK to create a session, retrieve a `changeSet` artifact, and apply the generated code modifications locally on your machine using Git. + +It specifically showcases how to: +- Create a simple text file locally to act as a target. +- Spin up a local git branch. +- Request Jules to modify the file content. +- Download the resulting `GitPatch` (`unidiffPatch`) and write it to a `.patch` file. +- Use `git apply` to patch the code on the local machine 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) + +## 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" +``` + +## Running the Example + +Using `bun`: + +```bash +bun run index.ts +``` + +Using `npm` and `tsx` (or similar TypeScript runner): + +```bash +npx tsx index.ts +``` + +## What it does + +The script creates a temporary file `test_patch_target.txt` and starts a local git branch. It creates a session using `jules.session` to ask an agent to change the second line of the file. Once complete, it searches the session activities 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 file. It then cleans up by rolling back the git changes and deleting the temporary file. diff --git a/packages/core/examples/gitpatch-local/index.ts b/packages/core/examples/gitpatch-local/index.ts new file mode 100644 index 0000000..15128a4 --- /dev/null +++ b/packages/core/examples/gitpatch-local/index.ts @@ -0,0 +1,123 @@ +import { jules } from '@google/jules-sdk'; +import { execSync, execFileSync } from 'child_process'; +import { writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; + +/** + * Gitpatch Local Example + * + * Demonstrates how to use Jules' session GitPatch data to download + * and patch the code locally in a new branch on the user's machine. + */ +async function main() { + 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); + } + + // Set up a simple target file to be modified by the agent + const testFileName = 'test_patch_target.txt'; + const testFilePath = join(process.cwd(), testFileName); + writeFileSync(testFilePath, 'This is a test file.\nIt will be modified by Jules.\n'); + + // Let's create a local branch to apply changes to + const branchName = `jules-patch-test-${Date.now()}`; + try { + console.log(`Creating a new local branch: ${branchName}`); + execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' }); + } catch (e) { + console.error(`Failed to create branch. Are you in a git repository?`); + // Fallback for execution outside git repos, but ideally this runs in one. + } + + console.log('Creating a new Jules session...'); + try { + // 1. Create a session asking for a specific code change + const session = await jules.session({ + prompt: `Modify the file named ${testFileName}. Change the second line to say "It has been modified by Jules!"`, + }); + + console.log(`Session created! ID: ${session.id}`); + console.log('Waiting for the agent to complete the task...'); + + // 2. Await the result of the session + const outcome = await session.result(); + console.log(`\nSession completed with state: ${outcome.state}`); + + if (outcome.state === 'completed') { + console.log('\nSearching for changeSet artifacts...'); + + // 3. Retrieve the activities to find the changeSet artifact + // In this example we query activities, but we could also use outcome.generatedFiles() + // or session.stream() depending on the workflow. + const activities = await jules.select({ + from: 'activities', + where: { 'session.id': session.id }, + }); + + let patchApplied = false; + + for (const activity of activities) { + if (!activity.artifacts) continue; + + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + console.log('Found a changeSet artifact!'); + + const gitPatch = artifact.gitPatch; + if (gitPatch && gitPatch.unidiffPatch) { + const patchPath = join(process.cwd(), 'jules_changes.patch'); + + // 4. Save the unidiff patch locally + console.log(`Writing patch to ${patchPath}...`); + writeFileSync(patchPath, gitPatch.unidiffPatch); + + // 5. Apply the patch using git + console.log('Applying the patch locally...'); + try { + // Using git apply to apply the patch + execFileSync('git', ['apply', patchPath], { stdio: 'inherit' }); + console.log('Patch applied successfully!'); + + // Commit the changes + // Using execFileSync to avoid shell command injection vulnerabilities + execFileSync('git', ['add', testFileName], { stdio: 'inherit' }); + const commitMsg = gitPatch.suggestedCommitMessage || 'Applied changes from Jules'; + execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'inherit' }); + console.log(`Changes committed to branch ${branchName}!`); + patchApplied = true; + } catch (applyError) { + console.error('Failed to apply or commit the patch:', applyError); + } finally { + // Clean up the patch file + try { + unlinkSync(patchPath); + } catch (e) {} + } + } + } + } + } + + if (!patchApplied) { + console.log('No patch was generated or applied.'); + } + + } else { + console.error('The session did not complete successfully.'); + } + } catch (error) { + console.error('An error occurred during the session:', error); + } finally { + // Clean up test target file + try { + unlinkSync(testFilePath); + console.log(`\nThe branch '${branchName}' has been left locally for you to inspect!`); + console.log(`When you are done, you can delete it with: git checkout - && git branch -D ${branchName}`); + } catch (e) {} + } +} + +// Run the example +main(); diff --git a/packages/core/examples/gitpatch-local/package.json b/packages/core/examples/gitpatch-local/package.json new file mode 100644 index 0000000..b070fd3 --- /dev/null +++ b/packages/core/examples/gitpatch-local/package.json @@ -0,0 +1,5 @@ +{ + "name": "gitpatch-local-example", + "private": true, + "type": "module" +} From eb8ef264e42d0e8b5e745cfbaa22a7035b5949ff Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:58:45 +0000 Subject: [PATCH 13/28] docs: update AI SDK example to Agent CLI Updates the previous Vercel AI SDK integration example to follow Agent DX best practices. - Wraps the AI integration in a `citty` CLI supporting human text and machine-readable JSON outputs. - Adopts the `@ai-sdk/google` provider using `gemini-3.1-flash-lite-preview`. - Refactors the Jules `tool` into a rigorous Typed Service Contract (spec/handler boundary) for explicit parsing and error handling. Fixes #227 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 5 +- packages/core/examples/ai-sdk/README.md | 38 +++--- packages/core/examples/ai-sdk/index.ts | 124 ------------------ packages/core/examples/ai-sdk/package.json | 5 +- packages/core/examples/ai-sdk/src/cli.ts | 83 ++++++++++++ .../src/tools/jules-coding-task/handler.ts | 70 ++++++++++ .../src/tools/jules-coding-task/index.ts | 23 ++++ .../src/tools/jules-coding-task/spec.ts | 47 +++++++ 8 files changed, 247 insertions(+), 148 deletions(-) delete mode 100644 packages/core/examples/ai-sdk/index.ts create mode 100644 packages/core/examples/ai-sdk/src/cli.ts create mode 100644 packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts create mode 100644 packages/core/examples/ai-sdk/src/tools/jules-coding-task/index.ts create mode 100644 packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts diff --git a/bun.lock b/bun.lock index 718b3b4..02ed177 100644 --- a/bun.lock +++ b/bun.lock @@ -56,9 +56,10 @@ "name": "ai-sdk-example", "version": "1.0.0", "dependencies": { - "@ai-sdk/openai": "^1.1.13", + "@ai-sdk/google": "^1.1.17", "@google/jules-sdk": "workspace:*", "ai": "^4.1.45", + "citty": "^0.1.6", "zod": "^3.24.2", }, }, @@ -181,7 +182,7 @@ "@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="], - "@ai-sdk/openai": ["@ai-sdk/openai@1.3.24", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q=="], + "@ai-sdk/google": ["@ai-sdk/google@1.2.22", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw=="], "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], diff --git a/packages/core/examples/ai-sdk/README.md b/packages/core/examples/ai-sdk/README.md index 688c348..d034247 100644 --- a/packages/core/examples/ai-sdk/README.md +++ b/packages/core/examples/ai-sdk/README.md @@ -1,8 +1,10 @@ -# Vercel AI SDK Integration Example +# Vercel AI SDK Integration Example (Agent DX CLI) This example demonstrates how to integrate the Jules SDK with the Vercel AI SDK to provide AI-powered coding capabilities within an AI application. -It uses the `generateText` function from the `ai` package and an OpenAI model. The AI is given a custom tool called `executeCodingTask` which internally uses the Jules SDK to spin up a cloud environment and perform a complex coding task. +Following **Agent DX best practices**, this example is packaged as a CLI built with `citty`. It utilizes the `generateText` function from the `ai` package and Google's `gemini-3.1-flash-lite-preview` model. + +The AI is given a composable, isolated tool called `executeCodingTask` that internally uses the Jules SDK to spin up a cloud environment and perform complex coding tasks. The tool is implemented using the **Typed Service Contract** pattern, providing rigorous type safety, input parsing (via Zod), and explicit error handling (Result Pattern). ## Prerequisites @@ -11,34 +13,30 @@ It uses the `generateText` function from the `ai` package and an OpenAI model. T ```bash export JULES_API_KEY= ``` -- An OpenAI API Key. Set it using: +- A Google Generative AI API Key. Set it using: ```bash - export OPENAI_API_KEY= + export GOOGLE_GENERATIVE_AI_API_KEY= ``` ## Running the Example -You can run this example using `bun`, `tsx`, or `ts-node`: - -### Using Bun +You can run this example using `bun`: +### Standard Human-Friendly Output ```bash -bun run index.ts +bun start --prompt "Fix visibility issues by changing background colors to a zinc palette." --repo "your-org/your-repo" ``` -### Using Node.js and TSX - -If you don't have `bun` installed, you can run the example using `tsx`: - +### Agent-Friendly JSON Output (Agent DX) ```bash -npm install -g tsx -tsx index.ts +bun start --prompt "Fix visibility issues." --output json ``` -## Example Overview +## Architecture + +This project is structured for predictability and predictability: -1. The script initializes an OpenAI model. -2. It calls `generateText` with a user prompt asking for a coding fix. -3. The AI model determines it needs to call the `executeCodingTask` tool. -4. The tool executes, creating a Jules session (`jules.session`) and waits for the result. -5. The outcome is returned to the AI model, which then summarizes the action to the user. +1. **CLI Entrypoint (`src/cli.ts`)**: Built with `citty`, accepts arguments like `--output json` tailored for both humans and agent orchestrators. Uses `@ai-sdk/google` to talk to Gemini. +2. **Tool Spec (`src/tools/jules-coding-task/spec.ts`)**: The Contract boundary. Parses tool input using Zod and defines a strict `Result` return type. +3. **Tool Handler (`src/tools/jules-coding-task/handler.ts`)**: The impure business logic. It initiates `jules.session()`, waits for the session to complete, and evaluates the `session.result()`. It *never* throws errors; it returns formatted Failure/Success objects. +4. **Tool Wrapper (`src/tools/jules-coding-task/index.ts`)**: Maps the typed contract into a standard Vercel AI SDK `tool()` wrapper, keeping the `generateText` invocation completely isolated from the execution details. diff --git a/packages/core/examples/ai-sdk/index.ts b/packages/core/examples/ai-sdk/index.ts deleted file mode 100644 index 2b333f9..0000000 --- a/packages/core/examples/ai-sdk/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { jules } from '@google/jules-sdk'; -import { generateText, tool } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import { z } from 'zod'; - -/** - * AI SDK Integration Example - * - * This example demonstrates how to integrate the Vercel AI SDK - * with the Jules SDK by creating an AI-powered application that - * can delegate coding tasks to Jules using an AI tool. - */ -async function main() { - console.log('Starting AI SDK Integration Example...'); - - // The task we want the general AI model to handle, which involves coding - const userRequest = - 'Please fix the visibility issues in the repository "your-org/your-repo" on branch "main". The backgrounds are too light and there is low contrast on button hovers.'; - - console.log(`User Request: "${userRequest}"`); - - // We use Vercel AI SDK to generate a response, providing it with - // a tool that can execute coding tasks via Jules. - const { text, toolCalls, toolResults } = await generateText({ - model: openai('gpt-4o'), // Or your preferred OpenAI model - prompt: userRequest, - tools: { - executeCodingTask: tool({ - description: - 'Executes a complex coding task in an ephemeral cloud environment and returns the result (like a PR URL).', - parameters: z.object({ - prompt: z - .string() - .describe( - 'Detailed instructions for the coding task, including what needs to be changed.', - ), - githubRepo: z - .string() - .describe( - 'The GitHub repository in the format "owner/repo".', - ), - baseBranch: z - .string() - .describe('The base branch to make the changes against.'), - }), - execute: async ({ prompt, githubRepo, baseBranch }) => { - console.log(`\nTool 'executeCodingTask' invoked!`); - console.log(` Repo: ${githubRepo} (${baseBranch})`); - console.log(` Prompt: ${prompt}\n`); - - try { - // Note: In a real scenario, you'd use a real repository you have access to. - // For this example, if the githubRepo is "your-org/your-repo", we'll run a repoless session - // to simulate the behavior, or use a known public repo. - const useRepoless = githubRepo === 'your-org/your-repo'; - - const sessionOptions: any = { - prompt, - }; - - if (!useRepoless) { - sessionOptions.source = { - github: githubRepo, - baseBranch: baseBranch, - }; - sessionOptions.autoPr = true; - } else { - // Simulate a basic repoless task if dummy repo provided - sessionOptions.prompt = `Simulate a fix for: ${prompt}`; - } - - // Create and start the Jules session - const session = await jules.session(sessionOptions); - console.log(` Jules session created: ${session.id}`); - console.log(` Waiting for session to complete...`); - - // Wait for the session to complete - const outcome = await session.result(); - console.log(` Session finished with state: ${outcome.state}`); - - if (outcome.pullRequest) { - return `Successfully completed the task. A Pull Request has been created: ${outcome.pullRequest.url}`; - } else if (outcome.state === 'succeeded') { - const files = outcome.generatedFiles(); - return `Successfully completed the task. Generated ${files.size} files in a repoless environment.`; - } else { - return `Failed to complete the task. Session state: ${outcome.state}`; - } - } catch (error: any) { - console.error(' Error during Jules session:', error); - return `Failed to execute coding task due to an error: ${error.message}`; - } - }, - }), - }, - maxSteps: 2, // Allow the model to call the tool and then respond to the user - }); - - console.log('\n--- Final Response from AI ---'); - console.log(text); - console.log('------------------------------'); - - if (toolCalls && toolCalls.length > 0) { - console.log('\nTool Calls Made:'); - for (const call of toolCalls) { - console.log(`- ${call.toolName} with args:`, call.args); - } - } -} - -// Ensure required environment variables are set -if (!process.env.JULES_API_KEY) { - console.error('Error: JULES_API_KEY environment variable is missing.'); - console.log('Please set it using: export JULES_API_KEY='); - process.exit(1); -} - -if (!process.env.OPENAI_API_KEY) { - console.error('Error: OPENAI_API_KEY environment variable is missing.'); - console.log('Please set it using: export OPENAI_API_KEY='); - process.exit(1); -} - -main().catch(console.error); diff --git a/packages/core/examples/ai-sdk/package.json b/packages/core/examples/ai-sdk/package.json index 95ea55c..224dabd 100644 --- a/packages/core/examples/ai-sdk/package.json +++ b/packages/core/examples/ai-sdk/package.json @@ -4,12 +4,13 @@ "description": "An example integrating Vercel AI SDK with Jules SDK", "type": "module", "scripts": { - "start": "bun run index.ts" + "start": "bun run src/cli.ts" }, "dependencies": { - "@ai-sdk/openai": "^1.1.13", + "@ai-sdk/google": "^1.1.17", "@google/jules-sdk": "workspace:*", "ai": "^4.1.45", + "citty": "^0.1.6", "zod": "^3.24.2" } } diff --git a/packages/core/examples/ai-sdk/src/cli.ts b/packages/core/examples/ai-sdk/src/cli.ts new file mode 100644 index 0000000..7b94aa1 --- /dev/null +++ b/packages/core/examples/ai-sdk/src/cli.ts @@ -0,0 +1,83 @@ +import { defineCommand, runMain } from 'citty'; +import { generateText } from 'ai'; +import { google } from '@ai-sdk/google'; +import { executeCodingTask } from './tools/jules-coding-task/index.js'; + +const main = defineCommand({ + meta: { + name: 'ai-sdk-example', + version: '1.0.0', + description: 'A CLI demonstrating Vercel AI SDK integration with Jules SDK using Agent DX principles.', + }, + args: { + prompt: { + type: 'string', + description: 'The coding prompt to feed the AI.', + required: true, + }, + output: { + type: 'string', + description: 'Output format (json or text). Use json for agents.', + default: 'text', + }, + repo: { + type: 'string', + description: 'Optional GitHub repository (e.g. "owner/repo").', + required: false, + }, + }, + async run({ args }) { + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is missing.'); + process.exit(1); + } + + if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) { + console.error('Error: GOOGLE_GENERATIVE_AI_API_KEY environment variable is missing.'); + process.exit(1); + } + + try { + // Create user context including the repo if provided + const contextPrompt = args.repo + ? `Task: ${args.prompt}\nContext: Apply this task to the repository "${args.repo}".` + : `Task: ${args.prompt}`; + + const { text, toolCalls } = await generateText({ + model: google('gemini-3.1-flash-lite-preview'), + prompt: contextPrompt, + tools: { + executeCodingTask, + }, + maxSteps: 2, + }); + + if (args.output === 'json') { + console.log(JSON.stringify({ + success: true, + result: text, + toolCalls: toolCalls?.map(c => ({ name: c.toolName, args: c.args })) || [] + }, null, 2)); + } else { + console.log('\n--- Final Response from AI ---'); + console.log(text); + if (toolCalls && toolCalls.length > 0) { + console.log('\n--- Tools Invoked ---'); + toolCalls.forEach(c => console.log(`- ${c.toolName}`)); + } + } + } catch (error: any) { + if (args.output === 'json') { + console.error(JSON.stringify({ + success: false, + error: error.message || String(error) + }, null, 2)); + } else { + console.error('Execution Failed:', error.message || String(error)); + } + process.exit(1); + } + }, +}); + +runMain(main); diff --git a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts new file mode 100644 index 0000000..a06e46a --- /dev/null +++ b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts @@ -0,0 +1,70 @@ +import { jules } from '@google/jules-sdk'; +import { JulesCodingTaskSpec, JulesCodingTaskInput, JulesCodingTaskResult } from './spec.js'; + +export class JulesCodingTaskHandler implements JulesCodingTaskSpec { + async execute(input: JulesCodingTaskInput): Promise { + try { + if (!process.env.JULES_API_KEY) { + return { + success: false, + error: { + code: 'MISSING_CREDENTIALS', + message: 'JULES_API_KEY environment variable is missing.', + recoverable: false, + }, + }; + } + + const sessionOptions: any = { + prompt: input.prompt, + }; + + if (input.githubRepo) { + sessionOptions.source = { + github: input.githubRepo, + baseBranch: input.baseBranch || 'main', + }; + sessionOptions.autoPr = true; + } + + // Create and start the Jules session + const session = await jules.session(sessionOptions); + + // Await final result (do not use cache/select for the current running task) + const outcome = await session.result(); + + if (outcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `Jules session failed. Session ID: ${session.id}`, + recoverable: false, + }, + }; + } + + const prUrl = outcome.pullRequest?.url; + const filesCount = outcome.generatedFiles().size; + + return { + success: true, + data: { + sessionId: session.id, + state: outcome.state, + pullRequestUrl: prUrl, + generatedFilesCount: filesCount, + }, + }; + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } +} diff --git a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/index.ts b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/index.ts new file mode 100644 index 0000000..09f01e6 --- /dev/null +++ b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/index.ts @@ -0,0 +1,23 @@ +import { tool } from 'ai'; +import { JulesCodingTaskInputSchema } from './spec.js'; +import { JulesCodingTaskHandler } from './handler.js'; + +export const executeCodingTask = tool({ + description: 'Executes a complex coding task in an ephemeral cloud environment and returns the result (like a PR URL).', + parameters: JulesCodingTaskInputSchema, + execute: async (input) => { + const handler = new JulesCodingTaskHandler(); + const result = await handler.execute(input); + + // Provide output formatting tailored to the AI context to easily parse the final success/failure + if (!result.success) { + return `Failed: ${result.error.code} - ${result.error.message}`; + } + + if (result.data.pullRequestUrl) { + return `Success: Task completed. PR Created at ${result.data.pullRequestUrl}`; + } + + return `Success: Task completed. ${result.data.generatedFilesCount} files generated in repoless session. Session ID: ${result.data.sessionId}`; + }, +}); diff --git a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts new file mode 100644 index 0000000..f071084 --- /dev/null +++ b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +// 1. INPUT +export const JulesCodingTaskInputSchema = z.object({ + prompt: z.string().describe('Detailed instructions for the coding task, including what needs to be changed.'), + githubRepo: z.string().optional().describe('The GitHub repository in the format "owner/repo" (e.g. "google/jules-sdk"). If omitted, it runs a repoless session.'), + baseBranch: z.string().optional().describe('The base branch to make the changes against. Defaults to "main" if repo provided.'), +}); + +export type JulesCodingTaskInput = z.infer; + +// 2. ERROR CODES +export const JulesCodingTaskErrorCode = z.enum([ + 'SESSION_FAILED', + 'MISSING_CREDENTIALS', + 'INVALID_INPUT', + 'UNKNOWN_ERROR' +]); + +// 3. RESULT +export const JulesCodingTaskSuccess = z.object({ + success: z.literal(true), + data: z.object({ + sessionId: z.string(), + state: z.string(), + pullRequestUrl: z.string().optional(), + generatedFilesCount: z.number().optional(), + }), +}); + +export const JulesCodingTaskFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: JulesCodingTaskErrorCode, + message: z.string(), + recoverable: z.boolean(), + }) +}); + +export type JulesCodingTaskResult = + | z.infer + | z.infer; + +// 4. INTERFACE +export interface JulesCodingTaskSpec { + execute(input: JulesCodingTaskInput): Promise; +} From 3970f9c2a4ff1eb2bf7b2d21a15e0bbcdd186a87 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 00:58:59 +0000 Subject: [PATCH 14/28] feat(sdk-examples): implement gitpatch improve example as CLI Fixes #235 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 13 -- .../core/examples/gitpatch-improve/README.md | 16 ++- .../core/examples/gitpatch-improve/cli.ts | 58 +++++++++ .../core/examples/gitpatch-improve/handler.ts | 115 ++++++++++++++++++ .../core/examples/gitpatch-improve/index.ts | 96 --------------- .../examples/gitpatch-improve/package.json | 8 +- .../core/examples/gitpatch-improve/spec.ts | 44 +++++++ 7 files changed, 233 insertions(+), 117 deletions(-) create mode 100644 packages/core/examples/gitpatch-improve/cli.ts create mode 100644 packages/core/examples/gitpatch-improve/handler.ts delete mode 100644 packages/core/examples/gitpatch-improve/index.ts create mode 100644 packages/core/examples/gitpatch-improve/spec.ts diff --git a/bun.lock b/bun.lock index a6cd1df..b93d13e 100644 --- a/bun.lock +++ b/bun.lock @@ -65,17 +65,6 @@ "typescript": "^5.0.0", }, }, - "packages/core/examples/gitpatch-improve": { - "name": "gitpatch-improve", - "version": "1.0.0", - "dependencies": { - "@google/jules-sdk": "workspace:*", - }, - "devDependencies": { - "tsx": "^4.19.2", - "typescript": "^5.7.3", - }, - }, "packages/core/examples/webhook": { "name": "webhook", "version": "1.0.0", @@ -708,8 +697,6 @@ "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "gitpatch-improve": ["gitpatch-improve@workspace:packages/core/examples/gitpatch-improve"], - "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], diff --git a/packages/core/examples/gitpatch-improve/README.md b/packages/core/examples/gitpatch-improve/README.md index 5be628c..78ad1bc 100644 --- a/packages/core/examples/gitpatch-improve/README.md +++ b/packages/core/examples/gitpatch-improve/README.md @@ -1,6 +1,8 @@ -# GitPatch Improve Example +# GitPatch Improve CLI Example -This example demonstrates how to use the Jules SDK to create a session that analyzes code using GitPatch data to identify areas for improvement or automation in a repository. +This example demonstrates how to use the Jules SDK to build a CLI that analyzes code using GitPatch data to identify areas for improvement or automation in a repository. + +It follows the Typed Service Contract pattern (Spec & Handler) for robust error handling and input validation. ## Setup @@ -13,10 +15,14 @@ export JULES_API_KEY="your-api-key-here" ## Running -You can run the script via: +You can run the CLI script via `bun run start` or passing arguments directly: ```sh -bun run index.ts +# Run with defaults (davideast/dataprompt) +bun run start + +# Run targeting a specific repo and querying the last 20 activities +bun run start --repo your-org/your-repo --limit 20 ``` -This will create a session targeting a specific repository and prompt the agent to analyze the code using GitPatch data to identify areas for improvement and print out the analysis. +This will find a recent local GitPatch activity, create a session targeting the specified repository, and prompt the agent to analyze the code using the GitPatch data to identify areas for improvement and print out the analysis. diff --git a/packages/core/examples/gitpatch-improve/cli.ts b/packages/core/examples/gitpatch-improve/cli.ts new file mode 100644 index 0000000..a176704 --- /dev/null +++ b/packages/core/examples/gitpatch-improve/cli.ts @@ -0,0 +1,58 @@ +import { defineCommand, runMain } from 'citty'; +import { AnalyzeGitPatchInputSchema } from './spec.js'; +import { AnalyzeGitPatchHandler } from './handler.js'; + +const main = defineCommand({ + meta: { + name: 'gitpatch-improve', + version: '1.0.0', + description: 'CLI to analyze GitPatch data for code improvements using Jules SDK.', + }, + args: { + repo: { + type: 'string', + description: 'The GitHub repository to analyze (e.g., davideast/dataprompt)', + default: 'davideast/dataprompt', + alias: 'r', + }, + limit: { + type: 'string', + description: 'Number of recent activities to search for GitPatch data', + default: '10', + alias: 'l', + }, + }, + async run({ args }) { + console.log(`Starting GitPatch Improve analysis...`); + + // Parse input + const input = AnalyzeGitPatchInputSchema.safeParse({ + sourceRepo: args.repo, + limit: parseInt(args.limit, 10), + }); + + if (!input.success) { + console.error('Invalid arguments provided:'); + console.error(input.error.errors); + process.exit(1); + } + + const handler = new AnalyzeGitPatchHandler(); + const result = await handler.execute(input.data); + + if (!result.success) { + console.error(`Error: [${result.error.code}] ${result.error.message}`); + if (result.error.suggestion) { + console.error(`Suggestion: ${result.error.suggestion}`); + } + process.exit(1); + } + + console.log('\n--- Analysis Report ---'); + console.log(`From Source Session ID: ${result.data.sourceSessionId}`); + console.log(result.data.analysis); + console.log('-----------------------\n'); + }, +}); + +runMain(main); diff --git a/packages/core/examples/gitpatch-improve/handler.ts b/packages/core/examples/gitpatch-improve/handler.ts new file mode 100644 index 0000000..d78b2b7 --- /dev/null +++ b/packages/core/examples/gitpatch-improve/handler.ts @@ -0,0 +1,115 @@ +import { jules, ChangeSetArtifact } from '@google/jules-sdk'; +import type { AnalyzeGitPatchSpec, AnalyzeGitPatchInput, AnalyzeGitPatchResult } from './spec.js'; + +export class AnalyzeGitPatchHandler implements AnalyzeGitPatchSpec { + async execute(input: AnalyzeGitPatchInput): Promise { + try { + console.log(`Searching recent ${input.limit} activities for a GitPatch...`); + + // 1. Search for a recent session with GitPatch data + const activitiesWithChanges = await jules.select({ + from: 'activities', + where: { artifactCount: { gt: 0 } }, + order: 'desc', + limit: input.limit, + }); + + let gitPatchData: string | null = null; + let sourceSessionId: string | null = null; + + for (const activity of activitiesWithChanges) { + if (activity.artifacts) { + for (const artifact of activity.artifacts) { + if (artifact.type === 'changeSet') { + const changeSet = artifact as ChangeSetArtifact; + if (changeSet.gitPatch?.unidiffPatch) { + gitPatchData = changeSet.gitPatch.unidiffPatch; + sourceSessionId = activity.session?.id || 'unknown'; + break; + } + } + } + } + if (gitPatchData) break; + } + + if (!gitPatchData) { + return { + success: false, + error: { + code: 'NO_GITPATCH_FOUND', + message: `Could not find any recent GitPatch data in the local cache.`, + suggestion: 'Run a session that modifies code and generates a changeset first.', + recoverable: true, + }, + }; + } + + console.log(`Found GitPatch data from session ${sourceSessionId}.`); + console.log(`Starting analysis session against ${input.sourceRepo}...`); + + // 2. Run a new session against the same source repository to analyze it + const session = await jules.session({ + prompt: `You are an expert code reviewer. Analyze the following GitPatch data against the repository context. + Identify any potential bugs, areas for optimization, or coding standard violations in the changes. + Explain how the automated code could be improved. + Write your analysis to a file named 'analysis.md'. + + ## GitPatch Data + \`\`\`diff + ${gitPatchData} + \`\`\` + `, + source: { github: input.sourceRepo }, + }); + + console.log(`Session created: ${session.id}`); + console.log('Waiting for the agent to complete the analysis...'); + + const outcome = await session.result(); + + if (outcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `The analysis session failed.`, + recoverable: false, + }, + }; + } + + const files = outcome.generatedFiles(); + const analysisFile = files.get('analysis.md'); + + if (!analysisFile) { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `The analysis file was not generated by the agent.`, + recoverable: true, + }, + }; + } + + return { + success: true, + data: { + analysis: analysisFile.content, + sourceSessionId: sourceSessionId || 'unknown', + }, + }; + + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } +} diff --git a/packages/core/examples/gitpatch-improve/index.ts b/packages/core/examples/gitpatch-improve/index.ts deleted file mode 100644 index da86d74..0000000 --- a/packages/core/examples/gitpatch-improve/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { jules, JulesError, ChangeSetArtifact } from '@google/jules-sdk'; - -/** - * GitPatch Improve Example - * - * Demonstrates how to: - * - Query recent sessions to find one with code changes (ChangeSet artifacts) - * - Retrieve the GitPatch data from that session - * - Use the GitPatch data in a new repoless session to analyze for improvements - */ -async function runGitPatchImproveSession() { - try { - console.log('Finding a recent session with a GitPatch...'); - - // We query the local cache for activities that have a changeSet artifact. - // This gives us activities containing code modifications. - const activitiesWithChanges = await jules.select({ - from: 'activities', - where: { artifactCount: { gt: 0 } }, - order: 'desc', - limit: 10, - }); - - let gitPatchData: string | null = null; - let sourceSessionId: string | null = null; - - // Find the first activity with a changeSet artifact - for (const activity of activitiesWithChanges) { - if (activity.artifacts) { - for (const artifact of activity.artifacts) { - if (artifact.type === 'changeSet') { - const changeSet = artifact as ChangeSetArtifact; - if (changeSet.gitPatch?.unidiffPatch) { - gitPatchData = changeSet.gitPatch.unidiffPatch; - sourceSessionId = activity.session?.id || 'unknown'; - break; - } - } - } - } - if (gitPatchData) break; - } - - if (!gitPatchData) { - console.log('No recent GitPatch data found in local cache. Please run another session that generates code changes first.'); - return; - } - - console.log(`Found GitPatch data from session ${sourceSessionId}.`); - console.log(`Starting analysis session...`); - - // Create a new repoless session to analyze the GitPatch data. - const session = await jules.session({ - prompt: `You are an expert code reviewer. Analyze the following GitPatch data. - Identify any potential bugs, areas for optimization, or coding standard violations. - Write your analysis to a file named 'analysis.md'. - - ## GitPatch Data - \`\`\`diff - ${gitPatchData} - \`\`\` - `, - }); - - console.log(`Session created: ${session.id}`); - - // Wait for the session to complete - console.log('Waiting for the agent to complete the analysis...'); - const outcome = await session.result(); - - console.log(`Session finished with state: ${outcome.state}`); - - // Retrieve the analysis file - const files = outcome.generatedFiles(); - const analysisFile = files.get('analysis.md'); - - if (analysisFile) { - console.log('\n--- Analysis Report ---'); - console.log(analysisFile.content); - console.log('-----------------------\n'); - } else { - console.log('Analysis file was not generated.'); - } - - } catch (error) { - if (error instanceof JulesError) { - console.error(`SDK error: ${error.message}`); - } else { - console.error('Unknown error:', error); - } - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - runGitPatchImproveSession(); -} diff --git a/packages/core/examples/gitpatch-improve/package.json b/packages/core/examples/gitpatch-improve/package.json index 386538b..d9274ec 100644 --- a/packages/core/examples/gitpatch-improve/package.json +++ b/packages/core/examples/gitpatch-improve/package.json @@ -2,13 +2,15 @@ "name": "gitpatch-improve", "version": "1.0.0", "private": true, - "description": "Example using Jules' GitPatch data to analyze how to improve or automate code in a repository.", + "description": "Example CLI using Jules' GitPatch data to analyze how to improve or automate code in a repository.", "type": "module", "scripts": { - "start": "tsx index.ts" + "start": "tsx cli.ts" }, "dependencies": { - "@google/jules-sdk": "workspace:*" + "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.25.0" }, "devDependencies": { "tsx": "^4.19.2", diff --git a/packages/core/examples/gitpatch-improve/spec.ts b/packages/core/examples/gitpatch-improve/spec.ts new file mode 100644 index 0000000..d1aabbf --- /dev/null +++ b/packages/core/examples/gitpatch-improve/spec.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; +import type { GitPatch } from '@google/jules-sdk'; + +// 1. INPUT +export const AnalyzeGitPatchInputSchema = z.object({ + sourceRepo: z.string().describe('The GitHub repository in format owner/repo (e.g., davideast/dataprompt)'), + limit: z.number().int().positive().default(10).describe('Number of recent activities to search for GitPatch data'), +}); +export type AnalyzeGitPatchInput = z.infer; + +// 2. ERROR CODES +export const AnalyzeGitPatchErrorCode = z.enum([ + 'NO_GITPATCH_FOUND', + 'SESSION_FAILED', + 'UNKNOWN_ERROR', +]); + +// 3. RESULT +export const AnalyzeGitPatchSuccess = z.object({ + success: z.literal(true), + data: z.object({ + analysis: z.string(), + sourceSessionId: z.string(), + }), +}); + +export const AnalyzeGitPatchFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: AnalyzeGitPatchErrorCode, + message: z.string(), + suggestion: z.string().optional(), + recoverable: z.boolean(), + }), +}); + +export type AnalyzeGitPatchResult = + | z.infer + | z.infer; + +// 4. INTERFACE +export interface AnalyzeGitPatchSpec { + execute(input: AnalyzeGitPatchInput): Promise; +} From 4794eefef6b7f72514cc9ba3440f0bebe3acb0d0 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:05:39 +0000 Subject: [PATCH 15/28] Refactor Custom MCP Server example to use citty CLI and Typed Service Contracts Fixes #231 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 7 + .../core/examples/custom-mcp-server/README.md | 16 +- .../core/examples/custom-mcp-server/index.ts | 145 ++---------------- .../examples/custom-mcp-server/package.json | 9 +- .../src/commands/session-analysis/handler.ts | 65 ++++++++ .../src/commands/session-analysis/index.ts | 2 + .../src/commands/session-analysis/spec.ts | 43 ++++++ .../examples/custom-mcp-server/src/index.ts | 63 ++++++++ 8 files changed, 213 insertions(+), 137 deletions(-) mode change 100644 => 100755 packages/core/examples/custom-mcp-server/index.ts create mode 100644 packages/core/examples/custom-mcp-server/src/commands/session-analysis/handler.ts create mode 100644 packages/core/examples/custom-mcp-server/src/commands/session-analysis/index.ts create mode 100644 packages/core/examples/custom-mcp-server/src/commands/session-analysis/spec.ts create mode 100644 packages/core/examples/custom-mcp-server/src/index.ts diff --git a/bun.lock b/bun.lock index 04a20cd..dd34efb 100644 --- a/bun.lock +++ b/bun.lock @@ -55,9 +55,14 @@ "packages/core/examples/custom-mcp-server": { "name": "custom-mcp-server-example", "version": "1.0.0", + "bin": { + "jules-mcp-cli": "./index.ts", + }, "dependencies": { "@google/jules-sdk": "workspace:*", "@modelcontextprotocol/sdk": "^1.2.0", + "citty": "^0.2.1", + "zod": "^3.23.0", }, }, "packages/core/examples/github-actions": { @@ -1225,6 +1230,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "custom-mcp-server-example/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], diff --git a/packages/core/examples/custom-mcp-server/README.md b/packages/core/examples/custom-mcp-server/README.md index af2f8d2..8a233cb 100644 --- a/packages/core/examples/custom-mcp-server/README.md +++ b/packages/core/examples/custom-mcp-server/README.md @@ -1,10 +1,8 @@ -# Custom MCP Server Example +# Custom MCP Server CLI Example -This example demonstrates how to create a custom MCP (Model Context Protocol) server that integrates with the Jules TypeScript SDK. +This example demonstrates how to create a custom MCP (Model Context Protocol) server wrapped as a CLI tool using the Jules TypeScript SDK, `citty`, and the **Typed Service Contract** pattern. -The custom MCP server provides tools to other AI assistants, allowing them to: -1. Orchestrate Jules sessions locally or in the cloud. -2. Query data, summarize files, or prepare plans to pass as context. +Instead of basic data forwarding, it provides an `analyze_session` tool. It hydrates a Jules session snapshot, extracting the actual file states and final AI context, avoiding partial cache-only issues. ## Setup and Running @@ -26,7 +24,7 @@ Example Claude Desktop Config: ```json { "mcpServers": { - "jules-custom": { + "jules-custom-cli": { "command": "bun", "args": ["run", "/absolute/path/to/this/example/index.ts"], "env": { @@ -36,3 +34,9 @@ Example Claude Desktop Config: } } ``` + +## Typed Service Contract Pattern + +This CLI implements the Vertical Slice Architecture using a strict `Spec` and `Handler` pattern in `src/commands/session-analysis/`: +- **spec.ts**: Defines the input Schema using Zod (parsing input, enforcing boundaries), exhaustively declares error codes, and strictly types the return interface as a Discriminated Union `Result`. +- **handler.ts**: The impure execution context. It implements the interface, interacts with the Jules SDK network layer, and maps runtime and API errors to the defined Spec failures instead of throwing them wildly. diff --git a/packages/core/examples/custom-mcp-server/index.ts b/packages/core/examples/custom-mcp-server/index.ts old mode 100644 new mode 100755 index 48ada15..a1c262c --- a/packages/core/examples/custom-mcp-server/index.ts +++ b/packages/core/examples/custom-mcp-server/index.ts @@ -1,138 +1,25 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import { jules } from '@google/jules-sdk'; +#!/usr/bin/env node +import { defineCommand, runMain } from 'citty'; +import { runMcpServer } from './src/index.js'; -/** - * Custom MCP Server Example - * - * This example demonstrates how to create a custom Model Context Protocol (MCP) - * server that wraps the Jules SDK. This allows other AI assistants (like Claude, - * Zed, etc.) to use Jules to orchestrate complex coding tasks or queries. - */ - -// 1. Initialize the MCP Server -const server = new Server( - { - name: 'jules-custom-mcp-server', +const main = defineCommand({ + meta: { + name: 'jules-mcp-cli', version: '1.0.0', + description: 'A Custom Jules MCP Server built as a CLI tool using citty and Typed Service Contracts.', }, - { - capabilities: { - tools: {}, + args: { + port: { + type: 'string', + description: 'Port for alternative transports (currently uses stdio)', }, }, -); - -// 2. Define the tools available to the MCP client -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: 'run_jules_task', - description: 'Run a Repoless Jules session to answer a question or complete a standalone task', - inputSchema: { - type: 'object', - properties: { - prompt: { - type: 'string', - description: 'The instructions or query for the Jules agent', - }, - }, - required: ['prompt'], - }, - }, - { - name: 'get_jules_sessions', - description: 'Query the local cache for recent Jules sessions', - inputSchema: { - type: 'object', - properties: { - limit: { - type: 'number', - description: 'Maximum number of sessions to return', - }, - }, - }, - }, - ], - }; -}); - -// 3. Handle tool execution requests -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - if (name === 'run_jules_task') { - const prompt = String(args?.prompt || ''); - if (!prompt) { - throw new Error('Prompt is required'); - } - - // Use the Jules SDK to run a session - const session = await jules.session({ prompt }); - const result = await session.result(); - - // Attempt to extract the primary generated markdown file or the final state - let answer = `Session finished with state: ${result.state}`; - const files = result.generatedFiles(); - if (files.size > 0) { - // Just return the first file's content for simplicity - for (const [_, content] of files.entries()) { - answer = content.content; - break; - } - } - - return { - content: [{ type: 'text', text: answer }], - }; - } - - if (name === 'get_jules_sessions') { - const limit = Number(args?.limit) || 5; - - // Query recent sessions using Jules SDK - const sessions = await jules.select({ - from: 'sessions', - limit, - }); - - const summary = sessions.map( - (s) => `ID: ${s.id} | Status: ${s.state} | URL: ${s.url}` - ).join('\n'); - - return { - content: [{ type: 'text', text: summary || 'No sessions found.' }], - }; - } - - throw new Error(`Unknown tool: ${name}`); - } catch (error) { - return { - isError: true, - content: [{ type: 'text', text: String(error) }], - }; - } + async run({ args }) { + await runMcpServer(); + }, }); -// 4. Start the server using stdio transport -async function main() { - if (!process.env.JULES_API_KEY) { - console.error('Warning: JULES_API_KEY environment variable is missing.'); - console.error('The tools will fail if they require SDK calls to the backend.'); - } - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error('Custom Jules MCP Server is running on stdio'); -} - -main().catch((error) => { - console.error('Fatal error in main():', error); +runMain(main).catch((error) => { + console.error('Fatal CLI Error:', error); process.exit(1); }); diff --git a/packages/core/examples/custom-mcp-server/package.json b/packages/core/examples/custom-mcp-server/package.json index 03dffba..a7c76a2 100644 --- a/packages/core/examples/custom-mcp-server/package.json +++ b/packages/core/examples/custom-mcp-server/package.json @@ -1,14 +1,19 @@ { "name": "custom-mcp-server-example", "version": "1.0.0", - "description": "Example demonstrating how to create custom MCP servers with the Jules SDK", + "description": "Example demonstrating how to create a CLI custom MCP server with the Jules SDK", "private": true, "type": "module", + "bin": { + "jules-mcp-cli": "./index.ts" + }, "scripts": { "start": "bun run index.ts" }, "dependencies": { "@google/jules-sdk": "workspace:*", - "@modelcontextprotocol/sdk": "^1.2.0" + "@modelcontextprotocol/sdk": "^1.2.0", + "citty": "^0.2.1", + "zod": "^3.23.0" } } diff --git a/packages/core/examples/custom-mcp-server/src/commands/session-analysis/handler.ts b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/handler.ts new file mode 100644 index 0000000..073101a --- /dev/null +++ b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/handler.ts @@ -0,0 +1,65 @@ +import { SessionAnalysisSpec, SessionAnalysisInput, SessionAnalysisResult } from './spec.js'; +import { jules, JulesApiError } from '@google/jules-sdk'; + +export class SessionAnalysisHandler implements SessionAnalysisSpec { + async execute(input: SessionAnalysisInput): Promise { + try { + const session = jules.session(input.sessionId); + + // Load session and activities to avoid cache-only selection issues + const outcome = await session.info(); + + let lastAgentMessage: string | undefined = undefined; + const history = await session.history().toArray(); + const agentMessages = history.filter(a => a.type === 'agentMessaged'); + if (agentMessages.length > 0) { + lastAgentMessage = agentMessages[agentMessages.length - 1].message; + } + + // We rely on full session snapshot rather than partial cache queries + const snapshot = session.snapshot(); + const files = snapshot?.generatedFiles; + let generatedFilesCount = 0; + if (files) { + generatedFilesCount = typeof files.entries === 'function' ? [...files.entries()].length : Object.keys(files).length; + } + + let summary = `Session ${outcome.id} is ${outcome.state}.`; + if (history.length) { + summary += ` It consists of ${history.length} activities.`; + } + + return { + success: true, + data: { + id: outcome.id, + state: outcome.state, + summary, + totalActivities: history.length, + generatedFilesCount, + lastAgentMessage, + }, + }; + + } catch (error) { + if (error instanceof JulesApiError) { + return { + success: false, + error: { + code: 'API_ERROR', + message: error.message, + recoverable: false, + } + }; + } + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } +} diff --git a/packages/core/examples/custom-mcp-server/src/commands/session-analysis/index.ts b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/index.ts new file mode 100644 index 0000000..768ed7c --- /dev/null +++ b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/index.ts @@ -0,0 +1,2 @@ +export * from './spec.js'; +export * from './handler.js'; diff --git a/packages/core/examples/custom-mcp-server/src/commands/session-analysis/spec.ts b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/spec.ts new file mode 100644 index 0000000..edd83a6 --- /dev/null +++ b/packages/core/examples/custom-mcp-server/src/commands/session-analysis/spec.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; + +export const SessionAnalysisInputSchema = z.object({ + sessionId: z.string().min(1, 'Session ID is required').startsWith('jules:session:', 'Invalid Session ID format'), +}); + +export type SessionAnalysisInput = z.infer; + +export const SessionAnalysisErrorCode = z.enum([ + 'SESSION_NOT_FOUND', + 'API_ERROR', + 'UNAUTHORIZED', + 'UNKNOWN_ERROR', +]); + +export const SessionAnalysisSuccess = z.object({ + success: z.literal(true), + data: z.object({ + id: z.string(), + state: z.string(), + summary: z.string(), + totalActivities: z.number(), + generatedFilesCount: z.number(), + lastAgentMessage: z.string().optional(), + }), +}); + +export const SessionAnalysisFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: SessionAnalysisErrorCode, + message: z.string(), + recoverable: z.boolean(), + }), +}); + +export type SessionAnalysisResult = + | z.infer + | z.infer; + +export interface SessionAnalysisSpec { + execute(input: SessionAnalysisInput): Promise; +} diff --git a/packages/core/examples/custom-mcp-server/src/index.ts b/packages/core/examples/custom-mcp-server/src/index.ts new file mode 100644 index 0000000..ff8b6a7 --- /dev/null +++ b/packages/core/examples/custom-mcp-server/src/index.ts @@ -0,0 +1,63 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { SessionAnalysisHandler } from './commands/session-analysis/handler.js'; +import { SessionAnalysisInputSchema } from './commands/session-analysis/spec.js'; + +export async function runMcpServer() { + if (!process.env.JULES_API_KEY) { + console.error('Warning: JULES_API_KEY environment variable is missing.'); + } + + // 1. Initialize the MCP Server + const server = new McpServer({ + name: 'jules-custom-mcp-server', + version: '1.0.0', + }); + + // 2. Define tools mapped to handlers + server.tool( + 'analyze_session', + 'Provides a detailed analysis and context of a Jules Session, including states and generated artifacts. Replaces brittle local cache lookups with full hydration.', + { + sessionId: z.string().describe('The Jules session ID to analyze, e.g., jules:session:123'), + }, + async ({ sessionId }) => { + const parsedInput = SessionAnalysisInputSchema.safeParse({ sessionId }); + if (!parsedInput.success) { + return { + content: [{ type: 'text', text: `Validation Error: ${parsedInput.error.message}` }], + isError: true, + }; + } + + const handler = new SessionAnalysisHandler(); + const result = await handler.execute(parsedInput.data); + + if (!result.success) { + return { + content: [{ type: 'text', text: `Analysis Failed [${result.error.code}]: ${result.error.message}` }], + isError: true, + }; + } + + const { data } = result; + const formatted = [ + `Session Analysis: ${data.id}`, + `State: ${data.state}`, + `Summary: ${data.summary}`, + `Files Generated: ${data.generatedFilesCount}`, + data.lastAgentMessage ? `\nLast Agent Message:\n${data.lastAgentMessage}` : '', + ].filter(Boolean).join('\n'); + + return { + content: [{ type: 'text', text: formatted }], + }; + } + ); + + // 3. Start server + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Custom Jules MCP Server is running on stdio'); +} From 21456dbfb90f48bb24e044bd4713c8afd21cb4b3 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:07:03 +0000 Subject: [PATCH 16/28] Update GitPatch goals example to CLI with Typed Service Contracts Refactored the gitpatch-goals SDK example based on AI Agent DX best practices: - Wrapped the example in the `citty` CLI framework. - Applied the "Typed Service Contract" (Spec and Handler) pattern using Zod schema validation and explicit Result types for deterministic error handling. - Removed reliance on `jules.select()` cache queries, opting instead to extract `changeSet` and `agentMessaged` activities directly from the awaited session snapshot (`session.result()`). - Added a `--json` output flag to cater to agent-first readability. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 2 + .../core/examples/gitpatch-goals/index.ts | 363 ++++++++++++------ .../core/examples/gitpatch-goals/package.json | 4 +- 3 files changed, 240 insertions(+), 129 deletions(-) diff --git a/bun.lock b/bun.lock index 2d61f94..b1b803b 100644 --- a/bun.lock +++ b/bun.lock @@ -70,6 +70,8 @@ "version": "1.0.0", "dependencies": { "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.25.76", }, "devDependencies": { "bun-types": "^1.1.8", diff --git a/packages/core/examples/gitpatch-goals/index.ts b/packages/core/examples/gitpatch-goals/index.ts index 6d1288b..b03309f 100644 --- a/packages/core/examples/gitpatch-goals/index.ts +++ b/packages/core/examples/gitpatch-goals/index.ts @@ -1,148 +1,255 @@ import { jules } from '@google/jules-sdk'; +import { defineCommand, runMain } from 'citty'; +import { z } from 'zod'; + +// ============================================================================ +// SPEC: Typed Service Contract +// ============================================================================ + +export const ReviewInputSchema = z.object({ + prompt: z.string().min(1, 'Prompt is required'), +}); + +export type ReviewInput = z.infer; + +export const ReviewErrorCode = z.enum([ + 'GENERATION_FAILED', + 'REVIEW_FAILED', + 'NO_PATCH_FOUND', + 'UNKNOWN_ERROR', +]); + +export const ReviewSuccessSchema = z.object({ + success: z.literal(true), + data: z.object({ + reviewMessage: z.string(), + patchSize: z.number(), + }), +}); + +export const ReviewFailureSchema = z.object({ + success: z.literal(false), + error: z.object({ + code: ReviewErrorCode, + message: z.string(), + recoverable: z.boolean(), + }), +}); + +export type ReviewResult = + | z.infer + | z.infer; + +export interface ReviewSpec { + execute(input: ReviewInput): Promise; +} -/** - * GitPatch Goals Review Example - * - * This example demonstrates how to evaluate an AI agent's output against - * its original goals and coding standards by extracting the generated code - * diff (GitPatch) and passing it to a second review session. - */ -async function main() { - if (!process.env.JULES_API_KEY) { - console.error('Error: JULES_API_KEY environment variable is missing.'); - process.exit(1); - } - - console.log('--- Step 1: Initiating Code Generation Session ---'); +// ============================================================================ +// HANDLER: Implementation +// ============================================================================ + +export class ReviewHandler implements ReviewSpec { + async execute(input: ReviewInput): Promise { + try { + console.error('--- Step 1: Initiating Code Generation Session ---'); + + const genSession = await jules.session({ + prompt: input.prompt, + }); + + console.error(`Generation Session created! ID: ${genSession.id}`); + console.error('Waiting for the agent to generate code...'); + + const genOutcome = await genSession.result(); + + if (genOutcome.state !== 'completed') { + return { + success: false, + error: { + code: 'GENERATION_FAILED', + message: `Generation session failed with state: ${genOutcome.state}`, + recoverable: false, + }, + }; + } - const originalPrompt = ` - Create a simple Node.js HTTP server. - Requirements: - - Listen on port 8080. - - Serve a "Hello, World!" plain text response for the root path (/). - - Return a 404 Not Found for all other paths. - - Use the built-in 'http' module. - `; + // Extract GitPatch from outcome directly if changeSet is present + // Alternatively build from generated files as fallback + let gitPatch = ''; - // 1. Create the generation session - const genSession = await jules.session({ - prompt: originalPrompt, - }); + // Let's first check if changeSet is available on the snapshot + if (typeof genOutcome.changeSet === 'function') { + const patch = genOutcome.changeSet(); + if (patch && typeof patch === 'string') { + gitPatch = patch; + } + } - console.log(`Generation Session created! ID: ${genSession.id}`); - console.log('Waiting for the agent to generate code...'); + // If we didn't find one via `changeSet()`, let's check generated files + if (!gitPatch) { + console.error('No direct changeSet found. Fallback to getting generated files.'); + const files = genOutcome.generatedFiles(); + if (files.size > 0) { + for (const [path, file] of files.entries()) { + const lineCount = file.content.split('\n').length; + gitPatch += `--- a/${path}\n+++ b/${path}\n@@ -0,0 +1,${lineCount} @@\n`; + gitPatch += + file.content + .split('\n') + .map((l: string) => '+' + l) + .join('\n') + '\n'; + } + } + } - const genOutcome = await genSession.result(); + if (!gitPatch) { + return { + success: false, + error: { + code: 'NO_PATCH_FOUND', + message: 'No GitPatch data or generated files found in the generation session.', + recoverable: false, + }, + }; + } - if (genOutcome.state !== 'completed') { - console.error(`Generation session failed with state: ${genOutcome.state}`); - process.exit(1); - } + console.error('\n--- Step 2: Extracted GitPatch ---'); + console.error(gitPatch.substring(0, 500) + '...\n(truncated for brevity)'); + + console.error('\n--- Step 3: Initiating Review Session ---'); + + const reviewPrompt = ` +You are an expert code reviewer. Review the following GitPatch generated by an AI agent. + +### Original Goals and Requirements +${input.prompt} + +### GitPatch to Review +\`\`\`diff +${gitPatch} +\`\`\` + +### Task +1. Determine if the generated code successfully meets ALL the Original Goals and Requirements. +2. Determine if the code adheres to general Node.js coding standards and best practices. +3. Provide a clear, structured markdown response with the following sections: + - **Goal Satisfaction**: Yes/No and why. + - **Code Quality**: Feedback on best practices. + - **Final Verdict**: Pass or Fail. +`; + + const reviewSession = await jules.session({ + prompt: reviewPrompt, + }); + + console.error(`Review Session created! ID: ${reviewSession.id}`); + console.error('Waiting for the agent to review the code...'); + + const reviewOutcome = await reviewSession.result(); + + if (reviewOutcome.state !== 'completed') { + return { + success: false, + error: { + code: 'REVIEW_FAILED', + message: `Review session failed with state: ${reviewOutcome.state}`, + recoverable: false, + }, + }; + } - // 2. Extract the GitPatch (diff) of the generated code - let gitPatch = ''; - - // Get activities for the session - const activities = await jules.select({ - from: 'activities', - where: { 'session.id': genSession.id }, - order: 'asc', - }); - - for (const activity of activities) { - if (activity.type === 'progressUpdated' && activity.artifacts) { - for (const artifact of activity.artifacts) { - if (artifact.type === 'changeSet') { - // The changeSet artifact has a raw content property representing the git diff - if (artifact.content) { - gitPatch += artifact.content + '\n'; - } - } + // Retrieve the final message from the review agent + let reviewMessage = 'No final message provided by the review agent.'; + + const activities = reviewOutcome.activities ?? []; + const agentMessages = activities.filter((a) => a.type === 'agentMessaged'); + + if (agentMessages.length > 0) { + // The activities array is ordered chronologically, so we take the last one + reviewMessage = agentMessages[agentMessages.length - 1].message; + } else { + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + reviewMessage = ''; + for (const [filename, content] of files.entries()) { + reviewMessage += `\nFile: ${filename}\n${content.content}\n`; } + } } - } - if (!gitPatch) { - console.log('No GitPatch data found in the generation session.'); - // Fallback to getting generated files - const files = genOutcome.generatedFiles(); - if (files.size > 0) { - for (const [path, file] of files.entries()) { - gitPatch += `--- a/${path}\n+++ b/${path}\n@@ -0,0 +1,${file.content.split('\n').length} @@\n`; - gitPatch += - file.content - .split('\n') - .map((l) => '+' + l) - .join('\n') + '\n'; - } - } else { - console.error('No generated files found either. Exiting.'); - process.exit(1); + return { + success: true, + data: { + reviewMessage, + patchSize: gitPatch.length, + }, + }; + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; } } +} - console.log('\n--- Step 2: Extracted GitPatch ---'); - console.log(gitPatch.substring(0, 500) + '...\n(truncated for brevity)'); - - console.log('\n--- Step 3: Initiating Review Session ---'); - - const reviewPrompt = ` - You are an expert code reviewer. Review the following GitPatch generated by an AI agent. - - ### Original Goals and Requirements - ${originalPrompt} - - ### GitPatch to Review - \`\`\`diff - ${gitPatch} - \`\`\` - - ### Task - 1. Determine if the generated code successfully meets ALL the Original Goals and Requirements. - 2. Determine if the code adheres to general Node.js coding standards and best practices. - 3. Provide a clear, structured markdown response with the following sections: - - **Goal Satisfaction**: Yes/No and why. - - **Code Quality**: Feedback on best practices. - - **Final Verdict**: Pass or Fail. - `; - - // 3. Create the review session - const reviewSession = await jules.session({ - prompt: reviewPrompt, - }); - - console.log(`Review Session created! ID: ${reviewSession.id}`); - console.log('Waiting for the agent to review the code...'); - - const reviewOutcome = await reviewSession.result(); +// ============================================================================ +// CLI CONFIGURATION (citty) +// ============================================================================ + +const main = defineCommand({ + meta: { + name: 'gitpatch-goals', + description: 'Generates code and reviews it via GitPatch against original goals.', + }, + args: { + prompt: { + type: 'string', + description: 'The initial prompt/goal for code generation', + default: + 'Create a simple Node.js HTTP server that listens on port 8080 and serves "Hello, World!".', + alias: 'p', + }, + json: { + type: 'boolean', + description: 'Output the result as JSON', + default: false, + }, + }, + async run({ args }) { + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is missing.'); + process.exit(1); + } - console.log('\n--- Step 4: Review Results ---'); + const parseResult = ReviewInputSchema.safeParse({ prompt: args.prompt }); + if (!parseResult.success) { + console.error('Invalid input:', parseResult.error.format()); + process.exit(1); + } - if (reviewOutcome.state === 'completed') { - // 4. Output the review results - const reviewActivities = await jules.select({ - from: 'activities', - where: { type: 'agentMessaged', 'session.id': reviewSession.id }, - order: 'desc', - limit: 1, - }); + const handler = new ReviewHandler(); + const result = await handler.execute(parseResult.data); - if (reviewActivities.length > 0) { - console.log(reviewActivities[0].message); + if (args.json) { + console.log(JSON.stringify(result, null, 2)); } else { - const files = reviewOutcome.generatedFiles(); - if (files.size > 0) { - for (const [filename, content] of files.entries()) { - console.log(`\nReview File: ${filename}`); - console.log(content.content); - } - } else { - console.log('Review agent did not leave a final message or file.'); - } + if (result.success) { + console.log('\n======================================================'); + console.log('REVIEW RESULTS'); + console.log('======================================================\n'); + console.log(result.data.reviewMessage); + } else { + console.error('\nFAILED:', result.error.message); + } } - } else { - console.error(`Review session failed with state: ${reviewOutcome.state}`); - } -} -main().catch(console.error); + process.exit(result.success ? 0 : 1); + }, +}); + +runMain(main); diff --git a/packages/core/examples/gitpatch-goals/package.json b/packages/core/examples/gitpatch-goals/package.json index 1ad9348..adb7453 100644 --- a/packages/core/examples/gitpatch-goals/package.json +++ b/packages/core/examples/gitpatch-goals/package.json @@ -7,7 +7,9 @@ "start": "bun run index.ts" }, "dependencies": { - "@google/jules-sdk": "workspace:*" + "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.25.76" }, "devDependencies": { "bun-types": "^1.1.8" From 8f9525e1c02c303b88122ad061fe8ed2019ebc46 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:11:27 +0000 Subject: [PATCH 17/28] refactor: restructure AI CLI to avoid hotspots Refactors the `ai-sdk` Agent CLI example to adhere strictly to scalable design patterns. - Extracts API integration to `src/services/agent.ts`. - Extracts CLI flag handling and rendering to `src/commands/start.ts`. - Updates `src/cli.ts` to be a minimal root entrypoint that lazily registers commands, preventing merge conflicts as the CLI scales. Fixes #227 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/examples/ai-sdk/README.md | 16 ++-- packages/core/examples/ai-sdk/src/cli.ts | 76 +---------------- .../examples/ai-sdk/src/commands/start.ts | 85 +++++++++++++++++++ .../examples/ai-sdk/src/services/agent.ts | 47 ++++++++++ 4 files changed, 145 insertions(+), 79 deletions(-) create mode 100644 packages/core/examples/ai-sdk/src/commands/start.ts create mode 100644 packages/core/examples/ai-sdk/src/services/agent.ts diff --git a/packages/core/examples/ai-sdk/README.md b/packages/core/examples/ai-sdk/README.md index d034247..106ae4f 100644 --- a/packages/core/examples/ai-sdk/README.md +++ b/packages/core/examples/ai-sdk/README.md @@ -24,19 +24,21 @@ You can run this example using `bun`: ### Standard Human-Friendly Output ```bash -bun start --prompt "Fix visibility issues by changing background colors to a zinc palette." --repo "your-org/your-repo" +bun start start --prompt "Fix visibility issues by changing background colors to a zinc palette." --repo "your-org/your-repo" ``` ### Agent-Friendly JSON Output (Agent DX) ```bash -bun start --prompt "Fix visibility issues." --output json +bun start start --prompt "Fix visibility issues." --output json ``` ## Architecture -This project is structured for predictability and predictability: +This project is structured for predictability and minimizing merge conflicts: -1. **CLI Entrypoint (`src/cli.ts`)**: Built with `citty`, accepts arguments like `--output json` tailored for both humans and agent orchestrators. Uses `@ai-sdk/google` to talk to Gemini. -2. **Tool Spec (`src/tools/jules-coding-task/spec.ts`)**: The Contract boundary. Parses tool input using Zod and defines a strict `Result` return type. -3. **Tool Handler (`src/tools/jules-coding-task/handler.ts`)**: The impure business logic. It initiates `jules.session()`, waits for the session to complete, and evaluates the `session.result()`. It *never* throws errors; it returns formatted Failure/Success objects. -4. **Tool Wrapper (`src/tools/jules-coding-task/index.ts`)**: Maps the typed contract into a standard Vercel AI SDK `tool()` wrapper, keeping the `generateText` invocation completely isolated from the execution details. +1. **CLI Entrypoint (`src/cli.ts`)**: Minimal registration boundary built with `citty`. It lazily registers subcommands (e.g. `start`) to avoid central hotspots as the CLI scales. +2. **Command Modules (`src/commands/*.ts`)**: Isolated entrypoints for flags, CLI argument parsing, environment variable logic checks, and selecting the output format (`--output json`). +3. **Services (`src/services/agent.ts`)**: Encapsulates the Vercel AI SDK logic (`generateText` with `@ai-sdk/google`) to abstract the specific LLM interactions away from the CLI layer. +4. **Tool Spec (`src/tools/jules-coding-task/spec.ts`)**: The Contract boundary. Parses tool input using Zod and defines a strict `Result` return type. +5. **Tool Handler (`src/tools/jules-coding-task/handler.ts`)**: The impure business logic. It initiates `jules.session()`, waits for the session to complete, and evaluates the `session.result()`. It *never* throws errors. +6. **Tool Wrapper (`src/tools/jules-coding-task/index.ts`)**: Maps the typed contract into a standard Vercel AI SDK `tool()` wrapper. diff --git a/packages/core/examples/ai-sdk/src/cli.ts b/packages/core/examples/ai-sdk/src/cli.ts index 7b94aa1..d076385 100644 --- a/packages/core/examples/ai-sdk/src/cli.ts +++ b/packages/core/examples/ai-sdk/src/cli.ts @@ -1,82 +1,14 @@ import { defineCommand, runMain } from 'citty'; -import { generateText } from 'ai'; -import { google } from '@ai-sdk/google'; -import { executeCodingTask } from './tools/jules-coding-task/index.js'; const main = defineCommand({ meta: { name: 'ai-sdk-example', version: '1.0.0', - description: 'A CLI demonstrating Vercel AI SDK integration with Jules SDK using Agent DX principles.', + description: + 'A CLI demonstrating Vercel AI SDK integration with Jules SDK using Agent DX principles.', }, - args: { - prompt: { - type: 'string', - description: 'The coding prompt to feed the AI.', - required: true, - }, - output: { - type: 'string', - description: 'Output format (json or text). Use json for agents.', - default: 'text', - }, - repo: { - type: 'string', - description: 'Optional GitHub repository (e.g. "owner/repo").', - required: false, - }, - }, - async run({ args }) { - if (!process.env.JULES_API_KEY) { - console.error('Error: JULES_API_KEY environment variable is missing.'); - process.exit(1); - } - - if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) { - console.error('Error: GOOGLE_GENERATIVE_AI_API_KEY environment variable is missing.'); - process.exit(1); - } - - try { - // Create user context including the repo if provided - const contextPrompt = args.repo - ? `Task: ${args.prompt}\nContext: Apply this task to the repository "${args.repo}".` - : `Task: ${args.prompt}`; - - const { text, toolCalls } = await generateText({ - model: google('gemini-3.1-flash-lite-preview'), - prompt: contextPrompt, - tools: { - executeCodingTask, - }, - maxSteps: 2, - }); - - if (args.output === 'json') { - console.log(JSON.stringify({ - success: true, - result: text, - toolCalls: toolCalls?.map(c => ({ name: c.toolName, args: c.args })) || [] - }, null, 2)); - } else { - console.log('\n--- Final Response from AI ---'); - console.log(text); - if (toolCalls && toolCalls.length > 0) { - console.log('\n--- Tools Invoked ---'); - toolCalls.forEach(c => console.log(`- ${c.toolName}`)); - } - } - } catch (error: any) { - if (args.output === 'json') { - console.error(JSON.stringify({ - success: false, - error: error.message || String(error) - }, null, 2)); - } else { - console.error('Execution Failed:', error.message || String(error)); - } - process.exit(1); - } + subCommands: { + start: () => import('./commands/start.js').then((m) => m.default), }, }); diff --git a/packages/core/examples/ai-sdk/src/commands/start.ts b/packages/core/examples/ai-sdk/src/commands/start.ts new file mode 100644 index 0000000..e23ba84 --- /dev/null +++ b/packages/core/examples/ai-sdk/src/commands/start.ts @@ -0,0 +1,85 @@ +import { defineCommand } from 'citty'; +import { runAgent } from '../services/agent.js'; + +export default defineCommand({ + meta: { + name: 'start', + description: 'Start an agent session to handle a coding prompt.', + }, + args: { + prompt: { + type: 'string', + description: 'The coding prompt to feed the AI.', + required: true, + }, + output: { + type: 'string', + description: 'Output format (json or text). Use json for agents.', + default: 'text', + }, + repo: { + type: 'string', + description: 'Optional GitHub repository (e.g. "owner/repo").', + required: false, + }, + }, + async run({ args }) { + // 1. Logic Checks: Validate Environment Context explicitly before attempting external operations + if (!process.env.JULES_API_KEY) { + console.error('Error: JULES_API_KEY environment variable is missing.'); + process.exit(1); + } + + if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY) { + console.error('Error: GOOGLE_GENERATIVE_AI_API_KEY environment variable is missing.'); + process.exit(1); + } + + // 2. Encapsulate execution in the service abstraction + const response = await runAgent({ + prompt: args.prompt, + repo: args.repo, + }); + + // 3. Render payload strictly conforming to output format expectation (Agent DX vs Human) + if (args.output === 'json') { + if (response.success) { + console.log( + JSON.stringify( + { + success: true, + result: response.result, + toolCalls: response.toolCalls, + }, + null, + 2, + ), + ); + } else { + console.error( + JSON.stringify( + { + success: false, + error: response.error, + }, + null, + 2, + ), + ); + process.exit(1); + } + } else { + if (response.success) { + console.log('\n--- Final Response from AI ---'); + console.log(response.result); + if (response.toolCalls && response.toolCalls.length > 0) { + console.log('\n--- Tools Invoked ---'); + response.toolCalls.forEach((c) => console.log(`- ${c.name}`)); + } + } else { + console.error('Execution Failed:', response.error); + process.exit(1); + } + } + }, +}); diff --git a/packages/core/examples/ai-sdk/src/services/agent.ts b/packages/core/examples/ai-sdk/src/services/agent.ts new file mode 100644 index 0000000..e8fcc74 --- /dev/null +++ b/packages/core/examples/ai-sdk/src/services/agent.ts @@ -0,0 +1,47 @@ +import { generateText } from 'ai'; +import { google } from '@ai-sdk/google'; +import { executeCodingTask } from '../tools/jules-coding-task/index.js'; + +export interface AgentRequest { + prompt: string; + repo?: string; +} + +export interface AgentResponse { + success: boolean; + result?: string; + toolCalls?: Array<{ name: string; args: any }>; + error?: string; +} + +/** + * Encapsulates the Vercel AI SDK logic. + * This service handles calling the LLM and managing available tools. + */ +export async function runAgent(request: AgentRequest): Promise { + const contextPrompt = request.repo + ? `Task: ${request.prompt}\nContext: Apply this task to the repository "${request.repo}".` + : `Task: ${request.prompt}`; + + try { + const { text, toolCalls } = await generateText({ + model: google('gemini-3.1-flash-lite-preview'), + prompt: contextPrompt, + tools: { + executeCodingTask, + }, + maxSteps: 2, + }); + + return { + success: true, + result: text, + toolCalls: toolCalls?.map((c) => ({ name: c.toolName, args: c.args })) || [], + }; + } catch (error: any) { + return { + success: false, + error: error.message || String(error), + }; + } +} 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 18/28] 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 73d4ded3a09a2970b960767031fc9b73c76c4ffd Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:14:15 +0000 Subject: [PATCH 19/28] feat(sdk): Add GitPatch Review CLI example using Typed Service Contract Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. The example is built as an Agent-ready CLI using the `citty` framework, follows the Typed Service Contract pattern to separate schemas from business logic, streams activities back to the user, and uses the `session.snapshot()` to safely extract the GitPatch. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 2 + .../core/examples/gitpatch-review/README.md | 86 ++++--- .../core/examples/gitpatch-review/index.ts | 220 ++++++------------ .../examples/gitpatch-review/package.json | 5 +- .../examples/gitpatch-review/src/handler.ts | 194 +++++++++++++++ .../core/examples/gitpatch-review/src/spec.ts | 47 ++++ 6 files changed, 369 insertions(+), 185 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/src/handler.ts create mode 100644 packages/core/examples/gitpatch-review/src/spec.ts diff --git a/bun.lock b/bun.lock index cd8fd04..e99d7a7 100644 --- a/bun.lock +++ b/bun.lock @@ -70,6 +70,8 @@ "version": "1.0.0", "dependencies": { "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.23.0", }, "devDependencies": { "bun-types": "^1.1.8", diff --git a/packages/core/examples/gitpatch-review/README.md b/packages/core/examples/gitpatch-review/README.md index b1cee00..4364448 100644 --- a/packages/core/examples/gitpatch-review/README.md +++ b/packages/core/examples/gitpatch-review/README.md @@ -1,18 +1,20 @@ -# GitPatch Review Example +# GitPatch Review Example (CLI) This example demonstrates how to use Jules' session GitPatch to review and analyze code generated by a Jules coding agent against the context of a GitHub repository. +It is structured as a CLI using [citty](https://github.com/unjs/citty) and follows the **Typed Service Contract** (Spec & Handler) pattern to clearly separate argument parsing, schema validation, and business logic execution. + ## Overview -The example runs two sequential sessions using the Jules SDK: +The CLI orchestrates two sequential sessions using the Jules SDK: -1. **Generation Session**: Instructs an agent to intentionally write poor code, producing a Git patch with terrible practices. +1. **Generation Session**: Takes a user-provided prompt (e.g., instructing an agent to write poor code), producing a Git patch with changes. 2. **Review Session**: Takes the resulting Git patch generated from the first session and creates a *new* session, passing the patch string as context to instruct the agent to review and analyze the code against standard coding practices. This demonstrates common workflows such as: -- Checking if code generated by Jules sticks to original prompt goals. -- Determining if generated code adheres to repository standards. -- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures from the SDK's abstraction layer. +- Structuring Agent CLIs for deterministic and predictable behavior. +- Using `session.stream()` to output real-time CLI feedback. +- Extracting raw `gitPatch` strings or parsed `ChangeSet` structures directly from the `session.snapshot()`. ## Prerequisites @@ -24,48 +26,66 @@ This demonstrates common workflows such as: ## Running the Example -You can run this example using `bun`, `tsx`, or `ts-node`: +You can run this example using `bun` (recommended) or via Node/TSX. -### Using Bun +### Basic Usage ```bash -bun run index.ts +bun run index.ts -r "owner/repo" -b "main" -p "Write a badly formatted hello world function in Python" ``` -### Using Node.js and TSX - -If you don't have `bun` installed, you can run the example using `tsx`: +### CLI Arguments -```bash -npm install -g tsx -tsx index.ts -``` +| Argument | Alias | Required | Default | Description | +| :--- | :--- | :--- | :--- | :--- | +| `repository` | `-r` | **Yes** | | The target GitHub repository (e.g. `davideast/dataprompt`) | +| `prompt` | `-p` | **Yes** | | The prompt to generate the code change | +| `baseBranch` | `-b` | No | `main` | The base branch of the repository | +| `--json` | | No | `false` | Output the final result or error as structured JSON. Useful for agents or piping outputs. | -## Example Output +### Example Output ```text -1. Starting a new session to generate code... +Starting code generation session for davideast/dataprompt... Code Generation Session ID: jules:session:12345 -Waiting for the code generation to complete... - -2. Retrieving GitPatch data from the session... -Found ChangeSet on session snapshot. +[Code Gen] Planning changes +[Code Gen] Generated plan with 1 steps. +[Code Gen] Editing code +[Code Gen Agent]: I've created the requested bad code. --- Extracted Patch Content --- -File: bad_math.ts (Additions: 4, Deletions: 0) +--- a/bad_code.py ++++ b/bad_code.py +@@ -0,0 +1,2 @@ ++def hw(): ++ print("hello") ------------------------------- -3. Starting a new session to review the GitPatch... +Starting review session... Review Session ID: jules:session:67890 -Waiting for the review to complete... +[Review] Analyzing changes +[Review Agent]: This code lacks typing, has bad indentation, and uses a poor function name. + +======================================= + REVIEW COMPLETE +======================================= ---- Review Agent Analysis --- -This code is terrible! Here are the issues... -1. The function is named 'a' and takes parameters 'x' and 'y', making it unreadable. -2. It lacks basic TypeScript type annotations. -3. The spacing is inconsistent. +This code lacks typing, has bad indentation, and uses a poor function name. I recommend renaming it to 'hello_world' and fixing the indentation. +``` + +### JSON Output + +When run with the `--json` flag, all stdout/stderr progress logs are suppressed or piped differently, and the final output is a structured JSON response (following the Result pattern). + +```bash +bun run index.ts -r "owner/repo" -p "Write bad code" --json +``` -Recommendation: -Rename the function to 'addNumbers', add type hints, and fix formatting. ------------------------------ +```json +{ + "codeGenSessionId": "jules:session:12345", + "reviewSessionId": "jules:session:67890", + "gitPatchStr": "...", + "reviewMessage": "This code lacks typing..." +} ``` diff --git a/packages/core/examples/gitpatch-review/index.ts b/packages/core/examples/gitpatch-review/index.ts index aa7286e..72a6645 100644 --- a/packages/core/examples/gitpatch-review/index.ts +++ b/packages/core/examples/gitpatch-review/index.ts @@ -1,164 +1,82 @@ -import { jules } from '@google/jules-sdk'; - -/** - * GitPatch Review Example - * - * Demonstrates how to use the Jules SDK to generate a code change - * and then review its GitPatch content. This is useful for analyzing - * generated code before applying it, checking it against coding standards, - * or using it to drive further automation. - */ -async function main() { - 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); - } - - // Define the target repository and base branch - const source = { github: 'davideast/dataprompt', baseBranch: 'main' }; - - console.log('1. Starting a new session to generate code...'); - - try { - // Start a session to generate some changes. We instruct the agent to - // intentionally write bad code so we have something obvious to review. - const codeGenSession = await jules.session({ - prompt: `Create a new file called 'bad_math.ts' with a poorly implemented -function that adds two numbers together. Include no comments and terrible variable names.`, - source, +import { defineCommand, runMain } from 'citty'; +import { ReviewHandler } from './src/handler.js'; +import { ReviewInputSchema } from './src/spec.js'; + +const main = defineCommand({ + meta: { + name: 'jules-gitpatch-review', + version: '1.0.0', + description: 'Use Jules to review generated code patches against GitHub repo context.', + }, + args: { + repository: { + type: 'string', + description: 'The target GitHub repository (e.g. owner/repo)', + required: true, + alias: 'r', + }, + baseBranch: { + type: 'string', + description: 'The base branch of the repository', + default: 'main', + alias: 'b', + }, + prompt: { + type: 'string', + description: 'The prompt to generate the code change', + required: true, + alias: 'p', + }, + json: { + type: 'boolean', + description: 'Output the final result as JSON', + default: false, + }, + }, + async run({ args }) { + // 1. Validate Input (Parse, don't validate) + const inputResult = ReviewInputSchema.safeParse({ + repository: args.repository, + baseBranch: args.baseBranch, + prompt: args.prompt, + json: args.json, }); - console.log(`Code Generation Session ID: ${codeGenSession.id}`); - console.log('Waiting for the code generation to complete...'); - - // Await the completion of the session - const genOutcome = await codeGenSession.result(); - - if (genOutcome.state !== 'completed' && genOutcome.state !== 'succeeded') { - console.error(`Code generation session failed or did not complete. State: ${genOutcome.state}`); - return; + if (!inputResult.success) { + console.error('Invalid arguments provided:'); + console.error(inputResult.error.format()); + process.exit(1); } - // Retrieve the activities to find the changeset artifact - console.log('\n2. Retrieving GitPatch data from the session...'); - const activities = await jules.select({ - from: 'activities', - where: { 'session.id': codeGenSession.id }, - order: 'desc', - }); - - let gitPatchStr = ''; + // 2. Instantiate the Handler + const handler = new ReviewHandler(); - // Iterate through activities to find a ChangeSet artifact - for (const activity of activities) { - if (activity.artifacts) { - for (const artifact of activity.artifacts) { - if (artifact.type === 'changeSet') { - // In the SDK, the underlying structure for ChangeSet includes the gitPatch. - // We can extract the unidiffPatch from the raw artifact data. - const parsed = artifact.parsed(); - // Often, you might want the raw patch string to send to another agent or tool. - // We'll reconstruct a simple patch string from the parsed diff for this example. + // 3. Execute Business Logic + const result = await handler.execute(inputResult.data); - for(const file of parsed.files) { - gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; - for(const chunk of file.chunks) { - gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; - for(const change of chunk.changes) { - if(change.type === 'add') gitPatchStr += `+${change.content}\n`; - if(change.type === 'del') gitPatchStr += `-${change.content}\n`; - if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; - } - } - } - } + // 4. Handle Results Deterministically + if (!result.success) { + if (args.json) { + console.error(JSON.stringify(result.error, null, 2)); + } else { + console.error(`\n[ERROR] ${result.error.code}: ${result.error.message}`); + if (result.error.suggestion) { + console.error(`Suggestion: ${result.error.suggestion}`); } } + process.exit(1); } - if (!gitPatchStr) { - console.log('No GitPatch found. The agent might not have written any code.'); - // If no gitpatch was found in activities, sometimes the final state has it on the outcome snapshot. - const snapshot = codeGenSession.snapshot(); - const changeSet = snapshot?.changeSet(); - if(changeSet) { - console.log("Found ChangeSet on session snapshot."); - // Note: in a real scenario you would access the raw gitpatch string if the API exposes it directly, - // but for the SDK's abstraction, parsing the diff is the recommended way. - const parsed = changeSet.parsed(); - for(const file of parsed.files) { - gitPatchStr += `File: ${file.path} (Additions: ${file.additions}, Deletions: ${file.deletions})\n`; - } - } - } - - if(!gitPatchStr) { - console.log("Could not find any changes. Exiting."); - return; - } - - console.log('\n--- Extracted Patch Content ---'); - console.log(gitPatchStr); - console.log('-------------------------------\n'); - - console.log('3. Starting a new session to review the GitPatch...'); - - // Now, create a second session to review the patch generated by the first session. - // This demonstrates using Jules to evaluate code against coding standards. - const reviewSession = await jules.session({ - prompt: `Review the following code change patch and determine if it adheres to clean coding standards. -Provide a short summary of the issues found and how they should be fixed. - -## Git Patch -\`\`\`diff -${gitPatchStr} -\`\`\` -`, - // We can optionally attach the same source repo context if the review needs to know about other files - source - }); - - console.log(`Review Session ID: ${reviewSession.id}`); - console.log('Waiting for the review to complete...'); - - const reviewOutcome = await reviewSession.result(); - - if (reviewOutcome.state === 'completed' || reviewOutcome.state === 'succeeded') { - // Find the agent's review message - const reviewActivities = await jules.select({ - from: 'activities', - where: { type: 'agentMessaged', 'session.id': reviewSession.id }, - order: 'desc', - limit: 1, - }); - - if (reviewActivities.length > 0) { - console.log('\n--- Review Agent Analysis ---'); - console.log(reviewActivities[0].message); - console.log('-----------------------------'); - } else { - // Fallback if the agent wrote the review to a file instead of a message - const files = reviewOutcome.generatedFiles(); - if (files.size > 0) { - console.log('\n--- Review Agent Analysis (Files) ---'); - for (const [filename, content] of files.entries()) { - console.log(`\nFile: ${filename}`); - console.log(content.content); - } - console.log('-------------------------------------'); - } else { - console.log("The review agent completed but didn't leave a message or file."); - } - } + // 5. Output Success State + if (args.json) { + console.log(JSON.stringify(result.data, null, 2)); } else { - console.error(`Review session failed or did not complete. State: ${reviewOutcome.state}`); + console.log('\n======================================='); + console.log(' REVIEW COMPLETE'); + console.log('=======================================\n'); + console.log(result.data.reviewMessage); } + }, +}); - } catch (error) { - console.error('An error occurred during the process:', error); - } -} - -// Run the example -main(); +runMain(main); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json index f2863f2..784a8ce 100644 --- a/packages/core/examples/gitpatch-review/package.json +++ b/packages/core/examples/gitpatch-review/package.json @@ -3,11 +3,14 @@ "version": "1.0.0", "description": "Example demonstrating how to use Jules' session GitPatch to review and analyze generated code", "main": "index.ts", + "type": "module", "scripts": { "start": "bun run index.ts" }, "dependencies": { - "@google/jules-sdk": "workspace:*" + "@google/jules-sdk": "workspace:*", + "citty": "^0.1.6", + "zod": "^3.23.0" }, "devDependencies": { "bun-types": "^1.1.8" diff --git a/packages/core/examples/gitpatch-review/src/handler.ts b/packages/core/examples/gitpatch-review/src/handler.ts new file mode 100644 index 0000000..bdf6c6b --- /dev/null +++ b/packages/core/examples/gitpatch-review/src/handler.ts @@ -0,0 +1,194 @@ +import { jules, Session, Activity } from '@google/jules-sdk'; +import { ReviewSpec, ReviewInput, ReviewResult } from './spec.js'; + +export class ReviewHandler implements ReviewSpec { + async execute(input: ReviewInput): Promise { + try { + if (!process.env.JULES_API_KEY) { + return { + success: false, + error: { + code: 'UNAUTHORIZED', + message: 'JULES_API_KEY environment variable is not set.', + suggestion: 'Export JULES_API_KEY="your-api-key" before running the CLI.', + recoverable: true, + }, + }; + } + + this.log(`Starting code generation session for ${input.repository}...`, input.json); + + const source = { github: input.repository, baseBranch: input.baseBranch }; + + // 1. Generate bad code + const codeGenSession = await jules.session({ + prompt: input.prompt, + source, + }); + + this.log(`Code Generation Session ID: ${codeGenSession.id}`, input.json); + + await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + const genOutcome = await codeGenSession.result(); + + if (genOutcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `Code generation session failed: ${codeGenSession.id}`, + recoverable: false, + }, + }; + } + + // 2. Extract GitPatch + const snapshot = codeGenSession.snapshot(); + let changeSet; + + // Type guarding the `snapshot.changeSet` function because the underlying SDK + // abstraction may change or omit it. + if (typeof snapshot.changeSet === 'function') { + changeSet = snapshot.changeSet(); + } + + let gitPatchStr = ''; + + if (changeSet && changeSet.gitPatch && changeSet.gitPatch.unidiffPatch) { + // Prefer the raw unidiff patch from the GitPatch object if available + gitPatchStr = changeSet.gitPatch.unidiffPatch; + } else if (changeSet && typeof changeSet.parsed === 'function') { + // Fallback to rebuilding it from parsed diff + const parsed = changeSet.parsed(); + for(const file of parsed.files) { + gitPatchStr += `--- a/${file.path}\n+++ b/${file.path}\n`; + for(const chunk of file.chunks) { + gitPatchStr += `@@ -${chunk.oldStart},${chunk.oldLines} +${chunk.newStart},${chunk.newLines} @@\n`; + for(const change of chunk.changes) { + if(change.type === 'add') gitPatchStr += `+${change.content}\n`; + if(change.type === 'del') gitPatchStr += `-${change.content}\n`; + if(change.type === 'normal') gitPatchStr += ` ${change.content}\n`; + } + } + } + } else { + // Fallback to checking generated files if changeset isn't structured nicely + const files = genOutcome.generatedFiles(); + for (const [filename, content] of files.entries()) { + gitPatchStr += `File: ${filename}\n${content.content}\n`; + } + } + + if (!gitPatchStr || gitPatchStr.trim() === '') { + return { + success: false, + error: { + code: 'NO_CHANGES_GENERATED', + message: 'The agent did not generate any code changes.', + recoverable: false, + }, + }; + } + + this.log('\n--- Extracted Patch Content ---', input.json); + this.log(gitPatchStr, input.json); + this.log('-------------------------------\n', input.json); + + this.log('Starting review session...', input.json); + + // 3. Review the code + const reviewSession = await jules.session({ + prompt: `Review the following code change patch and determine if it adheres to clean coding standards. +Provide a short summary of the issues found and how they should be fixed. + +## Git Patch +\`\`\`diff +${gitPatchStr} +\`\`\` +`, + source, + }); + + this.log(`Review Session ID: ${reviewSession.id}`, input.json); + + let reviewMessage = ''; + + // We will listen for the final agent message from the stream + for await (const activity of reviewSession.stream()) { + this.logActivity(activity, 'Review', input.json); + if (activity.type === 'agentMessaged') { + reviewMessage = activity.message; + } + } + + const reviewOutcome = await reviewSession.result(); + + if (reviewOutcome.state === 'failed') { + return { + success: false, + error: { + code: 'SESSION_FAILED', + message: `Review session failed: ${reviewSession.id}`, + recoverable: false, + }, + }; + } + + if (!reviewMessage) { + // Check files if no message + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + for (const [filename, content] of files.entries()) { + reviewMessage += `\nFile: ${filename}\n${content.content}\n`; + } + } else { + reviewMessage = "The review completed but the agent provided no feedback."; + } + } + + return { + success: true, + data: { + codeGenSessionId: codeGenSession.id, + reviewSessionId: reviewSession.id, + gitPatchStr, + reviewMessage, + }, + }; + + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } + + private async streamSessionActivities(session: Session, prefix: string, isJson: boolean) { + for await (const activity of session.stream()) { + this.logActivity(activity, prefix, isJson); + } + } + + private logActivity(activity: Activity, prefix: string, isJson: boolean) { + if (activity.type === 'progressUpdated') { + this.log(`[${prefix}] ${activity.title}`, isJson); + } else if (activity.type === 'agentMessaged') { + this.log(`[${prefix} Agent]: ${activity.message}`, isJson); + } else if (activity.type === 'planGenerated') { + this.log(`[${prefix}] Generated plan with ${activity.plan.steps.length} steps.`, isJson); + } + } + + private log(message: string, isJson: boolean) { + if (isJson) { + console.error(message); + } else { + console.log(message); + } + } +} diff --git a/packages/core/examples/gitpatch-review/src/spec.ts b/packages/core/examples/gitpatch-review/src/spec.ts new file mode 100644 index 0000000..0878041 --- /dev/null +++ b/packages/core/examples/gitpatch-review/src/spec.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +// 1. INPUT (The Command) - "Parse, don't validate" +export const ReviewInputSchema = z.object({ + repository: z.string().min(1, 'Repository must be provided (e.g., owner/repo)'), + baseBranch: z.string().min(1, 'Base branch must be provided (e.g., main)'), + prompt: z.string().min(1, 'A prompt to generate code must be provided'), + json: z.boolean().default(false), +}); + +export type ReviewInput = z.infer; + +// 2. ERROR CODES (Exhaustive) +export const ReviewErrorCode = z.enum([ + 'SESSION_FAILED', + 'NO_CHANGES_GENERATED', + 'UNKNOWN_ERROR', + 'UNAUTHORIZED', +]); + +// 3. RESULT (The Monad) +export const ReviewSuccess = z.object({ + success: z.literal(true), + data: z.object({ + reviewMessage: z.string(), + codeGenSessionId: z.string(), + reviewSessionId: z.string(), + gitPatchStr: z.string(), + }), +}); + +export const ReviewFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: ReviewErrorCode, + message: z.string(), + suggestion: z.string().optional(), + recoverable: z.boolean(), + }), +}); + +export type ReviewResult = z.infer | z.infer; + +// 4. INTERFACE (The Capability) +export interface ReviewSpec { + execute(input: ReviewInput): Promise; +} 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 20/28] 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 d967351c0b688fe7d5549d9a072a10b408b98948 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:23:40 +0000 Subject: [PATCH 21/28] Refactor GitPatch CLI to modular Typed Service Contract architecture - Split `handler.ts` into discrete modules (`generate.ts`, `patch.ts`, `review.ts`) to avoid merge conflicts and promote code cleanliness. - Implemented real-time progress streaming using `for await (const activity of session.stream())` to provide CLI feedback before sessions complete, rather than waiting entirely on `session.result()`. - Maintained the Typed Service Contract pattern and JSON output formatting. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../core/examples/gitpatch-goals/generate.ts | 36 ++++ .../core/examples/gitpatch-goals/handler.ts | 91 ++++++++ .../core/examples/gitpatch-goals/index.ts | 199 +----------------- .../core/examples/gitpatch-goals/patch.ts | 47 +++++ .../core/examples/gitpatch-goals/review.ts | 56 +++++ packages/core/examples/gitpatch-goals/spec.ts | 39 ++++ 6 files changed, 271 insertions(+), 197 deletions(-) create mode 100644 packages/core/examples/gitpatch-goals/generate.ts create mode 100644 packages/core/examples/gitpatch-goals/handler.ts create mode 100644 packages/core/examples/gitpatch-goals/patch.ts create mode 100644 packages/core/examples/gitpatch-goals/review.ts create mode 100644 packages/core/examples/gitpatch-goals/spec.ts diff --git a/packages/core/examples/gitpatch-goals/generate.ts b/packages/core/examples/gitpatch-goals/generate.ts new file mode 100644 index 0000000..369b64d --- /dev/null +++ b/packages/core/examples/gitpatch-goals/generate.ts @@ -0,0 +1,36 @@ +import { jules, SessionClient } from '@google/jules-sdk'; + +/** + * Initiates the generation session and streams progress updates back + * to the caller until the session completes. + */ +export async function generateCode(prompt: string): Promise { + console.error('--- Step 1: Initiating Code Generation Session ---'); + + // Repoless sessions don't always create the resource instantly, + // we must await the outcome state for streaming to reliably start without 404ing activities + const session = await jules.session({ prompt }); + + console.error(`Generation Session created! ID: ${session.id}`); + console.error('Streaming agent progress...\n'); + + try { + for await (const activity of session.stream()) { + if (activity.type === 'progressUpdated') { + console.error(`[Generation] ${activity.title}`); + } else if (activity.type === 'agentMessaged') { + console.error(`[Generation Agent]: ${activity.message.substring(0, 100)}...`); + } else if (activity.type === 'sessionCompleted') { + console.error('[Generation] Session complete.'); + } else if (activity.type === 'sessionFailed') { + console.error('[Generation] Session failed.'); + } + } + } catch (err) { + // A 404 indicates the activities sub-collection might not be ready yet. + // The safest fallback is waiting for the result. + console.error('[Generation] Streaming not available yet. Waiting for completion...'); + } + + return session; +} diff --git a/packages/core/examples/gitpatch-goals/handler.ts b/packages/core/examples/gitpatch-goals/handler.ts new file mode 100644 index 0000000..518c3f1 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/handler.ts @@ -0,0 +1,91 @@ +import { ReviewInput, ReviewResult, ReviewSpec } from './spec.js'; +import { generateCode } from './generate.js'; +import { extractGitPatch } from './patch.js'; +import { reviewCode } from './review.js'; + +/** + * Orchestrator Handler that runs the generation and review workflow. + * Uses Typed Service Contract implementation to encapsulate errors. + */ +export class ReviewHandler implements ReviewSpec { + async execute(input: ReviewInput): Promise { + try { + // 1. Generation phase + const genSession = await generateCode(input.prompt); + const genOutcome = await genSession.result(); + + if (genOutcome.state !== 'completed') { + return { + success: false, + error: { + code: 'GENERATION_FAILED', + message: `Generation session failed with state: ${genOutcome.state}`, + recoverable: false, + }, + }; + } + + // 2. Patch extraction phase + const gitPatch = extractGitPatch(genOutcome); + + if (!gitPatch) { + return { + success: false, + error: { + code: 'NO_PATCH_FOUND', + message: 'Failed to extract GitPatch from generation session.', + recoverable: false, + }, + }; + } + + // 3. Review phase + const reviewOutcome = await reviewCode(input.prompt, gitPatch); + + if (reviewOutcome.state !== 'completed') { + return { + success: false, + error: { + code: 'REVIEW_FAILED', + message: `Review session failed with state: ${reviewOutcome.state}`, + recoverable: false, + }, + }; + } + + // 4. Result Formatting + let reviewMessage = 'No final message provided by the review agent.'; + const activities = reviewOutcome.activities ?? []; + const agentMessages = activities.filter((a) => a.type === 'agentMessaged'); + + if (agentMessages.length > 0) { + reviewMessage = agentMessages[agentMessages.length - 1].message; + } else { + const files = reviewOutcome.generatedFiles(); + if (files.size > 0) { + reviewMessage = ''; + for (const [filename, content] of files.entries()) { + reviewMessage += `\nFile: ${filename}\n${content.content}\n`; + } + } + } + + return { + success: true, + data: { + reviewMessage, + patchSize: gitPatch.length, + }, + }; + } catch (error) { + return { + success: false, + error: { + code: 'UNKNOWN_ERROR', + message: error instanceof Error ? error.message : String(error), + recoverable: false, + }, + }; + } + } +} diff --git a/packages/core/examples/gitpatch-goals/index.ts b/packages/core/examples/gitpatch-goals/index.ts index b03309f..9ecc292 100644 --- a/packages/core/examples/gitpatch-goals/index.ts +++ b/packages/core/examples/gitpatch-goals/index.ts @@ -1,201 +1,6 @@ -import { jules } from '@google/jules-sdk'; import { defineCommand, runMain } from 'citty'; -import { z } from 'zod'; - -// ============================================================================ -// SPEC: Typed Service Contract -// ============================================================================ - -export const ReviewInputSchema = z.object({ - prompt: z.string().min(1, 'Prompt is required'), -}); - -export type ReviewInput = z.infer; - -export const ReviewErrorCode = z.enum([ - 'GENERATION_FAILED', - 'REVIEW_FAILED', - 'NO_PATCH_FOUND', - 'UNKNOWN_ERROR', -]); - -export const ReviewSuccessSchema = z.object({ - success: z.literal(true), - data: z.object({ - reviewMessage: z.string(), - patchSize: z.number(), - }), -}); - -export const ReviewFailureSchema = z.object({ - success: z.literal(false), - error: z.object({ - code: ReviewErrorCode, - message: z.string(), - recoverable: z.boolean(), - }), -}); - -export type ReviewResult = - | z.infer - | z.infer; - -export interface ReviewSpec { - execute(input: ReviewInput): Promise; -} - -// ============================================================================ -// HANDLER: Implementation -// ============================================================================ - -export class ReviewHandler implements ReviewSpec { - async execute(input: ReviewInput): Promise { - try { - console.error('--- Step 1: Initiating Code Generation Session ---'); - - const genSession = await jules.session({ - prompt: input.prompt, - }); - - console.error(`Generation Session created! ID: ${genSession.id}`); - console.error('Waiting for the agent to generate code...'); - - const genOutcome = await genSession.result(); - - if (genOutcome.state !== 'completed') { - return { - success: false, - error: { - code: 'GENERATION_FAILED', - message: `Generation session failed with state: ${genOutcome.state}`, - recoverable: false, - }, - }; - } - - // Extract GitPatch from outcome directly if changeSet is present - // Alternatively build from generated files as fallback - let gitPatch = ''; - - // Let's first check if changeSet is available on the snapshot - if (typeof genOutcome.changeSet === 'function') { - const patch = genOutcome.changeSet(); - if (patch && typeof patch === 'string') { - gitPatch = patch; - } - } - - // If we didn't find one via `changeSet()`, let's check generated files - if (!gitPatch) { - console.error('No direct changeSet found. Fallback to getting generated files.'); - const files = genOutcome.generatedFiles(); - if (files.size > 0) { - for (const [path, file] of files.entries()) { - const lineCount = file.content.split('\n').length; - gitPatch += `--- a/${path}\n+++ b/${path}\n@@ -0,0 +1,${lineCount} @@\n`; - gitPatch += - file.content - .split('\n') - .map((l: string) => '+' + l) - .join('\n') + '\n'; - } - } - } - - if (!gitPatch) { - return { - success: false, - error: { - code: 'NO_PATCH_FOUND', - message: 'No GitPatch data or generated files found in the generation session.', - recoverable: false, - }, - }; - } - - console.error('\n--- Step 2: Extracted GitPatch ---'); - console.error(gitPatch.substring(0, 500) + '...\n(truncated for brevity)'); - - console.error('\n--- Step 3: Initiating Review Session ---'); - - const reviewPrompt = ` -You are an expert code reviewer. Review the following GitPatch generated by an AI agent. - -### Original Goals and Requirements -${input.prompt} - -### GitPatch to Review -\`\`\`diff -${gitPatch} -\`\`\` - -### Task -1. Determine if the generated code successfully meets ALL the Original Goals and Requirements. -2. Determine if the code adheres to general Node.js coding standards and best practices. -3. Provide a clear, structured markdown response with the following sections: - - **Goal Satisfaction**: Yes/No and why. - - **Code Quality**: Feedback on best practices. - - **Final Verdict**: Pass or Fail. -`; - - const reviewSession = await jules.session({ - prompt: reviewPrompt, - }); - - console.error(`Review Session created! ID: ${reviewSession.id}`); - console.error('Waiting for the agent to review the code...'); - - const reviewOutcome = await reviewSession.result(); - - if (reviewOutcome.state !== 'completed') { - return { - success: false, - error: { - code: 'REVIEW_FAILED', - message: `Review session failed with state: ${reviewOutcome.state}`, - recoverable: false, - }, - }; - } - - // Retrieve the final message from the review agent - let reviewMessage = 'No final message provided by the review agent.'; - - const activities = reviewOutcome.activities ?? []; - const agentMessages = activities.filter((a) => a.type === 'agentMessaged'); - - if (agentMessages.length > 0) { - // The activities array is ordered chronologically, so we take the last one - reviewMessage = agentMessages[agentMessages.length - 1].message; - } else { - const files = reviewOutcome.generatedFiles(); - if (files.size > 0) { - reviewMessage = ''; - for (const [filename, content] of files.entries()) { - reviewMessage += `\nFile: ${filename}\n${content.content}\n`; - } - } - } - - return { - success: true, - data: { - reviewMessage, - patchSize: gitPatch.length, - }, - }; - } catch (error) { - return { - success: false, - error: { - code: 'UNKNOWN_ERROR', - message: error instanceof Error ? error.message : String(error), - recoverable: false, - }, - }; - } - } -} +import { ReviewInputSchema } from './spec.js'; +import { ReviewHandler } from './handler.js'; // ============================================================================ // CLI CONFIGURATION (citty) diff --git a/packages/core/examples/gitpatch-goals/patch.ts b/packages/core/examples/gitpatch-goals/patch.ts new file mode 100644 index 0000000..4fb781e --- /dev/null +++ b/packages/core/examples/gitpatch-goals/patch.ts @@ -0,0 +1,47 @@ +import { SessionOutcome } from '@google/jules-sdk'; + +/** + * Extracts a GitPatch diff from a completed session outcome. + * Prefers the native changeSet object but falls back to manually creating + * a diff from generated files if necessary. + */ +export function extractGitPatch(genOutcome: SessionOutcome): string | null { + console.error('\n--- Step 2: Extracting GitPatch ---'); + + let gitPatch = ''; + + // Let's first check if changeSet is available on the snapshot + if (typeof genOutcome.changeSet === 'function') { + const patch = genOutcome.changeSet(); + if (patch && typeof patch === 'string') { + gitPatch = patch; + } + } + + // If we didn't find one via `changeSet()`, let's check generated files + if (!gitPatch) { + console.error( + 'No direct changeSet found. Fallback to getting generated files.' + ); + const files = genOutcome.generatedFiles(); + if (files.size > 0) { + for (const [path, file] of files.entries()) { + const lineCount = file.content.split('\n').length; + gitPatch += `--- a/${path}\n+++ b/${path}\n@@ -0,0 +1,${lineCount} @@\n`; + gitPatch += + file.content + .split('\n') + .map((l: string) => '+' + l) + .join('\n') + '\n'; + } + } + } + + if (!gitPatch) { + console.error('No GitPatch data or generated files found in the generation session.'); + return null; + } + + console.error(gitPatch.substring(0, 500) + '...\n(truncated for brevity)'); + return gitPatch; +} diff --git a/packages/core/examples/gitpatch-goals/review.ts b/packages/core/examples/gitpatch-goals/review.ts new file mode 100644 index 0000000..02fc791 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/review.ts @@ -0,0 +1,56 @@ +import { jules, SessionClient, SessionOutcome } from '@google/jules-sdk'; + +/** + * Initiates the review session with the extracted GitPatch and original prompt. + * Streams progress back until completion and returns the final session outcome. + */ +export async function reviewCode( + originalPrompt: string, + gitPatch: string +): Promise { + console.error('\n--- Step 3: Initiating Review Session ---'); + + const reviewPrompt = ` +You are an expert code reviewer. Review the following GitPatch generated by an AI agent. + +### Original Goals and Requirements +${originalPrompt} + +### GitPatch to Review +\`\`\`diff +${gitPatch} +\`\`\` + +### Task +1. Determine if the generated code successfully meets ALL the Original Goals and Requirements. +2. Determine if the code adheres to general Node.js coding standards and best practices. +3. Provide a clear, structured markdown response with the following sections: + - **Goal Satisfaction**: Yes/No and why. + - **Code Quality**: Feedback on best practices. + - **Final Verdict**: Pass or Fail. +`; + + const session = await jules.session({ prompt: reviewPrompt }); + + console.error(`Review Session created! ID: ${session.id}`); + console.error('Streaming agent progress...\n'); + + try { + for await (const activity of session.stream()) { + if (activity.type === 'progressUpdated') { + console.error(`[Review] ${activity.title}`); + } else if (activity.type === 'agentMessaged') { + console.error(`[Review Agent]: ${activity.message.substring(0, 100)}...`); + } else if (activity.type === 'sessionCompleted') { + console.error('[Review] Session complete.'); + } else if (activity.type === 'sessionFailed') { + console.error('[Review] Session failed.'); + } + } + } catch (err) { + console.error('[Review] Streaming not available yet. Waiting for completion...'); + } + + const reviewOutcome = await session.result(); + return reviewOutcome; +} diff --git a/packages/core/examples/gitpatch-goals/spec.ts b/packages/core/examples/gitpatch-goals/spec.ts new file mode 100644 index 0000000..ac5c441 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/spec.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +export const ReviewInputSchema = z.object({ + prompt: z.string().min(1, 'Prompt is required'), +}); + +export type ReviewInput = z.infer; + +export const ReviewErrorCode = z.enum([ + 'GENERATION_FAILED', + 'REVIEW_FAILED', + 'NO_PATCH_FOUND', + 'UNKNOWN_ERROR', +]); + +export const ReviewSuccessSchema = z.object({ + success: z.literal(true), + data: z.object({ + reviewMessage: z.string(), + patchSize: z.number(), + }), +}); + +export const ReviewFailureSchema = z.object({ + success: z.literal(false), + error: z.object({ + code: ReviewErrorCode, + message: z.string(), + recoverable: z.boolean(), + }), +}); + +export type ReviewResult = + | z.infer + | z.infer; + +export interface ReviewSpec { + execute(input: ReviewInput): Promise; +} From ad9eb584859117836b8666ca4ff60245669e10db Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 01:39:18 +0000 Subject: [PATCH 22/28] Implement Agent CLI safety rails and schema introspection - Added `--dry-run` to support input validation without executing costly or destructive Jules API mutations. - Added `--describe` using `zod-to-json-schema` to allow agents to introspect the CLI schema dynamically at runtime. - Added `SKILL.md` to document usage guidelines for agents, encouraging context window discipline via `--json` and input hardening via `--dry-run`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 1 + .../core/examples/gitpatch-goals/SKILL.md | 28 +++++++++++++++++++ .../core/examples/gitpatch-goals/handler.ts | 11 ++++++++ .../core/examples/gitpatch-goals/index.ts | 22 ++++++++++++++- .../core/examples/gitpatch-goals/package.json | 3 +- packages/core/examples/gitpatch-goals/spec.ts | 1 + 6 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 packages/core/examples/gitpatch-goals/SKILL.md diff --git a/bun.lock b/bun.lock index b1b803b..c2c26df 100644 --- a/bun.lock +++ b/bun.lock @@ -72,6 +72,7 @@ "@google/jules-sdk": "workspace:*", "citty": "^0.1.6", "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.1", }, "devDependencies": { "bun-types": "^1.1.8", diff --git a/packages/core/examples/gitpatch-goals/SKILL.md b/packages/core/examples/gitpatch-goals/SKILL.md new file mode 100644 index 0000000..30cba74 --- /dev/null +++ b/packages/core/examples/gitpatch-goals/SKILL.md @@ -0,0 +1,28 @@ +--- +name: gitpatch-goals-cli +description: Evaluates generated code via GitPatch against original goals and coding standards. +version: 1.0.0 +--- + +# GitPatch Goals CLI + +This CLI uses Jules sessions to simulate code generation, extracts the resulting code as a GitPatch, and feeds it to a second Jules session to evaluate if it successfully met the original prompt's goals and adheres to coding standards. + +## Usage Guidelines for AI Agents + +When invoking this CLI, adhere to the following best practices: + +1. **Schema Introspection**: You can introspect the required arguments and schema at runtime by passing `--describe`. + ```bash + bun run index.ts --describe + ``` + +2. **Context Window Discipline**: Use `--json` for predictable, deterministic, machine-readable output. Avoid parsing raw terminal stdout. + ```bash + bun run index.ts --prompt "Create an API" --json + ``` + +3. **Input Hardening**: Before executing mutations or relying on long-running APIs (like creating Jules Sessions), validate your payload using the `--dry-run` flag to ensure the CLI safely accepts your arguments without executing side effects. + ```bash + bun run index.ts --prompt "Create an API" --dry-run + ``` diff --git a/packages/core/examples/gitpatch-goals/handler.ts b/packages/core/examples/gitpatch-goals/handler.ts index 518c3f1..eb0c3cd 100644 --- a/packages/core/examples/gitpatch-goals/handler.ts +++ b/packages/core/examples/gitpatch-goals/handler.ts @@ -10,6 +10,17 @@ import { reviewCode } from './review.js'; export class ReviewHandler implements ReviewSpec { async execute(input: ReviewInput): Promise { try { + if (input.dryRun) { + console.error('--- DRY RUN ENABLED: Simulating Code Generation & Review ---'); + return { + success: true, + data: { + reviewMessage: '[DRY RUN] Generated code successfully met the original goals.', + patchSize: 42, + }, + }; + } + // 1. Generation phase const genSession = await generateCode(input.prompt); const genOutcome = await genSession.result(); diff --git a/packages/core/examples/gitpatch-goals/index.ts b/packages/core/examples/gitpatch-goals/index.ts index 9ecc292..4417727 100644 --- a/packages/core/examples/gitpatch-goals/index.ts +++ b/packages/core/examples/gitpatch-goals/index.ts @@ -1,4 +1,5 @@ import { defineCommand, runMain } from 'citty'; +import { zodToJsonSchema } from 'zod-to-json-schema'; import { ReviewInputSchema } from './spec.js'; import { ReviewHandler } from './handler.js'; @@ -24,14 +25,33 @@ const main = defineCommand({ description: 'Output the result as JSON', default: false, }, + 'dry-run': { + type: 'boolean', + description: 'Simulates the command without making API calls', + default: false, + }, + describe: { + type: 'boolean', + description: 'Prints the JSON schema for this command and exits', + default: false, + }, }, async run({ args }) { + if (args.describe) { + const schema = zodToJsonSchema(ReviewInputSchema, 'ReviewInput'); + console.log(JSON.stringify(schema, null, 2)); + process.exit(0); + } + if (!process.env.JULES_API_KEY) { console.error('Error: JULES_API_KEY environment variable is missing.'); process.exit(1); } - const parseResult = ReviewInputSchema.safeParse({ prompt: args.prompt }); + const parseResult = ReviewInputSchema.safeParse({ + prompt: args.prompt, + dryRun: args['dry-run'], + }); if (!parseResult.success) { console.error('Invalid input:', parseResult.error.format()); process.exit(1); diff --git a/packages/core/examples/gitpatch-goals/package.json b/packages/core/examples/gitpatch-goals/package.json index adb7453..3938fd9 100644 --- a/packages/core/examples/gitpatch-goals/package.json +++ b/packages/core/examples/gitpatch-goals/package.json @@ -9,7 +9,8 @@ "dependencies": { "@google/jules-sdk": "workspace:*", "citty": "^0.1.6", - "zod": "^3.25.76" + "zod": "^3.25.76", + "zod-to-json-schema": "^3.24.1" }, "devDependencies": { "bun-types": "^1.1.8" diff --git a/packages/core/examples/gitpatch-goals/spec.ts b/packages/core/examples/gitpatch-goals/spec.ts index ac5c441..73020a5 100644 --- a/packages/core/examples/gitpatch-goals/spec.ts +++ b/packages/core/examples/gitpatch-goals/spec.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; export const ReviewInputSchema = z.object({ prompt: z.string().min(1, 'Prompt is required'), + dryRun: z.boolean().default(false).describe('Simulates the command without making API calls.'), }); export type ReviewInput = 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 23/28] 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 4a1a8706fac10322751d44ae1f841e54fb5d11a8 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:01:26 +0000 Subject: [PATCH 24/28] feat: implement Agent DX input hardening and dry-run Improves the ai-sdk CLI example by adhering directly to Agent DX best practices outlined by Justin Poehnelt. - **Input Hardening**: Upgrades the Zod tool specification to proactively block hallucinated or adversarial inputs (control characters, URL-encoded strings, path traversals). - **Dry-Run Validation**: Introduces a `--dry-run` flag via `citty`, plumbing it through the LLM system prompt and short-circuiting real Jules sessions. - **Agent Context Docs**: Adds a `CONTEXT.md` explicitly defining operating invariants (formatting, mutations, schema rules) for integrating agents. Fixes #227 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- packages/core/examples/ai-sdk/CONTEXT.md | 13 ++++++++++++ .../examples/ai-sdk/src/commands/start.ts | 6 ++++++ .../examples/ai-sdk/src/services/agent.ts | 4 ++++ .../src/tools/jules-coding-task/handler.ts | 13 ++++++++++++ .../src/tools/jules-coding-task/spec.ts | 20 +++++++++++++++---- 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 packages/core/examples/ai-sdk/CONTEXT.md diff --git a/packages/core/examples/ai-sdk/CONTEXT.md b/packages/core/examples/ai-sdk/CONTEXT.md new file mode 100644 index 0000000..9669512 --- /dev/null +++ b/packages/core/examples/ai-sdk/CONTEXT.md @@ -0,0 +1,13 @@ +# Agent Context Guidelines + +This file is explicitly for AI/LLM Agents acting as operators or integrators against this CLI. + +## Overview + +You are invoking the `ai-sdk-example` CLI. This CLI adheres to Agent DX best practices to ensure predictability and defense-in-depth logic. You are interacting with the entrypoints, primarily the `start` command. + +## Invariants + +1. **Output formatting:** Always specify `--output json` when executing the CLI to guarantee machine-readable output rather than human-readable text. Do not rely on stdout text parsing. +2. **Mutations:** Jules sessions represent large, complex mutating cloud tasks. If you are verifying parameters, assessing feasibility, or unsure about an input string, ALWAYS run `--dry-run` first to test the system boundary. +3. **Adversarial inputs:** Be aware that the `JulesCodingTaskSpec` strict schema parsing handles your inputs. Path traversals (`../`), query strings (`?`), fragments (`#`), control chars, or pre-URL encoded characters will be strictly rejected. Ensure the inputs are clean before calling the tool. diff --git a/packages/core/examples/ai-sdk/src/commands/start.ts b/packages/core/examples/ai-sdk/src/commands/start.ts index e23ba84..537c2b8 100644 --- a/packages/core/examples/ai-sdk/src/commands/start.ts +++ b/packages/core/examples/ai-sdk/src/commands/start.ts @@ -22,6 +22,11 @@ export default defineCommand({ description: 'Optional GitHub repository (e.g. "owner/repo").', required: false, }, + 'dry-run': { + type: 'boolean', + description: 'Validate input and logic without creating a real cloud session.', + default: false, + }, }, async run({ args }) { // 1. Logic Checks: Validate Environment Context explicitly before attempting external operations @@ -39,6 +44,7 @@ export default defineCommand({ const response = await runAgent({ prompt: args.prompt, repo: args.repo, + dryRun: args['dry-run'], }); // 3. Render payload strictly conforming to output format expectation (Agent DX vs Human) diff --git a/packages/core/examples/ai-sdk/src/services/agent.ts b/packages/core/examples/ai-sdk/src/services/agent.ts index e8fcc74..a0be0bc 100644 --- a/packages/core/examples/ai-sdk/src/services/agent.ts +++ b/packages/core/examples/ai-sdk/src/services/agent.ts @@ -5,6 +5,7 @@ import { executeCodingTask } from '../tools/jules-coding-task/index.js'; export interface AgentRequest { prompt: string; repo?: string; + dryRun?: boolean; } export interface AgentResponse { @@ -26,6 +27,9 @@ export async function runAgent(request: AgentRequest): Promise { try { const { text, toolCalls } = await generateText({ model: google('gemini-3.1-flash-lite-preview'), + system: request.dryRun + ? "You are in dry-run mode. ALWAYS pass dryRun: true to any tools you execute." + : "", prompt: contextPrompt, tools: { executeCodingTask, diff --git a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts index a06e46a..d81c408 100644 --- a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts +++ b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/handler.ts @@ -27,6 +27,19 @@ export class JulesCodingTaskHandler implements JulesCodingTaskSpec { sessionOptions.autoPr = true; } + // Agent DX: Dry run validation intercept + if (input.dryRun) { + return { + success: true, + data: { + sessionId: "dry-run-session", + state: "succeeded", + pullRequestUrl: "https://github.com/dry-run/mock-pr", + generatedFilesCount: 0, + } + }; + } + // Create and start the Jules session const session = await jules.session(sessionOptions); diff --git a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts index f071084..aa5eb20 100644 --- a/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts +++ b/packages/core/examples/ai-sdk/src/tools/jules-coding-task/spec.ts @@ -1,10 +1,22 @@ import { z } from 'zod'; -// 1. INPUT +// 1. INPUT HARDENING (Agent DX) +// Agents hallucinate and pass malformed inputs. We validate strictly at the boundary. +const SafeStringSchema = z.string() + .refine(s => !/[\x00-\x1F]/.test(s), "Control characters are not allowed") + .refine(s => !s.includes('%'), "Pre-URL encoded strings are not allowed"); + +const SafeRepoSchema = SafeStringSchema + .refine(r => !r.includes('..'), "Path traversals are not allowed in repo names") + .refine(r => !r.includes('?'), "Query parameters are not allowed in repo names") + .refine(r => !r.includes('#'), "Fragments are not allowed in repo names") + .refine(r => r.split('/').length === 2, "Repo must be in the format owner/repo"); + export const JulesCodingTaskInputSchema = z.object({ - prompt: z.string().describe('Detailed instructions for the coding task, including what needs to be changed.'), - githubRepo: z.string().optional().describe('The GitHub repository in the format "owner/repo" (e.g. "google/jules-sdk"). If omitted, it runs a repoless session.'), - baseBranch: z.string().optional().describe('The base branch to make the changes against. Defaults to "main" if repo provided.'), + prompt: SafeStringSchema.describe('Detailed instructions for the coding task, including what needs to be changed.'), + githubRepo: SafeRepoSchema.optional().describe('The GitHub repository in the format "owner/repo" (e.g. "google/jules-sdk"). If omitted, it runs a repoless session.'), + baseBranch: SafeStringSchema.optional().describe('The base branch to make the changes against. Defaults to "main" if repo provided.'), + dryRun: z.boolean().default(false).describe('If true, validates the input and returns a success message without actually creating a Jules session.'), }); export type JulesCodingTaskInput = 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 25/28] 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; From e9f55c819bd56e1ac1b6111d79df927d39ffc2dc Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:09:24 +0000 Subject: [PATCH 26/28] Add gitpatch-local CLI example to the Jules SDK This commit introduces a new example demonstrating how to retrieve a `changeSet` artifact's `gitPatch` from a Jules session snapshot and safely apply it locally. Per user instructions, the example has been refactored into a CLI utilizing: - `citty` for command-line argument parsing - `zod` and the Typed Service Contract pattern for strict input validation, separation of impure side-effects into `handler.ts`, and structured error mapping (Result types) - Agent-friendly `--json` flags to output machine-readable payloads The SDK's core README has also been updated to link to this new example. Fixes #237 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 8 + .../core/examples/gitpatch-local/README.md | 30 ++- .../core/examples/gitpatch-local/handler.ts | 120 ++++++++++++ .../core/examples/gitpatch-local/index.ts | 179 +++++++----------- .../core/examples/gitpatch-local/package.json | 6 +- packages/core/examples/gitpatch-local/spec.ts | 41 ++++ 6 files changed, 266 insertions(+), 118 deletions(-) create mode 100644 packages/core/examples/gitpatch-local/handler.ts create mode 100644 packages/core/examples/gitpatch-local/spec.ts diff --git a/bun.lock b/bun.lock index 6f09192..201fe53 100644 --- a/bun.lock +++ b/bun.lock @@ -67,6 +67,10 @@ }, "packages/core/examples/gitpatch-local": { "name": "gitpatch-local-example", + "dependencies": { + "citty": "^0.2.1", + "zod": "^4.3.6", + }, }, "packages/core/examples/webhook": { "name": "webhook", @@ -1224,6 +1228,10 @@ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "gitpatch-local-example/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + + "gitpatch-local-example/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "jules-github-actions-example/@types/node": ["@types/node@20.19.37", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], diff --git a/packages/core/examples/gitpatch-local/README.md b/packages/core/examples/gitpatch-local/README.md index c9d1619..eed3742 100644 --- a/packages/core/examples/gitpatch-local/README.md +++ b/packages/core/examples/gitpatch-local/README.md @@ -1,13 +1,14 @@ -# Gitpatch Local Example +# Gitpatch Local Example (CLI) -This example demonstrates how to use the Jules SDK to create a session, retrieve a `changeSet` artifact, and apply the generated code modifications locally on your machine using Git. +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: -- Create a simple text file locally to act as a target. -- Spin up a local git branch. -- Request Jules to modify the file content. +- 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 `git apply` to patch the code on the local machine and commit the changes. +- Use safely executed `execFileSync` to spin up a local git branch, `git apply` to patch the code, and commit the changes. ## Requirements @@ -15,6 +16,7 @@ It specifically showcases how to: - 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 @@ -26,20 +28,28 @@ It specifically showcases how to: 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 +bun run index.ts ``` -Using `npm` and `tsx` (or similar TypeScript runner): +**Options:** +- `--branch `: Provide a custom name for the local git branch to be created (default is `jules-patch-test-`). +- `--json`: Output the result of the operation as a strict JSON blob (ideal for AI Agent consumption). +Example: ```bash -npx tsx index.ts +bun run index.ts jules:session:123456789 --branch test-patch-fix --json ``` ## What it does -The script creates a temporary file `test_patch_target.txt` and starts a local git branch. It creates a session using `jules.session` to ask an agent to change the second line of the file. Once complete, it searches the session activities 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 file. It then cleans up by rolling back the git changes and deleting the temporary file. +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. diff --git a/packages/core/examples/gitpatch-local/handler.ts b/packages/core/examples/gitpatch-local/handler.ts new file mode 100644 index 0000000..db70d8b --- /dev/null +++ b/packages/core/examples/gitpatch-local/handler.ts @@ -0,0 +1,120 @@ +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 { + 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, + }, + }; + } + + // 3. 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, + }, + }; + } + + // 4. Save the patch to disk + patchPath = join(process.cwd(), 'jules_changes.patch'); + writeFileSync(patchPath, gitPatch.unidiffPatch); + + // 5. 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, + }, + }; + } + + // 6. Commit the applied changes + commitMessage = gitPatch.suggestedCommitMessage || 'Applied changes from Jules'; + 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, + }, + }; + } + + // 7. 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 + } + } + } + } +} diff --git a/packages/core/examples/gitpatch-local/index.ts b/packages/core/examples/gitpatch-local/index.ts index 15128a4..68c4c44 100644 --- a/packages/core/examples/gitpatch-local/index.ts +++ b/packages/core/examples/gitpatch-local/index.ts @@ -1,7 +1,6 @@ -import { jules } from '@google/jules-sdk'; -import { execSync, execFileSync } from 'child_process'; -import { writeFileSync, unlinkSync } from 'fs'; -import { join } from 'path'; +import { defineCommand, runMain } from 'citty'; +import { ApplyPatchInputSchema } from './spec.js'; +import { ApplyPatchHandler } from './handler.js'; /** * Gitpatch Local Example @@ -9,115 +8,81 @@ import { join } from 'path'; * Demonstrates how to use Jules' session GitPatch data to download * and patch the code locally in a new branch on the user's machine. */ -async function main() { - 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); - } - - // Set up a simple target file to be modified by the agent - const testFileName = 'test_patch_target.txt'; - const testFilePath = join(process.cwd(), testFileName); - writeFileSync(testFilePath, 'This is a test file.\nIt will be modified by Jules.\n'); - - // Let's create a local branch to apply changes to - const branchName = `jules-patch-test-${Date.now()}`; - try { - console.log(`Creating a new local branch: ${branchName}`); - execSync(`git checkout -b ${branchName}`, { stdio: 'inherit' }); - } catch (e) { - console.error(`Failed to create branch. Are you in a git repository?`); - // Fallback for execution outside git repos, but ideally this runs in one. - } +const main = defineCommand({ + meta: { + name: 'jules-gitpatch-local', + version: '1.0.0', + description: 'Applies a GitPatch from a Jules session to a local branch', + }, + args: { + sessionId: { + type: 'positional', + description: 'The ID of the Jules session to extract the patch from', + required: true, + }, + branch: { + type: 'string', + description: 'The name of the new local branch to apply changes to (optional)', + required: false, + }, + json: { + type: 'boolean', + description: 'Output the result as JSON for agent interoperability', + required: false, + default: false, + }, + }, + async run({ args }) { + if (!process.env.JULES_API_KEY) { + if (args.json) { + console.error(JSON.stringify({ error: 'JULES_API_KEY environment variable is not set.' })); + } else { + 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); + } - console.log('Creating a new Jules session...'); - try { - // 1. Create a session asking for a specific code change - const session = await jules.session({ - prompt: `Modify the file named ${testFileName}. Change the second line to say "It has been modified by Jules!"`, + // 1. Validate Input (Parse, don't validate) + const inputResult = ApplyPatchInputSchema.safeParse({ + sessionId: args.sessionId, + targetBranch: args.branch, }); - console.log(`Session created! ID: ${session.id}`); - console.log('Waiting for the agent to complete the task...'); - - // 2. Await the result of the session - const outcome = await session.result(); - console.log(`\nSession completed with state: ${outcome.state}`); - - if (outcome.state === 'completed') { - console.log('\nSearching for changeSet artifacts...'); - - // 3. Retrieve the activities to find the changeSet artifact - // In this example we query activities, but we could also use outcome.generatedFiles() - // or session.stream() depending on the workflow. - const activities = await jules.select({ - from: 'activities', - where: { 'session.id': session.id }, - }); - - let patchApplied = false; - - for (const activity of activities) { - if (!activity.artifacts) continue; - - for (const artifact of activity.artifacts) { - if (artifact.type === 'changeSet') { - console.log('Found a changeSet artifact!'); - - const gitPatch = artifact.gitPatch; - if (gitPatch && gitPatch.unidiffPatch) { - const patchPath = join(process.cwd(), 'jules_changes.patch'); - - // 4. Save the unidiff patch locally - console.log(`Writing patch to ${patchPath}...`); - writeFileSync(patchPath, gitPatch.unidiffPatch); - - // 5. Apply the patch using git - console.log('Applying the patch locally...'); - try { - // Using git apply to apply the patch - execFileSync('git', ['apply', patchPath], { stdio: 'inherit' }); - console.log('Patch applied successfully!'); - - // Commit the changes - // Using execFileSync to avoid shell command injection vulnerabilities - execFileSync('git', ['add', testFileName], { stdio: 'inherit' }); - const commitMsg = gitPatch.suggestedCommitMessage || 'Applied changes from Jules'; - execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'inherit' }); - console.log(`Changes committed to branch ${branchName}!`); - patchApplied = true; - } catch (applyError) { - console.error('Failed to apply or commit the patch:', applyError); - } finally { - // Clean up the patch file - try { - unlinkSync(patchPath); - } catch (e) {} - } - } - } - } + if (!inputResult.success) { + if (args.json) { + console.error(JSON.stringify({ error: 'Invalid input', details: inputResult.error.issues })); + } else { + console.error('Validation Error:'); + inputResult.error.issues.forEach((i) => console.error(` - ${i.message}`)); } + process.exit(1); + } - if (!patchApplied) { - console.log('No patch was generated or applied.'); - } + // 2. Execute Handler + const handler = new ApplyPatchHandler(); + const result = await handler.execute(inputResult.data); + // 3. Handle Output (Agent DX vs Human DX) + if (args.json) { + console.log(JSON.stringify(result, null, 2)); } else { - console.error('The session did not complete successfully.'); + if (!result.success) { + console.error(`[Error] ${result.error.code}: ${result.error.message}`); + process.exit(1); + } else { + console.log(`Successfully checked out branch: ${result.data.branchName}`); + console.log(`Patch applied and committed!`); + } } - } catch (error) { - console.error('An error occurred during the session:', error); - } finally { - // Clean up test target file - try { - unlinkSync(testFilePath); - console.log(`\nThe branch '${branchName}' has been left locally for you to inspect!`); - console.log(`When you are done, you can delete it with: git checkout - && git branch -D ${branchName}`); - } catch (e) {} - } -} -// Run the example -main(); + if (!result.success) { + process.exit(1); + } + }, +}); + +// Run the example CLI +if (import.meta.url === `file://${process.argv[1]}`) { + runMain(main); +} diff --git a/packages/core/examples/gitpatch-local/package.json b/packages/core/examples/gitpatch-local/package.json index b070fd3..cf31a8f 100644 --- a/packages/core/examples/gitpatch-local/package.json +++ b/packages/core/examples/gitpatch-local/package.json @@ -1,5 +1,9 @@ { "name": "gitpatch-local-example", "private": true, - "type": "module" + "type": "module", + "dependencies": { + "citty": "^0.2.1", + "zod": "^4.3.6" + } } diff --git a/packages/core/examples/gitpatch-local/spec.ts b/packages/core/examples/gitpatch-local/spec.ts new file mode 100644 index 0000000..54e6090 --- /dev/null +++ b/packages/core/examples/gitpatch-local/spec.ts @@ -0,0 +1,41 @@ +import { z } from 'zod'; + +export const ApplyPatchInputSchema = z.object({ + sessionId: z.string().min(1, 'Session ID cannot be empty'), + targetBranch: z.string().optional(), +}); +export type ApplyPatchInput = z.infer; + +export const ApplyPatchErrorCode = z.enum([ + 'SESSION_NOT_FOUND', + 'NO_CHANGESET_FOUND', + 'UNABLE_TO_CHECKOUT_BRANCH', + 'UNABLE_TO_APPLY_PATCH', + 'UNABLE_TO_COMMIT', + 'UNKNOWN_ERROR', +]); + +export const ApplyPatchSuccess = z.object({ + success: z.literal(true), + data: z.object({ + branchName: z.string(), + commitMessage: z.string().optional(), + }), +}); + +export const ApplyPatchFailure = z.object({ + success: z.literal(false), + error: z.object({ + code: ApplyPatchErrorCode, + message: z.string(), + recoverable: z.boolean(), + }), +}); + +export type ApplyPatchResult = + | z.infer + | z.infer; + +export interface ApplyPatchSpec { + execute(input: ApplyPatchInput): Promise; +} From 2266af62c18806ff0fd3cce94e2a93eca725e074 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:17:26 +0000 Subject: [PATCH 27/28] feat(sdk): Add GitPatch Review CLI example using Typed Service Contract Adds a new practical example to the Jules TypeScript SDK demonstrating how to use the SDK to generate code and review the resulting GitPatch against standard coding practices. The example is built as an Agent-ready CLI using the `citty` framework, follows the Typed Service Contract pattern to separate schemas from business logic, streams activities back to the user, and uses the `session.snapshot()` to safely extract the GitPatch. Includes an automated E2E test script (`e2e-test.ts`) to verify authentic LLM execution flows. Updates the main packages/core/README.md to include a link to the new example. Included a new package in packages/core/examples/gitpatch-review. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 31 ++++++ .../core/examples/gitpatch-review/e2e-test.ts | 104 ++++++++++++++++++ .../examples/gitpatch-review/package.json | 6 +- .../examples/gitpatch-review/src/handler.ts | 31 ++++-- 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 packages/core/examples/gitpatch-review/e2e-test.ts diff --git a/bun.lock b/bun.lock index e99d7a7..213b7a6 100644 --- a/bun.lock +++ b/bun.lock @@ -75,6 +75,7 @@ }, "devDependencies": { "bun-types": "^1.1.8", + "execa": "^9.6.1", }, }, "packages/core/examples/webhook": { @@ -451,6 +452,8 @@ "@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.1.5", "", { "dependencies": { "@rushstack/terminal": "0.19.5", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + "@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="], @@ -465,6 +468,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="], "@types/aws-lambda": ["@types/aws-lambda@8.10.160", "", {}, "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA=="], @@ -669,6 +674,8 @@ "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -687,6 +694,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="], @@ -707,6 +716,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], @@ -743,6 +754,8 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], @@ -759,10 +772,16 @@ "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="], @@ -851,6 +870,8 @@ "niftty": ["niftty@0.1.3", "", { "dependencies": { "chalk": "^5.6.2", "shiki": "^3.12.2", "string-length": "^6.0.0", "tinycolor2": "^1.6.0" } }, "sha512-wy8Kysxzh/R3hBq0BDlBbnzxDU/b/3PUtWfWVm1KwOestaVF3423U4iHD7TthPMF/RTHPXGenxh6YNERaD8M2g=="], + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -871,6 +892,8 @@ "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], @@ -901,6 +924,8 @@ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -993,6 +1018,8 @@ "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], @@ -1139,6 +1166,8 @@ "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -1237,6 +1266,8 @@ "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "octokit/@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="], "octokit/@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="], diff --git a/packages/core/examples/gitpatch-review/e2e-test.ts b/packages/core/examples/gitpatch-review/e2e-test.ts new file mode 100644 index 0000000..55647f7 --- /dev/null +++ b/packages/core/examples/gitpatch-review/e2e-test.ts @@ -0,0 +1,104 @@ +import { execa } from 'execa'; +import { z } from 'zod'; +import { ReviewSuccess } from './src/spec.js'; + +/** + * End-to-End Test for the GitPatch Review CLI + * + * This script invokes the CLI as a separate process to verify that: + * 1. The CLI can authenticate with the Jules API (using JULES_API_KEY). + * 2. It successfully starts and streams two consecutive sessions. + * 3. When the `--json` flag is provided, the final `stdout` is exclusively + * a valid JSON payload matching the `ReviewSuccess` schema. + * 4. Progress logs are successfully piped to `stderr` and don't corrupt the JSON. + */ +async function runE2E() { + const apiKey = process.env.JULES_API_KEY; + + if (!apiKey) { + console.error('❌ E2E Test Failed: JULES_API_KEY environment variable is missing.'); + process.exit(1); + } + + console.log('🚀 Starting GitPatch Review CLI E2E Test...\n'); + + try { + // We use execa to easily spawn the CLI, capture stdout/stderr separately, + // and provide a timeout. The target repo here is arbitrary but must be valid. + const subprocess = execa('bun', [ + 'run', + 'index.ts', + '-r', + 'davideast/dataprompt', + '-b', + 'main', + '-p', + 'Write a Python function that adds two numbers, but name it very badly, use no types, and mess up the indentation.', + '--json', + ], { + env: { JULES_API_KEY: apiKey }, + timeout: 900000, // 15 minute timeout for two LLM sessions + cwd: import.meta.dir // Ensure we run relative to this e2e script + }); + + // Pipe stderr to our current console so we can watch the progress logs live + if (subprocess.stderr) { + subprocess.stderr.pipe(process.stderr); + } + + const { stdout, exitCode } = await subprocess; + + console.log('\n\n✅ CLI process exited with code:', exitCode); + + if (exitCode !== 0) { + console.error('❌ E2E Test Failed: CLI exited with a non-zero status code.'); + process.exit(1); + } + + console.log('--- Raw CLI JSON Output (stdout) ---'); + console.log(stdout); + console.log('------------------------------------\n'); + + // Parse and validate the stdout output against our expected Zod schema + const parsedJson = JSON.parse(stdout); + const validationResult = ReviewSuccess.safeParse({ success: true, data: parsedJson }); + + if (!validationResult.success) { + console.error('❌ E2E Test Failed: The JSON output did not match the expected schema.'); + console.error(validationResult.error.format()); + process.exit(1); + } + + const { data } = validationResult.data; + + console.log('✅ Validation Passed: Output is valid JSON.'); + console.log(`- Code Gen Session ID: ${data.codeGenSessionId}`); + console.log(`- Review Session ID: ${data.reviewSessionId}`); + + if (data.gitPatchStr && data.gitPatchStr.length > 0) { + console.log(`- Git Patch Extracted: YES (${data.gitPatchStr.split('\\n').length} lines)`); + } else { + console.error('❌ E2E Test Failed: No Git Patch string was found in the output.'); + process.exit(1); + } + + if (data.reviewMessage && data.reviewMessage.length > 0) { + console.log(`- Review Message Generated: YES`); + } else { + console.error('❌ E2E Test Failed: No Review Message was found in the output.'); + process.exit(1); + } + + console.log('\n🎉 E2E Test Completed Successfully!'); + + } catch (error: any) { + console.error('\n❌ E2E Test Failed with an exception:'); + if (error.shortMessage) { + console.error(error.shortMessage); // execa formatting + } + console.error(error.message); + process.exit(1); + } +} + +runE2E(); diff --git a/packages/core/examples/gitpatch-review/package.json b/packages/core/examples/gitpatch-review/package.json index 784a8ce..b76a135 100644 --- a/packages/core/examples/gitpatch-review/package.json +++ b/packages/core/examples/gitpatch-review/package.json @@ -5,7 +5,8 @@ "main": "index.ts", "type": "module", "scripts": { - "start": "bun run index.ts" + "start": "bun run index.ts", + "test:e2e": "bun run e2e-test.ts" }, "dependencies": { "@google/jules-sdk": "workspace:*", @@ -13,6 +14,7 @@ "zod": "^3.23.0" }, "devDependencies": { - "bun-types": "^1.1.8" + "bun-types": "^1.1.8", + "execa": "^9.6.1" } } diff --git a/packages/core/examples/gitpatch-review/src/handler.ts b/packages/core/examples/gitpatch-review/src/handler.ts index bdf6c6b..afaf84e 100644 --- a/packages/core/examples/gitpatch-review/src/handler.ts +++ b/packages/core/examples/gitpatch-review/src/handler.ts @@ -28,7 +28,17 @@ export class ReviewHandler implements ReviewSpec { this.log(`Code Generation Session ID: ${codeGenSession.id}`, input.json); - await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + // If the session isn't immediately finished, stream it until it is + const genInfo = await codeGenSession.info(); + if (genInfo.state !== 'completed' && genInfo.state !== 'failed') { + try { + await this.streamSessionActivities(codeGenSession, 'Code Gen', input.json); + } catch(e) { + // Occasionally activities return 404 momentarily immediately after session creation + // in some environments. Ignore and fall through to wait on result(). + } + } + const genOutcome = await codeGenSession.result(); if (genOutcome.state === 'failed') { @@ -113,12 +123,19 @@ ${gitPatchStr} let reviewMessage = ''; - // We will listen for the final agent message from the stream - for await (const activity of reviewSession.stream()) { - this.logActivity(activity, 'Review', input.json); - if (activity.type === 'agentMessaged') { - reviewMessage = activity.message; - } + // Stream to get live updates and block until finished + const revInfo = await reviewSession.info(); + if (revInfo.state !== 'completed' && revInfo.state !== 'failed') { + try { + for await (const activity of reviewSession.stream()) { + this.logActivity(activity, 'Review', input.json); + if (activity.type === 'agentMessaged') { + reviewMessage = activity.message; + } + } + } catch(e) { + // Ignore stream fetch errors + } } const reviewOutcome = await reviewSession.result(); From 0f7c8bbfdc3694d51fec911dfa11cce9663e11d8 Mon Sep 17 00:00:00 2001 From: davideast <4570265+davideast@users.noreply.github.com> Date: Tue, 10 Mar 2026 02:26:38 +0000 Subject: [PATCH 28/28] Refactor gitpatch-local CLI for agent best practices This commit enhances the `gitpatch-local` example by introducing advanced AI Agent CLI conventions: - **Introspection:** Implements a `--describe` flag that dynamically outputs the JSON schema of the CLI's inputs and outputs using `zod-to-json-schema`, providing self-documenting capabilities to agents. - **Safety Rails:** Adds a `--dry-run` flag that safely simulates the patching operation (skipping `git checkout`, `git apply`, and `git commit` actions) to allow agents to "think out loud" before mutating a local system. - **Input Hardening:** Enhances the Zod spec to strictly reject path traversals, control characters, and embedded query string fragments in agent-provided arguments. - **Agent Skill documentation:** Ships an `SKILL.md` file designed to be injected into an agent's context, explicitly detailing the safety instructions and expected behaviors of this tool. Fixes #237 Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- bun.lock | 1 + .../core/examples/gitpatch-local/SKILL.md | 48 +++++++++++++++++++ .../core/examples/gitpatch-local/handler.ts | 24 +++++++--- .../core/examples/gitpatch-local/index.ts | 31 +++++++++++- .../core/examples/gitpatch-local/package.json | 3 +- packages/core/examples/gitpatch-local/spec.ts | 21 ++++++-- 6 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 packages/core/examples/gitpatch-local/SKILL.md diff --git a/bun.lock b/bun.lock index 201fe53..340135a 100644 --- a/bun.lock +++ b/bun.lock @@ -70,6 +70,7 @@ "dependencies": { "citty": "^0.2.1", "zod": "^4.3.6", + "zod-to-json-schema": "^3.25.1", }, }, "packages/core/examples/webhook": { diff --git a/packages/core/examples/gitpatch-local/SKILL.md b/packages/core/examples/gitpatch-local/SKILL.md new file mode 100644 index 0000000..74ebd08 --- /dev/null +++ b/packages/core/examples/gitpatch-local/SKILL.md @@ -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 --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 --json + ``` + +4. **Input Hardening:** + - The `` 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 } +} +``` diff --git a/packages/core/examples/gitpatch-local/handler.ts b/packages/core/examples/gitpatch-local/handler.ts index db70d8b..6df80b8 100644 --- a/packages/core/examples/gitpatch-local/handler.ts +++ b/packages/core/examples/gitpatch-local/handler.ts @@ -40,7 +40,20 @@ export class ApplyPatchHandler implements ApplyPatchSpec { }; } - // 3. Checkout a new branch to apply the changes + 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) { @@ -54,11 +67,11 @@ export class ApplyPatchHandler implements ApplyPatchSpec { }; } - // 4. Save the patch to disk + // 5. Save the patch to disk patchPath = join(process.cwd(), 'jules_changes.patch'); writeFileSync(patchPath, gitPatch.unidiffPatch); - // 5. Apply the patch + // 6. Apply the patch try { execFileSync('git', ['apply', patchPath], { stdio: 'pipe' }); } catch (e: any) { @@ -72,8 +85,7 @@ export class ApplyPatchHandler implements ApplyPatchSpec { }; } - // 6. Commit the applied changes - commitMessage = gitPatch.suggestedCommitMessage || 'Applied changes from Jules'; + // 7. Commit the applied changes try { execFileSync('git', ['add', '.'], { stdio: 'pipe' }); execFileSync('git', ['commit', '-m', commitMessage], { stdio: 'pipe' }); @@ -88,7 +100,7 @@ export class ApplyPatchHandler implements ApplyPatchSpec { }; } - // 7. Success + // 8. Success return { success: true, data: { diff --git a/packages/core/examples/gitpatch-local/index.ts b/packages/core/examples/gitpatch-local/index.ts index 68c4c44..0024ce8 100644 --- a/packages/core/examples/gitpatch-local/index.ts +++ b/packages/core/examples/gitpatch-local/index.ts @@ -1,6 +1,7 @@ import { defineCommand, runMain } from 'citty'; -import { ApplyPatchInputSchema } from './spec.js'; +import { ApplyPatchInputSchema, ApplyPatchResultSchema } from './spec.js'; import { ApplyPatchHandler } from './handler.js'; +import { zodToJsonSchema } from 'zod-to-json-schema'; /** * Gitpatch Local Example @@ -31,8 +32,35 @@ const main = defineCommand({ required: false, default: false, }, + 'dry-run': { + type: 'boolean', + description: 'Simulate the operation without mutating local files or git state', + required: false, + default: false, + }, + describe: { + type: 'boolean', + description: 'Output the JSON schemas for the input and output types for Agent introspection', + required: false, + default: false, + }, }, async run({ args }) { + // 0. Introspection (Agent Documentation) + if (args.describe) { + console.log( + JSON.stringify( + { + inputSchema: zodToJsonSchema(ApplyPatchInputSchema), + outputSchema: zodToJsonSchema(ApplyPatchResultSchema), + }, + null, + 2 + ) + ); + process.exit(0); + } + if (!process.env.JULES_API_KEY) { if (args.json) { console.error(JSON.stringify({ error: 'JULES_API_KEY environment variable is not set.' })); @@ -47,6 +75,7 @@ const main = defineCommand({ const inputResult = ApplyPatchInputSchema.safeParse({ sessionId: args.sessionId, targetBranch: args.branch, + dryRun: args['dry-run'], }); if (!inputResult.success) { diff --git a/packages/core/examples/gitpatch-local/package.json b/packages/core/examples/gitpatch-local/package.json index cf31a8f..d9d9601 100644 --- a/packages/core/examples/gitpatch-local/package.json +++ b/packages/core/examples/gitpatch-local/package.json @@ -4,6 +4,7 @@ "type": "module", "dependencies": { "citty": "^0.2.1", - "zod": "^4.3.6" + "zod": "^4.3.6", + "zod-to-json-schema": "^3.25.1" } } diff --git a/packages/core/examples/gitpatch-local/spec.ts b/packages/core/examples/gitpatch-local/spec.ts index 54e6090..df9939d 100644 --- a/packages/core/examples/gitpatch-local/spec.ts +++ b/packages/core/examples/gitpatch-local/spec.ts @@ -1,8 +1,16 @@ import { z } from 'zod'; +// 1. VALIDATION HELPERS (Input Hardening against hallucinations) +export const SafeStringSchema = z.string() + .min(1, 'Cannot be empty') + .refine(s => !/[\x00-\x1F\x7F]/.test(s), "No control characters allowed") + .refine(s => !s.includes('..'), "No path traversal allowed") + .refine(s => !s.includes('?') && !s.includes('#'), "No query or hash parameters allowed"); + export const ApplyPatchInputSchema = z.object({ - sessionId: z.string().min(1, 'Session ID cannot be empty'), - targetBranch: z.string().optional(), + sessionId: SafeStringSchema, + targetBranch: SafeStringSchema.optional(), + dryRun: z.boolean().default(false), }); export type ApplyPatchInput = z.infer; @@ -32,9 +40,12 @@ export const ApplyPatchFailure = z.object({ }), }); -export type ApplyPatchResult = - | z.infer - | z.infer; +export const ApplyPatchResultSchema = z.discriminatedUnion('success', [ + ApplyPatchSuccess, + ApplyPatchFailure, +]); + +export type ApplyPatchResult = z.infer; export interface ApplyPatchSpec { execute(input: ApplyPatchInput): Promise;