Jules Turndown
We’re writing to let you know that as part of our work to unify our agentic coding efforts, Jules will wind down on December 31, 2026. You have until November 11, 2026 to continue finishing up sessions. After November 1…
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
We’re writing to let you know that as part of our work to unify our agentic coding efforts, Jules will wind down on December 31, 2026. You have until November 11, 2026 to continue finishing up sessions. After November 11, 2026, we will be turning Jules to read-only until the end of the year.
When we launched Jules, it was an experiment to learn how asynchronous coding agents could help you tackle your backlog and ship faster. What you built with it taught us where agentic development is headed, and those learnings are shaping products across Google, including our agent-first platform, Google Antigravity.
You can now export everything Jules knows about your work into a local folder: account settings, environment variables, scheduled tasks, connected repositories, memories, setup scripts, and every session, including its prompt, plan, full activity log, and code patches.
Your code, branches, and pull requests already live in GitHub, so they stay where they are. The export covers the configuration and history that live inside Jules.
To export, install Jules Tools and sign in:
npm install -g @google/jules
jules login
Then open a CLI coding agent, such as Claude Code, from your home directory and paste the export prompt below. The agent checks your setup, runs the export, and tells you what it saved.
I want to export all my data from Google Jules into a local folder: settings, environment
variables, scheduled tasks, connected repos, memories, setup scripts, and every session (prompt,
plan, full activity log, and code patches). Please do this in order:
- Setup
- Check whether the Jules CLI is installed (
which jules). If it isn't, install it with
npm install -g @google/jules. If npm isn't installed, tell me and stop.
- Check whether I'm logged in:
security find-generic-password -s jules-cli -a default
on macOS (metadata only, without -w), or secret-tool lookup service jules-cli username default
on Linux. If there's no entry, ask me to run ! jules login myself. It's an interactive
browser sign-in, so don't try to run it for me. Wait until I say it's done.
- Run
jules remote list --repo. If it fails with a 401 and asks about GitHub, tell me to
connect my GitHub account at
https://github.com/apps/google-labs-jules/installations/select_target and wait for me to
confirm. If I've never used Jules with GitHub repos, I can skip this; the export will just
have no repo data.
- Export: save the script below to a file and run it from my home directory. Then tell me what
was exported (the counts it prints) and the full path of the folder. Don't print my access
token or the values in env-variables.json.
Problems someone else already ran into:
- The Jules CLI stores its login in the macOS Keychain (service "jules-cli", account "default")
as "go-keyring-base64:<base64 JSON>". The script decodes that. The access token expires about
an hour after login; if you get 401s on every call, run any jules command or ask me to run
! jules login again.
- If ListSources returns 401 "missing required authentication credential" but the other calls
work, my GitHub account isn't connected to Jules (see step 1).
- Settings, env vars, repos and memories come from an internal
$rpcendpoint. Sessions come
from REST endpoints under https://aida.googleapis.com/v1/swebot (/tasks, /tasks/{id},
/tasks/{id}/activities), which are what the CLI itself uses. Both are undocumented, so if a
call starts returning 404/400, tell me which one rather than guessing at fixes.
- The repo list doesn't include setup scripts or test commands; the script recovers them from
each session's code output (repoMetadata). Empty memories responses ({}) just mean there are
no memories.
- The API may return a few more sessions than
jules remote list --sessionshows (older ones).
That's expected.
import base64, json, pathlib, re, subprocess, sys, urllib.error, urllib.parse, urllib.request
def get_token():
for cmd in [["security", "find-generic-password", "-s", "jules-cli", "-a", "default", "-w"],
["secret-tool", "lookup", "service", "jules-cli", "username", "default"]]:
try:
out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode().strip()
if out.startswith("go-keyring-base64:"):
out = base64.b64decode(out.split(":", 1)[1]).decode()
if out: return json.loads(out)["access_token"]
except Exception: pass
p = pathlib.Path.home() / ".jules/cache/oauth_creds.json"
if p.exists(): return json.loads(p.read_text())["access_token"]
sys.exit("Run `jules login` first.")
TOKEN = get_token()
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
RPC = "https://aida.googleapis.com/$rpc/language_labs.aida.swebot.proto.SweBotService"
REST = "https://aida.googleapis.com/v1/swebot"
OUT = pathlib.Path("./jules-export"); OUT.mkdir(exist_ok=True)
errors = []
def rpc(m, b=None):
req = urllib.request.Request(f"{RPC}/{m}", data=json.dumps(b or {}).encode(), headers=HEADERS, method="POST")
with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read().decode() or "{}")
def get(path, **params):
q = ("?" + urllib.parse.urlencode(params)) if params else ""
with urllib.request.urlopen(urllib.request.Request(f"{REST}{path}{q}", headers=HEADERS), timeout=60) as r:
return json.loads(r.read().decode() or "{}")
def paged(fetch, key):
items, token = [], None
while True:
resp = fetch(token)
items += resp.get(key, [])
token = resp.get("nextPageToken")
if not token: return items
def safe(s): return re.sub(r"[^a-zA-Z0-9._-]", "_", s)
def dump(path, obj): path.parent.mkdir(parents=True, exist_ok=True); path.write_text(json.dumps(obj, indent=2))
# Account-level data
dump(OUT / "user-settings.json", rpc("GetUserSettings"))
dump(OUT / "env-variables.json", rpc("ListEnvironmentVariables"))
dump(OUT / "scheduled-tasks.json", rpc("ListScheduledTasks"))
# Repos (sources) and their memories
sources = paged(lambda t: rpc("ListSources", {"pageSize": 100, **({"pageToken": t} if t else {})}), "sources")
dump(OUT / "sources.json", sources)
mem_repos = 0
for src in sources:
sid = src.get("sourceId") or src.get("id") or src.get("name", "").removeprefix("sources/")
try:
mems = paged(lambda t: rpc("ListMemories", {"sourceId": sid, "pageSize": 100, **({"pageToken": t} if t else {})}), "memories")
except urllib.error.HTTPError as e:
errors.append(f"memories {sid}: HTTP {e.code}"); continue
if mems:
mem_repos += 1
d = OUT / "repos" / safe(sid)
dump(d / "memories.json", mems)
(d / "memories.md").write_text("# Memories\n\n" + "\n".join(
f"- {m.get('Fact') or m.get('fact') or m.get('content')}" for m in mems if not m.get("archived")) + "\n")
# Sessions: full detail, activity log, patches, and a readable summary
sessions = paged(lambda t: get("/tasks", pageSize=100, **({"pageToken": t} if t else {})), "tasks")
dump(OUT / "sessions.json", sessions)
setup = {} # latest setup script / test command seen per repo
patches = 0
for s in sorted(sessions, key=lambda s: s.get("createdAt", "")):
tid = s["id"]; d = OUT / "sessions" / tid
try:
task = get(f"/tasks/{tid}").get("task", {})
acts = paged(lambda t: get(f"/tasks/{tid}/activities", pageSize=100, **({"pageToken": t} if t else {})), "activitySteps")
except urllib.error.HTTPError as e:
errors.append(f"session {tid}: HTTP {e.code}"); continue
dump(d / "session.json", task)
dump(d / "activities.json", acts)
commits = [o["gitCommit"] for o in task.get("outputs", []) if "gitCommit" in o]
for i, c in enumerate(commits, 1):
if c.get("patch", {}).get("unidiff"):
(d / f"patch-{i}.diff").write_text(c["patch"]["unidiff"]); patches += 1
meta = c.get("repoMetadata", {})
if meta.get("environmentSetupScript") or meta.get("testCommand"):
setup[task.get("sourceId") or meta.get("name") or tid] = meta
plan = task.get("latestPlan", {}).get("steps", [])
kinds = {}
for a in acts:
for actor, v in a.items():
if isinstance(v, dict):
for k in v: kinds[f"{actor}.{k}"] = kinds.get(f"{actor}.{k}", 0) + 1
lines = [f"# {task.get('title') or s.get('suggestedTitle') or tid}", "",
f"- **ID:** {tid}", f"- **Repo:** {task.get('sourceId', '')}",
f"- **Status:** {task.get('taskStatus', '')}", f"- **Created:** {task.get('createdAt', '')}",
f"- **Completed:** {task.get('completedAt', '')}", "", "## Prompt", "", task.get("description", ""), ""]
if plan: lines += ["## Latest plan", ""] + [f"{p.get('index', 0) + 1}. {p.get('title', '')}" for p in plan] + [""]
if commits: lines += ["## Code outputs", ""] + [
f"- `patch-{i}.diff`: {c.get('title') or (c.get('commitMessage') or '').split(chr(10))[0]} (branch `{c.get('gitBranchName', '')}`)"
for i, c in enumerate(commits, 1)] + [""]
if pub := task.get("latestPublish", {}).get("commitInfo"):
lines += ["## Published", "", f"- Commit `{pub.get('commitId', '')}` on `{pub.get('branchName', '')}`", ""]
lines += ["## Activity", "", f"{len(acts)} steps (full log in `activities.json`):", ""] + [f"- {k}: {n}" for k, n in sorted(kinds.items())]
(d / "summary.md").write_text("\n".join(lines) + "\n")
for sid, meta in setup.items():
d = OUT / "repos" / safe(sid); d.mkdir(parents=True, exist_ok=True)
if meta.get("environmentSetupScript"): (d / "setup.sh").write_text(meta["environmentSetupScript"] + "\n")
if meta.get("testCommand"): (d / "test-command.txt").write_text(meta["testCommand"] + "\n")
print(json.dumps({"repos": len(sources), "repos_with_memories": mem_repos, "sessions": len(sessions),
"patches": patches, "repos_with_setup_or_test": len(setup),
"error_count": len(errors), "errors": errors[:10]}, indent=2))
print(f"Exported all Jules data to {OUT.resolve()}")
". The script decodes that. The access token expires about an hour after login; if you get 401s on every call, run any `jules` command or ask me to run `! jules login` again.- If ListSources returns 401 "missing required authentication credential" but the other calls work, my GitHub account isn't connected to Jules (see step 1).- Settings, env vars, repos and memories come from an internal `$rpc` endpoint. Sessions come from REST endpoints under https://aida.googleapis.com/v1/swebot (/tasks, /tasks/{id}, /tasks/{id}/activities), which are what the CLI itself uses. Both are undocumented, so if a call starts returning 404/400, tell me which one rather than guessing at fixes.- The repo list doesn't include setup scripts or test commands; the script recovers them from each session's code output (repoMetadata). Empty memories responses ({}) just mean there are no memories.- The API may return a few more sessions than `jules remote list --session` shows (older ones). That's expected.```pythonimport base64, json, pathlib, re, subprocess, sys, urllib.error, urllib.parse, urllib.requestdef get_token(): for cmd in [["security", "find-generic-password", "-s", "jules-cli", "-a", "default", "-w"], ["secret-tool", "lookup", "service", "jules-cli", "username", "default"]]: try: out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode().strip() if out.startswith("go-keyring-base64:"): out = base64.b64decode(out.split(":", 1)[1]).decode() if out: return json.loads(out)["access_token"] except Exception: pass p = pathlib.Path.home() / ".jules/cache/oauth_creds.json" if p.exists(): return json.loads(p.read_text())["access_token"] sys.exit("Run `jules login` first.")TOKEN = get_token()HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}RPC = "https://aida.googleapis.com/$rpc/language_labs.aida.swebot.pr
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。