@mseep/affine-mcp-server 2.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.
Files changed (43) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +270 -0
  3. package/bin/affine-mcp +5 -0
  4. package/dist/auth.js +61 -0
  5. package/dist/cli.js +726 -0
  6. package/dist/config.js +178 -0
  7. package/dist/edgeless/layout.js +222 -0
  8. package/dist/graphqlClient.js +116 -0
  9. package/dist/httpAuth.js +147 -0
  10. package/dist/httpDiagnostics.js +38 -0
  11. package/dist/index.js +209 -0
  12. package/dist/markdown/parse.js +559 -0
  13. package/dist/markdown/render.js +227 -0
  14. package/dist/markdown/types.js +1 -0
  15. package/dist/oauth.js +154 -0
  16. package/dist/sse.js +261 -0
  17. package/dist/toolSurface.js +349 -0
  18. package/dist/tools/accessTokens.js +45 -0
  19. package/dist/tools/auth.js +18 -0
  20. package/dist/tools/blobStorage.js +136 -0
  21. package/dist/tools/comments.js +104 -0
  22. package/dist/tools/docs.js +7478 -0
  23. package/dist/tools/history.js +22 -0
  24. package/dist/tools/icons.js +125 -0
  25. package/dist/tools/notifications.js +79 -0
  26. package/dist/tools/organize.js +1145 -0
  27. package/dist/tools/properties.js +426 -0
  28. package/dist/tools/user.js +13 -0
  29. package/dist/tools/userCRUD.js +77 -0
  30. package/dist/tools/workspaces.js +322 -0
  31. package/dist/util/explorerIcon.js +95 -0
  32. package/dist/util/mcp.js +28 -0
  33. package/dist/ws.js +113 -0
  34. package/docs/assets/edgeless-canvas-demo-advanced-dark.png +0 -0
  35. package/docs/assets/edgeless-canvas-demo-advanced-light.png +0 -0
  36. package/docs/client-setup.md +174 -0
  37. package/docs/configuration-and-deployment.md +265 -0
  38. package/docs/edgeless-canvas-cookbook.md +226 -0
  39. package/docs/getting-started.md +229 -0
  40. package/docs/tool-reference.md +200 -0
  41. package/docs/workflow-recipes.md +147 -0
  42. package/package.json +118 -0
  43. package/tool-manifest.json +99 -0
