@jclvsh/xmark 0.2.1 → 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 +11 -2
- package/dist/cli.js +41 -5
- package/dist/client.d.ts +6 -0
- package/dist/client.js +28 -1
- package/dist/index.js +3 -1
- package/dist/tools/bookmarks.js +17 -6
- package/dist/tools/chat.js +18 -2
- package/dist/tools/usage.d.ts +3 -0
- package/dist/tools/usage.js +8 -0
- package/dist/types.d.ts +32 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ npx -y -p @jclvsh/xmark xmark --help
|
|
|
21
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=
|
|
24
|
+
export XMARK_API_KEY=nt_live_...
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
Optionally override the API host (default is `https://api.xmark.dev`):
|
|
@@ -39,7 +39,11 @@ export XMARK_API_URL=https://api.xmark.dev
|
|
|
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,12 +52,17 @@ 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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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().
|
|
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().
|
|
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,5 +1,6 @@
|
|
|
1
1
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
2
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;
|
|
@@ -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":
|
|
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.
|
|
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);
|
package/dist/tools/bookmarks.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
}
|
package/dist/tools/chat.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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,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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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