Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Repo: https://github.com/openclaw/acpx

### Changes

- CLI/sessions: add `sessions export` and `sessions import` for moving portable session archives between machines.

### Breaking

### Fixes
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ One command surface for Pi, OpenClaw ACP, Codex, Claude, and other ACP-compatibl
- **Prompt from file/stdin**: `--file <path>` or piped stdin for prompt content
- **Config files**: global + project JSON config with `acpx config show|init`
- **Session inspect/history**: `sessions show` and `sessions history --limit <n>`
- **Session export/import**: move portable session archives between machines
- **Local status checks**: `status` reports running/idle/dead/no-session, pid, uptime, last prompt
- **Client methods**: stable `fs/*` and `terminal/*` handlers with permission controls and cwd sandboxing
- **Auth handshake**: stable `authenticate` support via env/config credentials
Expand Down
18 changes: 18 additions & 0 deletions docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ acpx codex sessions show # metadata for the cwd-scoped default
acpx codex sessions show api # metadata for the named session
acpx codex sessions history # last 20 turn previews
acpx codex sessions history --limit 50
acpx codex sessions export api --output api-session.json
acpx codex sessions import api-session.json --name api-restored
acpx codex sessions close # soft-close cwd default
acpx codex sessions close api # soft-close named session
acpx codex sessions prune --dry-run
Expand Down Expand Up @@ -88,6 +90,22 @@ Named sessions are independent. They do not share state, queue owners, or histor
- Closed sessions can still be loaded explicitly through embedding APIs.
- `sessions prune` is the explicit way to delete closed records.

## Export / import

`acpx` persists sessions per cwd in `~/.acpx/sessions/`. To move a session between machines or share one with a teammate:

```bash
# On the source machine:
acpx codex sessions export my-debug-session --output debug.json

# On the destination machine:
acpx codex sessions import debug.json --name debug-on-laptop
```

Export refuses to run if the session is locked by a live queue owner. Run `acpx codex sessions close my-debug-session` first.

The archive is plain JSON. Paths are rewritten relative to home, so an imported session lands at `~/<original-cwd-relative>` on the destination machine. Override with `--cwd`.

## Prune

`sessions prune` removes closed records once you actually want them gone:
Expand Down
71 changes: 71 additions & 0 deletions src/cli/command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
PromptInputValidationError,
textPrompt,
} from "../prompt-content.js";
import { exportSession } from "../session/export.js";
import { importSession } from "../session/import.js";
import {
findGitRepositoryRoot,
findSession,
Expand All @@ -33,7 +35,9 @@ import {
resolveSessionNameFromFlags,
type ExecFlags,
type GlobalFlags,
type SessionsExportFlags,
type PromptFlags,
type SessionsImportFlags,
type SessionsHistoryFlags,
type SessionsNewFlags,
type SessionsPruneFlags,
Expand Down Expand Up @@ -927,6 +931,73 @@ export async function handleSessionsHistory(
printSessionHistoryByFormat(record, flags.limit, globalFlags.format);
}

export async function handleSessionsExport(
explicitAgentName: string | undefined,
sessionName: string | undefined,
flags: SessionsExportFlags,
command: Command,
config: ResolvedAcpxConfig,
): Promise<void> {
const globalFlags = resolveGlobalFlags(command, config);
const agent = resolveAgentInvocation(explicitAgentName, globalFlags, config);
const cwd = flags.cwd ? path.resolve(flags.cwd) : agent.cwd;

await exportSession(
{
agentCommand: agent.agentCommand,
cwd,
name: sessionName,
},
flags.output,
);

if (
emitJsonResult(globalFlags.format, {
action: "session_exported",
output: flags.output,
})
) {
return;
}

if (globalFlags.format === "quiet") {
process.stdout.write(`${flags.output}\n`);
return;
}

process.stdout.write(`exported session to ${flags.output}\n`);
}

export async function handleSessionsImport(
archivePath: string,
flags: SessionsImportFlags,
command: Command,
config: ResolvedAcpxConfig,
): Promise<void> {
const globalFlags = resolveGlobalFlags(command, config);
const result = await importSession(archivePath, {
name: flags.name,
newCwd: flags.cwd,
});

if (
emitJsonResult(globalFlags.format, {
action: "session_imported",
record_id: result.record_id,
cwd: result.cwd,
})
) {
return;
}

if (globalFlags.format === "quiet") {
process.stdout.write(`${result.record_id}\n`);
return;
}

process.stdout.write(`imported session ${result.record_id} at ${result.cwd}\n`);
}

export async function handleSessionsPrune(
explicitAgentName: string | undefined,
flags: SessionsPruneFlags,
Expand Down
32 changes: 32 additions & 0 deletions src/cli/command-registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
handlePrompt,
handleSessionsClose,
handleSessionsEnsure,
handleSessionsExport,
handleSessionsHistory,
handleSessionsImport,
handleSessionsList,
handleSessionsNew,
handleSessionsPrune,
Expand All @@ -26,7 +28,9 @@ import {
parsePruneBeforeDate,
parseSessionName,
type PromptFlags,
type SessionsExportFlags,
type SessionsHistoryFlags,
type SessionsImportFlags,
type SessionsNewFlags,
type SessionsPruneFlags,
type StatusFlags,
Expand Down Expand Up @@ -139,6 +143,34 @@ export function registerSessionsCommand(
);
});

sessionsCommand
.command("export")
.description("Export a portable session archive")
.argument("[name]", "Session name", parseSessionName)
.requiredOption("--output <path>", "Output archive path", (value: string) =>
parseNonEmptyValue("Output path", value),
)
.option("--cwd <cwd>", "Session cwd to export", (value: string) =>
parseNonEmptyValue("Session cwd", value),
)
.action(async function (this: Command, name: string | undefined, flags: SessionsExportFlags) {
await handleSessionsExport(explicitAgentName, name, flags, this, config);
});

sessionsCommand
.command("import")
.description("Import a portable session archive")
.argument("<archive-path>", "Archive path", (value: string) =>
parseNonEmptyValue("Archive path", value),
)
.option("--name <name>", "Imported session name", parseSessionName)
.option("--cwd <cwd>", "Imported session cwd", (value: string) =>
parseNonEmptyValue("Imported session cwd", value),
)
.action(async function (this: Command, archivePath: string, flags: SessionsImportFlags) {
await handleSessionsImport(archivePath, flags, this, config);
});

sessionsCommand
.command("prune")
.description("Delete closed sessions and free disk space")
Expand Down
10 changes: 10 additions & 0 deletions src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ export type SessionsHistoryFlags = {
limit: number;
};

export type SessionsExportFlags = {
output: string;
cwd?: string;
};

export type SessionsImportFlags = {
name?: string;
cwd?: string;
};

export type StatusFlags = {
session?: string;
};
Expand Down
Loading