@ainote/mcp 1.3.2 → 1.3.3
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/LICENSE +20 -20
- package/README-ko.md +374 -374
- package/README.md +800 -800
- package/index.js +61 -61
- package/lib/auth/device-flow.js +285 -274
- package/lib/auth/mcp-key.js +40 -40
- package/lib/auth/oauth.js +18 -18
- package/lib/auth/refresh.js +78 -78
- package/lib/auth/strategy-factory.js +31 -31
- package/lib/auth/token-store.js +302 -286
- package/lib/cli/call.js +211 -211
- package/lib/cli/login.js +238 -238
- package/lib/cli/logout.js +39 -39
- package/lib/cli/signup.js +118 -118
- package/lib/cli/whoami.js +77 -77
- package/lib/core/api-client.js +1 -1
- package/lib/core/config-manager.js +106 -106
- package/lib/core/config.js +1 -1
- package/lib/core/mcp-server.js +2 -2
- package/lib/core/server-factory.js +82 -82
- package/lib/core/tool-registry.js +31 -31
- package/lib/tools/chatgpt-tools.js +93 -93
- package/lib/tools/dev-doc-tools.js +643 -643
- package/lib/tools/index.js +5 -5
- package/lib/tools/internal/claude-scanner.js +150 -150
- package/lib/tools/internal/encryption.js +88 -88
- package/lib/tools/internal/formatters.js +80 -80
- package/lib/tools/internal/keychain.js +98 -98
- package/lib/tools/internal/path-mapper.js +221 -221
- package/lib/tools/onboarding-tools.js +124 -124
- package/lib/tools/shared-tools.js +280 -280
- package/lib/utils/api-client.js +152 -152
- package/package.json +101 -101
- package/server.js +99 -99
package/lib/cli/login.js
CHANGED
|
@@ -1,238 +1,238 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ainote-mcp login` — interactive device-authorization grant flow.
|
|
3
|
-
*
|
|
4
|
-
* UX:
|
|
5
|
-
* 1. Request device code from server.
|
|
6
|
-
* 2. Display the user_code in an ASCII box.
|
|
7
|
-
* 3. Print verification URL and try to open it in a browser (unless --no-browser).
|
|
8
|
-
* 4. Spin while polling for token.
|
|
9
|
-
* 5. On success, persist to keychain/file.
|
|
10
|
-
*
|
|
11
|
-
* Flags:
|
|
12
|
-
* --scope=mcp,read,write Comma-separated scope list (default: mcp,read,write).
|
|
13
|
-
* --no-browser Don't auto-open the verification URL.
|
|
14
|
-
* --client-name=<name> Override client_name shown on the consent screen.
|
|
15
|
-
*
|
|
16
|
-
* SIGINT cancels the in-flight authorization (best effort) and exits 1.
|
|
17
|
-
*/
|
|
18
|
-
import process from 'node:process';
|
|
19
|
-
import os from 'node:os';
|
|
20
|
-
import {
|
|
21
|
-
requestDeviceCode,
|
|
22
|
-
pollForToken,
|
|
23
|
-
cancel,
|
|
24
|
-
DeviceAccessDeniedError,
|
|
25
|
-
DeviceExpiredError,
|
|
26
|
-
DevicePkceError,
|
|
27
|
-
DeviceNetworkError
|
|
28
|
-
} from '../auth/device-flow.js';
|
|
29
|
-
import { setCredentials, _credentialsLocations } from '../auth/token-store.js';
|
|
30
|
-
|
|
31
|
-
const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
|
|
32
|
-
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
33
|
-
|
|
34
|
-
function parseArgs(argv) {
|
|
35
|
-
const args = {
|
|
36
|
-
scope: 'mcp,read,write',
|
|
37
|
-
noBrowser: false,
|
|
38
|
-
clientName: `ainote-mcp CLI (${os.hostname()})`
|
|
39
|
-
};
|
|
40
|
-
for (const arg of argv) {
|
|
41
|
-
if (arg === '--no-browser') {
|
|
42
|
-
args.noBrowser = true;
|
|
43
|
-
} else if (arg.startsWith('--scope=')) {
|
|
44
|
-
args.scope = arg.slice('--scope='.length);
|
|
45
|
-
} else if (arg === '--scope') {
|
|
46
|
-
// next-token form is not supported in this minimal parser
|
|
47
|
-
args.scope = args.scope;
|
|
48
|
-
} else if (arg.startsWith('--client-name=')) {
|
|
49
|
-
args.clientName = arg.slice('--client-name='.length);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return args;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function drawUserCodeBox(userCode) {
|
|
56
|
-
const padded = ` ${userCode} `;
|
|
57
|
-
const width = padded.length;
|
|
58
|
-
const top = `┌${'─'.repeat(width)}┐`;
|
|
59
|
-
const mid = `│${padded}│`;
|
|
60
|
-
const bot = `└${'─'.repeat(width)}┘`;
|
|
61
|
-
return `${top}\n${mid}\n${bot}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
class Spinner {
|
|
65
|
-
constructor() {
|
|
66
|
-
this.frame = 0;
|
|
67
|
-
this.suffix = '';
|
|
68
|
-
this.timer = null;
|
|
69
|
-
this.active = false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
start(initialSuffix = '') {
|
|
73
|
-
if (this.active) return;
|
|
74
|
-
this.active = true;
|
|
75
|
-
this.suffix = initialSuffix;
|
|
76
|
-
this.timer = setInterval(() => this._render(), 100);
|
|
77
|
-
this._render();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
update(suffix) {
|
|
81
|
-
this.suffix = suffix;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
_render() {
|
|
85
|
-
const frame = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
86
|
-
this.frame += 1;
|
|
87
|
-
if (process.stderr.isTTY) {
|
|
88
|
-
process.stderr.write(`\r${frame} ${this.suffix}\x1b[K`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
stop(finalLine) {
|
|
93
|
-
if (!this.active) return;
|
|
94
|
-
this.active = false;
|
|
95
|
-
if (this.timer) {
|
|
96
|
-
clearInterval(this.timer);
|
|
97
|
-
this.timer = null;
|
|
98
|
-
}
|
|
99
|
-
if (process.stderr.isTTY) {
|
|
100
|
-
process.stderr.write('\r\x1b[K');
|
|
101
|
-
}
|
|
102
|
-
if (finalLine) {
|
|
103
|
-
process.stderr.write(`${finalLine}\n`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function tryOpenBrowser(uri) {
|
|
109
|
-
try {
|
|
110
|
-
const mod = await import('open');
|
|
111
|
-
const open = mod.default || mod;
|
|
112
|
-
await open(uri);
|
|
113
|
-
return true;
|
|
114
|
-
} catch (err) {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function run(argv = process.argv.slice(3)) {
|
|
120
|
-
const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
121
|
-
const args = parseArgs(argv);
|
|
122
|
-
const scopes = args.scope.split(',').map((s) => s.trim()).filter(Boolean);
|
|
123
|
-
|
|
124
|
-
console.log('');
|
|
125
|
-
console.log(` AI Note MCP — Login`);
|
|
126
|
-
console.log(` API: ${apiUrl}`);
|
|
127
|
-
console.log('');
|
|
128
|
-
|
|
129
|
-
let device;
|
|
130
|
-
try {
|
|
131
|
-
device = await requestDeviceCode({
|
|
132
|
-
apiUrl,
|
|
133
|
-
clientName: args.clientName,
|
|
134
|
-
scopes
|
|
135
|
-
});
|
|
136
|
-
} catch (err) {
|
|
137
|
-
if (err instanceof DeviceNetworkError) {
|
|
138
|
-
console.error(` Network error: ${err.message}`);
|
|
139
|
-
} else {
|
|
140
|
-
console.error(` Failed to start login: ${err.message}`);
|
|
141
|
-
}
|
|
142
|
-
process.exit(1);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
console.log(` Visit the URL below and enter the code:`);
|
|
147
|
-
console.log('');
|
|
148
|
-
console.log(drawUserCodeBox(device.user_code));
|
|
149
|
-
console.log('');
|
|
150
|
-
console.log(` ${device.verification_uri}`);
|
|
151
|
-
if (device.verification_uri_complete && device.verification_uri_complete !== device.verification_uri) {
|
|
152
|
-
console.log(` (direct link: ${device.verification_uri_complete})`);
|
|
153
|
-
}
|
|
154
|
-
console.log('');
|
|
155
|
-
|
|
156
|
-
if (!args.noBrowser) {
|
|
157
|
-
const target = device.verification_uri_complete || device.verification_uri;
|
|
158
|
-
const opened = await tryOpenBrowser(target);
|
|
159
|
-
if (opened) {
|
|
160
|
-
console.log(' Opening your browser…');
|
|
161
|
-
} else {
|
|
162
|
-
console.log(' (Could not open browser automatically — please open the URL above.)');
|
|
163
|
-
}
|
|
164
|
-
console.log('');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const spinner = new Spinner();
|
|
168
|
-
spinner.start(`Waiting for authorization… (0s elapsed, next poll in ${device.interval}s)`);
|
|
169
|
-
|
|
170
|
-
let cancelled = false;
|
|
171
|
-
const sigintHandler = async () => {
|
|
172
|
-
if (cancelled) return;
|
|
173
|
-
cancelled = true;
|
|
174
|
-
spinner.stop('Cancelled.');
|
|
175
|
-
await cancel({ apiUrl, deviceCode: device.device_code });
|
|
176
|
-
process.exit(1);
|
|
177
|
-
};
|
|
178
|
-
process.once('SIGINT', sigintHandler);
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const tokens = await pollForToken({
|
|
182
|
-
apiUrl,
|
|
183
|
-
deviceCode: device.device_code,
|
|
184
|
-
codeVerifier: device.code_verifier,
|
|
185
|
-
interval: device.interval,
|
|
186
|
-
expiresIn: device.expires_in,
|
|
187
|
-
onTick: ({ elapsed, interval }) => {
|
|
188
|
-
spinner.update(`Waiting for authorization… (${elapsed}s elapsed, next poll in ${interval}s)`);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
spinner.stop();
|
|
193
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
194
|
-
|
|
195
|
-
const expiresAt = tokens.expires_in
|
|
196
|
-
? Date.now() + Number(tokens.expires_in) * 1000
|
|
197
|
-
: undefined;
|
|
198
|
-
|
|
199
|
-
const persisted = await setCredentials(apiUrl, {
|
|
200
|
-
mcpKey: tokens.mcp_key,
|
|
201
|
-
accessToken: tokens.access_token,
|
|
202
|
-
refreshToken: tokens.refresh_token,
|
|
203
|
-
expiresAt
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
const email = tokens.user?.email;
|
|
207
|
-
const locationDesc = persisted.storage === 'keychain'
|
|
208
|
-
? 'OS keychain'
|
|
209
|
-
: persisted.storage === 'mixed'
|
|
210
|
-
? `OS keychain + ${persisted.file}`
|
|
211
|
-
: persisted.file || _credentialsLocations().file;
|
|
212
|
-
|
|
213
|
-
console.log('');
|
|
214
|
-
if (email) {
|
|
215
|
-
console.log(` ✓ Logged in as ${email}.`);
|
|
216
|
-
} else {
|
|
217
|
-
console.log(` ✓ Logged in.`);
|
|
218
|
-
}
|
|
219
|
-
console.log(` Credentials saved to ${locationDesc}.`);
|
|
220
|
-
console.log('');
|
|
221
|
-
} catch (err) {
|
|
222
|
-
spinner.stop();
|
|
223
|
-
process.removeListener('SIGINT', sigintHandler);
|
|
224
|
-
|
|
225
|
-
if (err instanceof DeviceAccessDeniedError) {
|
|
226
|
-
console.error(' Access denied. The authorization request was rejected.');
|
|
227
|
-
} else if (err instanceof DeviceExpiredError) {
|
|
228
|
-
console.error(' Code expired. Please run `ainote-mcp login` again.');
|
|
229
|
-
} else if (err instanceof DevicePkceError) {
|
|
230
|
-
console.error(' Authentication failed (PKCE mismatch). Please retry.');
|
|
231
|
-
} else if (err instanceof DeviceNetworkError) {
|
|
232
|
-
console.error(` Network error: ${err.message}`);
|
|
233
|
-
} else {
|
|
234
|
-
console.error(` Login failed: ${err.message}`);
|
|
235
|
-
}
|
|
236
|
-
process.exit(1);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `ainote-mcp login` — interactive device-authorization grant flow.
|
|
3
|
+
*
|
|
4
|
+
* UX:
|
|
5
|
+
* 1. Request device code from server.
|
|
6
|
+
* 2. Display the user_code in an ASCII box.
|
|
7
|
+
* 3. Print verification URL and try to open it in a browser (unless --no-browser).
|
|
8
|
+
* 4. Spin while polling for token.
|
|
9
|
+
* 5. On success, persist to keychain/file.
|
|
10
|
+
*
|
|
11
|
+
* Flags:
|
|
12
|
+
* --scope=mcp,read,write Comma-separated scope list (default: mcp,read,write).
|
|
13
|
+
* --no-browser Don't auto-open the verification URL.
|
|
14
|
+
* --client-name=<name> Override client_name shown on the consent screen.
|
|
15
|
+
*
|
|
16
|
+
* SIGINT cancels the in-flight authorization (best effort) and exits 1.
|
|
17
|
+
*/
|
|
18
|
+
import process from 'node:process';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import {
|
|
21
|
+
requestDeviceCode,
|
|
22
|
+
pollForToken,
|
|
23
|
+
cancel,
|
|
24
|
+
DeviceAccessDeniedError,
|
|
25
|
+
DeviceExpiredError,
|
|
26
|
+
DevicePkceError,
|
|
27
|
+
DeviceNetworkError
|
|
28
|
+
} from '../auth/device-flow.js';
|
|
29
|
+
import { setCredentials, _credentialsLocations } from '../auth/token-store.js';
|
|
30
|
+
|
|
31
|
+
const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
|
|
32
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const args = {
|
|
36
|
+
scope: 'mcp,read,write',
|
|
37
|
+
noBrowser: false,
|
|
38
|
+
clientName: `ainote-mcp CLI (${os.hostname()})`
|
|
39
|
+
};
|
|
40
|
+
for (const arg of argv) {
|
|
41
|
+
if (arg === '--no-browser') {
|
|
42
|
+
args.noBrowser = true;
|
|
43
|
+
} else if (arg.startsWith('--scope=')) {
|
|
44
|
+
args.scope = arg.slice('--scope='.length);
|
|
45
|
+
} else if (arg === '--scope') {
|
|
46
|
+
// next-token form is not supported in this minimal parser
|
|
47
|
+
args.scope = args.scope;
|
|
48
|
+
} else if (arg.startsWith('--client-name=')) {
|
|
49
|
+
args.clientName = arg.slice('--client-name='.length);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function drawUserCodeBox(userCode) {
|
|
56
|
+
const padded = ` ${userCode} `;
|
|
57
|
+
const width = padded.length;
|
|
58
|
+
const top = `┌${'─'.repeat(width)}┐`;
|
|
59
|
+
const mid = `│${padded}│`;
|
|
60
|
+
const bot = `└${'─'.repeat(width)}┘`;
|
|
61
|
+
return `${top}\n${mid}\n${bot}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Spinner {
|
|
65
|
+
constructor() {
|
|
66
|
+
this.frame = 0;
|
|
67
|
+
this.suffix = '';
|
|
68
|
+
this.timer = null;
|
|
69
|
+
this.active = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
start(initialSuffix = '') {
|
|
73
|
+
if (this.active) return;
|
|
74
|
+
this.active = true;
|
|
75
|
+
this.suffix = initialSuffix;
|
|
76
|
+
this.timer = setInterval(() => this._render(), 100);
|
|
77
|
+
this._render();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
update(suffix) {
|
|
81
|
+
this.suffix = suffix;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_render() {
|
|
85
|
+
const frame = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
|
|
86
|
+
this.frame += 1;
|
|
87
|
+
if (process.stderr.isTTY) {
|
|
88
|
+
process.stderr.write(`\r${frame} ${this.suffix}\x1b[K`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
stop(finalLine) {
|
|
93
|
+
if (!this.active) return;
|
|
94
|
+
this.active = false;
|
|
95
|
+
if (this.timer) {
|
|
96
|
+
clearInterval(this.timer);
|
|
97
|
+
this.timer = null;
|
|
98
|
+
}
|
|
99
|
+
if (process.stderr.isTTY) {
|
|
100
|
+
process.stderr.write('\r\x1b[K');
|
|
101
|
+
}
|
|
102
|
+
if (finalLine) {
|
|
103
|
+
process.stderr.write(`${finalLine}\n`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function tryOpenBrowser(uri) {
|
|
109
|
+
try {
|
|
110
|
+
const mod = await import('open');
|
|
111
|
+
const open = mod.default || mod;
|
|
112
|
+
await open(uri);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function run(argv = process.argv.slice(3)) {
|
|
120
|
+
const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
121
|
+
const args = parseArgs(argv);
|
|
122
|
+
const scopes = args.scope.split(',').map((s) => s.trim()).filter(Boolean);
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(` AI Note MCP — Login`);
|
|
126
|
+
console.log(` API: ${apiUrl}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
|
|
129
|
+
let device;
|
|
130
|
+
try {
|
|
131
|
+
device = await requestDeviceCode({
|
|
132
|
+
apiUrl,
|
|
133
|
+
clientName: args.clientName,
|
|
134
|
+
scopes
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof DeviceNetworkError) {
|
|
138
|
+
console.error(` Network error: ${err.message}`);
|
|
139
|
+
} else {
|
|
140
|
+
console.error(` Failed to start login: ${err.message}`);
|
|
141
|
+
}
|
|
142
|
+
process.exit(1);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(` Visit the URL below and enter the code:`);
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log(drawUserCodeBox(device.user_code));
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(` ${device.verification_uri}`);
|
|
151
|
+
if (device.verification_uri_complete && device.verification_uri_complete !== device.verification_uri) {
|
|
152
|
+
console.log(` (direct link: ${device.verification_uri_complete})`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
|
|
156
|
+
if (!args.noBrowser) {
|
|
157
|
+
const target = device.verification_uri_complete || device.verification_uri;
|
|
158
|
+
const opened = await tryOpenBrowser(target);
|
|
159
|
+
if (opened) {
|
|
160
|
+
console.log(' Opening your browser…');
|
|
161
|
+
} else {
|
|
162
|
+
console.log(' (Could not open browser automatically — please open the URL above.)');
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const spinner = new Spinner();
|
|
168
|
+
spinner.start(`Waiting for authorization… (0s elapsed, next poll in ${device.interval}s)`);
|
|
169
|
+
|
|
170
|
+
let cancelled = false;
|
|
171
|
+
const sigintHandler = async () => {
|
|
172
|
+
if (cancelled) return;
|
|
173
|
+
cancelled = true;
|
|
174
|
+
spinner.stop('Cancelled.');
|
|
175
|
+
await cancel({ apiUrl, deviceCode: device.device_code });
|
|
176
|
+
process.exit(1);
|
|
177
|
+
};
|
|
178
|
+
process.once('SIGINT', sigintHandler);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const tokens = await pollForToken({
|
|
182
|
+
apiUrl,
|
|
183
|
+
deviceCode: device.device_code,
|
|
184
|
+
codeVerifier: device.code_verifier,
|
|
185
|
+
interval: device.interval,
|
|
186
|
+
expiresIn: device.expires_in,
|
|
187
|
+
onTick: ({ elapsed, interval }) => {
|
|
188
|
+
spinner.update(`Waiting for authorization… (${elapsed}s elapsed, next poll in ${interval}s)`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
spinner.stop();
|
|
193
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
194
|
+
|
|
195
|
+
const expiresAt = tokens.expires_in
|
|
196
|
+
? Date.now() + Number(tokens.expires_in) * 1000
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
const persisted = await setCredentials(apiUrl, {
|
|
200
|
+
mcpKey: tokens.mcp_key,
|
|
201
|
+
accessToken: tokens.access_token,
|
|
202
|
+
refreshToken: tokens.refresh_token,
|
|
203
|
+
expiresAt
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const email = tokens.user?.email;
|
|
207
|
+
const locationDesc = persisted.storage === 'keychain'
|
|
208
|
+
? 'OS keychain'
|
|
209
|
+
: persisted.storage === 'mixed'
|
|
210
|
+
? `OS keychain + ${persisted.file}`
|
|
211
|
+
: persisted.file || _credentialsLocations().file;
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
if (email) {
|
|
215
|
+
console.log(` ✓ Logged in as ${email}.`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(` ✓ Logged in.`);
|
|
218
|
+
}
|
|
219
|
+
console.log(` Credentials saved to ${locationDesc}.`);
|
|
220
|
+
console.log('');
|
|
221
|
+
} catch (err) {
|
|
222
|
+
spinner.stop();
|
|
223
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
224
|
+
|
|
225
|
+
if (err instanceof DeviceAccessDeniedError) {
|
|
226
|
+
console.error(' Access denied. The authorization request was rejected.');
|
|
227
|
+
} else if (err instanceof DeviceExpiredError) {
|
|
228
|
+
console.error(' Code expired. Please run `ainote-mcp login` again.');
|
|
229
|
+
} else if (err instanceof DevicePkceError) {
|
|
230
|
+
console.error(' Authentication failed (PKCE mismatch). Please retry.');
|
|
231
|
+
} else if (err instanceof DeviceNetworkError) {
|
|
232
|
+
console.error(` Network error: ${err.message}`);
|
|
233
|
+
} else {
|
|
234
|
+
console.error(` Login failed: ${err.message}`);
|
|
235
|
+
}
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
package/lib/cli/logout.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `ainote-mcp logout` — revoke server-side tokens and clear local credentials.
|
|
3
|
-
*/
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import axios from 'axios';
|
|
6
|
-
import { getCredentials, clearCredentials } from '../auth/token-store.js';
|
|
7
|
-
|
|
8
|
-
const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
|
|
9
|
-
|
|
10
|
-
export async function run() {
|
|
11
|
-
const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
12
|
-
|
|
13
|
-
const creds = await getCredentials(apiUrl);
|
|
14
|
-
|
|
15
|
-
if (!creds) {
|
|
16
|
-
console.log(' No credentials found. Already logged out.');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (creds.refreshToken) {
|
|
21
|
-
try {
|
|
22
|
-
await axios.post(
|
|
23
|
-
`${apiUrl}/api/auth/token/revoke`,
|
|
24
|
-
{ refresh_token: creds.refreshToken },
|
|
25
|
-
{
|
|
26
|
-
headers: { 'Content-Type': 'application/json' },
|
|
27
|
-
timeout: 10000,
|
|
28
|
-
validateStatus: () => true
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
} catch (err) {
|
|
32
|
-
// Server-side revoke is best-effort. Continue with local clear.
|
|
33
|
-
process.stderr.write(`[ainote-mcp] Server revoke failed (continuing): ${err.message}\n`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await clearCredentials(apiUrl);
|
|
38
|
-
console.log(' Logged out.');
|
|
39
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `ainote-mcp logout` — revoke server-side tokens and clear local credentials.
|
|
3
|
+
*/
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { getCredentials, clearCredentials } from '../auth/token-store.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_API_URL = 'https://ainote-5muq.onrender.com';
|
|
9
|
+
|
|
10
|
+
export async function run() {
|
|
11
|
+
const apiUrl = (process.env.AINOTE_API_URL || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
12
|
+
|
|
13
|
+
const creds = await getCredentials(apiUrl);
|
|
14
|
+
|
|
15
|
+
if (!creds) {
|
|
16
|
+
console.log(' No credentials found. Already logged out.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (creds.refreshToken) {
|
|
21
|
+
try {
|
|
22
|
+
await axios.post(
|
|
23
|
+
`${apiUrl}/api/auth/token/revoke`,
|
|
24
|
+
{ refresh_token: creds.refreshToken },
|
|
25
|
+
{
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
timeout: 10000,
|
|
28
|
+
validateStatus: () => true
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Server-side revoke is best-effort. Continue with local clear.
|
|
33
|
+
process.stderr.write(`[ainote-mcp] Server revoke failed (continuing): ${err.message}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await clearCredentials(apiUrl);
|
|
38
|
+
console.log(' Logged out.');
|
|
39
|
+
}
|