@jclvsh/xmark 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @jclvsh/xmark
2
2
 
3
- MCP server and CLI for the [xmark](https://www.xmark.chat) API — chat with your X (Twitter) bookmarks from inside Claude Code, Cursor, or any MCP-compatible client.
3
+ MCP server and CLI for the [xmark](https://xmark.dev) API — chat with your X (Twitter) bookmarks from inside Claude Code, Cursor, or any MCP-compatible client.
4
4
 
5
5
  ## What is xmark?
6
6
 
7
- xmark syncs your X bookmarks, embeds them with OpenAI, and lets you query them in natural language with citations back to the original tweets. This package wraps the [xmark REST API](https://api.xmark.chat/openapi.json) as an MCP server (for AI clients) and a CLI (for terminal use).
7
+ xmark syncs your X bookmarks, embeds them with OpenAI, and lets you query them in natural language with citations back to the original tweets. This package wraps the [xmark REST API](https://api.xmark.dev/openapi.json) as an MCP server (for AI clients) and a CLI (for terminal use).
8
8
 
9
9
  ## Install
10
10
 
@@ -18,16 +18,16 @@ npx -y -p @jclvsh/xmark xmark --help
18
18
 
19
19
  ## Setup
20
20
 
21
- Set your xmark API key (get one at [/settings/api](https://www.xmark.chat/settings/api)):
21
+ Set your xmark API key (get one at [/settings/api](https://xmark.dev/settings/api)):
22
22
 
23
23
  ```bash
24
- export XMARK_API_KEY=xm_live_...
24
+ export XMARK_API_KEY=nt_live_...
25
25
  ```
26
26
 
27
- Optionally override the API host (default is `https://api.xmark.chat`):
27
+ Optionally override the API host (default is `https://api.xmark.dev`):
28
28
 
29
29
  ```bash
30
- export XMARK_API_URL=https://api.xmark.chat
30
+ export XMARK_API_URL=https://api.xmark.dev
31
31
  ```
32
32
 
33
33
  ## Tools exposed via MCP
@@ -39,7 +39,11 @@ export XMARK_API_URL=https://api.xmark.chat
39
39
  | `sync_bookmarks` | Pull the latest bookmarks from X. |
40
40
  | `list_bookmarks` | Paginated bookmark list. |
41
41
  | `get_bookmark` | Fetch a single bookmark by ID. |
42
+ | `delete_bookmark` | Remove a bookmark (local-only, or also on X with `unbookmark`). |
42
43
  | `list_conversations` | Past chat conversations. |
44
+ | `get_conversation_messages` | A conversation's message history. |
45
+ | `delete_conversation` | Permanently delete a conversation. |
46
+ | `get_usage` | Plan + quota usage (chat this month, syncs today, bookmark count). |
43
47
 
44
48
  ## CLI commands
45
49
 
@@ -48,19 +52,24 @@ xmark search "what did I save about Claude?"
48
52
  xmark chat "summarize what I saved this month about agents"
49
53
  xmark sync
50
54
  xmark list --page 1 --page-size 20
55
+ xmark get <bookmark-id>
56
+ xmark delete <bookmark-id> --unbookmark
51
57
  xmark conversations
58
+ xmark messages <conversation-id>
59
+ xmark delete-conversation <conversation-id>
60
+ xmark usage
52
61
  ```
53
62
 
54
63
  ## Pricing
55
64
 
56
- xmark is $5/month flat. The same subscription covers the web app, CLI, MCP server, and developer REST API — no separate API tier.
65
+ Free tier: 20 chat messages/month, 1 sync/day. Pro is $5/month — unlimited chat and sync. The same plan covers the web app, CLI, MCP server, and developer REST API — no separate API tier.
57
66
 
58
67
  ## Links
59
68
 
60
- - Homepage: https://www.xmark.chat
61
- - API reference: https://www.xmark.chat/docs
62
- - OpenAPI spec: https://api.xmark.chat/openapi.json
63
- - llms.txt: https://api.xmark.chat/llms.txt
69
+ - Homepage: https://xmark.dev
70
+ - API reference: https://xmark.dev/docs
71
+ - OpenAPI spec: https://api.xmark.dev/openapi.json
72
+ - llms.txt: https://api.xmark.dev/llms.txt
64
73
 
65
74
  ## License
66
75
 
package/dist/cli.js CHANGED
@@ -5,13 +5,13 @@ const program = new Command();
5
5
  program
6
6
  .name("xmark")
7
7
  .description("xmark CLI — chat with your X bookmarks from the terminal")
8
- .version("0.1.0");
8
+ .version("0.3.0");
9
9
  function client() {
10
10
  return new XmarkClient();
11
11
  }
12
12
  program
13
13
  .command("search <query>")
14
- .description("Semantic search over your bookmarks ($0.04/call for agents)")
14
+ .description("Semantic search over your bookmarks")
15
15
  .option("-l, --limit <n>", "max results", "10")
16
16
  .action(async (query, opts) => {
17
17
  const result = await client().post("/bookmarks/search", {
@@ -22,7 +22,7 @@ program
22
22
  });
23
23
  program
24
24
  .command("chat <question...>")
25
- .description("Ask a question about your bookmarks ($0.20/call for agents)")
25
+ .description("Ask a question about your bookmarks")
26
26
  .option("-c, --conversation <id>", "continue an existing conversation")
27
27
  .action(async (questionParts, opts) => {
28
28
  const question = questionParts.join(" ");
@@ -49,17 +49,53 @@ program
49
49
  .option("-p, --page <n>", "page number", "1")
50
50
  .option("-s, --page-size <n>", "items per page", "50")
51
51
  .action(async (opts) => {
52
- const result = await client().get("/bookmarks", {
52
+ const result = await client().getPage("/bookmarks", {
53
53
  page: parseInt(opts.page, 10),
54
54
  pageSize: parseInt(opts.pageSize, 10),
55
55
  });
56
56
  console.log(JSON.stringify(result, null, 2));
57
57
  });
58
+ program
59
+ .command("get <id>")
60
+ .description("Fetch a single bookmark by ID")
61
+ .action(async (id) => {
62
+ const result = await client().get(`/bookmarks/${id}`);
63
+ console.log(JSON.stringify(result, null, 2));
64
+ });
65
+ program
66
+ .command("delete <id>")
67
+ .description("Remove a bookmark (local-only unless --unbookmark)")
68
+ .option("-u, --unbookmark", "also remove the bookmark on X, not just locally")
69
+ .action(async (id, opts) => {
70
+ await client().delete(`/bookmarks/${id}${opts.unbookmark ? "?unbookmark=true" : ""}`);
71
+ console.log(opts.unbookmark ? "Removed from xmark and X." : "Removed from xmark (local only).");
72
+ });
58
73
  program
59
74
  .command("conversations")
60
75
  .description("List your past chat conversations")
61
76
  .action(async () => {
62
- const result = await client().get("/conversations");
77
+ const result = await client().getPage("/conversations");
78
+ console.log(JSON.stringify(result, null, 2));
79
+ });
80
+ program
81
+ .command("messages <conversationId>")
82
+ .description("A conversation's message history, oldest first")
83
+ .action(async (conversationId) => {
84
+ const result = await client().get(`/conversations/${conversationId}/messages`);
85
+ console.log(JSON.stringify(result, null, 2));
86
+ });
87
+ program
88
+ .command("delete-conversation <id>")
89
+ .description("Permanently delete a conversation and its messages")
90
+ .action(async (id) => {
91
+ await client().delete(`/conversations/${id}`);
92
+ console.log("Conversation deleted.");
93
+ });
94
+ program
95
+ .command("usage")
96
+ .description("Your plan + quota usage (chat this month, syncs today, bookmark count)")
97
+ .action(async () => {
98
+ const result = await client().get("/usage");
63
99
  console.log(JSON.stringify(result, null, 2));
64
100
  });
65
101
  program.parseAsync(process.argv).catch((err) => {
package/dist/client.d.ts CHANGED
@@ -1,8 +1,14 @@
1
+ import type { Paginated } from "./types.js";
1
2
  export declare class XmarkClient {
2
3
  private apiKey;
3
4
  private baseUrl;
4
5
  constructor(apiKey?: string, baseUrl?: string);
5
6
  get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
7
+ /**
8
+ * GET a paginated list endpoint, preserving the pagination envelope
9
+ * (`get`/`request` unwrap to `data`, which silently drops it).
10
+ */
11
+ getPage<T>(path: string, params?: Record<string, string | number | undefined>): Promise<Paginated<T>>;
6
12
  post<T>(path: string, body?: unknown): Promise<T>;
7
13
  delete(path: string): Promise<void>;
8
14
  private request;
package/dist/client.js CHANGED
@@ -1,12 +1,13 @@
1
1
  const REQUEST_TIMEOUT_MS = 30_000;
2
- const DEFAULT_BASE_URL = "https://api.xmark.chat";
2
+ const DEFAULT_BASE_URL = "https://api.xmark.dev";
3
+ const USER_AGENT = "xmark-mcp/0.3.0";
3
4
  export class XmarkClient {
4
5
  apiKey;
5
6
  baseUrl;
6
7
  constructor(apiKey, baseUrl) {
7
8
  const key = apiKey ?? process.env.XMARK_API_KEY;
8
9
  if (!key) {
9
- throw new Error("XMARK_API_KEY environment variable is required. Get one at https://www.xmark.chat/settings/api");
10
+ throw new Error("XMARK_API_KEY environment variable is required. Get one at https://xmark.dev/settings/api");
10
11
  }
11
12
  this.apiKey = key;
12
13
  this.baseUrl = (baseUrl ?? process.env.XMARK_API_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -21,6 +22,32 @@ export class XmarkClient {
21
22
  }
22
23
  return this.request(url.toString(), { method: "GET" });
23
24
  }
25
+ /**
26
+ * GET a paginated list endpoint, preserving the pagination envelope
27
+ * (`get`/`request` unwrap to `data`, which silently drops it).
28
+ */
29
+ async getPage(path, params) {
30
+ const url = new URL(`${this.baseUrl}${path}`);
31
+ if (params) {
32
+ for (const [k, v] of Object.entries(params)) {
33
+ if (v !== undefined && v !== "")
34
+ url.searchParams.set(k, String(v));
35
+ }
36
+ }
37
+ const response = await fetch(url.toString(), {
38
+ method: "GET",
39
+ headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT },
40
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
41
+ });
42
+ if (!response.ok) {
43
+ await this.handleError(response);
44
+ }
45
+ const json = (await response.json());
46
+ if (!json.success) {
47
+ throw new Error(`${json.error.code}: ${json.error.message}`);
48
+ }
49
+ return { data: json.data, pagination: json.pagination };
50
+ }
24
51
  async post(path, body) {
25
52
  return this.request(`${this.baseUrl}${path}`, {
26
53
  method: "POST",
@@ -44,7 +71,7 @@ export class XmarkClient {
44
71
  headers: {
45
72
  ...init.headers,
46
73
  Authorization: `Bearer ${this.apiKey}`,
47
- "User-Agent": "xmark-mcp/0.1.0",
74
+ "User-Agent": USER_AGENT,
48
75
  },
49
76
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
50
77
  });
package/dist/index.js CHANGED
@@ -4,12 +4,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { XmarkClient } from "./client.js";
5
5
  import { registerBookmarkTools } from "./tools/bookmarks.js";
6
6
  import { registerChatTools } from "./tools/chat.js";
7
+ import { registerUsageTools } from "./tools/usage.js";
7
8
  const server = new McpServer({
8
9
  name: "xmark",
9
- version: "0.1.0",
10
+ version: "0.3.0",
10
11
  });
11
12
  const client = new XmarkClient();
12
13
  registerBookmarkTools(server, client);
13
14
  registerChatTools(server, client);
15
+ registerUsageTools(server, client);
14
16
  const transport = new StdioServerTransport();
15
17
  await server.connect(transport);
@@ -4,10 +4,7 @@ export function registerBookmarkTools(server, client) {
4
4
  page: z.number().int().min(1).default(1).describe("Page number (1-indexed)"),
5
5
  pageSize: z.number().int().min(1).max(100).default(50).describe("Items per page (max 100)"),
6
6
  }, async ({ page, pageSize }) => {
7
- const result = await client.get("/bookmarks", {
8
- page,
9
- pageSize,
10
- });
7
+ const result = await client.getPage("/bookmarks", { page, pageSize });
11
8
  return {
12
9
  content: [
13
10
  {
@@ -25,7 +22,7 @@ export function registerBookmarkTools(server, client) {
25
22
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
26
23
  };
27
24
  });
28
- server.tool("search_bookmarks", "Semantic search over the user's bookmarks. Returns ranked matches with relevance scores. Charged at $0.04/call for agents (first 50/month free).", {
25
+ server.tool("search_bookmarks", "Semantic search over the user's bookmarks. Returns ranked matches with relevance scores.", {
29
26
  query: z.string().min(1).describe("Natural-language search query"),
30
27
  limit: z.number().int().min(1).max(50).default(10).describe("Max results to return"),
31
28
  }, async ({ query, limit }) => {
@@ -34,7 +31,7 @@ export function registerBookmarkTools(server, client) {
34
31
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
35
32
  };
36
33
  });
37
- server.tool("sync_bookmarks", "Trigger a fresh sync from X. Pulls latest bookmarks, embeds new ones for semantic search.", {}, async () => {
34
+ server.tool("sync_bookmarks", "Trigger a fresh sync from X. Pulls latest bookmarks, embeds new ones for semantic search. Free tier allows one sync per day.", {}, async () => {
38
35
  const result = await client.post("/bookmarks/sync");
39
36
  return {
40
37
  content: [
@@ -45,4 +42,18 @@ export function registerBookmarkTools(server, client) {
45
42
  ],
46
43
  };
47
44
  });
45
+ server.tool("delete_bookmark", "Remove a bookmark. Local-only by default — the next sync re-creates anything still bookmarked on X. Set unbookmark=true to also remove it on X (sticks across syncs; requires a connected X account).", {
46
+ id: z.string().uuid().describe("Bookmark UUID"),
47
+ unbookmark: z.boolean().default(false).describe("Also remove the bookmark on X, not just locally"),
48
+ }, async ({ id, unbookmark }) => {
49
+ await client.delete(`/bookmarks/${id}${unbookmark ? "?unbookmark=true" : ""}`);
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: unbookmark ? "Bookmark removed from xmark and X." : "Bookmark removed from xmark (local only).",
55
+ },
56
+ ],
57
+ };
58
+ });
48
59
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  export function registerChatTools(server, client) {
3
- server.tool("chat", "Ask a natural-language question about the user's X bookmarks. Returns an answer with citations back to the original tweets. Charged at $0.20/call for agents.", {
3
+ server.tool("chat", "Ask a natural-language question about the user's X bookmarks. Returns an answer with citations back to the original tweets. Free tier includes 20 chat messages per month.", {
4
4
  question: z.string().min(1).describe("The question to ask"),
5
5
  conversationId: z
6
6
  .string()
@@ -36,9 +36,25 @@ export function registerChatTools(server, client) {
36
36
  page: z.number().int().min(1).default(1),
37
37
  pageSize: z.number().int().min(1).max(100).default(50),
38
38
  }, async ({ page, pageSize }) => {
39
- const result = await client.get("/conversations", { page, pageSize });
39
+ const result = await client.getPage("/conversations", { page, pageSize });
40
40
  return {
41
41
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
42
42
  };
43
43
  });
44
+ server.tool("get_conversation_messages", "A conversation's message history, oldest first. Assistant messages carry the IDs of the bookmarks retrieved for that answer.", {
45
+ conversationId: z.string().uuid().describe("Conversation UUID"),
46
+ }, async ({ conversationId }) => {
47
+ const result = await client.get(`/conversations/${conversationId}/messages`);
48
+ return {
49
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
50
+ };
51
+ });
52
+ server.tool("delete_conversation", "Permanently delete a conversation and its messages. Free tier allows one concurrent conversation, so deleting is how a free account starts a fresh chat.", {
53
+ conversationId: z.string().uuid().describe("Conversation UUID"),
54
+ }, async ({ conversationId }) => {
55
+ await client.delete(`/conversations/${conversationId}`);
56
+ return {
57
+ content: [{ type: "text", text: "Conversation deleted." }],
58
+ };
59
+ });
44
60
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { XmarkClient } from "../client.js";
3
+ export declare function registerUsageTools(server: McpServer, client: XmarkClient): void;
@@ -0,0 +1,8 @@
1
+ export function registerUsageTools(server, client) {
2
+ server.tool("get_usage", "The authenticated user's plan and quota usage: chat messages this month, syncs today, bookmark count, and whether an X account is connected. Limits are null on unlimited plans.", {}, async () => {
3
+ const result = await client.get("/usage");
4
+ return {
5
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
6
+ };
7
+ });
8
+ }
package/dist/types.d.ts CHANGED
@@ -20,12 +20,13 @@ export interface Bookmark {
20
20
  author: {
21
21
  username: string;
22
22
  name: string;
23
- id: string;
23
+ id?: string;
24
+ profile_image_url?: string | null;
24
25
  };
25
- bookmarked_at: string;
26
+ bookmarked_at: string | null;
26
27
  url?: string;
27
- media?: unknown[];
28
- topics?: string[];
28
+ public_metrics?: Record<string, number> | null;
29
+ media?: unknown[] | null;
29
30
  }
30
31
  export interface BookmarkSearchResult {
31
32
  bookmark: Bookmark;
@@ -50,9 +51,30 @@ export interface SyncResult {
50
51
  }
51
52
  export interface Conversation {
52
53
  id: string;
53
- title: string;
54
+ title: string | null;
54
55
  created_at: string;
55
- message_count: number;
56
+ updated_at: string;
57
+ }
58
+ export interface Message {
59
+ id: string;
60
+ role: string;
61
+ content: string;
62
+ retrieved_bookmark_ids: string[] | null;
63
+ created_at: string;
64
+ }
65
+ export interface QuotaUsage {
66
+ used: number;
67
+ /** null = unlimited */
68
+ limit: number | null;
69
+ }
70
+ export interface UsageData {
71
+ plan: string;
72
+ x_connected: boolean;
73
+ bookmarks: {
74
+ total: number;
75
+ };
76
+ chat: QuotaUsage;
77
+ syncs_today: QuotaUsage;
56
78
  }
57
79
  export interface PaginationMeta {
58
80
  page: number;
@@ -60,3 +82,7 @@ export interface PaginationMeta {
60
82
  total: number;
61
83
  totalPages: number;
62
84
  }
85
+ export interface Paginated<T> {
86
+ data: T[];
87
+ pagination: PaginationMeta;
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jclvsh/xmark",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server and CLI for the xmark API — chat with your X bookmarks from inside Claude Code, Cursor, or any MCP client.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -46,5 +46,5 @@
46
46
  "type": "git",
47
47
  "url": "git+https://github.com/jclvsh/xmark.git"
48
48
  },
49
- "homepage": "https://www.xmark.chat"
49
+ "homepage": "https://xmark.dev"
50
50
  }