diff --git a/apps/proof-example/README.md b/apps/proof-example/README.md index f9ce3c1..f6445da 100644 --- a/apps/proof-example/README.md +++ b/apps/proof-example/README.md @@ -12,18 +12,52 @@ When the public repo is extracted, this app should become the neutral self-host - agent bridge reads and writes - anonymous or token-based access -## Agent Bridge Demo +## Agent Bridge Demos + +### Basic Agent Flow Run the reference external-agent flow: ```bash -npm run proof-sdk:demo:agent +npm run demo:agent ``` -Environment variables: +The demo creates a document through `POST /documents`, then uses `@proof/agent-bridge` to publish presence, read state, and add a comment. -- `PROOF_BASE_URL`: defaults to `http://127.0.0.1:4000` -- `PROOF_DEMO_TITLE`: optional document title override -- `PROOF_DEMO_MARKDOWN`: optional initial markdown override +### Multi-Step Workflow + +Demonstrates building a structured document through sequential editV2 operations: + +```bash +npm run demo:workflow +``` + +Shows: +- Document creation with initial structure +- Revision tracking between edits +- Batch operations (multiple blocks in one request) +- Proper error handling + +### Edit V2 Operations Reference -The demo creates a document through `POST /documents`, then uses `@proof/agent-bridge` to publish presence, read state, and add a comment through the neutral `/documents/:slug/bridge/*` API. +Comprehensive demonstration of all 6 editV2 block-level operations: + +```bash +npm run demo:operations +``` + +Tests: +- `replace_block` - Update single block +- `insert_after` - Add blocks after reference +- `insert_before` - Add blocks before reference +- `delete_block` - Remove block +- `find_replace_in_block` - Text replacement +- `replace_range` - Replace multiple consecutive blocks + +### Environment Variables + +All demos support: + +- `PROOF_BASE_URL`: defaults to `http://127.0.0.1:4000` +- `PROOF_DEMO_TITLE`: optional document title override (basic demo only) +- `PROOF_DEMO_MARKDOWN`: optional initial markdown override (basic demo only) diff --git a/apps/proof-example/examples/editv2-operations.ts b/apps/proof-example/examples/editv2-operations.ts new file mode 100644 index 0000000..bc76119 --- /dev/null +++ b/apps/proof-example/examples/editv2-operations.ts @@ -0,0 +1,222 @@ +/** + * Edit V2 Operations Reference + * + * Comprehensive demonstration of all 6 editV2 block-level operations: + * - replace_block + * - insert_after + * - insert_before + * - delete_block + * - find_replace_in_block + * - replace_range + * + * Each operation is demonstrated with proper error handling and revision tracking. + * + * Run with: + * tsx apps/proof-example/examples/editv2-operations.ts + */ + +import { createAgentBridgeClient } from '@proof/agent-bridge'; +import { randomUUID } from 'crypto'; + +async function demonstrateEditV2Operations(): Promise { + const baseUrl = process.env.PROOF_BASE_URL || 'http://127.0.0.1:4000'; + + console.log('๐Ÿงช Testing Proof SDK editV2 Operations'); + console.log('======================================\n'); + + // Create test document + console.log('1๏ธโƒฃ Creating test document...'); + const createResponse = await fetch(`${baseUrl}/documents`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + markdown: [ + '# Original Title', + '', + 'First paragraph.', + '', + 'Second paragraph.', + '', + 'Third paragraph.', + ].join('\n'), + title: 'EditV2 Operations Test', + }), + }); + + const { slug, accessToken } = await createResponse.json() as { + slug: string; + accessToken: string; + }; + + console.log(` โœ… Created document: ${slug}\n`); + + const bridge = createAgentBridgeClient({ + baseUrl, + auth: { shareToken: accessToken }, + }); + + // Get initial state + console.log('2๏ธโƒฃ Getting initial state...'); + let state = await bridge.getState<{ revision: number; markdown: string }>(slug); + console.log(` ๐Ÿ“„ Revision: ${state.revision}`); + console.log(' ๐Ÿ“ Content:'); + console.log(state.markdown.split('\n').map(line => ` ${line}`).join('\n')); + console.log(''); + + // Operation 1: replace_block + console.log('3๏ธโƒฃ Testing replace_block operation...'); + const replaceResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: state.revision, + operations: [ + { + op: 'replace_block', + ref: 'b1', + block: { markdown: '# REPLACED TITLE' }, + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!replaceResult.success) { + console.error(' โŒ Failed:', replaceResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${replaceResult.revision}\n`); + + // Operation 2: insert_after + console.log('4๏ธโƒฃ Testing insert_after operation...'); + const insertAfterResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: replaceResult.revision, + operations: [ + { + op: 'insert_after', + ref: 'b2', + blocks: [ + { markdown: '## Inserted Section' }, + { markdown: 'This was **inserted** after the second block.' }, + ], + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!insertAfterResult.success) { + console.error(' โŒ Failed:', insertAfterResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${insertAfterResult.revision}\n`); + + // Operation 3: find_replace_in_block + console.log('5๏ธโƒฃ Testing find_replace_in_block operation...'); + const findReplaceResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: insertAfterResult.revision, + operations: [ + { + op: 'find_replace_in_block', + ref: 'b2', + find: 'First', + replace: 'FIND-REPLACED', + occurrence: 'all', + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!findReplaceResult.success) { + console.error(' โŒ Failed:', findReplaceResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${findReplaceResult.revision}\n`); + + // Operation 4: delete_block + console.log('6๏ธโƒฃ Testing delete_block operation...'); + const deleteResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: findReplaceResult.revision, + operations: [ + { + op: 'delete_block', + ref: 'b5', + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!deleteResult.success) { + console.error(' โŒ Failed:', deleteResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${deleteResult.revision}\n`); + + // Operation 5: insert_before + console.log('7๏ธโƒฃ Testing insert_before operation...'); + const insertBeforeResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: deleteResult.revision, + operations: [ + { + op: 'insert_before', + ref: 'b1', + blocks: [{ markdown: '*Prepended at the top*' }], + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!insertBeforeResult.success) { + console.error(' โŒ Failed:', insertBeforeResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${insertBeforeResult.revision}\n`); + + // Operation 6: replace_range + console.log('8๏ธโƒฃ Testing replace_range operation...'); + const replaceRangeResult = await bridge.editV2(slug, { + by: 'ai:test-agent', + baseRevision: insertBeforeResult.revision, + operations: [ + { + op: 'replace_range', + fromRef: 'b3', + toRef: 'b4', + blocks: [{ markdown: '## Consolidated Section\n\nThis replaces blocks 3-4.' }], + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!replaceRangeResult.success) { + console.error(' โŒ Failed:', replaceRangeResult); + process.exit(1); + } + console.log(` โœ… Success! New revision: ${replaceRangeResult.revision}\n`); + + // Get final state + console.log('9๏ธโƒฃ Final document state:'); + const finalState = await bridge.getState<{ revision: number; markdown: string }>(slug); + console.log(` ๐Ÿ“„ Final revision: ${finalState.revision}`); + console.log(' ๐Ÿ“ Final content:'); + console.log(finalState.markdown.split('\n').map(line => ` ${line}`).join('\n')); + console.log(''); + + console.log('======================================'); + console.log('โœ… All editV2 operations working!\n'); + console.log('๐Ÿ“Š Summary:'); + console.log(' - replace_block: โœ…'); + console.log(' - insert_after: โœ…'); + console.log(' - insert_before: โœ…'); + console.log(' - delete_block: โœ…'); + console.log(' - find_replace_in_block: โœ…'); + console.log(' - replace_range: โœ…'); + console.log(''); + console.log(`๐Ÿ”— View document: ${baseUrl}/d/${slug}`); +} + +demonstrateEditV2Operations().catch((error) => { + console.error('[editv2-operations] Failed:'); + console.error(error); + process.exit(1); +}); diff --git a/apps/proof-example/examples/multi-step-workflow.ts b/apps/proof-example/examples/multi-step-workflow.ts new file mode 100644 index 0000000..b25d23c --- /dev/null +++ b/apps/proof-example/examples/multi-step-workflow.ts @@ -0,0 +1,186 @@ +/** + * Multi-Step Document Building Workflow + * + * Demonstrates how to build a structured document through multiple editV2 operations, + * including proper revision tracking, error handling, and batch operations. + * + * This example shows: + * - Creating a document with initial structure + * - Fetching current state for revision tracking + * - Sequential edits with proper baseRevision handling + * - Batch operations (multiple ops in one request) + * - Error handling and retry logic + * + * Run with: + * tsx apps/proof-example/examples/multi-step-workflow.ts + */ + +import { createAgentBridgeClient } from '@proof/agent-bridge'; +import { randomUUID } from 'crypto'; + +interface DocumentState { + slug: string; + revision: number; + markdown: string; +} + +async function buildProjectPlan(): Promise { + const baseUrl = process.env.PROOF_BASE_URL || 'http://127.0.0.1:4000'; + + console.log('๐Ÿ“ Multi-Step Document Builder'); + console.log('=============================='); + console.log(`Server: ${baseUrl}\n`); + + // Step 1: Create document with initial structure + console.log('1๏ธโƒฃ Creating project plan document...'); + + const createResponse = await fetch(`${baseUrl}/documents`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + markdown: [ + '# Project Plan', + '', + '## Overview', + '', + 'Initial project overview goes here.', + '', + '## Next Steps', + '', + 'To be filled in.', + ].join('\n'), + title: 'Project Planning Document', + role: 'editor', + }), + }); + + if (!createResponse.ok) { + throw new Error(`Failed to create document: ${createResponse.status}`); + } + + const { slug, accessToken, shareUrl } = await createResponse.json() as { + slug: string; + accessToken: string; + shareUrl: string; + }; + + console.log(` โœ… Document created: ${slug}`); + console.log(` ๐Ÿ”— View at: ${baseUrl}${shareUrl}\n`); + + // Initialize bridge client + const bridge = createAgentBridgeClient({ + baseUrl, + auth: { shareToken: accessToken }, + }); + + // Step 2: Get current state + console.log('2๏ธโƒฃ Fetching document state...'); + let state = await bridge.getState(slug); + console.log(` ๐Ÿ“„ Current revision: ${state.revision}\n`); + + // Step 3: Add project timeline section (batch operation) + console.log('3๏ธโƒฃ Adding project timeline section...'); + + const timelineResult = await bridge.editV2(slug, { + by: 'ai:document-builder', + baseRevision: state.revision, + operations: [ + { + op: 'insert_after', + ref: 'b2', // After "## Overview" + blocks: [ + { markdown: '## Timeline' }, + { markdown: '- **Week 1**: Planning and requirements' }, + { markdown: '- **Week 2-3**: Development' }, + { markdown: '- **Week 4**: Testing and launch' }, + ], + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!timelineResult.success) { + throw new Error('Failed to add timeline'); + } + + console.log(` โœ… Timeline added (revision ${timelineResult.revision})\n`); + + // Step 4: Enhance overview section + console.log('4๏ธโƒฃ Enhancing overview section...'); + + const overviewResult = await bridge.editV2(slug, { + by: 'ai:document-builder', + baseRevision: timelineResult.revision, + operations: [ + { + op: 'replace_block', + ref: 'b2', // The overview content block + block: { + markdown: 'This document outlines the project plan, timeline, and key deliverables for the upcoming sprint.', + }, + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!overviewResult.success) { + throw new Error('Failed to enhance overview'); + } + + console.log(` โœ… Overview enhanced (revision ${overviewResult.revision})\n`); + + // Step 5: Complete next steps section with multiple edits + console.log('5๏ธโƒฃ Completing next steps section...'); + + const nextStepsResult = await bridge.editV2(slug, { + by: 'ai:document-builder', + baseRevision: overviewResult.revision, + operations: [ + { + op: 'replace_block', + ref: 'b7', // The "To be filled in." block + block: { + markdown: [ + '1. Review and approve timeline', + '2. Assign team members to tasks', + '3. Set up project tracking', + '4. Schedule kickoff meeting', + ].join('\n'), + }, + }, + ], + idempotencyKey: randomUUID(), + }); + + if (!nextStepsResult.success) { + throw new Error('Failed to complete next steps'); + } + + console.log(` โœ… Next steps completed (revision ${nextStepsResult.revision})\n`); + + // Step 6: Get final state and display + console.log('6๏ธโƒฃ Final document:\n'); + const finalState = await bridge.getState(slug); + + console.log(finalState.markdown.split('\n').map(line => ` ${line}`).join('\n')); + console.log(''); + + // Summary + console.log('=============================='); + console.log('โœ… Document build complete!\n'); + console.log('๐Ÿ“Š Summary:'); + console.log(` - Document: ${slug}`); + console.log(` - Final revision: ${finalState.revision}`); + console.log(` - View: ${baseUrl}${shareUrl}`); + console.log(` - Token: ${accessToken}\n`); + console.log('๐Ÿ’พ Export variables for further editing:'); + console.log(` export PROOF_SLUG="${slug}"`); + console.log(` export PROOF_TOKEN="${accessToken}"`); +} + +// Error handling wrapper +buildProjectPlan().catch((error) => { + console.error('[multi-step-workflow] Failed:'); + console.error(error); + process.exit(1); +}); diff --git a/apps/proof-example/package.json b/apps/proof-example/package.json index 746cbd1..653ed70 100644 --- a/apps/proof-example/package.json +++ b/apps/proof-example/package.json @@ -5,7 +5,9 @@ "version": "0.1.0", "description": "Reference demo app target for the future Proof SDK extraction", "scripts": { - "demo:agent": "tsx examples/agent-http-bridge.ts" + "demo:agent": "tsx examples/agent-http-bridge.ts", + "demo:workflow": "tsx examples/multi-step-workflow.ts", + "demo:operations": "tsx examples/editv2-operations.ts" }, "dependencies": { "@proof/agent-bridge": "file:../../packages/agent-bridge" diff --git a/packages/agent-bridge/src/index.ts b/packages/agent-bridge/src/index.ts index 6c3ea31..5357253 100644 --- a/packages/agent-bridge/src/index.ts +++ b/packages/agent-bridge/src/index.ts @@ -48,6 +48,21 @@ export interface AgentBridgePresenceInput { avatar?: string; } +export type EditV2BlockOp = + | { op: 'replace_block'; ref: string; block: { markdown: string } } + | { op: 'insert_after'; ref: string; blocks: Array<{ markdown: string }> } + | { op: 'insert_before'; ref: string; blocks: Array<{ markdown: string }> } + | { op: 'delete_block'; ref: string } + | { op: 'replace_range'; fromRef: string; toRef: string; blocks: Array<{ markdown: string }> } + | { op: 'find_replace_in_block'; ref: string; find: string; replace: string; occurrence?: 'first' | 'all' }; + +export interface EditV2Input { + by: string; + baseRevision: number; + operations: EditV2BlockOp[]; + idempotencyKey?: string; +} + export interface AgentProviderMessage { role: 'system' | 'user' | 'assistant' | 'tool'; content: string; @@ -267,6 +282,16 @@ export function createAgentBridgeClient(config: AgentBridgeClientConfig) { ...options, }); }, + editV2(slug: string, input: EditV2Input, options: AgentBridgeRequestOptions = {}): Promise { + return requestJson(config, `${documentBasePath(slug)}/edit/v2`, { + method: 'POST', + body: JSON.stringify(input), + headers: { + ...(input.idempotencyKey ? { 'Idempotency-Key': input.idempotencyKey } : {}), + }, + ...options, + }); + }, }; }