import { PaperAPI } from './paper-api.js';
import { PaperWebSocketClient } from './paper-client.js';
import { PaperScreenshot, ScreenshotOptions, ScreenshotResult } from './screenshot.js';
import type { PaperNode } from './types.js';
export class PaperTools {
private api: PaperAPI;
private wsClient: PaperWebSocketClient | null = null;
private screenshot: PaperScreenshot;
private cookies: string;
constructor(cookies: string, debug = false) {
this.cookies = cookies;
this.api = new PaperAPI(cookies);
this.screenshot = new PaperScreenshot(cookies, debug);
}
setWebSocketClient(client: PaperWebSocketClient) {
this.wsClient = client;
}
async listUserDocuments() {
try {
const userInfo = await this.api.getUserInfo();
return {
documents: userInfo.user.recentFiles.map((id) => ({
id,
name: `Document ${id}`, // Paper API doesn't return names in /auth/me
})),
user: {
id: userInfo.user.id,
email: userInfo.user.email,
firstName: userInfo.user.firstName,
},
};
} catch (error) {
throw new Error(`Failed to list user documents: ${error instanceof Error ? error.message : String(error)}`);
}
}
async listNodes() {
console.error('[PaperTools] listNodes() called');
if (!this.wsClient || !this.wsClient.isConnected()) {
console.error('[PaperTools] ❌ WebSocket not connected. State:', this.wsClient?.getState());
throw new Error('WebSocket is not connected. Please connect to a document first.');
}
console.error('[PaperTools] ✅ WebSocket is connected');
// Wait a bit for document state to load if it's empty
let state = this.wsClient.getDocumentState();
let nodeCount = state ? state.nodes.size : 0;
if (!state || nodeCount === 0) {
console.error('[PaperTools] Document state empty, waiting for initial load...');
// Wait up to 2 seconds for document state to arrive
for (let i = 0; i < 20; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
state = this.wsClient.getDocumentState();
nodeCount = state ? state.nodes.size : 0;
if (nodeCount > 0) {
console.error(`[PaperTools] Document state loaded: ${nodeCount} nodes`);
break;
}
}
}
console.error(`[PaperTools] Document state: ${state ? `Found ${nodeCount} nodes` : 'null'}`);
if (state && nodeCount > 0) {
// Log first few node IDs as sample
const sampleIds = Array.from(state.nodes.keys()).slice(0, 5);
console.error(`[PaperTools] Sample node IDs: ${sampleIds.join(', ')}`);
}
if (!state || nodeCount === 0) {
// Try to request nodes if state not available
console.error('[PaperTools] No document state after wait, attempting to request nodes...');
try {
const response = await this.wsClient.sendRequest({ type: 'get_nodes' });
console.error('[PaperTools] Request response:', JSON.stringify(response, null, 2));
if (response.nodes) {
return { nodes: Array.from(Object.values(response.nodes)) };
}
} catch (error) {
console.error('[PaperTools] Request failed:', error);
// Fall through to return empty if request fails
}
return { nodes: [] };
}
const result = {
nodes: Array.from(state.nodes.values()),
count: state.nodes.size,
};
console.error('[PaperTools] Returning nodes:', result.count);
return result;
}
async getNode(nodeId: string) {
console.error(`[PaperTools] getNode() called with nodeId: ${nodeId}`);
if (!this.wsClient || !this.wsClient.isConnected()) {
const state = this.wsClient?.getState();
console.error('[PaperTools] ❌ WebSocket not connected. State:', state);
throw new Error(`WebSocket is not connected. Current state: ${state}. Please connect to a document first.`);
}
console.error('[PaperTools] ✅ WebSocket is connected');
const state = this.wsClient.getDocumentState();
const nodeCount = state ? state.nodes.size : 0;
console.error('[PaperTools] Document state:', state ? `Found ${nodeCount} nodes` : 'null');
if (state && state.nodes.has(nodeId)) {
const node = state.nodes.get(nodeId);
console.error('[PaperTools] ✅ Found node in local state:', JSON.stringify(node, null, 2));
return { node };
}
// Build diagnostic info
const diagnosticInfo = {
nodeId,
wsConnected: this.wsClient.isConnected(),
wsState: this.wsClient.getState(),
documentStateExists: !!state,
nodeCount: nodeCount,
availableNodeIds: state ? Array.from(state.nodes.keys()).slice(0, 10) : [],
};
console.error('[PaperTools] Node not in local state, attempting to fetch from WebSocket...');
console.error('[PaperTools] Diagnostic info:', JSON.stringify(diagnosticInfo, null, 2));
// Try to fetch node directly
try {
const response = await this.wsClient.sendRequest({
type: 'get_node',
nodeId,
});
console.error('[PaperTools] WebSocket response:', JSON.stringify(response, null, 2));
return { node: response.node || response };
} catch (error) {
console.error('[PaperTools] ❌ Failed to fetch node:', error);
const errorMsg = `Node not found: ${nodeId}. Diagnostic info: ${JSON.stringify(diagnosticInfo)}`;
throw new Error(errorMsg);
}
}
async listPages() {
if (!this.wsClient || !this.wsClient.isConnected()) {
throw new Error('WebSocket is not connected. Please connect to a document first.');
}
// Wait for pages to be available
let attempts = 0;
while (this.wsClient.getPages().length === 0 && attempts < 20) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
const pages = this.wsClient.getPages();
return {
pages: pages.map((page, index) => ({
pageIndex: index + 1,
id: page.id,
label: page.label,
})),
count: pages.length,
};
}
async createNode(nodeData: Partial<PaperNode>, pageIndex: number = 1) {
if (!this.wsClient || !this.wsClient.isConnected()) {
throw new Error('WebSocket is not connected. Please connect to a document first.');
}
// Wait for pages to be available
let attempts = 0;
while (this.wsClient.getPages().length === 0 && attempts < 20) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
const pages = this.wsClient.getPages();
if (pages.length === 0) {
throw new Error('Document not fully loaded. No pages found.');
}
if (pageIndex < 1 || pageIndex > pages.length) {
throw new Error(`Invalid page index ${pageIndex}. Available pages: 1-${pages.length}`);
}
try {
// Use the new createNode method that uses $t: 6 protocol
const nodeId = this.wsClient.createNode(nodeData, pageIndex);
const state = this.wsClient.getDocumentState();
const node = state?.nodes.get(nodeId);
const pageName = pages[pageIndex - 1]?.label || `Page ${pageIndex}`;
console.error(`[PaperTools] ✅ Created node: ${nodeId} on ${pageName}`);
return { node: node || { id: nodeId, ...nodeData }, nodeId, page: pageName };
} catch (error) {
throw new Error(`Failed to create node: ${error instanceof Error ? error.message : String(error)}`);
}
}
async updateNode(nodeId: string, updates: Partial<PaperNode>) {
if (!this.wsClient || !this.wsClient.isConnected()) {
throw new Error('WebSocket is not connected. Please connect to a document first.');
}
try {
// Update each property using the $t: 6 protocol
for (const [key, value] of Object.entries(updates)) {
if (key !== 'id') {
this.wsClient.updateNodeProperty(nodeId, key, value);
}
}
// Get updated node from local state
const state = this.wsClient.getDocumentState();
const node = state?.nodes.get(nodeId);
console.error(`[PaperTools] ✅ Updated node: ${nodeId}`);
return { node: node || { id: nodeId, ...updates } };
} catch (error) {
throw new Error(`Failed to update node: ${error instanceof Error ? error.message : String(error)}`);
}
}
async deleteNode(nodeId: string) {
if (!this.wsClient || !this.wsClient.isConnected()) {
throw new Error('WebSocket is not connected. Please connect to a document first.');
}
try {
// Use the new deleteNodeById method that uses $t: 6 protocol
this.wsClient.deleteNodeById(nodeId);
console.error(`[PaperTools] ✅ Deleted node: ${nodeId}`);
return { success: true, nodeId };
} catch (error) {
throw new Error(`Failed to delete node: ${error instanceof Error ? error.message : String(error)}`);
}
}
async takeScreenshot(options: ScreenshotOptions = {}): Promise<ScreenshotResult> {
if (!this.wsClient) {
throw new Error('WebSocket client not set. Please connect to a document first.');
}
const documentId = this.wsClient.getDocumentId();
console.error(`[PaperTools] 📸 Taking screenshot of document ${documentId}...`);
try {
// Close any existing browser to force a fresh instance
await this.screenshot.close();
const result = await this.screenshot.takeScreenshot(documentId, options);
if (result.success) {
console.error(`[PaperTools] ✅ Screenshot saved to: ${result.filePath}`);
} else {
console.error(`[PaperTools] ❌ Screenshot failed: ${result.error}`);
}
return result;
} catch (error) {
throw new Error(`Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`);
}
}
async closeScreenshot(): Promise<void> {
await this.screenshot.close();
}
}