diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e1744..6092e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- **Agent Reflection (`cloudcode summary`)**: A new CLI command that pipes the semantic transcript of a past session into a local AI agent (like Claude or Gemini). It automatically spins up a hidden tmux session, injects the transcript as context, and prompts the agent to summarize the architectural decisions and files changed—perfect for generating PR descriptions from your mobile pairing sessions. + ## [0.1.9] — 2026-03-27 ### Added diff --git a/README.md b/README.md index 2be3f4b..996b470 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Beyond the dashboard, you can manage your sessions directly from the terminal: - **`cloudcode status`**: List all active sessions, their agents, and uptime. - **`cloudcode attach `**: Directly attach to a session's native `tmux` environment. - **`cloudcode logs `**: Stream clean semantic transcripts (Markdown) of a session. Use `-f` to follow live. +- **`cloudcode summary `**: Automatically spin up a local agent to read the transcript and summarize the session for you (great for PR descriptions!). - **`cloudcode stop `**: Gracefully stop an active agent session. - **`cloudcode init`**: Verify your environment (Node, Go, tmux, git) and detect installed agents. diff --git a/backend/src/cli.ts b/backend/src/cli.ts index d8b1b5b..752de8d 100644 --- a/backend/src/cli.ts +++ b/backend/src/cli.ts @@ -9,6 +9,7 @@ import { join } from 'path'; import { tmpdir, networkInterfaces } from 'os'; import { randomBytes } from 'crypto'; import { nanoid } from 'nanoid'; +import { writeFileSync, chmodSync } from 'fs'; import { buildApp } from './index.js'; import { runMigrations } from './db/migrations.js'; import { getFirstAdminUser, createPairingToken, hashPassword, createUser } from './auth/service.js'; @@ -261,6 +262,118 @@ program } }); +program + .command('summary') + .description('Generate a summary of a session using a local AI agent') + .argument('', 'Public ID of the session to summarize') + .option('--agent ', 'Agent profile slug to use (e.g. claude-code)', 'claude-code') + .action(async (id: string, options: { agent: string }) => { + runMigrations(); + + // 1. Fetch Session + const session = db.prepare('SELECT id, title, workdir FROM sessions WHERE public_id = ?').get(id) as { id: string; title: string; workdir: string } | undefined; + if (!session) { + console.error(chalk.red(`Error: Session "${id}" not found.`)); + process.exit(1); + } + + // 2. Fetch Agent + const profile = db.prepare('SELECT * FROM agent_profiles WHERE slug = ?').get(options.agent) as any; + if (!profile) { + console.error(chalk.red(`Error: Agent profile "${options.agent}" not found.`)); + process.exit(1); + } + + console.log(chalk.blue(`\n📝 Generating summary for: ${session.title} [${id}]`)); + console.log(chalk.dim(`Using agent: ${profile.name}`)); + + // 3. Get Transcript + let transcript = ''; + try { + transcript = await readTranscript(session.id, { asMarkdown: true }); + } catch (err) { + console.error(chalk.red('Failed to read transcript.'), err); + process.exit(1); + } + + if (!transcript || transcript.trim().length === 0) { + console.log(chalk.yellow('Transcript is empty. Nothing to summarize.')); + process.exit(0); + } + + // 4. Create Temp File + const tmpPromptPath = join(tmpdir(), `cc-summary-${id}-${Date.now()}.md`); + const prompt = `Summarize the following coding session transcript. Focus on: +1. The primary goal or issue being addressed. +2. The architectural decisions made. +3. The specific files changed. +4. The final outcome or status. + +--- TRANSCRIPT --- +${transcript} +`; + + try { + writeFileSync(tmpPromptPath, prompt, 'utf8'); + chmodSync(tmpPromptPath, 0o600); // Only owner can read/write + } catch (err) { + console.error(chalk.red('Failed to write temporary prompt file.'), err); + process.exit(1); + } + + // 5. Execution Strategy + const tmuxSessionName = `cc-summary-${nanoid(6)}`; + const args = JSON.parse(profile.args_json) as string[]; + const env = JSON.parse(profile.env_json) as Record; + + try { + // Import tmux adapter dynamically or if already imported, use it + const tmux = await import('./tmux/adapter.js'); + + await tmux.createSession( + tmuxSessionName, + profile.command, + args, + session.workdir || process.cwd(), + Object.keys(env).length > 0 ? env : undefined + ); + + // Wait a moment for the agent to boot up + await new Promise(resolve => setTimeout(resolve, 1500)); + + // Instruct the agent to read the file + // Different agents have different ways to read files. + // For Claude Code and Gemini CLI, usually just asking them to read the absolute path works. + const readCommand = `Please read the file at ${tmpPromptPath} and provide the summary.`; + + await tmux.sendLiteralText(tmuxSessionName, readCommand); + await tmux.sendEnter(tmuxSessionName); + + console.log(chalk.green(`\n✅ Summary agent launched.`)); + console.log(chalk.yellow(`Attaching to session... (Type /exit, Ctrl-D, or Ctrl-b d to leave when finished)\n`)); + + // Attach to show output + const child = spawn('tmux', ['attach-session', '-t', tmuxSessionName], { + stdio: 'inherit' + }); + + child.on('exit', () => { + console.log(chalk.gray(`\nDetached from summary session.`)); + // Cleanup temp file + try { + // fs.unlinkSync(tmpPromptPath); + // Leaving it might be useful if they want to see what was sent, but better to clean up. + import('fs').then(fs => fs.unlinkSync(tmpPromptPath)).catch(() => {}); + } catch {} + process.exit(0); + }); + + } catch (err) { + console.error(chalk.red('Failed to run summary session:'), err); + process.exit(1); + } + }); + program .command('start') .description('Start the CloudCode server') diff --git a/package-lock.json b/package-lock.json index ac6ad83..8aa8f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,9 +126,9 @@ } }, "frontend/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -1878,9 +1878,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -2672,9 +2672,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastify": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.2.tgz", - "integrity": "sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==", + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz", + "integrity": "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==", "funding": [ { "type": "github", @@ -4685,9 +4685,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -5863,9 +5863,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6260,9 +6260,9 @@ } }, "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": {