@gpsglobal-ai/gpsglobal 1.4.7 → 1.4.9
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/CHANGELOG.md +9 -0
- package/dist/cli/browser-login-internals.d.ts +5 -0
- package/dist/cli/browser-login-internals.js +24 -0
- package/dist/cli/browser-login.js +75 -69
- package/dist/cli/doctor.js +21 -0
- package/dist/cli/setup.js +3 -0
- package/dist/cli.js +14 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog — gpsglobal
|
|
2
2
|
|
|
3
|
+
## 1.4.9 — 2026-06-21
|
|
4
|
+
|
|
5
|
+
- **`doctor`:** checks OAuth AS `registration_endpoint` (DCR) for VS Code zero-config Connect
|
|
6
|
+
- **`setup`:** prints VS Code Connect hint after OAuth config merge
|
|
7
|
+
|
|
8
|
+
## 1.4.8 — 2026-06-21
|
|
9
|
+
|
|
10
|
+
- **fix(setup):** CLI exits cleanly after `setup` / `login` — clear OAuth timeout, close loopback server, disable axios keep-alive (no more Ctrl+C)
|
|
11
|
+
|
|
3
12
|
## 1.4.7 — 2026-06-21
|
|
4
13
|
|
|
5
14
|
- **README** — document `get_fund_wiki` `format=linked` default (PDF page deep links)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createServer as createNetServer } from 'node:net';
|
|
2
|
+
/** @internal — exported for unit tests only */
|
|
3
|
+
export async function findFreePort() {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const srv = createNetServer();
|
|
6
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
7
|
+
const addr = srv.address();
|
|
8
|
+
if (!addr || typeof addr === 'string') {
|
|
9
|
+
reject(new Error('Could not bind loopback port'));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const port = addr.port;
|
|
13
|
+
srv.close(() => resolve(port));
|
|
14
|
+
});
|
|
15
|
+
srv.on('error', reject);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** @internal — exported for unit tests only */
|
|
19
|
+
export function closeHttpServer(server) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
server.closeAllConnections?.();
|
|
22
|
+
server.close(() => resolve());
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
import
|
|
2
|
+
import https from 'node:https';
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
4
|
import axios from 'axios';
|
|
6
5
|
import { generatePkce, randomState } from '../lib/pkce.js';
|
|
7
6
|
import { normalizeApiBase, writeGpsEnvFile, DEFAULT_ENV_PATH } from '../config/env.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
srv.listen(0, '127.0.0.1', () => {
|
|
13
|
-
const addr = srv.address();
|
|
14
|
-
if (!addr || typeof addr === 'string') {
|
|
15
|
-
reject(new Error('Could not bind loopback port'));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
const port = addr.port;
|
|
19
|
-
srv.close(() => resolve(port));
|
|
20
|
-
});
|
|
21
|
-
srv.on('error', reject);
|
|
22
|
-
});
|
|
23
|
-
}
|
|
7
|
+
import { closeHttpServer, findFreePort } from './browser-login-internals.js';
|
|
8
|
+
/** One-shot HTTP clients — avoid keep-alive sockets keeping the CLI alive after setup. */
|
|
9
|
+
const ephemeralHttpAgent = new http.Agent({ keepAlive: false });
|
|
10
|
+
const ephemeralHttpsAgent = new https.Agent({ keepAlive: false });
|
|
24
11
|
function openBrowser(url) {
|
|
25
12
|
const cmd = process.platform === 'darwin' ? `open "${url}"` :
|
|
26
13
|
process.platform === 'win32' ? `start "" "${url}"` :
|
|
@@ -38,58 +25,77 @@ export async function browserLogin(apiBase) {
|
|
|
38
25
|
authorizeUrl.searchParams.set('state', state);
|
|
39
26
|
authorizeUrl.searchParams.set('code_challenge', codeChallenge);
|
|
40
27
|
authorizeUrl.searchParams.set('code_challenge_method', 'S256');
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
let server;
|
|
29
|
+
let timeoutId;
|
|
30
|
+
try {
|
|
31
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
32
|
+
server = http.createServer((req, res) => {
|
|
33
|
+
const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
|
|
34
|
+
if (url.pathname !== '/callback') {
|
|
35
|
+
res.writeHead(404).end('Not found');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const code = url.searchParams.get('code');
|
|
39
|
+
const returnedState = url.searchParams.get('state');
|
|
40
|
+
if (!code || returnedState !== state) {
|
|
41
|
+
res.writeHead(400).end('Invalid callback');
|
|
42
|
+
reject(new Error('OAuth callback missing code or state mismatch'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
46
|
+
res.end(`<!DOCTYPE html><html><body style="font-family:system-ui;background:#09090b;color:#fafafa;display:flex;align-items:center;justify-content:center;min-height:100vh">
|
|
58
47
|
<div style="text-align:center"><h1 style="color:#d4af37">GPS MCP connected</h1><p>You can close this tab and return to your terminal.</p></div></body></html>`);
|
|
59
|
-
|
|
60
|
-
|
|
48
|
+
resolve(code);
|
|
49
|
+
});
|
|
50
|
+
server.listen(port, '127.0.0.1');
|
|
51
|
+
server.on('error', reject);
|
|
61
52
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
53
|
+
console.log(`Opening browser for GPS sign-in…\n${authorizeUrl.href}\n`);
|
|
54
|
+
openBrowser(authorizeUrl.href);
|
|
55
|
+
const code = await Promise.race([
|
|
56
|
+
codePromise,
|
|
57
|
+
new Promise((_, reject) => {
|
|
58
|
+
timeoutId = setTimeout(() => reject(new Error('Browser login timed out after 3 minutes')), 180_000);
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
if (timeoutId) {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
timeoutId = undefined;
|
|
64
|
+
}
|
|
65
|
+
const tokenResp = await axios.post(`${base}/api/v2/oauth/mcp-cli/token`, {
|
|
66
|
+
grant_type: 'authorization_code',
|
|
67
|
+
code,
|
|
68
|
+
redirect_uri: redirectUri,
|
|
69
|
+
code_verifier: codeVerifier,
|
|
70
|
+
}, {
|
|
71
|
+
validateStatus: () => true,
|
|
72
|
+
httpAgent: ephemeralHttpAgent,
|
|
73
|
+
httpsAgent: ephemeralHttpsAgent,
|
|
74
|
+
});
|
|
75
|
+
if (tokenResp.status >= 400) {
|
|
76
|
+
const desc = tokenResp.data?.error_description ?? tokenResp.statusText;
|
|
77
|
+
throw new Error(`Token exchange failed: ${desc}`);
|
|
78
|
+
}
|
|
79
|
+
const data = tokenResp.data;
|
|
80
|
+
writeGpsEnvFile({
|
|
81
|
+
apiBase: base,
|
|
82
|
+
accessToken: data.access_token,
|
|
83
|
+
lpId: data.lp_id,
|
|
84
|
+
role: data.role,
|
|
85
|
+
username: data.username,
|
|
86
|
+
}, DEFAULT_ENV_PATH);
|
|
87
|
+
return {
|
|
88
|
+
accessToken: data.access_token,
|
|
89
|
+
lpId: data.lp_id,
|
|
90
|
+
username: data.username,
|
|
91
|
+
role: data.role,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (timeoutId)
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
if (server?.listening) {
|
|
98
|
+
await closeHttpServer(server).catch(() => undefined);
|
|
99
|
+
}
|
|
80
100
|
}
|
|
81
|
-
const data = tokenResp.data;
|
|
82
|
-
writeGpsEnvFile({
|
|
83
|
-
apiBase: base,
|
|
84
|
-
accessToken: data.access_token,
|
|
85
|
-
lpId: data.lp_id,
|
|
86
|
-
role: data.role,
|
|
87
|
-
username: data.username,
|
|
88
|
-
}, DEFAULT_ENV_PATH);
|
|
89
|
-
return {
|
|
90
|
-
accessToken: data.access_token,
|
|
91
|
-
lpId: data.lp_id,
|
|
92
|
-
username: data.username,
|
|
93
|
-
role: data.role,
|
|
94
|
-
};
|
|
95
101
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -78,6 +78,27 @@ export async function runDoctor() {
|
|
|
78
78
|
catch {
|
|
79
79
|
checks.push({ name: 'mcp-http', ok: true, detail: 'remote MCP unreachable (stdio mode OK)' });
|
|
80
80
|
}
|
|
81
|
+
try {
|
|
82
|
+
const asMeta = await axios.get(`${apiBase}/api/v2/oauth/mcp-cli/.well-known/oauth-authorization-server`, {
|
|
83
|
+
timeout: 10_000,
|
|
84
|
+
validateStatus: () => true,
|
|
85
|
+
});
|
|
86
|
+
const hasDcr = asMeta.status === 200 && Boolean(asMeta.data?.registration_endpoint);
|
|
87
|
+
checks.push({
|
|
88
|
+
name: 'oauth-dcr',
|
|
89
|
+
ok: hasDcr,
|
|
90
|
+
detail: hasDcr
|
|
91
|
+
? 'registration_endpoint present (VS Code Connect OK)'
|
|
92
|
+
: `AS metadata → ${asMeta.status} (VS Code may ask for manual client ID)`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
checks.push({
|
|
97
|
+
name: 'oauth-dcr',
|
|
98
|
+
ok: false,
|
|
99
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
81
102
|
return { ok: checks.every((c) => c.ok), checks };
|
|
82
103
|
}
|
|
83
104
|
export function printDoctor(result) {
|
package/dist/cli/setup.js
CHANGED
|
@@ -50,5 +50,8 @@ export async function runSetup(options) {
|
|
|
50
50
|
}
|
|
51
51
|
console.log(`\nVerify: npx ${PACKAGE} doctor`);
|
|
52
52
|
console.log(`Local dev: GPS_API_BASE=${LOCAL_GPS_API_BASE} npx ${PACKAGE} setup --mode=stdio`);
|
|
53
|
+
if (mode === 'oauth' || mode === 'remote') {
|
|
54
|
+
console.log('\nVS Code / Copilot: restart IDE → MCP: List Servers → Connect gpsglobal (DCR — no manual client ID).');
|
|
55
|
+
}
|
|
53
56
|
console.log(`Done. Ask your AI: "list my GPS funds using ${MCP_SERVER_KEY}"`);
|
|
54
57
|
}
|
package/dist/cli.js
CHANGED
|
@@ -100,11 +100,11 @@ async function main() {
|
|
|
100
100
|
const args = process.argv.slice(2);
|
|
101
101
|
if (args.includes('--stdio')) {
|
|
102
102
|
await startStdioServer();
|
|
103
|
-
return;
|
|
103
|
+
return true;
|
|
104
104
|
}
|
|
105
105
|
if (args.includes('--http')) {
|
|
106
106
|
await startHttpFromEnv();
|
|
107
|
-
return;
|
|
107
|
+
return true;
|
|
108
108
|
}
|
|
109
109
|
const cmd = args[0];
|
|
110
110
|
if (!cmd || cmd === 'help' || args.includes('--help')) {
|
|
@@ -118,7 +118,7 @@ async function main() {
|
|
|
118
118
|
npx ${PACKAGE} --stdio
|
|
119
119
|
npx ${PACKAGE} --http
|
|
120
120
|
`);
|
|
121
|
-
return;
|
|
121
|
+
return false;
|
|
122
122
|
}
|
|
123
123
|
if (cmd === 'setup') {
|
|
124
124
|
const hostArg = args.find((a) => a.startsWith('--host='));
|
|
@@ -130,31 +130,36 @@ async function main() {
|
|
|
130
130
|
useBrowser: !args.includes('--no-browser'),
|
|
131
131
|
mode: mode ?? 'oauth',
|
|
132
132
|
});
|
|
133
|
-
return;
|
|
133
|
+
return false;
|
|
134
134
|
}
|
|
135
135
|
if (cmd === 'doctor') {
|
|
136
136
|
const result = await runDoctor();
|
|
137
137
|
printDoctor(result);
|
|
138
138
|
if (!result.ok)
|
|
139
139
|
process.exit(1);
|
|
140
|
-
return;
|
|
140
|
+
return false;
|
|
141
141
|
}
|
|
142
142
|
if (cmd === 'login') {
|
|
143
143
|
await runLogin(args.includes('--refresh'), args.includes('--browser'));
|
|
144
|
-
return;
|
|
144
|
+
return false;
|
|
145
145
|
}
|
|
146
146
|
if (cmd === 'status') {
|
|
147
147
|
runStatus();
|
|
148
|
-
return;
|
|
148
|
+
return false;
|
|
149
149
|
}
|
|
150
150
|
if (cmd === 'print-config') {
|
|
151
151
|
printConfig(process.env.GPS_MCP_ENV_PATH ?? DEFAULT_ENV_PATH);
|
|
152
|
-
return;
|
|
152
|
+
return false;
|
|
153
153
|
}
|
|
154
154
|
console.error(`Unknown command: ${cmd}`);
|
|
155
155
|
process.exit(1);
|
|
156
156
|
}
|
|
157
|
-
main()
|
|
157
|
+
main()
|
|
158
|
+
.then((longRunning) => {
|
|
159
|
+
if (!longRunning)
|
|
160
|
+
process.exit(0);
|
|
161
|
+
})
|
|
162
|
+
.catch((err) => {
|
|
158
163
|
const msg = err instanceof Error ? err.message : String(err);
|
|
159
164
|
console.error(msg);
|
|
160
165
|
process.exit(1);
|