Skip to content
Merged
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
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.

65 changes: 61 additions & 4 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ interface StartOptions {
debug?: boolean;
trust?: boolean;
version?: string;
/** Start a new Franklin session seeded from another agent's saved context. */
from?: string;
/** Optional external agent session id/path for --from. If omitted, show a picker. */
fromSessionId?: string;
/** Resume: explicit session ID, or true for "most recent in cwd", or 'picker' to prompt */
resume?: string | boolean | 'picker';
/** Continue: resume most recent session matching the current working directory */
Expand Down Expand Up @@ -100,7 +104,47 @@ export async function startCommand(options: StartOptions) {
model = 'blockrun/auto';
}

const workDir = process.cwd();
let workDir = process.cwd();

let importedKickoffPrompt: string | undefined;
if (options.from) {
const { importExternalSessionAsFranklin, parseExternalAgentSource } = await import('../session/from-import.js');
const source = parseExternalAgentSource(options.from);
if (!source) {
console.error(chalk.red(`Unknown --from source: ${options.from}`));
console.error(chalk.dim('Supported sources: claude, codex'));
process.exitCode = 1;
return;
}

try {
const imported = await importExternalSessionAsFranklin(source, options.fromSessionId, { model, workDir });
if (imported.imported.cwd) {
try {
process.chdir(imported.imported.cwd);
workDir = process.cwd();
} catch {
// Keep the caller's cwd if the source session directory no longer exists.
}
}
options.resume = imported.sessionId;
options.continue = false;
importedKickoffPrompt = [
`Continue from the imported ${source} handoff context.`,
'Briefly explain what you understand the previous session was working on, what state it appears to be in, and the most likely next step.',
'Do not claim you resumed or modified the source agent session. This is a new Franklin session with imported context awareness.',
'If the next action is clear, offer to proceed; if it is not clear, ask one concise question.',
].join('\n');
console.log(chalk.green(` Imported ${source} context into Franklin session ${imported.sessionId.slice(0, 24)}…`));
console.log(chalk.dim(` Source session: ${imported.imported.id}`));
if (imported.imported.cwd) console.log(chalk.dim(` Dir: ${workDir}`));
console.log('');
} catch (err) {
console.error(chalk.red((err as Error).message));
process.exitCode = 1;
return;
}
}

// --prompt batch mode: skip all interactive startup UI/side effects so
// stdout stays clean for scripts and one-shot callers. Keep the capability surface to the
Expand Down Expand Up @@ -348,9 +392,9 @@ export async function startCommand(options: StartOptions) {
if (process.stdin.isTTY) {
await runWithInkUI(agentConfig, model, workDir, version, walletInfo, (cb) => {
onBalanceFetched = cb;
}, fetchBalance);
}, fetchBalance, importedKickoffPrompt);
} else {
await runWithBasicUI(agentConfig, model, workDir);
await runWithBasicUI(agentConfig, model, workDir, importedKickoffPrompt);
}
}

Expand Down Expand Up @@ -391,6 +435,7 @@ async function runWithInkUI(
walletInfo?: { address: string; balance: string; chain: string },
onBalanceReady?: (cb: (bal: string) => void) => void,
fetchBalance?: () => Promise<string>,
initialInput?: string,
) {
const startSnapshot = snapshotStats();
const ui = launchInkUI({
Expand Down Expand Up @@ -430,10 +475,15 @@ async function runWithInkUI(
}

let sessionHistory: Dialogue[] | undefined;
let deliveredInitialInput = false;
try {
sessionHistory = await interactiveSession(
agentConfig,
async () => {
if (initialInput && !deliveredInitialInput) {
deliveredInitialInput = true;
return initialInput;
}
const input = await ui.waitForInput();
if (input === null) return null;
if (input === '') return '';
Expand Down Expand Up @@ -499,18 +549,25 @@ async function runWithInkUI(
async function runWithBasicUI(
agentConfig: AgentConfig,
model: string,
workDir: string
workDir: string,
initialInput?: string,
) {
const { TerminalUI } = await import('../ui/terminal.js');
const ui = new TerminalUI();
ui.printWelcome(model, workDir);
const startSnapshot = snapshotStats();

let lastTerminalPrompt = '';
let deliveredInitialInput = false;
try {
await interactiveSession(
agentConfig,
async () => {
if (initialInput && !deliveredInitialInput) {
deliveredInitialInput = true;
lastTerminalPrompt = initialInput;
return initialInput;
}
while (true) {
const input = await ui.promptUser();
if (input === null) return null;
Expand Down
13 changes: 11 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,20 @@ program

program
.command('start')
.argument('[fromSessionId]', 'External agent session id/path for --from')
.description('Start the franklin agent')
.option(
'-m, --model <model>',
'Model to use (e.g. openai/gpt-5.5, anthropic/claude-sonnet-4.6). Default from config or claude-sonnet-4.6'
)
.option('--debug', 'Enable debug logging')
.option('--trust', 'Trust mode — skip permission prompts for all tools')
.option('--from <agent>', 'Start a new Franklin session from another agent context (claude or codex)')
.option('-r, --resume [sessionId]', 'Resume a session by ID (or show picker if omitted)')
.option('-c, --continue', 'Continue the most recent session in this directory')
.option('--max-spend <usd>', 'Hard USD cap on total session API spend — session stops when exceeded')
.option('-p, --prompt <text>', 'Run a single prompt non-interactively (for batch/scripted use)')
.action((options) => startCommand({ ...options, version }));
.action((fromSessionId: string | undefined, options) => startCommand({ ...options, fromSessionId, version }));

program
.command('resume [sessionId]')
Expand Down Expand Up @@ -292,7 +294,7 @@ const args = process.argv.slice(2);
const firstArg = args[0];
const HELP_FLAGS = new Set(['-h', '--help']);
const VERSION_FLAGS = new Set(['-V', '--version']);
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue', '-p', '--prompt', '--max-spend']);
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '--from', '-r', '--resume', '-c', '--continue', '-p', '--prompt', '--max-spend']);

function hasAnyFlag(argv: string[], flags: Set<string>): boolean {
return argv.some(arg => flags.has(arg));
Expand All @@ -314,6 +316,13 @@ function parseStartFlags(argv: string[], startIdx = 0): Record<string, unknown>
opts.prompt = argv[++i];
} else if (arg === '--max-spend' && argv[i + 1]) {
opts.maxSpend = argv[++i];
} else if (arg === '--from') {
opts.from = argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : '';
const next = argv[i + 1];
if (next && !next.startsWith('-')) {
opts.fromSessionId = next;
i++;
}
} else if (arg === '-c' || arg === '--continue') {
opts.continue = true;
} else if (arg === '-r' || arg === '--resume') {
Expand Down
Loading
Loading