@plaud-ai/mcp 0.1.32 → 0.2.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/dist/chunk-4QBEOJPX.js +92 -0
- package/dist/chunk-7KGB7GSZ.js +33 -0
- package/dist/chunk-MPCF6HMK.js +151 -0
- package/dist/chunk-SNSGVRCU.js +234 -0
- package/dist/chunk-UPEENHCG.js +70 -0
- package/dist/index.js +78 -30953
- package/dist/install-TU2Y3ARS.js +370 -0
- package/dist/server-3256GMAD.js +294 -0
- package/dist/{setup-3PYAGQRG.js → setup-7JTG3C2W.js} +17 -61
- package/package.json +17 -10
- package/plugin.json +1 -1
- package/skills/plaud-browse/SKILL.md +39 -0
- package/skills/plaud-digest/SKILL.md +41 -0
- package/skills/plaud-export/SKILL.md +51 -0
- package/skills/plaud-find/SKILL.md +54 -0
- package/skills/plaud-followup/SKILL.md +59 -0
- package/skills/plaud-read/SKILL.md +51 -0
- package/skills/plaud-shared/SKILL.md +67 -0
- package/dist/chunk-5JTZ7NKP.js +0 -144
- package/skills/plaud/SKILL.md +0 -40
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// src/skills.ts
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { statSync } from "fs";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
function parseFrontmatter(raw) {
|
|
7
|
+
if (!raw.startsWith("---\n")) return { meta: {}, body: raw };
|
|
8
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
9
|
+
if (end === -1) return { meta: {}, body: raw };
|
|
10
|
+
const yaml = raw.slice(4, end);
|
|
11
|
+
const body = raw.slice(end + 5);
|
|
12
|
+
const meta = {};
|
|
13
|
+
let currentKey = null;
|
|
14
|
+
for (const line of yaml.split("\n")) {
|
|
15
|
+
if (!line.trim()) continue;
|
|
16
|
+
const topMatch = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
|
|
17
|
+
if (topMatch && !line.startsWith(" ")) {
|
|
18
|
+
const [, k, v] = topMatch;
|
|
19
|
+
currentKey = k;
|
|
20
|
+
if (v !== "") {
|
|
21
|
+
const unq = v.replace(/^["']|["']$/g, "");
|
|
22
|
+
meta[k] = unq;
|
|
23
|
+
currentKey = null;
|
|
24
|
+
} else {
|
|
25
|
+
meta[k] = {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { meta, body };
|
|
30
|
+
}
|
|
31
|
+
function findSkillsDir() {
|
|
32
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const candidates = [
|
|
34
|
+
resolve(here, "..", "skills"),
|
|
35
|
+
resolve(here, "..", "..", "skills")
|
|
36
|
+
];
|
|
37
|
+
for (const c of candidates) {
|
|
38
|
+
try {
|
|
39
|
+
if (statSync(c).isDirectory()) return c;
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return candidates[0];
|
|
44
|
+
}
|
|
45
|
+
var cached = null;
|
|
46
|
+
async function loadSkills() {
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
const dir = findSkillsDir();
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await readdir(dir);
|
|
52
|
+
} catch {
|
|
53
|
+
cached = [];
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
const skills = [];
|
|
57
|
+
for (const name of entries) {
|
|
58
|
+
try {
|
|
59
|
+
const skillPath = join(dir, name, "SKILL.md");
|
|
60
|
+
const raw = await readFile(skillPath, "utf-8");
|
|
61
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
62
|
+
const skillName = typeof meta.name === "string" ? meta.name : name;
|
|
63
|
+
const description = typeof meta.description === "string" ? meta.description : "";
|
|
64
|
+
const version = typeof meta.version === "string" ? meta.version : "0.0.0";
|
|
65
|
+
skills.push({
|
|
66
|
+
name: skillName,
|
|
67
|
+
version,
|
|
68
|
+
description,
|
|
69
|
+
body: body.trimStart(),
|
|
70
|
+
content: raw,
|
|
71
|
+
supported: true
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
skills.sort((a, b) => {
|
|
77
|
+
if (a.name === "plaud-shared") return -1;
|
|
78
|
+
if (b.name === "plaud-shared") return 1;
|
|
79
|
+
return a.name.localeCompare(b.name);
|
|
80
|
+
});
|
|
81
|
+
cached = skills;
|
|
82
|
+
return cached;
|
|
83
|
+
}
|
|
84
|
+
async function skillsCombined() {
|
|
85
|
+
const skills = await loadSkills();
|
|
86
|
+
return skills.map((s) => s.content).join("\n\n---\n\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
loadSkills,
|
|
91
|
+
skillsCombined
|
|
92
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlaudClient
|
|
3
|
+
} from "./chunk-SNSGVRCU.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
function buildExtraHeaders() {
|
|
7
|
+
const headers = {};
|
|
8
|
+
if (process.env.PLAUD_ENV) headers["x-pld-env"] = process.env.PLAUD_ENV;
|
|
9
|
+
if (process.env.PLAUD_REGION) headers["x-pld-region"] = process.env.PLAUD_REGION;
|
|
10
|
+
return headers;
|
|
11
|
+
}
|
|
12
|
+
var CONFIG = {
|
|
13
|
+
clientId: process.env.PLAUD_MCP_CLIENT_ID ?? process.env.PLAUD_CLIENT_ID ?? "client_9c501dad-8a0d-40b2-a7b0-d1cb8787f674",
|
|
14
|
+
clientSecret: process.env.PLAUD_CLIENT_SECRET ?? "",
|
|
15
|
+
redirectUri: "http://localhost:8199/auth/callback",
|
|
16
|
+
tokenFile: "tokens-mcp.json",
|
|
17
|
+
apiBase: process.env.PLAUD_API_BASE,
|
|
18
|
+
authorizationUrl: process.env.PLAUD_AUTH_URL,
|
|
19
|
+
tokenUrl: process.env.PLAUD_TOKEN_URL,
|
|
20
|
+
refreshUrl: process.env.PLAUD_REFRESH_URL,
|
|
21
|
+
extraHeaders: buildExtraHeaders()
|
|
22
|
+
};
|
|
23
|
+
var client = null;
|
|
24
|
+
function getClient() {
|
|
25
|
+
if (!client) {
|
|
26
|
+
client = new PlaudClient(CONFIG);
|
|
27
|
+
}
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
getClient
|
|
33
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
var logger = pino(
|
|
4
|
+
{ level: process.env.LOG_LEVEL ?? "info" },
|
|
5
|
+
pino.destination(2)
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
// src/tools/index.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var MAX_FILTER_PAGES = 5;
|
|
11
|
+
var FILTER_PAGE_SIZE = 100;
|
|
12
|
+
function parseDate(s) {
|
|
13
|
+
if (!s) return null;
|
|
14
|
+
const d = new Date(s);
|
|
15
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
16
|
+
return d.getTime();
|
|
17
|
+
}
|
|
18
|
+
function registerTools(server, client) {
|
|
19
|
+
server.tool(
|
|
20
|
+
"list_files",
|
|
21
|
+
"List Plaud recordings. Supports optional client-side filtering: `query` (case-insensitive name substring), `date_from`/`date_to` (YYYY-MM-DD, inclusive). When any filter is set, paginates up to 5 pages \xD7 100 recordings and returns all matches.",
|
|
22
|
+
{
|
|
23
|
+
page: z.number().optional().default(1).describe("Page number (ignored when filters are set)"),
|
|
24
|
+
page_size: z.number().optional().default(20).describe("Items per page (ignored when filters are set)"),
|
|
25
|
+
query: z.string().optional().describe("Case-insensitive substring match on recording name"),
|
|
26
|
+
date_from: z.string().optional().describe("Start date inclusive, YYYY-MM-DD"),
|
|
27
|
+
date_to: z.string().optional().describe("End date inclusive, YYYY-MM-DD")
|
|
28
|
+
},
|
|
29
|
+
async ({ page, page_size, query, date_from, date_to }) => {
|
|
30
|
+
const start = Date.now();
|
|
31
|
+
const hasFilter = Boolean(query || date_from || date_to);
|
|
32
|
+
logger.info({ event: "tool_call", tool: "list_files", has_filter: hasFilter });
|
|
33
|
+
try {
|
|
34
|
+
if (!hasFilter) {
|
|
35
|
+
const result = await client.listFiles(page, page_size);
|
|
36
|
+
logger.info({ event: "tool_call_end", tool: "list_files", duration_ms: Date.now() - start });
|
|
37
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
38
|
+
}
|
|
39
|
+
const q = query?.toLowerCase();
|
|
40
|
+
const from = parseDate(date_from);
|
|
41
|
+
const toRaw = parseDate(date_to);
|
|
42
|
+
const to = toRaw !== null ? toRaw + 24 * 60 * 60 * 1e3 - 1 : null;
|
|
43
|
+
const matches = [];
|
|
44
|
+
let scanned = 0;
|
|
45
|
+
let truncated = false;
|
|
46
|
+
for (let p = 1; p <= MAX_FILTER_PAGES; p++) {
|
|
47
|
+
const pageResult = await client.listFiles(p, FILTER_PAGE_SIZE);
|
|
48
|
+
const items = pageResult.data;
|
|
49
|
+
scanned += items.length;
|
|
50
|
+
for (const item of items) {
|
|
51
|
+
if (q && !(item.name ?? "").toLowerCase().includes(q)) continue;
|
|
52
|
+
if (from !== null || to !== null) {
|
|
53
|
+
const created = parseDate(item.created_at);
|
|
54
|
+
if (created === null) continue;
|
|
55
|
+
if (from !== null && created < from) continue;
|
|
56
|
+
if (to !== null && created > to) continue;
|
|
57
|
+
}
|
|
58
|
+
matches.push(item);
|
|
59
|
+
}
|
|
60
|
+
if (items.length < FILTER_PAGE_SIZE) break;
|
|
61
|
+
if (p === MAX_FILTER_PAGES) truncated = true;
|
|
62
|
+
}
|
|
63
|
+
logger.info({ event: "tool_call_end", tool: "list_files", duration_ms: Date.now() - start, scanned, matched: matches.length, truncated });
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
66
|
+
data: matches,
|
|
67
|
+
scanned,
|
|
68
|
+
matched: matches.length,
|
|
69
|
+
truncated,
|
|
70
|
+
note: truncated ? `Scanned first ${MAX_FILTER_PAGES * FILTER_PAGE_SIZE} recordings; narrow filters for a complete match.` : void 0
|
|
71
|
+
}, null, 2) }]
|
|
72
|
+
};
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.error({ event: "tool_call_error", tool: "list_files", duration_ms: Date.now() - start, error: String(err) });
|
|
75
|
+
return { content: [{ type: "text", text: `Failed to list files: ${err}` }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
server.tool(
|
|
80
|
+
"get_file",
|
|
81
|
+
"Get details of a specific Plaud recording by ID",
|
|
82
|
+
{ file_id: z.string().describe("The file ID to retrieve") },
|
|
83
|
+
async ({ file_id }) => {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
logger.info({ event: "tool_call", tool: "get_file", file_id });
|
|
86
|
+
try {
|
|
87
|
+
const file = await client.getFile(file_id);
|
|
88
|
+
logger.info({ event: "tool_call_end", tool: "get_file", duration_ms: Date.now() - start });
|
|
89
|
+
return { content: [{ type: "text", text: JSON.stringify(file, null, 2) }] };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.error({ event: "tool_call_error", tool: "get_file", duration_ms: Date.now() - start, error: String(err) });
|
|
92
|
+
return { content: [{ type: "text", text: `Failed to get file: ${err}` }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
server.tool(
|
|
97
|
+
"get_note",
|
|
98
|
+
"Fetch AI-generated notes for a Plaud recording \u2014 compact summary, action items, and key topics",
|
|
99
|
+
{ file_id: z.string().describe("The file ID to retrieve notes for") },
|
|
100
|
+
async ({ file_id }) => {
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
logger.info({ event: "tool_call", tool: "get_note", file_id });
|
|
103
|
+
try {
|
|
104
|
+
const file = await client.getFile(file_id);
|
|
105
|
+
logger.info({ event: "tool_call_end", tool: "get_note", duration_ms: Date.now() - start });
|
|
106
|
+
return { content: [{ type: "text", text: JSON.stringify(file.note_list ?? [], null, 2) }] };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.error({ event: "tool_call_error", tool: "get_note", duration_ms: Date.now() - start, error: String(err) });
|
|
109
|
+
return { content: [{ type: "text", text: `Failed to get note: ${err}` }], isError: true };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
server.tool(
|
|
114
|
+
"get_transcript",
|
|
115
|
+
"Fetch the full timestamped transcript with speaker attribution for a Plaud recording",
|
|
116
|
+
{ file_id: z.string().describe("The file ID to retrieve transcript for") },
|
|
117
|
+
async ({ file_id }) => {
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
logger.info({ event: "tool_call", tool: "get_transcript", file_id });
|
|
120
|
+
try {
|
|
121
|
+
const file = await client.getFile(file_id);
|
|
122
|
+
logger.info({ event: "tool_call_end", tool: "get_transcript", duration_ms: Date.now() - start });
|
|
123
|
+
return { content: [{ type: "text", text: JSON.stringify(file.source_list ?? [], null, 2) }] };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.error({ event: "tool_call_error", tool: "get_transcript", duration_ms: Date.now() - start, error: String(err) });
|
|
126
|
+
return { content: [{ type: "text", text: `Failed to get transcript: ${err}` }], isError: true };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
server.tool(
|
|
131
|
+
"get_current_user",
|
|
132
|
+
"Get current authenticated user info",
|
|
133
|
+
async () => {
|
|
134
|
+
const start = Date.now();
|
|
135
|
+
logger.info({ event: "tool_call", tool: "get_current_user" });
|
|
136
|
+
try {
|
|
137
|
+
const user = await client.getCurrentUser();
|
|
138
|
+
logger.info({ event: "tool_call_end", tool: "get_current_user", duration_ms: Date.now() - start });
|
|
139
|
+
return { content: [{ type: "text", text: JSON.stringify(user, null, 2) }] };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.error({ event: "tool_call_error", tool: "get_current_user", duration_ms: Date.now() - start, error: String(err) });
|
|
142
|
+
return { content: [{ type: "text", text: `Failed to get user info: ${err}` }], isError: true };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
logger,
|
|
150
|
+
registerTools
|
|
151
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// ../shared/dist/oauth.js
|
|
2
|
+
import { randomBytes, createHash } from "crypto";
|
|
3
|
+
|
|
4
|
+
// ../shared/dist/token-store.js
|
|
5
|
+
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
var TokenStore = class {
|
|
9
|
+
configDir;
|
|
10
|
+
tokenPath;
|
|
11
|
+
constructor(filename = "tokens.json") {
|
|
12
|
+
this.configDir = join(homedir(), ".plaud");
|
|
13
|
+
this.tokenPath = join(this.configDir, filename);
|
|
14
|
+
}
|
|
15
|
+
async save(tokenSet) {
|
|
16
|
+
await mkdir(this.configDir, { recursive: true });
|
|
17
|
+
await writeFile(this.tokenPath, JSON.stringify(tokenSet, null, 2), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
async load() {
|
|
20
|
+
try {
|
|
21
|
+
const data = await readFile(this.tokenPath, "utf-8");
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async clear() {
|
|
28
|
+
try {
|
|
29
|
+
await rm(this.tokenPath);
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ../shared/dist/oauth.js
|
|
36
|
+
var DEFAULT_AUTHORIZATION_URL = "https://web.plaud.ai/platform/oauth";
|
|
37
|
+
var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
|
|
38
|
+
var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
|
|
39
|
+
function generateCodeVerifier() {
|
|
40
|
+
return randomBytes(32).toString("base64url");
|
|
41
|
+
}
|
|
42
|
+
function generateCodeChallenge(verifier) {
|
|
43
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
44
|
+
}
|
|
45
|
+
function generateState() {
|
|
46
|
+
return randomBytes(16).toString("base64url");
|
|
47
|
+
}
|
|
48
|
+
var OAuth = class {
|
|
49
|
+
config;
|
|
50
|
+
tokenStore;
|
|
51
|
+
authorizationUrl;
|
|
52
|
+
tokenUrl;
|
|
53
|
+
refreshUrl;
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
this.tokenStore = new TokenStore(config.tokenFile);
|
|
57
|
+
this.authorizationUrl = config.authorizationUrl ?? DEFAULT_AUTHORIZATION_URL;
|
|
58
|
+
this.tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
59
|
+
this.refreshUrl = config.refreshUrl ?? DEFAULT_REFRESH_URL;
|
|
60
|
+
}
|
|
61
|
+
createAuthorizationRequest() {
|
|
62
|
+
const codeVerifier = generateCodeVerifier();
|
|
63
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
64
|
+
const state = generateState();
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
client_id: this.config.clientId,
|
|
67
|
+
redirect_uri: this.config.redirectUri,
|
|
68
|
+
response_type: "code",
|
|
69
|
+
code_challenge: codeChallenge,
|
|
70
|
+
code_challenge_method: "S256",
|
|
71
|
+
state
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
url: `${this.authorizationUrl}?${params.toString()}`,
|
|
75
|
+
codeVerifier,
|
|
76
|
+
state
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @deprecated Use createAuthorizationRequest() for PKCE flow
|
|
81
|
+
*/
|
|
82
|
+
getAuthorizationUrl() {
|
|
83
|
+
return this.createAuthorizationRequest().url;
|
|
84
|
+
}
|
|
85
|
+
async exchangeCode(code, codeVerifier, state) {
|
|
86
|
+
const basicAuth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
|
|
87
|
+
const body = {
|
|
88
|
+
code,
|
|
89
|
+
redirect_uri: this.config.redirectUri
|
|
90
|
+
};
|
|
91
|
+
if (codeVerifier) {
|
|
92
|
+
body.code_verifier = codeVerifier;
|
|
93
|
+
}
|
|
94
|
+
if (state) {
|
|
95
|
+
body.state = state;
|
|
96
|
+
}
|
|
97
|
+
const res = await fetch(this.tokenUrl, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
101
|
+
Accept: "application/json",
|
|
102
|
+
Authorization: `Basic ${basicAuth}`,
|
|
103
|
+
...this.config.extraHeaders
|
|
104
|
+
},
|
|
105
|
+
body: new URLSearchParams(body)
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
throw new Error(`Token exchange failed: ${res.status} ${await res.text()}`);
|
|
109
|
+
}
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
const tokenSet = {
|
|
112
|
+
access_token: data.access_token,
|
|
113
|
+
refresh_token: data.refresh_token,
|
|
114
|
+
token_type: data.token_type ?? "Bearer",
|
|
115
|
+
expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
|
|
116
|
+
};
|
|
117
|
+
await this.tokenStore.save(tokenSet);
|
|
118
|
+
return tokenSet;
|
|
119
|
+
}
|
|
120
|
+
async getAccessToken() {
|
|
121
|
+
const tokenSet = await this.tokenStore.load();
|
|
122
|
+
if (!tokenSet)
|
|
123
|
+
return null;
|
|
124
|
+
if (tokenSet.expires_at && Date.now() > tokenSet.expires_at - 6e4) {
|
|
125
|
+
if (tokenSet.refresh_token) {
|
|
126
|
+
try {
|
|
127
|
+
const refreshed = await this.refresh(tokenSet.refresh_token);
|
|
128
|
+
return refreshed.access_token;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return tokenSet.access_token;
|
|
136
|
+
}
|
|
137
|
+
async refresh(refreshToken) {
|
|
138
|
+
const res = await fetch(this.refreshUrl, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
142
|
+
Accept: "application/json",
|
|
143
|
+
...this.config.extraHeaders
|
|
144
|
+
},
|
|
145
|
+
body: new URLSearchParams({
|
|
146
|
+
refresh_token: refreshToken
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
const body = await res.text();
|
|
151
|
+
throw new Error(`Token refresh failed: ${res.status} ${body}`);
|
|
152
|
+
}
|
|
153
|
+
const data = await res.json();
|
|
154
|
+
const tokenSet = {
|
|
155
|
+
access_token: data.access_token,
|
|
156
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
157
|
+
token_type: data.token_type ?? "Bearer",
|
|
158
|
+
expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
|
|
159
|
+
};
|
|
160
|
+
await this.tokenStore.save(tokenSet);
|
|
161
|
+
return tokenSet;
|
|
162
|
+
}
|
|
163
|
+
async logout() {
|
|
164
|
+
await this.tokenStore.clear();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// ../shared/dist/client.js
|
|
169
|
+
var DEFAULT_API_BASE = "https://platform.plaud.ai/developer/api";
|
|
170
|
+
var PlaudClient = class {
|
|
171
|
+
oauth;
|
|
172
|
+
apiBase;
|
|
173
|
+
extraHeaders;
|
|
174
|
+
staticToken;
|
|
175
|
+
constructor(config) {
|
|
176
|
+
this.oauth = new OAuth(config);
|
|
177
|
+
this.apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
178
|
+
this.extraHeaders = config.extraHeaders ?? {};
|
|
179
|
+
this.staticToken = config.staticToken;
|
|
180
|
+
}
|
|
181
|
+
get auth() {
|
|
182
|
+
return this.oauth;
|
|
183
|
+
}
|
|
184
|
+
async request(path, init) {
|
|
185
|
+
const token = this.staticToken ?? await this.oauth.getAccessToken();
|
|
186
|
+
if (!token) {
|
|
187
|
+
throw new Error("Not authenticated. Please login first.");
|
|
188
|
+
}
|
|
189
|
+
const url = `${this.apiBase}${path}`;
|
|
190
|
+
const method = init?.method ?? "GET";
|
|
191
|
+
const headers = {
|
|
192
|
+
Authorization: `Bearer ${token}`,
|
|
193
|
+
Accept: "application/json",
|
|
194
|
+
...this.extraHeaders,
|
|
195
|
+
...init?.headers
|
|
196
|
+
};
|
|
197
|
+
const res = await fetch(url, { ...init, headers });
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
const body = await res.text();
|
|
200
|
+
if (res.status === 422) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(body);
|
|
203
|
+
const messages = parsed.detail.map((d) => `${d.loc.at(-1)}: ${d.msg}`).join("; ");
|
|
204
|
+
throw new Error(messages);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (e instanceof SyntaxError)
|
|
207
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
208
|
+
throw e;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
const json = await res.json();
|
|
214
|
+
return json;
|
|
215
|
+
}
|
|
216
|
+
async getCurrentUser() {
|
|
217
|
+
return this.request("/open/third-party/users/current");
|
|
218
|
+
}
|
|
219
|
+
async revokeCurrentUser() {
|
|
220
|
+
await this.request("/open/third-party/users/current/revoke", {
|
|
221
|
+
method: "POST"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async listFiles(page = 1, pageSize = 20) {
|
|
225
|
+
return this.request(`/open/third-party/files/?page=${page}&page_size=${pageSize}`);
|
|
226
|
+
}
|
|
227
|
+
async getFile(fileId) {
|
|
228
|
+
return this.request(`/open/third-party/files/${fileId}`);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
PlaudClient
|
|
234
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
skillsCombined
|
|
3
|
+
} from "./chunk-4QBEOJPX.js";
|
|
4
|
+
|
|
5
|
+
// src/install-utils.ts
|
|
6
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { homedir, platform } from "os";
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
var SKILLS_MARKER_START = "<!-- plaud-skills:start -->";
|
|
11
|
+
var SKILLS_MARKER_END = "<!-- plaud-skills:end -->";
|
|
12
|
+
function getMcpEntry() {
|
|
13
|
+
return {
|
|
14
|
+
command: "npx",
|
|
15
|
+
args: ["-y", "@plaud-ai/mcp@latest"]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function copyToClipboard(content) {
|
|
19
|
+
if (platform() === "win32") {
|
|
20
|
+
return spawnSync("clip", [], { input: content }).status === 0;
|
|
21
|
+
}
|
|
22
|
+
if (platform() === "darwin") {
|
|
23
|
+
return spawnSync("pbcopy", [], { input: content }).status === 0;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
async function writeSkillsToClaudeCode() {
|
|
28
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
29
|
+
const combined = await skillsCombined();
|
|
30
|
+
const block = `${SKILLS_MARKER_START}
|
|
31
|
+
${combined}
|
|
32
|
+
${SKILLS_MARKER_END}`;
|
|
33
|
+
let existing = "";
|
|
34
|
+
try {
|
|
35
|
+
existing = await readFile(claudeMdPath, "utf-8");
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
if (existing.includes(SKILLS_MARKER_START)) {
|
|
39
|
+
const updated = existing.replace(
|
|
40
|
+
new RegExp(`${SKILLS_MARKER_START}[\\s\\S]*?${SKILLS_MARKER_END}`),
|
|
41
|
+
block
|
|
42
|
+
);
|
|
43
|
+
await mkdir(dirname(claudeMdPath), { recursive: true });
|
|
44
|
+
await writeFile(claudeMdPath, updated, "utf-8");
|
|
45
|
+
} else {
|
|
46
|
+
await mkdir(dirname(claudeMdPath), { recursive: true });
|
|
47
|
+
await writeFile(claudeMdPath, existing + (existing.endsWith("\n") ? "" : "\n") + block + "\n", "utf-8");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function removeSkillsFromClaudeCode() {
|
|
51
|
+
const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
|
|
52
|
+
let existing = "";
|
|
53
|
+
try {
|
|
54
|
+
existing = await readFile(claudeMdPath, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!existing.includes(SKILLS_MARKER_START)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const updated = existing.replace(new RegExp(`\\n?${SKILLS_MARKER_START}[\\s\\S]*?${SKILLS_MARKER_END}\\n?`), "").trimEnd();
|
|
62
|
+
await writeFile(claudeMdPath, updated ? updated + "\n" : "", "utf-8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
getMcpEntry,
|
|
67
|
+
copyToClipboard,
|
|
68
|
+
writeSkillsToClaudeCode,
|
|
69
|
+
removeSkillsFromClaudeCode
|
|
70
|
+
};
|