返回资讯中心
外部精选
软件工程
#开发者实时流

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 最新triyambakam2026/10/30 · 昨天30 分钟阅读

以下正文同步自 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:

  1. 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.

  1. 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 $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.


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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →