Skip to content
Open
39 changes: 39 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,45 @@ opengap run -d ./my-agent -a prompt

---

### session

Transform and resume conversation **sessions** across tools. OpenGAP reads a session from one tool into a tool-neutral **canonical session format**, then writes it out to another tool — so a chat can move between Copilot, Claude Code, and gitagent (any-to-any). Carries **messages + tool calls (+ memory)**; model settings are excluded.

```bash
opengap session list --from <tool> [--dir <agentDir>]
opengap session export --from <tool> --session <id> [-o file.json]
opengap session import --from <tool> --session <id> --to <tool> [--agent <dir>] [--session-id <id>]
```

Tools: `copilot`, `claude`, `gitagent`.

| Command | What it does |
|---------|--------------|
| `list` | List available sessions for a tool (Copilot session-state dirs, Claude project transcripts, or gitagent branches). |
| `export` | Read a session and output the canonical session JSON (stdout or `-o`). This is the standalone Transformer. |
| `import` | Read from `--from`, convert to canonical, and write into `--to`. Prints how to resume. |

**Resume mechanics per target:**
- **gitagent** — distills the session into `memory/MEMORY.md` (what the gitagent CLI reloads and recalls) and writes the raw chat-history as an archive. Resume: `cd <agent> && gitagent`.
- **claude** — writes an Anthropic-format transcript into `~/.claude/projects/<cwd>/<uuid>.jsonl`. Resume via the shipped passthrough: `opengap run -a claude --resume <uuid> --workspace <cwd>`. _(Best-effort — Claude Code's transcript format is internal; verify resume.)_
- **copilot** — experimental: writes `events.jsonl` but not Copilot's SQLite index, so the Copilot CLI may not auto-list/resume it. Interop/archival only for now.

```bash
# List Copilot sessions, then bring one into a gitagent agent and continue it
opengap session list --from copilot
opengap session import --from copilot --session <id> --to gitagent --agent ./my-agent
cd ./my-agent && gitagent # recalls the imported session from MEMORY.md

# Move a Copilot session into Claude Code and resume it
opengap session import --from copilot --session <id> --to claude --agent ~/code/my-app
opengap run -a claude --resume <uuid> --workspace ~/code/my-app -p "continue"

# Just inspect the canonical form
opengap session export --from claude --session <uuid> -o session.json
```

---

### lyzr

Manage Lyzr Studio agents — create, update, inspect, and run.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@open-gitagent/opengap",
"version": "0.5.0",
"version": "0.6.0",
"description": "OpenGAP — the Git Agent Protocol: a framework-agnostic, git-native standard for defining AI agents",
"type": "module",
"bin": {
Expand Down
98 changes: 98 additions & 0 deletions spec/schemas/session.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://gitagent.dev/schemas/session.schema.json",
"title": "gitagent Canonical Session",
"description": "Tool-neutral representation of a conversation session. Read from / written to any tool's native session format (Copilot, Claude Code, gitagent, ...).",
"type": "object",
"required": ["schema_version", "source", "items"],
"properties": {
"schema_version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$",
"description": "Canonical session schema version"
},
"source": {
"type": "string",
"description": "Tool the session was read from (e.g. copilot, claude, gitagent)"
},
"session_id": {
"type": "string",
"description": "Source session identifier"
},
"created_at": {
"type": "string",
"description": "ISO-8601 timestamp of session creation, if known"
},
"cwd": {
"type": "string",
"description": "Working directory the session ran in, if known"
},
"summary": {
"type": "string",
"description": "Short human-readable summary of the session, if known"
},
"items": {
"type": "array",
"description": "Ordered conversation items",
"items": {
"oneOf": [
{
"type": "object",
"required": ["type", "role", "text"],
"properties": {
"type": { "const": "message" },
"role": { "type": "string", "enum": ["user", "assistant", "system"] },
"text": { "type": "string" }
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type", "name"],
"properties": {
"type": { "const": "tool_call" },
"id": { "type": "string" },
"name": { "type": "string" },
"args": { "type": "object" }
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type", "content"],
"properties": {
"type": { "const": "tool_result" },
"id": { "type": "string" },
"content": { "type": "string" },
"is_error": { "type": "boolean" }
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type", "text"],
"properties": {
"type": { "const": "reasoning" },
"text": { "type": "string" }
},
"additionalProperties": false
}
]
}
},
"memory": {
"type": "array",
"description": "Distilled cross-session memory facts, if any",
"items": {
"type": "object",
"required": ["text"],
"properties": {
"kind": { "type": "string" },
"text": { "type": "string" }
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
129 changes: 129 additions & 0 deletions src/commands/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Command } from 'commander';
import { resolve } from 'node:path';
import { error, info, success, heading, label, divider } from '../utils/format.js';
import { getSessionAdapter } from '../sessions/index.js';

interface ListOptions {
from: string;
dir?: string;
}
interface ExportOptions {
from: string;
session: string;
dir?: string;
output?: string;
}
interface ImportOptions {
from: string;
to: string;
session: string;
dir?: string;
agent?: string;
sessionId?: string;
}

export const sessionCommand = new Command('session').description(
'Transform and resume conversation sessions across tools (copilot, claude, gitagent)',
);

// opengap session list --from <tool>
sessionCommand
.command('list')
.description('List available sessions for a tool')
.requiredOption('--from <tool>', 'Source tool: copilot, claude, gitagent')
.option('-d, --dir <dir>', 'Agent directory (required for gitagent)')
.action((options: ListOptions) => {
try {
const adapter = getSessionAdapter(options.from);
if (!adapter.list) {
error(`Tool "${options.from}" does not support listing`);
process.exit(1);
}
const entries = adapter.list({ dir: options.dir });
heading(`Sessions (${options.from})`);
if (entries.length === 0) {
info('No sessions found.');
return;
}
for (const e of entries) {
label(e.id, [e.updated_at, e.summary, e.cwd].filter(Boolean).join(' · ') || '');
}
} catch (e) {
error((e as Error).message);
process.exit(1);
}
});

// opengap session export --from <tool> --session <id> [-o file]
sessionCommand
.command('export')
.description('Read a session and output the canonical session format (JSON)')
.requiredOption('--from <tool>', 'Source tool: copilot, claude, gitagent')
.requiredOption('--session <id>', 'Session id (Copilot/Claude uuid, or gitagent branch)')
.option('-d, --dir <dir>', 'Agent directory (required for gitagent)')
.option('-o, --output <output>', 'Write canonical JSON to a file instead of stdout')
.action(async (options: ExportOptions) => {
try {
const adapter = getSessionAdapter(options.from);
if (!adapter.read) {
error(`Tool "${options.from}" does not support reading`);
process.exit(1);
}
const session = adapter.read({ sessionId: options.session, dir: options.dir });
const json = JSON.stringify(session, null, 2);
if (options.output) {
const { writeFileSync } = await import('node:fs');
writeFileSync(resolve(options.output), json, 'utf-8');
success(`Exported canonical session → ${options.output}`);
} else {
console.log(json);
}
} catch (e) {
error((e as Error).message);
process.exit(1);
}
});

// opengap session import --from <tool> --session <id> --to <tool> [--agent <dir>]
sessionCommand
.command('import')
.description('Convert a session from one tool and write it into another (resume there)')
.requiredOption('--from <tool>', 'Source tool: copilot, claude, gitagent')
.requiredOption('--session <id>', 'Source session id')
.requiredOption('--to <tool>', 'Target tool: gitagent, claude, copilot')
.option('-d, --dir <dir>', 'Source agent directory (required if source is gitagent)')
.option('--agent <dir>', 'Target agent/working directory')
.option('--session-id <id>', 'Target session id / branch to write under')
.action((options: ImportOptions) => {
try {
const src = getSessionAdapter(options.from);
const dst = getSessionAdapter(options.to);
if (!src.read) {
error(`Source tool "${options.from}" does not support reading`);
process.exit(1);
}
if (!dst.write) {
error(`Target tool "${options.to}" does not support writing yet`);
process.exit(1);
}

heading(`Transforming session: ${options.from} → ${options.to}`);
const session = src.read({ sessionId: options.session, dir: options.dir });
const counts = session.items.reduce<Record<string, number>>((acc, i) => {
acc[i.type] = (acc[i.type] ?? 0) + 1;
return acc;
}, {});
info(
`Read ${session.items.length} items (${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ')})`,
);

const result = dst.write(session, { dir: options.agent, sessionId: options.sessionId });
for (const p of result.paths) success(`Wrote ${p}`);
divider();
info('Resume with:');
info(` ${result.resumeHint}`);
} catch (e) {
error((e as Error).message);
process.exit(1);
}
});
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import { skillsCommand } from './commands/skills.js';
import { runCommand } from './commands/run.js';
import { lyzrCommand } from './commands/lyzr.js';
import { registryCommand } from './commands/registry.js';
import { sessionCommand } from './commands/session.js';

const program = new Command();

program
.name('opengap')
.description('OpenGAP — the Git Agent Protocol: a framework-agnostic, git-native standard for defining AI agents')
.version('0.5.0');
.version('0.6.0');

program.addCommand(initCommand);
program.addCommand(validateCommand);
Expand All @@ -31,5 +32,6 @@ program.addCommand(skillsCommand);
program.addCommand(runCommand);
program.addCommand(lyzrCommand);
program.addCommand(registryCommand);
program.addCommand(sessionCommand);

program.parse();
Loading