note-mcp 1.0.0 → 1.1.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.
Files changed (3) hide show
  1. package/README.md +91 -12
  2. package/dist/index.js +363 -130
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -7,6 +7,12 @@ Unofficial stdio MCP server for note.com. It uses cookie-based access to note.co
7
7
 
8
8
  ## Install / run
9
9
 
10
+ ```bash
11
+ npx note-mcp
12
+ ```
13
+
14
+ For advanced/server setups, you can still provide a Cookie header through the environment:
15
+
10
16
  ```bash
11
17
  NOTE_COOKIE='your note.com cookie string' npx note-mcp
12
18
  ```
@@ -16,12 +22,25 @@ For local development:
16
22
  ```bash
17
23
  npm install
18
24
  npm run build
19
- NOTE_COOKIE='your note.com cookie string' node dist/index.js
25
+ node dist/index.js
20
26
  ```
21
27
 
22
28
  ## MCP client configuration
23
29
 
24
- Example:
30
+ Desktop/local browser-login friendly setup:
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "note": {
36
+ "command": "npx",
37
+ "args": ["-y", "note-mcp"]
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ Advanced env-based setup:
25
44
 
26
45
  ```json
27
46
  {
@@ -37,15 +56,84 @@ Example:
37
56
  }
38
57
  ```
39
58
 
59
+ ## Authentication
60
+
61
+ `note-mcp` supports two authentication paths.
62
+
63
+ ### 1. Local/desktop: browser login
64
+
65
+ For local desktop agents, ask the agent to call:
66
+
67
+ - `note_auth_login`
68
+
69
+ Or run it directly:
70
+
71
+ ```bash
72
+ npx note-mcp auth
73
+ ```
74
+
75
+ This opens a browser, lets you log in to note.com normally, then stores note.com cookies in:
76
+
77
+ ```text
78
+ ~/.config/note-mcp/config.json
79
+ ```
80
+
81
+ The config file is written with `0600` permissions where supported.
82
+
83
+ Useful CLI commands:
84
+
85
+ ```bash
86
+ npx note-mcp auth --status
87
+ npx note-mcp auth --clear
88
+ npx note-mcp auth --headless
89
+ npx note-mcp auth --headed
90
+ ```
91
+
92
+ ### 2. Advanced/server/CI: secret, env, or config file
93
+
94
+ For remote agents, servers, CI, and secret managers, provide a Cookie header via:
95
+
96
+ - `NOTE_COOKIE`
97
+ - `NOTE_SESSION_COOKIE`
98
+ - `NOTE_MCP_CONFIG` pointing to a config JSON file
99
+ - MCP tool `note_set_cookie`
100
+
101
+ Example config file:
102
+
103
+ ```json
104
+ {
105
+ "cookie": "your note.com Cookie header",
106
+ "updatedAt": "2026-06-21T00:00:00.000Z"
107
+ }
108
+ ```
109
+
110
+ Cookie lookup priority:
111
+
112
+ 1. `NOTE_COOKIE`
113
+ 2. `NOTE_SESSION_COOKIE`
114
+ 3. config file cookie
115
+
40
116
  ## Tools
41
117
 
42
- - `note_auth_check` — verify cookie-based access to note.com internal APIs
118
+ Authentication/setup tools:
119
+
120
+ - `note_auth_status` — inspect whether auth is configured
121
+ - `note_auth_login` — open a browser login flow and save cookies locally
122
+ - `note_set_cookie` — save a Cookie header to the local config file, optionally verifying it first
123
+ - `note_clear_cookie` — delete the stored config-file cookie
124
+ - `note_login_help` — explain supported setup paths
125
+
126
+ note.com tools:
127
+
128
+ - `note_auth_check` — verify configured cookie-based access to note.com internal APIs
43
129
  - `note_list_my_notes` — list notes for the authenticated account
44
130
  - `note_list_drafts` — list drafts for the authenticated account
45
131
  - `note_get_note` — fetch a note by note key, e.g. `n1a0b26f944f4`
46
132
  - `note_create_draft` — create a draft
47
133
  - `note_update_draft` — update a draft by draft id
48
134
 
135
+ If authentication is missing, note tools return an `auth_required` error suggesting `note_auth_login` or `note_set_cookie`.
136
+
49
137
  ## API basis
50
138
 
51
139
  The initial endpoints are based on public, unofficial note API references, including:
@@ -60,15 +148,6 @@ Known endpoint basis:
60
148
  - Draft save: `POST /v1/text_notes/draft_save?id={draftId}`
61
149
  - Auth smoke test: `GET /v3/notice_counts`
62
150
 
63
- ## Authentication
64
-
65
- Set one of these environment variables before launching the server:
66
-
67
- - `NOTE_COOKIE`
68
- - `NOTE_SESSION_COOKIE`
69
-
70
- Use the full Cookie header value from an authenticated browser session.
71
-
72
151
  ## Release
73
152
 
74
153
  Releases are handled by GitHub Actions + semantic-release.
package/dist/index.js CHANGED
@@ -6,15 +6,98 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { z } from "zod";
7
7
 
8
8
  // src/note/auth.ts
9
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
10
+ import { dirname, join } from "path";
11
+ import { homedir } from "os";
9
12
  var COOKIE_ENV_KEYS = ["NOTE_COOKIE", "NOTE_SESSION_COOKIE"];
10
- function readCookieFromEnv(env = process.env) {
13
+ var CONFIG_ENV_KEY = "NOTE_MCP_CONFIG";
14
+ var AuthRequiredError = class extends Error {
15
+ constructor(message = "note.com authentication is not configured.") {
16
+ super(message);
17
+ this.name = "AuthRequiredError";
18
+ }
19
+ };
20
+ async function readCookie(options = {}) {
21
+ const envCookie = readCookieFromEnvValue(options.env ?? process.env);
22
+ if (envCookie) return envCookie;
23
+ const storedCookie = await readCookieFromConfig(options);
24
+ if (storedCookie) return storedCookie;
25
+ throw new AuthRequiredError();
26
+ }
27
+ async function authStatus(options = {}) {
28
+ const env = options.env ?? process.env;
29
+ const envCookie = readCookieFromEnvValue(env);
30
+ const configPath = resolveConfigPath(options);
31
+ if (envCookie) {
32
+ return {
33
+ configured: true,
34
+ source: "env",
35
+ configPath,
36
+ cookiePreview: previewCookie(envCookie),
37
+ message: "note.com cookie is configured from environment variables."
38
+ };
39
+ }
40
+ const configCookie = await readCookieFromConfig(options);
41
+ if (configCookie) {
42
+ return {
43
+ configured: true,
44
+ source: "config",
45
+ configPath,
46
+ cookiePreview: previewCookie(configCookie),
47
+ message: "note.com cookie is configured from note-mcp config file."
48
+ };
49
+ }
50
+ return {
51
+ configured: false,
52
+ source: "none",
53
+ configPath,
54
+ message: "note.com cookie is not configured. Use note_auth_login for browser login or note_set_cookie / NOTE_COOKIE for advanced setups.",
55
+ suggestedTools: ["note_auth_login", "note_set_cookie"]
56
+ };
57
+ }
58
+ async function saveCookie(cookie, options = {}) {
59
+ const trimmed = cookie.trim();
60
+ if (!trimmed) throw new Error("Cookie must not be empty.");
61
+ const configPath = resolveConfigPath(options);
62
+ await mkdir(dirname(configPath), { recursive: true });
63
+ const config = {
64
+ cookie: trimmed,
65
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
66
+ };
67
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
68
+ `, { mode: 384 });
69
+ return authStatus({ ...options, env: {} });
70
+ }
71
+ async function clearStoredCookie(options = {}) {
72
+ await rm(resolveConfigPath(options), { force: true });
73
+ return authStatus({ ...options, env: {} });
74
+ }
75
+ function resolveConfigPath(options = {}) {
76
+ const env = options.env ?? process.env;
77
+ return options.configPath ?? env[CONFIG_ENV_KEY] ?? join(homedir(), ".config", "note-mcp", "config.json");
78
+ }
79
+ function readCookieFromEnvValue(env) {
11
80
  for (const key of COOKIE_ENV_KEYS) {
12
81
  const value = env[key];
13
82
  if (value?.trim()) return value.trim();
14
83
  }
15
- throw new Error(
16
- `Missing note.com cookie. Set ${COOKIE_ENV_KEYS.join(" or ")} before starting note-mcp.`
17
- );
84
+ return null;
85
+ }
86
+ async function readCookieFromConfig(options) {
87
+ try {
88
+ const raw = await readFile(resolveConfigPath(options), "utf8");
89
+ const parsed = JSON.parse(raw);
90
+ return parsed.cookie?.trim() || null;
91
+ } catch (error) {
92
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
93
+ return null;
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ function previewCookie(cookie) {
99
+ if (cookie.length <= 8) return "********";
100
+ return `${cookie.slice(0, 4)}\u2026${cookie.slice(-4)}`;
18
101
  }
19
102
 
20
103
  // src/note/errors.ts
@@ -108,13 +191,266 @@ async function parseBody(response) {
108
191
  }
109
192
  }
110
193
 
194
+ // src/note/browser-login.ts
195
+ function cookiesToHeader(cookies) {
196
+ const noteCookies = cookies.filter((cookie) => isNoteDomain(cookie.domain));
197
+ if (noteCookies.length === 0) {
198
+ throw new Error("No note.com cookies were found in the browser session.");
199
+ }
200
+ return noteCookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
201
+ }
202
+ async function runBrowserLogin(options = {}) {
203
+ const timeoutMs = options.timeoutMs ?? 18e4;
204
+ const headless = options.headless ?? process.env.NOTE_MCP_HEADLESS === "true";
205
+ const { chromium } = await importPlaywright();
206
+ const browser = await chromium.launch({ headless });
207
+ try {
208
+ const context = await browser.newContext();
209
+ const page = await context.newPage();
210
+ await page.goto("https://note.com/login", { waitUntil: "domcontentloaded" });
211
+ const deadline = Date.now() + timeoutMs;
212
+ let lastCookie = "";
213
+ while (Date.now() < deadline) {
214
+ await page.waitForTimeout(2e3);
215
+ const cookie = cookiesToHeader(await context.cookies("https://note.com"));
216
+ lastCookie = cookie;
217
+ const client = new NoteClient({ cookie });
218
+ try {
219
+ await client.authCheck();
220
+ if (options.save ?? true) {
221
+ await saveCookie(cookie);
222
+ }
223
+ return {
224
+ authenticated: true,
225
+ saved: options.save ?? true,
226
+ cookiePreview: previewCookie2(cookie),
227
+ message: "note.com authentication configured from browser login."
228
+ };
229
+ } catch {
230
+ }
231
+ }
232
+ throw new Error(
233
+ lastCookie ? "Timed out waiting for note.com authentication to become valid." : "Timed out waiting for note.com login cookies."
234
+ );
235
+ } finally {
236
+ await browser.close();
237
+ }
238
+ }
239
+ async function importPlaywright() {
240
+ try {
241
+ return await import("playwright");
242
+ } catch (error) {
243
+ throw new Error(
244
+ `Browser login requires the optional "playwright" package and a usable desktop browser environment. Original error: ${error instanceof Error ? error.message : String(error)}`
245
+ );
246
+ }
247
+ }
248
+ function isNoteDomain(domain) {
249
+ const normalized = domain.replace(/^\./, "").toLowerCase();
250
+ return normalized === "note.com" || normalized.endsWith(".note.com");
251
+ }
252
+ function previewCookie2(cookie) {
253
+ if (cookie.length <= 8) return "********";
254
+ return `${cookie.slice(0, 4)}\u2026${cookie.slice(-4)}`;
255
+ }
256
+
111
257
  // src/index.ts
112
- var server = new McpServer({
113
- name: "note-mcp",
114
- version: "0.0.0-development"
115
- });
116
- function createClient() {
117
- return new NoteClient({ cookie: readCookieFromEnv() });
258
+ if (process.argv[2] === "auth") {
259
+ await runAuthCli(process.argv.slice(3));
260
+ } else {
261
+ await runMcpServer();
262
+ }
263
+ async function runAuthCli(args) {
264
+ try {
265
+ if (args.includes("--status")) {
266
+ console.log(jsonText(await authStatus()));
267
+ return;
268
+ }
269
+ if (args.includes("--clear")) {
270
+ console.log(jsonText(await clearStoredCookie()));
271
+ return;
272
+ }
273
+ const headless = args.includes("--headless") ? true : args.includes("--headed") ? false : void 0;
274
+ console.error("Opening note.com login in a browser. Complete login there; note-mcp will save cookies locally.");
275
+ console.log(jsonText(await runBrowserLogin(headless === void 0 ? {} : { headless })));
276
+ } catch (error) {
277
+ console.error(jsonText(errorDetail(error)));
278
+ process.exitCode = 1;
279
+ }
280
+ }
281
+ async function runMcpServer() {
282
+ const server = new McpServer({
283
+ name: "note-mcp",
284
+ version: "1.1.0-development"
285
+ });
286
+ server.registerTool(
287
+ "note_auth_status",
288
+ {
289
+ title: "Get note.com authentication status",
290
+ description: "Checks whether note-mcp has a note.com cookie from env or config file.",
291
+ inputSchema: {}
292
+ },
293
+ async () => result(await authStatus())
294
+ );
295
+ server.registerTool(
296
+ "note_auth_login",
297
+ {
298
+ title: "Log in to note.com with a browser",
299
+ description: "Opens a local Playwright browser login flow and saves note.com cookies to the note-mcp config file. Intended for desktop/local agents; remote/headless servers should use env or note_set_cookie.",
300
+ inputSchema: {
301
+ headless: z.boolean().optional()
302
+ }
303
+ },
304
+ async ({ headless }) => {
305
+ try {
306
+ return result(await runBrowserLogin({ ...headless === void 0 ? {} : { headless } }));
307
+ } catch (error) {
308
+ return errorResult(error);
309
+ }
310
+ }
311
+ );
312
+ server.registerTool(
313
+ "note_set_cookie",
314
+ {
315
+ title: "Set note.com cookie",
316
+ description: "Stores a note.com Cookie header in the local note-mcp config file. By default, verifies the cookie before saving.",
317
+ inputSchema: {
318
+ cookie: z.string().min(1),
319
+ verify: z.boolean().default(true)
320
+ }
321
+ },
322
+ async ({ cookie, verify }) => {
323
+ try {
324
+ if (verify) {
325
+ await new NoteClient({ cookie }).authCheck();
326
+ }
327
+ return result(await saveCookie(cookie));
328
+ } catch (error) {
329
+ return errorResult(error);
330
+ }
331
+ }
332
+ );
333
+ server.registerTool(
334
+ "note_clear_cookie",
335
+ {
336
+ title: "Clear stored note.com cookie",
337
+ description: "Deletes the note-mcp config file cookie. Environment cookies are not modified.",
338
+ inputSchema: {}
339
+ },
340
+ async () => {
341
+ try {
342
+ return result(await clearStoredCookie());
343
+ } catch (error) {
344
+ return errorResult(error);
345
+ }
346
+ }
347
+ );
348
+ server.registerTool(
349
+ "note_login_help",
350
+ {
351
+ title: "Get note-mcp login help",
352
+ description: "Explains the supported note-mcp authentication setup paths.",
353
+ inputSchema: {}
354
+ },
355
+ async () => result({
356
+ recommended: "For local/desktop agents, call note_auth_login to open a browser login flow.",
357
+ advanced: "For servers/CI, provide NOTE_COOKIE / NOTE_SESSION_COOKIE or call note_set_cookie with a Cookie header obtained by a trusted operator.",
358
+ configFile: (await authStatus()).configPath,
359
+ cli: ["npx note-mcp auth", "npx note-mcp auth --status", "npx note-mcp auth --clear"]
360
+ })
361
+ );
362
+ server.registerTool(
363
+ "note_auth_check",
364
+ {
365
+ title: "Check note.com authentication",
366
+ description: "Checks whether configured note.com cookies can access note.com internal APIs.",
367
+ inputSchema: {}
368
+ },
369
+ async () => withClient((client) => client.authCheck())
370
+ );
371
+ server.registerTool(
372
+ "note_list_my_notes",
373
+ {
374
+ title: "List my note.com notes",
375
+ description: "Lists notes for the authenticated note.com account.",
376
+ inputSchema: {
377
+ page: z.number().int().positive().default(1)
378
+ }
379
+ },
380
+ async ({ page }) => withClient((client) => client.listMyNotes(page))
381
+ );
382
+ server.registerTool(
383
+ "note_list_drafts",
384
+ {
385
+ title: "List note.com drafts",
386
+ description: "Lists drafts for the authenticated note.com account. This uses an unofficial internal API and may need adjustment if note.com changes endpoints.",
387
+ inputSchema: {
388
+ page: z.number().int().positive().default(1)
389
+ }
390
+ },
391
+ async ({ page }) => withClient((client) => client.listDrafts(page))
392
+ );
393
+ server.registerTool(
394
+ "note_get_note",
395
+ {
396
+ title: "Get note.com note",
397
+ description: "Fetches a note by note key, e.g. n1a0b26f944f4.",
398
+ inputSchema: {
399
+ noteKey: z.string().min(1)
400
+ }
401
+ },
402
+ async ({ noteKey }) => withClient((client) => client.getNote(noteKey))
403
+ );
404
+ server.registerTool(
405
+ "note_create_draft",
406
+ {
407
+ title: "Create note.com draft",
408
+ description: "Creates a note.com draft with title/body/hashtags using an unofficial internal API.",
409
+ inputSchema: {
410
+ title: z.string().min(1),
411
+ body: z.string().min(1),
412
+ hashtags: z.array(z.string().min(1)).optional()
413
+ }
414
+ },
415
+ async ({ title, body, hashtags }) => withClient(
416
+ (client) => client.createDraft({
417
+ title,
418
+ body,
419
+ ...hashtags ? { hashtags } : {}
420
+ })
421
+ )
422
+ );
423
+ server.registerTool(
424
+ "note_update_draft",
425
+ {
426
+ title: "Update note.com draft",
427
+ description: "Updates a note.com draft by draft id using an unofficial internal API.",
428
+ inputSchema: {
429
+ draftId: z.string().min(1),
430
+ title: z.string().min(1),
431
+ body: z.string().min(1),
432
+ hashtags: z.array(z.string().min(1)).optional()
433
+ }
434
+ },
435
+ async ({ draftId, title, body, hashtags }) => withClient(
436
+ (client) => client.updateDraft({
437
+ draftId,
438
+ title,
439
+ body,
440
+ ...hashtags ? { hashtags } : {}
441
+ })
442
+ )
443
+ );
444
+ const transport = new StdioServerTransport();
445
+ await server.connect(transport);
446
+ }
447
+ async function withClient(fn) {
448
+ try {
449
+ const client = new NoteClient({ cookie: await readCookie() });
450
+ return result(await fn(client));
451
+ } catch (error) {
452
+ return errorResult(error);
453
+ }
118
454
  }
119
455
  function jsonText(value) {
120
456
  return JSON.stringify(value, null, 2);
@@ -130,134 +466,31 @@ function result(value) {
130
466
  };
131
467
  }
132
468
  function errorResult(error) {
133
- const detail = error instanceof NoteApiError ? { message: error.message, status: error.status, body: error.body } : { message: toErrorMessage(error) };
134
469
  return {
135
470
  isError: true,
136
471
  content: [
137
472
  {
138
473
  type: "text",
139
- text: jsonText(detail)
474
+ text: jsonText(errorDetail(error))
140
475
  }
141
476
  ]
142
477
  };
143
478
  }
144
- server.registerTool(
145
- "note_auth_check",
146
- {
147
- title: "Check note.com authentication",
148
- description: "Checks whether NOTE_COOKIE / NOTE_SESSION_COOKIE can access note.com internal APIs.",
149
- inputSchema: {}
150
- },
151
- async () => {
152
- try {
153
- return result(await createClient().authCheck());
154
- } catch (error) {
155
- return errorResult(error);
156
- }
479
+ function errorDetail(error) {
480
+ if (error instanceof AuthRequiredError) {
481
+ return {
482
+ error: "auth_required",
483
+ message: error.message,
484
+ suggestedTools: ["note_auth_login", "note_set_cookie"]
485
+ };
157
486
  }
158
- );
159
- server.registerTool(
160
- "note_list_my_notes",
161
- {
162
- title: "List my note.com notes",
163
- description: "Lists notes for the authenticated note.com account.",
164
- inputSchema: {
165
- page: z.number().int().positive().default(1)
166
- }
167
- },
168
- async ({ page }) => {
169
- try {
170
- return result(await createClient().listMyNotes(page));
171
- } catch (error) {
172
- return errorResult(error);
173
- }
487
+ if (error instanceof NoteApiError) {
488
+ return {
489
+ error: "note_api_error",
490
+ message: error.message,
491
+ status: error.status,
492
+ body: error.body
493
+ };
174
494
  }
175
- );
176
- server.registerTool(
177
- "note_list_drafts",
178
- {
179
- title: "List note.com drafts",
180
- description: "Lists drafts for the authenticated note.com account. This uses an unofficial internal API and may need adjustment if note.com changes endpoints.",
181
- inputSchema: {
182
- page: z.number().int().positive().default(1)
183
- }
184
- },
185
- async ({ page }) => {
186
- try {
187
- return result(await createClient().listDrafts(page));
188
- } catch (error) {
189
- return errorResult(error);
190
- }
191
- }
192
- );
193
- server.registerTool(
194
- "note_get_note",
195
- {
196
- title: "Get note.com note",
197
- description: "Fetches a note by note key, e.g. n1a0b26f944f4.",
198
- inputSchema: {
199
- noteKey: z.string().min(1)
200
- }
201
- },
202
- async ({ noteKey }) => {
203
- try {
204
- return result(await createClient().getNote(noteKey));
205
- } catch (error) {
206
- return errorResult(error);
207
- }
208
- }
209
- );
210
- server.registerTool(
211
- "note_create_draft",
212
- {
213
- title: "Create note.com draft",
214
- description: "Creates a note.com draft with title/body/hashtags using an unofficial internal API.",
215
- inputSchema: {
216
- title: z.string().min(1),
217
- body: z.string().min(1),
218
- hashtags: z.array(z.string().min(1)).optional()
219
- }
220
- },
221
- async ({ title, body, hashtags }) => {
222
- try {
223
- return result(
224
- await createClient().createDraft({
225
- title,
226
- body,
227
- ...hashtags ? { hashtags } : {}
228
- })
229
- );
230
- } catch (error) {
231
- return errorResult(error);
232
- }
233
- }
234
- );
235
- server.registerTool(
236
- "note_update_draft",
237
- {
238
- title: "Update note.com draft",
239
- description: "Updates a note.com draft by draft id using an unofficial internal API.",
240
- inputSchema: {
241
- draftId: z.string().min(1),
242
- title: z.string().min(1),
243
- body: z.string().min(1),
244
- hashtags: z.array(z.string().min(1)).optional()
245
- }
246
- },
247
- async ({ draftId, title, body, hashtags }) => {
248
- try {
249
- return result(
250
- await createClient().updateDraft({
251
- draftId,
252
- title,
253
- body,
254
- ...hashtags ? { hashtags } : {}
255
- })
256
- );
257
- } catch (error) {
258
- return errorResult(error);
259
- }
260
- }
261
- );
262
- var transport = new StdioServerTransport();
263
- await server.connect(transport);
495
+ return { error: "error", message: toErrorMessage(error) };
496
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "note-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Unofficial stdio MCP server for note.com using cookie-based internal APIs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "scripts": {
15
- "build": "tsup src/index.ts --format esm --dts --clean",
15
+ "build": "tsup src/index.ts --format esm --dts --clean --external playwright",
16
16
  "dev": "tsx src/index.ts",
17
17
  "lint": "eslint .",
18
18
  "format": "prettier --check .",
@@ -61,5 +61,8 @@
61
61
  "typescript": "^6.0.3",
62
62
  "typescript-eslint": "^8.61.1",
63
63
  "vitest": "^4.1.9"
64
+ },
65
+ "optionalDependencies": {
66
+ "playwright": "^1.61.0"
64
67
  }
65
68
  }