Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/live/manual-edit-routes.mjs
1import { validateEvent } from './event-validation.mjs';2import {3countByPage as countPendingByPage,4readBuffer as readManualEditsBuffer,5removeEntries as removeManualEditEntries,6stageEntry as stageManualEditEntry,7truncateBuffer as truncateManualEditsBuffer,8} from './manual-edits-buffer.mjs';9import {10summarizeManualApplyFailures,11summarizeManualDiagnostics,12summarizeManualLogFile,13} from './manual-apply.mjs';14import { buildManualEditEvidence } from '../live-manual-edit-evidence.mjs';15import { commitManualEdits } from '../live-commit-manual-edits.mjs';1617export function createManualEditRoutes({18getToken,19manualApply,20recordManualEditActivity,21getManualEditStatus,22chatAgentLikelyActive,23cwd = () => process.cwd(),24env = () => process.env,25} = {}) {26const projectCwd = () => typeof cwd === 'function' ? cwd() : cwd || process.cwd();27const currentEnv = () => typeof env === 'function' ? env() : env || process.env;2829return function handleManualEditRoute(req, res, url) {30const p = url.pathname;3132// Save stages entries; Apply commits the staged page batch through the33// local AI copy-edit runner.34if (p === '/manual-edit-stash' && req.method === 'POST') {35let body = '';36req.on('data', (c) => { body += c; });37req.on('end', () => {38let msg;39try { msg = JSON.parse(body); } catch {40sendJson(res, 400, { error: 'Invalid JSON' });41return;42}43if (msg.token !== getToken()) {44sendJson(res, 401, { error: 'Unauthorized' });45return;46}47const error = validateEvent({ ...msg, type: 'manual_edits' });48if (error) {49sendJson(res, 400, { error });50return;51}52try {53stageManualEditEntry(projectCwd(), {54id: msg.id,55pageUrl: msg.pageUrl,56element: msg.element,57ops: msg.ops,58});59} catch (err) {60sendJson(res, 500, { error: 'stash_write_failed', message: err.message });61return;62}63const { totalCount, perPage } = countPendingByPage(projectCwd());64const pendingCount = perPage[msg.pageUrl] || 0;65recordManualEditActivity('manual_edit_stashed', {66id: msg.id,67pageUrl: msg.pageUrl,68opCount: msg.ops.length,69pendingCount,70totalCount,71hintedFileCount: new Set((msg.ops || []).map((op) => summarizeManualLogFile(op.sourceHint?.file, projectCwd())).filter(Boolean)).size,72});73sendJson(res, 200, { ok: true, pendingCount, totalCount, perPage });74});75return true;76}7778if (p === '/manual-edit-stash' && req.method === 'GET') {79const token = url.searchParams.get('token');80if (token !== getToken()) { res.writeHead(401); res.end('Unauthorized'); return true; }81const pageUrl = url.searchParams.get('pageUrl') || '';82const { totalCount, perPage } = countPendingByPage(projectCwd());83const buffer = readManualEditsBuffer(projectCwd());84const entriesForPage = pageUrl ? buffer.entries.filter((e) => e.pageUrl === pageUrl) : buffer.entries;85sendJson(res, 200, {86count: pageUrl ? (perPage[pageUrl] || 0) : totalCount,87totalCount,88perPage,89entries: entriesForPage,90});91return true;92}9394if (p === '/manual-edit-commit' && req.method === 'POST') {95const token = url.searchParams.get('token');96if (token !== getToken()) { res.writeHead(401); res.end('Unauthorized'); return true; }97const pageUrl = url.searchParams.get('pageUrl');98const asyncMode = /^(1|true|yes)$/i.test(url.searchParams.get('async') || '');99const repairOnly = /^(1|true|yes)$/i.test(url.searchParams.get('repair') || '');100const existingTransaction = manualApply.readTransaction();101if (repairOnly && !existingTransaction) {102sendJson(res, 409, { error: 'manual_edit_repair_transaction_missing' });103return true;104}105const recoveredTransaction = repairOnly ? null : manualApply.rollbackTransaction({106pageUrl,107reason: 'manual_edit_commit_recovered_abandoned_transaction',108});109const before = getManualEditStatus();110const pendingCount = pageUrl ? (before.perPage[pageUrl] || 0) : before.totalCount;111recordManualEditActivity('manual_edit_commit_started', {112pageUrl,113repairOnly,114pendingCount,115totalCount: before.totalCount,116recoveredTransaction: recoveredTransaction ? {117id: recoveredTransaction.id,118reason: recoveredTransaction.reason,119skipped: recoveredTransaction.skipped,120rolledBackFiles: recoveredTransaction.rolledBackFiles,121rollbackFailures: summarizeManualDiagnostics(recoveredTransaction.rollbackFailures, projectCwd()),122} : null,123...summarizePendingManualEditBatch(projectCwd(), pageUrl),124});125if (asyncMode) {126sendJson(res, 202, {127status: 'started',128pendingCount,129totalCount: before.totalCount,130perPage: before.perPage,131});132}133(async () => {134let result;135let routedProvider = 'subprocess';136let transaction = null;137let commitBatch = null;138try {139if (pendingCount > 0) {140const transactionBatch = buildManualEditEvidence({ cwd: projectCwd(), pageUrl });141commitBatch = transactionBatch;142if (!repairOnly && manualApply.countOps(transactionBatch) > 0) {143transaction = manualApply.writeTransaction({144pageUrl,145batch: transactionBatch,146});147} else if (repairOnly && existingTransaction) {148transaction = existingTransaction;149}150}151const envValue = currentEnv();152const requestedMode = (envValue.IMPECCABLE_LIVE_COPY_AGENT || 'auto').trim().toLowerCase();153const useChatRoute = requestedMode === 'chat'154|| (requestedMode === 'auto' && chatAgentLikelyActive());155if (useChatRoute) {156routedProvider = 'chat';157const timeoutMs = Number(envValue.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);158result = await commitManualEdits({159cwd: projectCwd(),160pageUrl,161provider: 'chat',162env: envValue,163timeoutMs,164chatAvailable: chatAgentLikelyActive,165applyBatchToSource: (batch, context) => manualApply.pushBatchInChunksAndWait(batch, pageUrl, context),166repairOnly,167transactionId: transaction?.id || existingTransaction?.id || null,168batch: commitBatch,169});170} else {171const timeoutMs = Number(envValue.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);172const provider = ['codex', 'claude', 'mock'].includes(requestedMode) ? requestedMode : undefined;173result = await commitManualEdits({174cwd: projectCwd(),175pageUrl,176provider,177env: envValue,178timeoutMs,179chatAvailable: chatAgentLikelyActive,180repairOnly,181transactionId: transaction?.id || existingTransaction?.id || null,182batch: commitBatch,183});184}185} catch (err) {186if (transaction) {187manualApply.rollbackTransaction({188pageUrl,189reason: 'manual_edit_commit_exception',190});191}192const message = err.stderr?.toString?.() || err.message;193recordManualEditActivity('manual_edit_commit_failed', {194pageUrl,195provider: routedProvider,196error: 'manual_edit_commit_failed',197message,198transactionId: transaction?.id || null,199});200if (!asyncMode) {201sendJson(res, 500, {202error: 'manual_edit_commit_failed',203message,204});205}206return;207} finally {208if (transaction) {209const shouldKeepTransaction = result?.needsManualDecision === true;210if (!shouldKeepTransaction) manualApply.clearTransaction(transaction.id);211}212}213const { totalCount, perPage } = countPendingByPage(projectCwd());214if (result?.needsManualDecision) {215recordManualEditActivity('manual_edit_repair_needs_decision', {216pageUrl,217provider: routedProvider,218transactionId: transaction?.id || existingTransaction?.id || null,219repair: result.repair || null,220failed: summarizeManualApplyFailures(result.failed, projectCwd()),221files: Array.isArray(result.files) ? result.files.slice(0, 20).map((file) => summarizeManualLogFile(file, projectCwd())).filter(Boolean) : [],222remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,223totalCount,224});225} else {226recordManualEditActivity('manual_edit_commit_done', {227pageUrl,228provider: routedProvider,229reason: result.reason || null,230repair: result.repair || null,231appliedCount: Array.isArray(result.applied) ? result.applied.length : 0,232failedCount: Array.isArray(result.failed) ? result.failed.length : 0,233failed: summarizeManualApplyFailures(result.failed, projectCwd()),234files: Array.isArray(result.files) ? result.files.slice(0, 20).map((file) => summarizeManualLogFile(file, projectCwd())).filter(Boolean) : [],235warnings: summarizeManualDiagnostics(result.warnings, projectCwd()),236rolledBackFiles: Array.isArray(result.rolledBackFiles) ? result.rolledBackFiles.slice(0, 20).map((file) => summarizeManualLogFile(file, projectCwd())).filter(Boolean) : [],237rollbackFailures: summarizeManualDiagnostics(result.rollbackFailures, projectCwd()),238unreportedFiles: Array.isArray(result.unreportedFiles) ? result.unreportedFiles.slice(0, 20).map((file) => summarizeManualLogFile(file, projectCwd())).filter(Boolean) : undefined,239noteCount: Array.isArray(result.notes) ? result.notes.length : 0,240cleared: result.cleared || 0,241remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,242totalCount,243});244}245if (!asyncMode) {246sendJson(res, 200, { ...result, totalCount, perPage });247}248})();249return true;250}251252if (p === '/manual-edit-repair-decision' && req.method === 'POST') {253let body = '';254req.on('data', (chunk) => { body += chunk; });255req.on('end', () => {256let payload = {};257try { payload = body ? JSON.parse(body) : {}; } catch {258sendJson(res, 400, { error: 'Invalid JSON' });259return;260}261const token = payload.token || url.searchParams.get('token');262if (token !== getToken()) { res.writeHead(401); res.end('Unauthorized'); return; }263const pageUrl = payload.pageUrl || url.searchParams.get('pageUrl') || null;264const action = String(payload.action || url.searchParams.get('action') || '').trim().toLowerCase();265if (action !== 'rollback') {266sendJson(res, 400, { error: 'unsupported_manual_edit_repair_decision', action });267return;268}269const rollback = manualApply.rollbackTransaction({270pageUrl,271reason: 'manual_edit_user_requested_rollback',272});273const { totalCount, perPage } = countPendingByPage(projectCwd());274const response = {275action,276pageUrl,277rollback,278remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,279totalCount,280perPage,281};282recordManualEditActivity('manual_edit_repair_rollback_done', response);283sendJson(res, 200, response);284});285return true;286}287288if (p === '/manual-edit-discard' && req.method === 'POST') {289const token = url.searchParams.get('token');290if (token !== getToken()) { res.writeHead(401); res.end('Unauthorized'); return true; }291const pageUrl = url.searchParams.get('pageUrl');292let discarded;293let discardedEntries = [];294let canceledApplyEvents = [];295let transactionRollback = null;296try {297const buffer = readManualEditsBuffer(projectCwd());298transactionRollback = manualApply.rollbackTransaction({299pageUrl,300reason: 'manual_edit_discarded',301});302if (pageUrl) {303discardedEntries = buffer.entries.filter((entry) => entry.pageUrl === pageUrl);304discarded = removeManualEditEntries(projectCwd(), (entry) => entry.pageUrl === pageUrl);305} else {306discardedEntries = buffer.entries;307discarded = truncateManualEditsBuffer(projectCwd());308}309canceledApplyEvents = manualApply.cancelPendingEvents(pageUrl);310} catch (err) {311sendJson(res, 500, { error: 'discard_failed', message: err.message });312return true;313}314const { totalCount, perPage } = countPendingByPage(projectCwd());315recordManualEditActivity('manual_edit_discarded', {316pageUrl,317discarded,318canceledApplyIds: canceledApplyEvents.map((event) => event.id),319transactionRollback: transactionRollback ? {320id: transactionRollback.id,321rolledBackFiles: transactionRollback.rolledBackFiles?.map((file) => summarizeManualLogFile(file, projectCwd())).filter(Boolean) || [],322rollbackFailures: summarizeManualDiagnostics(transactionRollback.rollbackFailures, projectCwd()),323skipped: transactionRollback.skipped,324} : undefined,325totalCount,326});327sendJson(res, 200, { discarded, entries: discardedEntries, canceledApplyEvents, totalCount, perPage });328return true;329}330331if (p === '/manual-edit' && req.method === 'POST') {332sendJson(res, 410, { error: '/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits.' });333return true;334}335336return false;337};338}339340function sendJson(res, status, body) {341res.writeHead(status, { 'Content-Type': 'application/json' });342res.end(JSON.stringify(body));343}344345function summarizePendingManualEditBatch(cwd, pageUrl = null) {346try {347const buffer = readManualEditsBuffer(cwd);348const entries = (buffer.entries || [])349.filter((entry) => !pageUrl || entry.pageUrl === pageUrl);350return {351pendingEntryCount: entries.length,352pendingOpCount: entries.reduce((sum, entry) => sum + (entry.ops?.length || 0), 0),353};354} catch (err) {355return { pendingSummaryError: err.message || String(err) };356}357}358