import type {
LogseqClientConfig,
Page,
Block,
SearchResult,
QueryResult,
ApiResponse,
} from "./types.js";
import { LogseqApiError, LogseqConnectionError } from "./errors.js";
/**
* Low-level HTTP client for Logseq's local API server.
*
* This client provides direct access to Logseq's HTTP API methods.
* For most use cases, prefer {@link LogseqOperations} which provides
* a higher-level interface with better error handling.
*
* @remarks
* Requires Logseq HTTP API server to be enabled:
* Settings → Features → HTTP APIs server
*
* @example
* ```typescript
* const client = new LogseqClient({
* baseUrl: "http://localhost:12315",
* token: "your-api-token"
* });
*
* const result = await client.getPage("My Page");
* if (result.success) {
* console.log(result.data);
* }
* ```
*/
export class LogseqClient {
private baseUrl: string;
private token?: string;
/**
* Creates a new LogseqClient instance.
*
* @param config - Configuration options
* @param config.baseUrl - API base URL (default: "http://localhost:12315")
* @param config.token - Optional authorization token for authenticated requests
*/
constructor(config: LogseqClientConfig = {}) {
this.baseUrl = config.baseUrl ?? "http://localhost:12315";
if (config.token !== undefined) {
this.token = config.token;
}
}
/**
* Make an API call to Logseq
* @param method - The Logseq API method to call (e.g., "logseq.Editor.getPage")
* @param args - Arguments to pass to the method
*/
private async call<T>(method: string, args: unknown[] = []): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}/api`;
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (this.token) {
headers["Authorization"] = `Bearer ${this.token}`;
}
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({ method, args }),
});
if (!response.ok) {
const apiError = LogseqApiError.fromHttpError(method, response.status, response.statusText);
return {
success: false,
error: apiError.message,
errorDetails: {
type: "api",
method,
statusCode: response.status,
statusText: response.statusText,
},
};
}
const data = (await response.json()) as T;
return { success: true, data };
} catch (error) {
const isNetworkError = error instanceof TypeError && error.message.includes("fetch");
const connectionError = isNetworkError
? LogseqConnectionError.fromNetworkError(url, error as Error)
: null;
return {
success: false,
error: connectionError?.message ?? (error instanceof Error ? error.message : String(error)),
errorDetails: {
type: isNetworkError ? "connection" : "unknown",
method,
url,
cause: error instanceof Error ? error.message : String(error),
},
};
}
}
/**
* Retrieves a page by its name.
*
* @param pageName - The name of the page to retrieve
* @returns The page data, or null if not found
*
* @example
* ```typescript
* const result = await client.getPage("My Notes");
* if (result.success && result.data) {
* console.log(`Page UUID: ${result.data.uuid}`);
* }
* ```
*/
async getPage(pageName: string): Promise<ApiResponse<Page | null>> {
return this.call<Page | null>("logseq.Editor.getPage", [pageName]);
}
/**
* Retrieves all pages in the current graph.
*
* @returns Array of all pages
*
* @remarks
* This can be slow on large graphs. Consider using {@link search} for
* finding specific pages.
*/
async getAllPages(): Promise<ApiResponse<Page[]>> {
return this.call<Page[]>("logseq.Editor.getAllPages", []);
}
/**
* Retrieves the block tree for a page.
*
* @param pageName - The name of the page
* @returns Array of top-level blocks with nested children
*
* @remarks
* Blocks are returned as a tree structure. Each block may have a
* `children` array containing nested blocks.
*/
async getPageBlocksTree(pageName: string): Promise<ApiResponse<Block[]>> {
return this.call<Block[]>("logseq.Editor.getPageBlocksTree", [pageName]);
}
/**
* Retrieves a block by its UUID.
*
* @param uuid - The UUID of the block
* @returns The block data, or null if not found
*/
async getBlock(uuid: string): Promise<ApiResponse<Block | null>> {
return this.call<Block | null>("logseq.Editor.getBlock", [uuid]);
}
/**
* Creates a new page.
*
* @param pageName - The name for the new page
* @param properties - Optional page properties (key-value pairs)
* @param options - Creation options
* @param options.redirect - Whether to navigate to the new page in Logseq UI
* @param options.createFirstBlock - Whether to create an initial empty block
* @param options.journal - Whether this is a journal page
* @returns The created page, or null if creation failed
*/
async createPage(
pageName: string,
properties?: Record<string, unknown>,
options?: { redirect?: boolean; createFirstBlock?: boolean; journal?: boolean }
): Promise<ApiResponse<Page | null>> {
return this.call<Page | null>("logseq.Editor.createPage", [
pageName,
properties ?? {},
options ?? {},
]);
}
/**
* Deletes a page by name.
*
* @param pageName - The name of the page to delete
*
* @remarks
* This permanently deletes the page and all its blocks. Use with caution.
*/
async deletePage(pageName: string): Promise<ApiResponse<void>> {
return this.call<void>("logseq.Editor.deletePage", [pageName]);
}
/**
* Inserts a new block relative to an existing block.
*
* @param srcBlockUuid - UUID of the reference block
* @param content - Content for the new block (supports Markdown)
* @param options - Insertion options
* @param options.before - Insert before the reference block (default: after)
* @param options.sibling - Insert as sibling (default: as child)
* @param options.properties - Block properties
* @returns The created block
*/
async insertBlock(
srcBlockUuid: string,
content: string,
options?: { before?: boolean; sibling?: boolean; properties?: Record<string, unknown> }
): Promise<ApiResponse<Block | null>> {
return this.call<Block | null>("logseq.Editor.insertBlock", [
srcBlockUuid,
content,
options ?? {},
]);
}
/**
* Appends a block to the end of a page.
*
* @param pageName - The name of the page
* @param content - Content for the new block (supports Markdown)
* @param options - Block options
* @param options.properties - Block properties
* @returns The created block
*
* @remarks
* The block is added as the last top-level block on the page.
*/
async appendBlockToPage(
pageName: string,
content: string,
options?: { properties?: Record<string, unknown> }
): Promise<ApiResponse<Block | null>> {
// Get the page first to find where to append
const pageResult = await this.getPage(pageName);
if (!pageResult.success || !pageResult.data) {
return { success: false, error: `Page not found: ${pageName}` };
}
return this.call<Block | null>("logseq.Editor.appendBlockInPage", [
pageName,
content,
options ?? {},
]);
}
/**
* Updates an existing block's content.
*
* @param uuid - UUID of the block to update
* @param content - New content for the block
* @param options - Update options
* @param options.properties - Properties to set/update
*/
async updateBlock(
uuid: string,
content: string,
options?: { properties?: Record<string, unknown> }
): Promise<ApiResponse<void>> {
return this.call<void>("logseq.Editor.updateBlock", [uuid, content, options ?? {}]);
}
/**
* Deletes a block by UUID.
*
* @param uuid - UUID of the block to delete
*
* @remarks
* This also deletes all child blocks. Use with caution.
*/
async deleteBlock(uuid: string): Promise<ApiResponse<void>> {
return this.call<void>("logseq.Editor.removeBlock", [uuid]);
}
/**
* Searches for pages and blocks matching a query.
*
* @param query - Search query string
* @returns Array of search results with page names and matching blocks
*
* @example
* ```typescript
* const result = await client.search("meeting notes");
* if (result.success) {
* result.data?.forEach(r => console.log(r.page));
* }
* ```
*/
async search(query: string): Promise<ApiResponse<SearchResult[]>> {
return this.call<SearchResult[]>("logseq.App.search", [query]);
}
/**
* Executes a Datalog query against the Logseq database.
*
* @param queryString - Datalog query string
* @returns Query results as nested arrays
*
* @example
* ```typescript
* // Find all TODO blocks
* const result = await client.query(`
* [:find (pull ?b [*])
* :where [?b :block/marker "TODO"]]
* `);
* ```
*
* @see https://docs.logseq.com/#/page/advanced%20queries for query syntax
*/
async query(queryString: string): Promise<ApiResponse<QueryResult>> {
// Use datascriptQuery instead of q - the q method returns null via HTTP API
return this.call<QueryResult>("logseq.DB.datascriptQuery", [queryString]);
}
/**
* Gets information about the currently open graph.
*
* @returns Graph name and file path, or null if no graph is open
*/
async getCurrentGraph(): Promise<ApiResponse<{ name: string; path: string } | null>> {
return this.call<{ name: string; path: string } | null>("logseq.App.getCurrentGraph", []);
}
/**
* Displays a message in the Logseq UI.
*
* @param message - Message text to display
* @param status - Message type: "success", "warning", or "error"
*
* @remarks
* Messages appear as toast notifications in Logseq.
*/
async showMsg(
message: string,
status?: "success" | "warning" | "error"
): Promise<ApiResponse<void>> {
return this.call<void>("logseq.UI.showMsg", [message, status]);
}
}