package/dist/cli.js ADDED
@@ -0,0 +1,726 @@
1
+ import { fetch } from "undici";
2
+ import * as fs from "fs";
3
+ import * as readline from "readline";
4
+ import { CONFIG_FILE, loadConfig, loadConfigFile, validateBaseUrl, VERSION, writeConfigFile } from "./config.js";
5
+ import { loginWithPassword } from "./auth.js";
6
+ const CLI_FETCH_TIMEOUT_MS = 30_000;
7
+ class CliError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "CliError";
11
+ }
12
+ }
13
+ function ask(prompt, hidden = false) {
14
+ if (hidden && process.stdin.isTTY) {
15
+ return readHidden(prompt);
16
+ }
17
+ return new Promise((resolve) => {
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stderr,
21
+ terminal: process.stdin.isTTY ?? false,
22
+ });
23
+ rl.question(prompt, (answer) => {
24
+ rl.close();
25
+ resolve((answer || "").trim());
26
+ });
27
+ });
28
+ }
29
+ /** Read a line with echo disabled using raw-mode stdin (no private API hacks). */
30
+ function readHidden(prompt) {
31
+ return new Promise((resolve, reject) => {
32
+ process.stderr.write(prompt);
33
+ const buf = [];
34
+ process.stdin.setRawMode(true);
35
+ process.stdin.resume();
36
+ process.stdin.setEncoding("utf8");
37
+ const onData = (ch) => {
38
+ switch (ch) {
39
+ case "\r":
40
+ case "\n":
41
+ cleanup();
42
+ process.stderr.write("\n");
43
+ resolve(buf.join(""));
44
+ break;
45
+ case "\u0003":
46
+ cleanup();
47
+ process.stderr.write("\n");
48
+ reject(new CliError("Aborted."));
49
+ break;
50
+ case "\u007F":
51
+ case "\b":
52
+ buf.pop();
53
+ break;
54
+ default:
55
+ buf.push(ch);
56
+ }
57
+ };
58
+ const cleanup = () => {
59
+ process.stdin.setRawMode(false);
60
+ process.stdin.pause();
61
+ process.stdin.removeListener("data", onData);
62
+ };
63
+ process.stdin.on("data", onData);
64
+ });
65
+ }
66
+ async function gql(baseUrl, auth, query, variables) {
67
+ const headers = {
68
+ "Content-Type": "application/json",
69
+ "User-Agent": `affine-mcp-server/${VERSION}`,
70
+ };
71
+ if (auth.token)
72
+ headers.Authorization = `Bearer ${auth.token}`;
73
+ if (auth.cookie)
74
+ headers.Cookie = auth.cookie;
75
+ const body = { query };
76
+ if (variables)
77
+ body.variables = variables;
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), CLI_FETCH_TIMEOUT_MS);
80
+ let res;
81
+ try {
82
+ res = await fetch(`${baseUrl}/graphql`, {
83
+ method: "POST",
84
+ headers,
85
+ body: JSON.stringify(body),
86
+ signal: controller.signal,
87
+ });
88
+ }
89
+ catch (err) {
90
+ if (err.name === "AbortError") {
91
+ throw new Error(`Request timed out after ${CLI_FETCH_TIMEOUT_MS / 1000}s`);
92
+ }
93
+ throw err;
94
+ }
95
+ finally {
96
+ clearTimeout(timer);
97
+ }
98
+ if (!res.ok)
99
+ throw new Error(`HTTP ${res.status}`);
100
+ const json = await res.json();
101
+ if (json.errors)
102
+ throw new Error(json.errors.map((e) => e.message).join("; "));
103
+ return json.data;
104
+ }
105
+ function parseFlag(args, ...flags) {
106
+ return args.some((arg) => flags.includes(arg));
107
+ }
108
+ function consumeOption(args, flag) {
109
+ const index = args.indexOf(flag);
110
+ if (index === -1)
111
+ return undefined;
112
+ const value = args[index + 1];
113
+ if (!value || value.startsWith("--")) {
114
+ throw new CliError(`Missing value for '${flag}'.`);
115
+ }
116
+ args.splice(index, 2);
117
+ return value;
118
+ }
119
+ function consumeFlags(args, ...flags) {
120
+ let found = false;
121
+ for (const flag of flags) {
122
+ let index = args.indexOf(flag);
123
+ while (index !== -1) {
124
+ args.splice(index, 1);
125
+ found = true;
126
+ index = args.indexOf(flag);
127
+ }
128
+ }
129
+ return found;
130
+ }
131
+ function ensureNoUnexpectedArgs(args, command) {
132
+ if (args.length > 0) {
133
+ throw new CliError(`Unexpected arguments for '${command}': ${args.join(" ")}`);
134
+ }
135
+ }
136
+ function redactSecret(value) {
137
+ if (!value)
138
+ return null;
139
+ if (value.length <= 8)
140
+ return "*".repeat(value.length);
141
+ return `${value.slice(0, 4)}…${value.slice(-4)}`;
142
+ }
143
+ function getConfigValueSource(name, file, fallback) {
144
+ if (process.env[name])
145
+ return "env";
146
+ if (file[name])
147
+ return "config";
148
+ if (fallback !== undefined)
149
+ return "default";
150
+ return "unset";
151
+ }
152
+ function buildEffectiveConfigSummary() {
153
+ const stored = loadConfigFile();
154
+ const effective = loadConfig();
155
+ const authKind = effective.apiToken
156
+ ? "api-token"
157
+ : effective.cookie
158
+ ? "cookie"
159
+ : effective.email && effective.password
160
+ ? "email-password"
161
+ : "none";
162
+ return {
163
+ configFile: CONFIG_FILE,
164
+ configFileExists: fs.existsSync(CONFIG_FILE),
165
+ baseUrl: effective.baseUrl,
166
+ graphqlPath: effective.graphqlPath,
167
+ workspaceId: effective.defaultWorkspaceId || null,
168
+ authMode: effective.authMode,
169
+ authKind,
170
+ apiToken: effective.apiToken ? redactSecret(effective.apiToken) : null,
171
+ cookie: effective.cookie ? "(set)" : null,
172
+ email: effective.email || null,
173
+ publicBaseUrl: effective.publicBaseUrl || null,
174
+ oauthIssuerUrl: effective.oauthIssuerUrl || null,
175
+ oauthScopes: effective.oauthScopes,
176
+ sources: {
177
+ baseUrl: getConfigValueSource("AFFINE_BASE_URL", stored, "http://localhost:3010"),
178
+ apiToken: getConfigValueSource("AFFINE_API_TOKEN", stored),
179
+ cookie: getConfigValueSource("AFFINE_COOKIE", stored),
180
+ email: getConfigValueSource("AFFINE_EMAIL", stored),
181
+ password: getConfigValueSource("AFFINE_PASSWORD", stored),
182
+ workspaceId: getConfigValueSource("AFFINE_WORKSPACE_ID", stored),
183
+ authMode: getConfigValueSource("AFFINE_MCP_AUTH_MODE", stored, "bearer"),
184
+ publicBaseUrl: getConfigValueSource("AFFINE_MCP_PUBLIC_BASE_URL", stored),
185
+ oauthIssuerUrl: getConfigValueSource("AFFINE_OAUTH_ISSUER_URL", stored),
186
+ oauthScopes: getConfigValueSource("AFFINE_OAUTH_SCOPES", stored, "mcp"),
187
+ },
188
+ };
189
+ }
190
+ async function resolveCliAuth(baseUrl) {
191
+ const effective = loadConfig();
192
+ if (effective.apiToken) {
193
+ return { auth: { token: effective.apiToken }, authKind: "api-token" };
194
+ }
195
+ if (effective.cookie) {
196
+ return { auth: { cookie: effective.cookie }, authKind: "cookie" };
197
+ }
198
+ if (effective.email && effective.password) {
199
+ const { cookieHeader } = await loginWithPassword(baseUrl, effective.email, effective.password);
200
+ return { auth: { cookie: cookieHeader }, authKind: "email-password" };
201
+ }
202
+ throw new CliError("No authentication configured. Run 'affine-mcp login' or set AFFINE_API_TOKEN.");
203
+ }
204
+ async function inspectConnection(baseUrl, auth) {
205
+ const data = await gql(baseUrl, auth, "query { currentUser { name email } workspaces { id } }");
206
+ return {
207
+ userName: data.currentUser.name,
208
+ userEmail: data.currentUser.email,
209
+ workspaceCount: data.workspaces.length,
210
+ };
211
+ }
212
+ function printHelp(command) {
213
+ if (command) {
214
+ const definition = COMMANDS[command];
215
+ if (!definition) {
216
+ throw new CliError(`Unknown command '${command}'.`);
217
+ }
218
+ console.log(`${definition.usage}\n`);
219
+ console.log(definition.summary);
220
+ return;
221
+ }
222
+ console.log(`affine-mcp ${VERSION}`);
223
+ console.log("");
224
+ console.log("Usage:");
225
+ console.log(" affine-mcp Start the MCP server over stdio");
226
+ console.log(" affine-mcp <command> Run a CLI command");
227
+ console.log("");
228
+ console.log("Commands:");
229
+ for (const [name, definition] of Object.entries(COMMANDS)) {
230
+ console.log(` ${name.padEnd(12)} ${definition.summary}`);
231
+ }
232
+ console.log("");
233
+ console.log("Common examples:");
234
+ console.log(" affine-mcp login");
235
+ console.log(" affine-mcp status");
236
+ console.log(" affine-mcp doctor");
237
+ console.log(" affine-mcp show-config --json");
238
+ console.log(" affine-mcp snippet claude --env");
239
+ console.log(" affine-mcp --version");
240
+ console.log(" affine-mcp --help");
241
+ }
242
+ async function detectWorkspace(baseUrl, auth, preferredWorkspaceId) {
243
+ if (preferredWorkspaceId) {
244
+ console.error(`Using workspace override: ${preferredWorkspaceId}`);
245
+ return preferredWorkspaceId;
246
+ }
247
+ console.error("Detecting workspaces...");
248
+ try {
249
+ const data = await gql(baseUrl, auth, `query {
250
+ workspaces {
251
+ id createdAt memberCount
252
+ owner { name }
253
+ }
254
+ }`);
255
+ const workspaces = data.workspaces;
256
+ if (workspaces.length === 0) {
257
+ console.error(" No workspaces found.");
258
+ return "";
259
+ }
260
+ const formatWs = (w) => {
261
+ const owner = w.owner?.name || "unknown";
262
+ const members = w.memberCount ?? 0;
263
+ const date = w.createdAt ? new Date(w.createdAt).toLocaleDateString() : "";
264
+ const membersStr = members === 1 ? "1 member" : `${members} members`;
265
+ return `${w.id} (by ${owner}, ${membersStr}, ${date})`;
266
+ };
267
+ if (workspaces.length === 1) {
268
+ console.error(` Found 1 workspace: ${formatWs(workspaces[0])}`);
269
+ console.error(" Auto-selected.");
270
+ return workspaces[0].id;
271
+ }
272
+ console.error(` Found ${workspaces.length} workspaces:`);
273
+ workspaces.forEach((w, i) => console.error(` ${i + 1}) ${formatWs(w)}`));
274
+ const choice = (await ask(`\nSelect [1]: `)) || "1";
275
+ const idx = parseInt(choice, 10) - 1;
276
+ if (idx < 0 || idx >= workspaces.length) {
277
+ throw new CliError("Invalid selection.");
278
+ }
279
+ return workspaces[idx].id;
280
+ }
281
+ catch (err) {
282
+ if (err instanceof CliError)
283
+ throw err;
284
+ console.error(` Could not list workspaces: ${err.message}`);
285
+ return "";
286
+ }
287
+ }
288
+ async function loginWithEmail(baseUrl) {
289
+ const email = await ask("Email: ");
290
+ const password = await ask("Password: ", true);
291
+ if (!email || !password) {
292
+ throw new CliError("Email and password are required.");
293
+ }
294
+ console.error("Signing in...");
295
+ let cookieHeader;
296
+ try {
297
+ ({ cookieHeader } = await loginWithPassword(baseUrl, email, password));
298
+ }
299
+ catch (err) {
300
+ throw new CliError(`Sign-in failed: ${err.message}`);
301
+ }
302
+ const auth = { cookie: cookieHeader };
303
+ try {
304
+ const data = await gql(baseUrl, auth, "query { currentUser { name email } }");
305
+ console.error(`✓ Signed in as: ${data.currentUser.name} <${data.currentUser.email}>\n`);
306
+ }
307
+ catch (err) {
308
+ throw new CliError(`Session verification failed: ${err.message}`);
309
+ }
310
+ console.error("Generating API token...");
311
+ let token;
312
+ try {
313
+ const data = await gql(baseUrl, auth, `mutation($input: GenerateAccessTokenInput!) { generateUserAccessToken(input: $input) { id name token } }`, { input: { name: `affine-mcp-${new Date().toISOString().slice(0, 10)}` } });
314
+ token = data.generateUserAccessToken.token;
315
+ console.error(`✓ Token created (name: ${data.generateUserAccessToken.name})\n`);
316
+ }
317
+ catch (err) {
318
+ throw new CliError(`Failed to generate token: ${err.message}\n` +
319
+ "You can create one manually in Affine Settings → Integrations → MCP Server");
320
+ }
321
+ const workspaceId = await detectWorkspace(baseUrl, { token });
322
+ return { token, workspaceId };
323
+ }
324
+ async function loginWithToken(baseUrl) {
325
+ console.error("\nTo generate a token:");
326
+ console.error(` 1. Open ${baseUrl}/settings in your browser`);
327
+ console.error(" 2. Account Settings → Integrations → MCP Server");
328
+ console.error(" 3. Copy the Personal access token\n");
329
+ const token = await ask("API token: ", true);
330
+ if (!token) {
331
+ throw new CliError("No token provided.");
332
+ }
333
+ console.error("Testing connection...");
334
+ try {
335
+ const data = await gql(baseUrl, { token }, "query { currentUser { name email } }");
336
+ console.error(`✓ Authenticated as: ${data.currentUser.name} <${data.currentUser.email}>\n`);
337
+ }
338
+ catch (err) {
339
+ throw new CliError(`Authentication failed: ${err.message}`);
340
+ }
341
+ const workspaceId = await detectWorkspace(baseUrl, { token });
342
+ return { token, workspaceId };
343
+ }
344
+ async function login(args) {
345
+ const parsedArgs = [...args];
346
+ const providedUrl = consumeOption(parsedArgs, "--url");
347
+ const providedToken = consumeOption(parsedArgs, "--token");
348
+ const providedWorkspaceId = consumeOption(parsedArgs, "--workspace-id");
349
+ const force = consumeFlags(parsedArgs, "--force", "-f");
350
+ ensureNoUnexpectedArgs(parsedArgs, "login");
351
+ console.error("Affine MCP Server — Login\n");
352
+ const existing = loadConfigFile();
353
+ if (existing.AFFINE_API_TOKEN) {
354
+ console.error(`Existing config: ${CONFIG_FILE}`);
355
+ console.error(` URL: ${existing.AFFINE_BASE_URL || "(default)"}`);
356
+ console.error(" Token: (set)");
357
+ console.error(` Workspace: ${existing.AFFINE_WORKSPACE_ID || "(none)"}\n`);
358
+ if (!force) {
359
+ const overwrite = await ask("Overwrite? [y/N] ");
360
+ if (!/^[yY]$/.test(overwrite)) {
361
+ console.error("Keeping existing config.");
362
+ return;
363
+ }
364
+ console.error("");
365
+ }
366
+ else {
367
+ console.error("Overwriting existing config (--force).\n");
368
+ }
369
+ }
370
+ const defaultUrl = "https://app.affine.pro";
371
+ const rawUrl = providedUrl ?? ((await ask(`Affine URL [${defaultUrl}]: `)) || defaultUrl);
372
+ const baseUrl = validateBaseUrl(rawUrl);
373
+ let result;
374
+ if (providedToken) {
375
+ console.error("Testing provided token...");
376
+ try {
377
+ const info = await inspectConnection(baseUrl, { token: providedToken });
378
+ console.error(`✓ Authenticated as: ${info.userName} <${info.userEmail}>\n`);
379
+ }
380
+ catch (err) {
381
+ throw new CliError(`Authentication failed: ${err.message}`);
382
+ }
383
+ result = {
384
+ token: providedToken,
385
+ workspaceId: await detectWorkspace(baseUrl, { token: providedToken }, providedWorkspaceId),
386
+ };
387
+ }
388
+ else {
389
+ const isSelfHosted = !baseUrl.includes("affine.pro");
390
+ if (isSelfHosted) {
391
+ const method = await ask("\nAuth method — [1] Email/password (recommended) [2] Paste API token: ");
392
+ const loginResult = method === "2" ? await loginWithToken(baseUrl) : await loginWithEmail(baseUrl);
393
+ result = {
394
+ ...loginResult,
395
+ workspaceId: providedWorkspaceId || loginResult.workspaceId,
396
+ };
397
+ }
398
+ else {
399
+ const loginResult = await loginWithToken(baseUrl);
400
+ result = {
401
+ ...loginResult,
402
+ workspaceId: providedWorkspaceId || loginResult.workspaceId,
403
+ };
404
+ }
405
+ }
406
+ writeConfigFile({
407
+ AFFINE_BASE_URL: baseUrl,
408
+ AFFINE_API_TOKEN: result.token,
409
+ AFFINE_WORKSPACE_ID: result.workspaceId,
410
+ });
411
+ console.error(`\n✓ Saved to ${CONFIG_FILE} (mode 600)`);
412
+ console.error("The MCP server will use these credentials automatically.");
413
+ }
414
+ async function status(args) {
415
+ const parsedArgs = [...args];
416
+ const asJson = consumeFlags(parsedArgs, "--json");
417
+ ensureNoUnexpectedArgs(parsedArgs, "status");
418
+ const config = loadConfigFile();
419
+ if (!config.AFFINE_API_TOKEN) {
420
+ throw new CliError("Not logged in. Run: affine-mcp login");
421
+ }
422
+ try {
423
+ const inspection = await inspectConnection(config.AFFINE_BASE_URL || "https://app.affine.pro", { token: config.AFFINE_API_TOKEN });
424
+ if (asJson) {
425
+ console.log(JSON.stringify({
426
+ configFile: CONFIG_FILE,
427
+ baseUrl: config.AFFINE_BASE_URL || "https://app.affine.pro",
428
+ workspaceId: config.AFFINE_WORKSPACE_ID || null,
429
+ userName: inspection.userName,
430
+ userEmail: inspection.userEmail,
431
+ workspaceCount: inspection.workspaceCount,
432
+ }, null, 2));
433
+ return;
434
+ }
435
+ console.error(`Config: ${CONFIG_FILE}`);
436
+ console.error(`URL: ${config.AFFINE_BASE_URL || "(default)"}`);
437
+ console.error("Token: (set)");
438
+ console.error(`Workspace: ${config.AFFINE_WORKSPACE_ID || "(none)"}\n`);
439
+ console.error(`User: ${inspection.userName} <${inspection.userEmail}>`);
440
+ console.error(`Workspaces: ${inspection.workspaceCount}`);
441
+ }
442
+ catch (err) {
443
+ throw new CliError(`Connection failed: ${err.message}`);
444
+ }
445
+ }
446
+ function logout(args) {
447
+ ensureNoUnexpectedArgs(args, "logout");
448
+ if (fs.existsSync(CONFIG_FILE)) {
449
+ fs.unlinkSync(CONFIG_FILE);
450
+ console.error(`Removed ${CONFIG_FILE}`);
451
+ }
452
+ else {
453
+ console.error("No config file found.");
454
+ }
455
+ }
456
+ function configPath(args) {
457
+ ensureNoUnexpectedArgs(args, "config-path");
458
+ console.log(CONFIG_FILE);
459
+ }
460
+ function showConfig(args) {
461
+ const parsedArgs = [...args];
462
+ const asJson = consumeFlags(parsedArgs, "--json");
463
+ ensureNoUnexpectedArgs(parsedArgs, "show-config");
464
+ const summary = buildEffectiveConfigSummary();
465
+ if (asJson) {
466
+ console.log(JSON.stringify(summary, null, 2));
467
+ return;
468
+ }
469
+ console.log(`Config file: ${summary.configFile} (${summary.configFileExists ? "found" : "missing"})`);
470
+ console.log(`Base URL: ${summary.baseUrl} (${summary.sources.baseUrl})`);
471
+ console.log(`GraphQL path: ${summary.graphqlPath}`);
472
+ console.log(`Auth mode: ${summary.authMode} (${summary.sources.authMode})`);
473
+ console.log(`Auth kind: ${summary.authKind}`);
474
+ console.log(`Workspace: ${summary.workspaceId || "(none)"} (${summary.sources.workspaceId})`);
475
+ if (summary.apiToken)
476
+ console.log(`API token: ${summary.apiToken} (${summary.sources.apiToken})`);
477
+ if (summary.cookie)
478
+ console.log(`Cookie: ${summary.cookie} (${summary.sources.cookie})`);
479
+ if (summary.email)
480
+ console.log(`Email: ${summary.email} (${summary.sources.email})`);
481
+ if (summary.publicBaseUrl)
482
+ console.log(`Public base URL: ${summary.publicBaseUrl} (${summary.sources.publicBaseUrl})`);
483
+ if (summary.oauthIssuerUrl)
484
+ console.log(`OAuth issuer URL: ${summary.oauthIssuerUrl} (${summary.sources.oauthIssuerUrl})`);
485
+ if (summary.authMode === "oauth")
486
+ console.log(`OAuth scopes: ${summary.oauthScopes.join(", ")} (${summary.sources.oauthScopes})`);
487
+ }
488
+ async function doctor(args) {
489
+ const parsedArgs = [...args];
490
+ const asJson = consumeFlags(parsedArgs, "--json");
491
+ ensureNoUnexpectedArgs(parsedArgs, "doctor");
492
+ const summary = buildEffectiveConfigSummary();
493
+ const checks = [];
494
+ checks.push({
495
+ name: "config-file",
496
+ ok: summary.configFileExists,
497
+ detail: summary.configFileExists ? summary.configFile : "No saved config file found",
498
+ });
499
+ let authKind = "none";
500
+ try {
501
+ const { auth, authKind: resolvedAuthKind } = await resolveCliAuth(summary.baseUrl);
502
+ authKind = resolvedAuthKind;
503
+ checks.push({
504
+ name: "auth-configured",
505
+ ok: true,
506
+ detail: `Using ${resolvedAuthKind}`,
507
+ });
508
+ const healthController = new AbortController();
509
+ const healthTimer = setTimeout(() => healthController.abort(), CLI_FETCH_TIMEOUT_MS);
510
+ try {
511
+ const response = await fetch(summary.baseUrl, { signal: healthController.signal });
512
+ checks.push({
513
+ name: "base-url",
514
+ ok: response.ok,
515
+ detail: `HTTP ${response.status}`,
516
+ });
517
+ }
518
+ catch (err) {
519
+ checks.push({
520
+ name: "base-url",
521
+ ok: false,
522
+ detail: err?.message || "Could not reach base URL",
523
+ });
524
+ }
525
+ finally {
526
+ clearTimeout(healthTimer);
527
+ }
528
+ try {
529
+ const data = await inspectConnection(summary.baseUrl, auth);
530
+ checks.push({
531
+ name: "graphql-auth",
532
+ ok: true,
533
+ detail: `${data.userEmail} (${data.workspaceCount} workspace(s))`,
534
+ });
535
+ }
536
+ catch (err) {
537
+ checks.push({
538
+ name: "graphql-auth",
539
+ ok: false,
540
+ detail: err?.message || "GraphQL auth failed",
541
+ });
542
+ }
543
+ }
544
+ catch (err) {
545
+ checks.push({
546
+ name: "auth-configured",
547
+ ok: false,
548
+ detail: err?.message || "No authentication configured",
549
+ });
550
+ }
551
+ if (summary.authMode === "oauth") {
552
+ const oauthReady = Boolean(summary.publicBaseUrl && summary.oauthIssuerUrl && summary.oauthScopes.length > 0);
553
+ checks.push({
554
+ name: "oauth-config",
555
+ ok: oauthReady,
556
+ detail: oauthReady
557
+ ? `${summary.publicBaseUrl} -> ${summary.oauthIssuerUrl}`
558
+ : "OAuth mode requires AFFINE_MCP_PUBLIC_BASE_URL and AFFINE_OAUTH_ISSUER_URL",
559
+ });
560
+ }
561
+ const ok = checks.every((check) => check.ok);
562
+ if (asJson) {
563
+ console.log(JSON.stringify({
564
+ ok,
565
+ config: summary,
566
+ checks,
567
+ authKind,
568
+ }, null, 2));
569
+ if (!ok)
570
+ process.exit(1);
571
+ return;
572
+ }
573
+ console.log(`Doctor: ${ok ? "OK" : "FAILED"}`);
574
+ console.log(`Base URL: ${summary.baseUrl}`);
575
+ console.log(`Auth mode: ${summary.authMode}`);
576
+ for (const check of checks) {
577
+ console.log(`${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
578
+ }
579
+ if (!ok) {
580
+ throw new CliError("Doctor checks failed.");
581
+ }
582
+ }
583
+ function getSnippetEnv() {
584
+ const effective = loadConfig();
585
+ const env = {};
586
+ if (effective.baseUrl)
587
+ env.AFFINE_BASE_URL = effective.baseUrl;
588
+ if (effective.apiToken)
589
+ env.AFFINE_API_TOKEN = effective.apiToken;
590
+ if (effective.defaultWorkspaceId)
591
+ env.AFFINE_WORKSPACE_ID = effective.defaultWorkspaceId;
592
+ if (effective.authMode === "oauth") {
593
+ env.AFFINE_MCP_AUTH_MODE = "oauth";
594
+ if (effective.publicBaseUrl)
595
+ env.AFFINE_MCP_PUBLIC_BASE_URL = effective.publicBaseUrl;
596
+ if (effective.oauthIssuerUrl)
597
+ env.AFFINE_OAUTH_ISSUER_URL = effective.oauthIssuerUrl;
598
+ if (effective.oauthScopes.length > 0)
599
+ env.AFFINE_OAUTH_SCOPES = effective.oauthScopes.join(" ");
600
+ }
601
+ return env;
602
+ }
603
+ function snippet(args) {
604
+ const parsedArgs = [...args];
605
+ const includeEnv = consumeFlags(parsedArgs, "--env");
606
+ const target = parsedArgs[0];
607
+ if (!target) {
608
+ throw new CliError("Usage: affine-mcp snippet <claude|cursor|codex> [--env]");
609
+ }
610
+ ensureNoUnexpectedArgs(parsedArgs.slice(1), "snippet");
611
+ const env = includeEnv ? getSnippetEnv() : undefined;
612
+ if (target === "all") {
613
+ const payload = {
614
+ claude: {
615
+ mcpServers: {
616
+ affine: {
617
+ command: "affine-mcp",
618
+ ...(env && Object.keys(env).length > 0 ? { env } : {}),
619
+ },
620
+ },
621
+ },
622
+ cursor: {
623
+ mcpServers: {
624
+ affine: {
625
+ command: "affine-mcp",
626
+ ...(env && Object.keys(env).length > 0 ? { env } : {}),
627
+ },
628
+ },
629
+ },
630
+ codex: env && Object.keys(env).length > 0
631
+ ? `codex mcp add affine ${Object.entries(env).map(([key, value]) => `--env ${key}=${JSON.stringify(value)}`).join(" ")} -- affine-mcp`
632
+ : "codex mcp add affine -- affine-mcp",
633
+ };
634
+ console.log(JSON.stringify(payload, null, 2));
635
+ return;
636
+ }
637
+ if (target === "claude" || target === "cursor") {
638
+ const payload = {
639
+ mcpServers: {
640
+ affine: {
641
+ command: "affine-mcp",
642
+ ...(env && Object.keys(env).length > 0 ? { env } : {}),
643
+ },
644
+ },
645
+ };
646
+ console.log(JSON.stringify(payload, null, 2));
647
+ return;
648
+ }
649
+ if (target === "codex") {
650
+ if (!env || Object.keys(env).length === 0) {
651
+ console.log("codex mcp add affine -- affine-mcp");
652
+ return;
653
+ }
654
+ const envArgs = Object.entries(env)
655
+ .map(([key, value]) => `--env ${key}=${JSON.stringify(value)}`)
656
+ .join(" ");
657
+ console.log(`codex mcp add affine ${envArgs} -- affine-mcp`);
658
+ return;
659
+ }
660
+ throw new CliError(`Unknown snippet target '${target}'. Expected claude, cursor, codex, or all.`);
661
+ }
662
+ function help(args) {
663
+ if (args.length > 1) {
664
+ throw new CliError("Usage: affine-mcp help [command]");
665
+ }
666
+ printHelp(args[0]);
667
+ }
668
+ const COMMANDS = {
669
+ help: {
670
+ summary: "Show CLI help",
671
+ usage: "affine-mcp help [command]",
672
+ handler: help,
673
+ },
674
+ login: {
675
+ summary: "Interactive login and config bootstrap",
676
+ usage: "affine-mcp login [--url <url>] [--token <token>] [--workspace-id <id>] [--force]",
677
+ handler: login,
678
+ },
679
+ status: {
680
+ summary: "Test the saved config and print current user info",
681
+ usage: "affine-mcp status [--json]",
682
+ handler: status,
683
+ },
684
+ logout: {
685
+ summary: "Remove the saved config file",
686
+ usage: "affine-mcp logout",
687
+ handler: logout,
688
+ },
689
+ "config-path": {
690
+ summary: "Print the config file path",
691
+ usage: "affine-mcp config-path",
692
+ handler: configPath,
693
+ },
694
+ "show-config": {
695
+ summary: "Print the effective config (redacted)",
696
+ usage: "affine-mcp show-config [--json]",
697
+ handler: showConfig,
698
+ },
699
+ doctor: {
700
+ summary: "Run local config and connectivity diagnostics",
701
+ usage: "affine-mcp doctor [--json]",
702
+ handler: doctor,
703
+ },
704
+ snippet: {
705
+ summary: "Print ready-to-paste Claude/Cursor/Codex snippets",
706
+ usage: "affine-mcp snippet <claude|cursor|codex|all> [--env]",
707
+ handler: snippet,
708
+ },
709
+ };
710
+ export async function runCli(command, args = []) {
711
+ const normalizedCommand = command.trim().toLowerCase();
712
+ const definition = COMMANDS[normalizedCommand];
713
+ if (!definition)
714
+ return false;
715
+ try {
716
+ await definition.handler(args);
717
+ }
718
+ catch (err) {
719
+ if (err instanceof CliError) {
720
+ console.error(`✗ ${err.message}`);
721
+ process.exit(1);
722
+ }
723
+ throw err;
724
+ }
725
+ return true;
726
+ }