Skip to content

feat(sessions): continue a Claude Code or Codex session in Base Code - #679

Merged
ImriKochWix merged 5 commits into
mainfrom
import-session-33ca952d
Oct 28, 2026
Merged

ImriKochWix merged 5 commits into
mainfrom
import-session-33ca952d

Conversation

@ImriKochWix

@ImriKochWix ImriKochWix commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Note

Description

Adds base44 sessions import, a command that continues an existing Claude Code or Codex session in a new Base Code branch. It uploads one session file to POST /api/apps/{app_id}/imported/session-imports and prints the resulting branch link. The session can be the one the command runs inside (--current), a claude.ai cloud session pulled down with claude --teleport, an explicit --file, or one picked interactively. Because Base Code starts the new branch from GitHub, the command refuses to upload a local session whose work isn't pushed yet, and it sends the current branch as source_branch so the new branch starts from the right place.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • New base44 sessions command group (src/cli/commands/sessions/index.ts, registered in program.ts) with one subcommand, import (src/cli/commands/sessions/import.ts), taking [session-link], --current, --file <path>, --skip-push-check and --token <key>; only one session source may be given at a time.
  • Session discovery (src/core/session-import/sessions.ts): --current resolves CLAUDE_CODE_SESSION_ID to ~/.claude/projects/*/<id>.jsonl, then CODEX_THREAD_ID to its rollout file, else the newest ~/.codex/sessions/*/*/*/rollout-*.jsonl whose session_meta cwd is this folder. A CODEX_THREAD_ID with no rollout file means Codex cloud, which is reported as not importable. The picker lists this folder's 20 newest Claude Code and Codex sessions labelled by first user message. JSONL files are streamed line by line, so large transcripts are never loaded whole.
  • claude.ai session links: parseSessionLink extracts the session_… id (rejecting cshk_ "Copy link" URLs); teleportSession runs claude --teleport <id> -p "Reply with OK." inside a temp clone of this checkout's origin, strips the teleport exchange and the continued-from-another-machine note from the transcript, then deletes the clone and its ~/.claude/projects folder.
  • Upload (src/core/session-import/api.ts): multipart POST with a 25 MB client-side cap, 3-minute timeout, and a Zod snake_case-to-camelCase response transform. With --token the request goes through a bare ky call rather than base44Client, whose auth hook would replace the header and whose 401 handler would refresh and retry as the stored login.
  • Push check (readGitState in src/core/utils/git.ts): parses git status --porcelain --branch plus git rev-parse --abbrev-ref HEAD to detect uncommitted changes, unpushed commits, a missing upstream or a [gone] upstream, and stops before the upload so a one-time key stays valid for the re-run. --skip-push-check imports anyway and says the local changes aren't included. Links, --file and non-git folders skip the check. The branch is sent as the source_branch form field, omitted when HEAD is detached.
  • Result and error messages: a success with continued: true reports that Base Code continued on the session's own branch; a 409 branch_exists prints the existing details.branch_url. Known statuses (401 with a key, 403, 404, 413, 429) and codes (unsupported_file, repository_mismatch, repository_not_connected) get actionable messages. Inside claude.ai (CLAUDE_CODE_REMOTE) a refused connection or non-JSON 403 explains that app.base44.com must be added to the environment's allowed domains.
  • Privacy: import errors are rebuilt as plain messages carrying no request or response body (the body is the user's conversation), and the action replaces the reported command context so the session link and --token never reach telemetry.
  • --json support: the import result plus the full URL is returned via RunCommandResult.stdout; human status goes to outroMessage.
  • Testkit: new claudeBin test override (TestOverridesSchema, CLITestkit.givenClaudeBin, TestContext) so the teleport path can run a stub binary.
  • Docs: new "Session import" section in docs/commands.md and a CHANGELOG entry under Unreleased / Added.

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

packages/cli/tests/cli/sessions_import.spec.ts adds 21 tests covering source selection and mutual exclusion, the --current lookups for Claude Code and Codex, Codex cloud, the size cap, --token auth isolation and expiry, the branch_exists and continued responses, telemetry redaction, --json output, the teleport flow against a stubbed claude binary, and the push-check branches (clean/dirty/never-pushed/ahead/detached/--file) against a real temp git repo. The build and test commands were not run in this environment; CI lint, typecheck, knip and publish-preview are green, and the build and functions-compiler jobs were still in progress when this description was written.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The command sits under a sessions group rather than at the top level, leaving room for more session commands later. It is requireAuth: false so a one-time b44imp_ key works with no CLI login; ensureAuth runs only when no key is given. All session parsing, teleport and upload logic lives in the new src/core/session-import/ module, leaving import.ts with prompts and output only, per the core/cli split. The only new behaviour outside the command is readGitState, added to the existing core/utils/git.ts.


🤖 Generated by Claude | 2026-10-28 09:25 UTC | 12185af

`base44 import` uploads one session file to
POST /api/apps/{app_id}/imported/session-imports and prints the new
branch's link.

- `--current` finds the running session: CLAUDE_CODE_SESSION_ID, then
  CODEX_THREAD_ID, then the newest Codex rollout recorded in this folder.
- A claude.ai session link runs `claude --teleport` in a temp clone of
  this checkout's origin, drops the teleport exchange, and cleans up.
- `--file` uploads a file as is; no argument opens a picker.
- `--token <b44imp_…>` sends the one-time key through its own client, so
  the stored login is never used, refreshed or cleared.
- Errors are plain messages an agent can relay, carry no request body,
  and the link and key never reach telemetry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.2.3-pr.679.8a2af8d

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.2.3-pr.679.8a2af8d"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.2.3-pr.679.8a2af8d"
  }
}

Preview published to npm registry — try new features instantly!

ImriKochWix and others added 2 commits October 25, 2026 16:26
…ce_branch

Base Code starts the new branch from GitHub, so a session imported with
local-only work lands without it. For --current and the picker, the
command now stops before uploading (the one-time key stays valid) when
the checkout has uncommitted changes, unpushed commits, or a branch with
no upstream or a deleted one. --skip-push-check imports anyway and says
the local changes aren't included. A link (its changes live in the cloud
session), --file, and runs outside a git checkout skip the check.

The current branch goes with the upload as source_branch, left out when
HEAD is detached; the backend starts the new branch from it when it's on
GitHub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ImriKochWix

Copy link
Copy Markdown
Contributor Author

Remaining CLI changes to match the server side in base44-dev/apper#34732 (which this PR merges with). Already done here: the network-blocked message names only app.base44.com.

1. 409 branch_exists: print the existing branch's link (core/session-import/api.ts, messageFor)
When the session's pushed feature branch already exists in Base Code, the upload now returns:

{"error":{"code":"branch_exists","message":"Base Code already has a branch for feature/x.","details":{"branch_name":"feature/x","branch_url":"/apps/<id>/editor/preview?branch=feature%2Fx"}}}

Today it falls through to the server message with no link. Suggested:

// ErrorEnvelopeSchema: add details: z.object({ branch_name: z.string(), branch_url: z.string() }).partial().optional()
if (code === "branch_exists" && details?.branch_url) {
  return `Base Code already has ${details.branch_name}. Open it: ${new URL(details.branch_url, getBase44ApiUrl())}`;
}
if (code === "repository_not_connected") return "Connect this app to its GitHub repository in Base Code first.";

2. "Continued on" when the import continued the session's branch (commands/import.ts, success output)
The response now also has continued: boolean and source_ref: string. When the session was on a pushed feature branch, Base Code continues on that branch instead of creating a new one. Add both to SessionImportResponseSchema and:

imported.continued
  ? `✓ Imported ${n} messages from ${tool}. Continued on "${imported.branchName}".`
  : `✓ Imported ${n} messages from ${tool} into branch "${imported.branchName}".` // as today

3. Codex cloud: say it's not supported instead of "Couldn't find the current session" (core/session-import/sessions.ts, the CODEX_THREAD_ID branch)
Verified on a Codex cloud task: CODEX_THREAD_ID is set, but there's no rollout file anywhere in the container (the conversation stays on OpenAI's side). When CODEX_THREAD_ID is set and no rollout matches, fail with:

Codex cloud sessions can't be imported yet. Run this from the Codex app or CLI on your computer.

4. Wording of the network-blocked message (NETWORK_BLOCKED)
The dialog now calls this a Claude cloud environment (claude.ai can also run sessions on a local environment, which needs nothing), and avoids "blocking". Suggested to match:

Your Claude cloud environment can't reach Base44 yet. In the environment's settings, open Network access, add app.base44.com to Allowed domains, and try again.

Tests: import.spec.ts would need a case each for 1–3 (409 with details → link printed; continued: true → "Continued on"; CODEX_THREAD_ID with no rollout → the Codex cloud message).

…loud cases

- 409 branch_exists prints the existing branch's link from
  details.branch_url; 409 repository_not_connected says to connect it.
- A success with continued: true reads "Continued on <branch>".
- CODEX_THREAD_ID with no rollout file is Codex cloud, which keeps the
  conversation on OpenAI's side: say it isn't supported.
- The network message names the Claude cloud environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ImriKochWix

Copy link
Copy Markdown
Contributor Author

All four are done in fd22d22:

  1. 409 branch_exists: prints Base Code already has <branch_name>. Open it: <full link> from details.branch_url. A 409 repository_not_connected says "Connect this app to its GitHub repository in Base Code first."
  2. continued: continued and source_ref are now part of the response schema. When continued is true, the success line reads ✓ Imported N messages from <tool>. Continued on "<branch>".
  3. Codex cloud: when CODEX_THREAD_ID is set but no rollout file matches, the command stops with "Codex cloud sessions can't be imported yet. Run this from the Codex app or CLI on your computer."
  4. Network message: now reads "Your Claude cloud environment can't reach Base44 yet. In the environment's settings, open Network access, add app.base44.com to Allowed domains, and try again."

Tests: there's a new case for each of 1–3, and repository_not_connected is added to the error-message table. import.spec.ts has 27 tests and all pass.

Note: continued and source_ref are required in the response schema, so this needs base44-dev/apper#34732 deployed before it ships.

@ImriKochWix

Copy link
Copy Markdown
Contributor Author

Rename base44 import → base44 sessions import

A bare top-level import sits next to create, eject, scaffold and link, so base44 import reads as importing a project or repo, which is what "Import" already means in Base Code ("Import a project"). Every other group in this CLI is a plural noun (entities, functions, branches, agents, secrets), so the session command fits as base44 sessions import.

Please:

  1. Register it as sessions → import (same options: [session-link], --current, --file, --app-id, --token, --skip-push-check), and update import.spec.ts, the CHANGELOG and docs.
  2. Fix its help line. "Continue a Claude Code or Codex session in a new Base Code branch" is no longer always true, since it now continues on the session's own pushed branch when it can. Suggested: "Continue a Claude Code or Codex session in Base Code".

base44-dev/apper#34732 already uses the new name: the Import session dialog's prompt now runs npx base44 sessions import --current --app-id … --token …. So both PRs need to merge together with the rename in place, or the pasted prompt will fail with an unknown command. No alias for import is needed, because the command hasn't shipped yet.

A bare top-level `import` reads as importing a project, which is what
"Import" means in Base Code, and every other group is a plural noun.
The help line drops "in a new branch": the import now continues on the
session's own pushed branch when it can. No alias, since `import` never
shipped. base44-dev/apper#34732's dialog prompt already uses the new name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ImriKochWix ImriKochWix changed the title feat(import): continue a Claude Code or Codex session in Base Code feat(sessions): continue a Claude Code or Codex session in Base Code Oct 28, 2026
@ImriKochWix

Copy link
Copy Markdown
Contributor Author

Renamed in 12185af:

  1. New name: the command is now base44 sessions import, in src/cli/commands/sessions/ (index.ts for the group, import.ts for the command). The options are unchanged. There's no import alias, so base44 import is now an unknown command.
  2. Help line: now "Continue a Claude Code or Codex session in Base Code". The sessions group reads "Move Claude Code and Codex sessions to Base Code".

Also updated:

  • The test file is renamed to sessions_import.spec.ts and every run uses sessions import. All 27 tests pass.
  • Telemetry records the command as sessions import.
  • The CHANGELOG entry and the docs/commands.md section use the new name.
  • The PR title is now feat(sessions): ….

This and base44-dev/apper#34732 need to merge together, because the dialog's prompt runs npx base44 sessions import.

@ImriKochWix

Copy link
Copy Markdown
Contributor Author

Send an Idempotency-Key on the upload

The upload (POST /api/apps/{app_id}/imported/session-imports) now accepts an optional Idempotency-Key header, which the API standard requires for a public POST (base44-dev/apper#34732). A retry with the same key and the same file gets the first response back instead of creating a second branch.

In sessions import, generate the key once per run, before any upload attempt, and send it on the request:

import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // once per `sessions import` run
// in uploadSession(), on both the import-key ky.post and the getAppClient() post:
headers: { ...headers, "Idempotency-Key": idempotencyKey },

Reuse the same value for any retry within the run, and never share it across runs.

Two new 409 codes are worth a message in messageFor, though both are unlikely while the CLI doesn't retry:

  • idempotency_key_in_use → "An import of this session is still running. Wait a moment and try again."
  • idempotency_key_mismatch → shouldn't happen with one key per run; fall back to the server message.

@ImriKochWix
ImriKochWix merged commit 6b55d72 into main Oct 28, 2026
16 checks passed
@ImriKochWix
ImriKochWix deleted the import-session-33ca952d branch October 28, 2026 12:05
@ImriKochWix

Copy link
Copy Markdown
Contributor Author

Idempotency-Key: done in #682, since this PR was already merged.

  • One key per run: each sessions import run creates one randomUUID() before any upload attempt. Retries within the run reuse it, and runs never share one.
  • Both auth paths: the header goes on the import-key request and on the logged-in request.
  • 409 idempotency_key_in_use: shows "An import of this session is still running. Wait a moment and try again."
  • 409 idempotency_key_mismatch: keeps the server's message.

All 29 tests in sessions_import.spec.ts pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants