Phase 2 of v2.0. BooCoder is now a functional write-capable chatbot.
Write-path guard: resolveWritePath() uses resolve() (no realpath — files may
not exist for creates) + prefix-check + secret-file deny list (.env, *.pem,
id_rsa*, etc.). 23 unit tests cover traversal attacks.
Pending-changes service: queueEdit/Create/Delete → applyOne/All →
rejectOne/All → rewindOne. Edit diffs stored as JSON {old, new}. All writes
queue before touching disk; apply re-validates the path guard.
5 write tools: edit_file, create_file, delete_file, apply_pending, rewind.
Registered alongside 25 read-only tools from BooChat (30 total, alpha-sorted).
Write tools use a module-level inference context for sql+sessionId injection.
Inference loop via workspace dependency: apps/coder imports
createInferenceRunner, createBroker, ALL_TOOLS from @boocode/server (dist/).
apps/server gains declaration: true + exports map with typed subpath entries.
No code duplication — one inference engine shared by both apps.
API routes: POST /api/sessions/:id/messages (user msg → inference), POST stop,
GET/POST pending-changes CRUD (5 endpoints), WebSocket session streaming.
Dockerfile updated to build apps/server first (coder depends on its .d.ts).
Health endpoint reports tool count: {"ok":true,"db":true,"tools":30}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
import { z } from 'zod';
|
|
import type { ToolDef, ToolContext } from './types.js';
|
|
import { rewindOne } from '../pending_changes.js';
|
|
|
|
const RewindInput = z.object({
|
|
change_id: z.string().uuid().optional(),
|
|
all: z.boolean().optional(),
|
|
});
|
|
type RewindInputT = z.infer<typeof RewindInput>;
|
|
|
|
export const rewindTool: ToolDef<RewindInputT> = {
|
|
name: 'rewind',
|
|
description:
|
|
'Revert applied changes. Provide change_id to revert a specific change, ' +
|
|
'or set all=true to revert all applied changes for the session (in reverse order).',
|
|
inputSchema: RewindInput,
|
|
jsonSchema: {
|
|
type: 'function',
|
|
function: {
|
|
name: 'rewind',
|
|
description:
|
|
'Revert applied changes. Provide change_id to revert a specific change, ' +
|
|
'or set all=true to revert all applied changes for the session (in reverse order).',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
change_id: { type: 'string', format: 'uuid', description: 'ID of a specific change to revert' },
|
|
all: { type: 'boolean', description: 'If true, revert all applied changes for this session' },
|
|
},
|
|
required: [],
|
|
},
|
|
},
|
|
},
|
|
async execute(input: RewindInputT, projectRoot: string, context: ToolContext): Promise<unknown> {
|
|
if (input.change_id) {
|
|
const result = await rewindOne(context.sql, input.change_id, projectRoot);
|
|
return {
|
|
results: [result],
|
|
message: result.success
|
|
? `Reverted change ${input.change_id} (${result.operation} on ${result.file_path}).`
|
|
: `Failed to revert: ${result.error}`,
|
|
};
|
|
}
|
|
|
|
if (input.all) {
|
|
// Rewind all applied changes for this session in reverse order
|
|
const applied = await context.sql<{ id: string }[]>`
|
|
SELECT id FROM pending_changes
|
|
WHERE session_id = ${context.sessionId} AND status = 'applied'
|
|
ORDER BY created_at DESC
|
|
`;
|
|
const results = [];
|
|
for (const row of applied) {
|
|
results.push(await rewindOne(context.sql, row.id, projectRoot));
|
|
}
|
|
const succeeded = results.filter((r) => r.success).length;
|
|
return {
|
|
total: results.length,
|
|
succeeded,
|
|
failed: results.length - succeeded,
|
|
results,
|
|
message:
|
|
results.length === 0
|
|
? 'No applied changes to revert.'
|
|
: `Reverted ${succeeded}/${results.length} changes.`,
|
|
};
|
|
}
|
|
|
|
return { error: 'Provide either change_id or all=true.' };
|
|
},
|
|
};
|