@aexol/spectral 0.2.3 → 0.2.6
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/cli.js +18 -49
- package/dist/commands/login-oauth.js +116 -0
- package/dist/commands/serve.js +1 -0
- package/dist/config.js +5 -1
- package/dist/mcp/agent-dir.js +18 -0
- package/dist/mcp/app-bridge.bundle.js +67 -0
- package/dist/mcp/commands.js +263 -0
- package/dist/mcp/config.js +532 -0
- package/dist/mcp/consent-manager.js +59 -0
- package/dist/mcp/direct-tools.js +354 -0
- package/dist/mcp/errors.js +165 -0
- package/dist/mcp/glimpse-ui.js +67 -0
- package/dist/mcp/host-html-template.js +412 -0
- package/dist/mcp/index.js +291 -0
- package/dist/mcp/init.js +280 -0
- package/dist/mcp/lifecycle.js +79 -0
- package/dist/mcp/logger.js +130 -0
- package/dist/mcp/mcp-auth-flow.js +283 -0
- package/dist/mcp/mcp-auth.js +226 -0
- package/dist/mcp/mcp-callback-server.js +225 -0
- package/dist/mcp/mcp-oauth-provider.js +243 -0
- package/dist/mcp/mcp-panel.js +646 -0
- package/dist/mcp/mcp-setup-panel.js +485 -0
- package/dist/mcp/metadata-cache.js +158 -0
- package/dist/mcp/npx-resolver.js +385 -0
- package/dist/mcp/oauth-handler.js +54 -0
- package/dist/mcp/onboarding-state.js +56 -0
- package/dist/mcp/proxy-modes.js +714 -0
- package/dist/mcp/resource-tools.js +14 -0
- package/dist/mcp/sampling-handler.js +206 -0
- package/dist/mcp/server-manager.js +301 -0
- package/dist/mcp/state.js +1 -0
- package/dist/mcp/tool-metadata.js +128 -0
- package/dist/mcp/tool-registrar.js +43 -0
- package/dist/mcp/types.js +93 -0
- package/dist/mcp/ui-resource-handler.js +113 -0
- package/dist/mcp/ui-server.js +522 -0
- package/dist/mcp/ui-session.js +306 -0
- package/dist/mcp/ui-stream-types.js +58 -0
- package/dist/mcp/utils.js +104 -0
- package/dist/mcp/vitest.config.js +13 -0
- package/dist/relay/machine-store.js +4 -0
- package/dist/relay/registration.js +12 -7
- package/package.json +9 -3
package/dist/cli.js
CHANGED
|
@@ -79,40 +79,9 @@ function resolvePiBin() {
|
|
|
79
79
|
function resolveAexolExtensionPath() {
|
|
80
80
|
return resolve(__dirname, "extensions", "aexol-mcp.js");
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* pi-mcp-adapter declares its extension entry via `"pi": { "extensions":
|
|
86
|
-
* ["./index.ts"] }` — it does NOT export a `main` or `exports` field. We
|
|
87
|
-
* therefore walk up node_modules ourselves, locate the package directory, and
|
|
88
|
-
* return the absolute path to `index.ts`.
|
|
89
|
-
*
|
|
90
|
-
* Returns null if the package is not installed (graceful degradation).
|
|
91
|
-
*/
|
|
92
|
-
function resolveMcpAdapterPath() {
|
|
93
|
-
const rel = "node_modules/pi-mcp-adapter/package.json";
|
|
94
|
-
let dir = __dirname;
|
|
95
|
-
const root = "/";
|
|
96
|
-
for (let i = 0; i < 20; i++) {
|
|
97
|
-
const pkgPath = resolve(dir, rel);
|
|
98
|
-
try {
|
|
99
|
-
const raw = readFileSync(pkgPath, "utf8");
|
|
100
|
-
const pkg = JSON.parse(raw);
|
|
101
|
-
const extRel = pkg.pi?.extensions?.[0];
|
|
102
|
-
if (extRel) {
|
|
103
|
-
return resolve(dirname(pkgPath), extRel);
|
|
104
|
-
}
|
|
105
|
-
break; // package found but no pi.extensions — shouldn't happen
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
// package.json not readable at this level, keep walking up
|
|
109
|
-
}
|
|
110
|
-
const parent = dirname(dir);
|
|
111
|
-
if (parent === dir || parent === root)
|
|
112
|
-
break;
|
|
113
|
-
dir = parent;
|
|
114
|
-
}
|
|
115
|
-
return null;
|
|
82
|
+
/** Absolute path to the bundled pi-mcp-adapter extension, sitting next to this file in dist/. */
|
|
83
|
+
function resolveMcpExtensionPath() {
|
|
84
|
+
return resolve(__dirname, "mcp", "index.js");
|
|
116
85
|
}
|
|
117
86
|
// ---- Branded helpers ---------------------------------------------------------
|
|
118
87
|
function printVersion() {
|
|
@@ -124,8 +93,9 @@ function printHeader() {
|
|
|
124
93
|
TAGLINE,
|
|
125
94
|
"",
|
|
126
95
|
"Subcommands:",
|
|
127
|
-
" spectral login
|
|
128
|
-
" spectral
|
|
96
|
+
" spectral login Authenticate with a team API key",
|
|
97
|
+
" spectral login --oauth Authorize via browser (Aexol Studio OAuth)",
|
|
98
|
+
" spectral logout Remove stored Aexol credentials",
|
|
129
99
|
" spectral serve Connect this machine to the Aexol relay backend",
|
|
130
100
|
" spectral bind Link this directory to an Aexol Studio project",
|
|
131
101
|
" spectral unbind Remove the Aexol Studio project binding",
|
|
@@ -193,6 +163,11 @@ async function main() {
|
|
|
193
163
|
// Subcommands. Dynamic import keeps the cold-start path light when users
|
|
194
164
|
// are just running pi.
|
|
195
165
|
if (first === "login") {
|
|
166
|
+
if (args[1] === "--oauth") {
|
|
167
|
+
const { runLoginOAuth } = await import("./commands/login-oauth.js");
|
|
168
|
+
await runLoginOAuth();
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
196
171
|
const { runLogin } = await import("./commands/login.js");
|
|
197
172
|
await runLogin();
|
|
198
173
|
process.exit(0);
|
|
@@ -231,20 +206,14 @@ async function main() {
|
|
|
231
206
|
process.stderr.write(`spectral: bundled Aexol MCP extension not found at ${aexolExtPath}. This is a packaging bug.\n`);
|
|
232
207
|
process.exit(1);
|
|
233
208
|
}
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
process.
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
process.stderr.write("[spectral] pi-mcp-adapter not found; standard MCP servers disabled.\n");
|
|
243
|
-
}
|
|
244
|
-
const extFlags = ["--extension", aexolExtPath];
|
|
245
|
-
if (mcpAdapterPath) {
|
|
246
|
-
extFlags.push("--extension", mcpAdapterPath);
|
|
209
|
+
// Bundled pi-mcp-adapter extension for standard MCP server support
|
|
210
|
+
// (stdio + SSE/HTTP transports, lazy loading, OAuth, /mcp panel).
|
|
211
|
+
const mcpExtPath = resolveMcpExtensionPath();
|
|
212
|
+
if (!existsSync(mcpExtPath)) {
|
|
213
|
+
process.stderr.write(`spectral: bundled MCP extension not found at ${mcpExtPath}. This is a packaging bug.\n`);
|
|
214
|
+
process.exit(1);
|
|
247
215
|
}
|
|
216
|
+
const extFlags = ["--extension", aexolExtPath, "--extension", mcpExtPath];
|
|
248
217
|
const finalArgs = [...extFlags, ...args];
|
|
249
218
|
delegateToPi(finalArgs);
|
|
250
219
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spectral login --oauth` — browser-based OAuth login for CLI machine ownership.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Start a temporary HTTP server on a random localhost port.
|
|
6
|
+
* 2. Open the browser to `https://studio.aexol.ai/cli-auth?port=<PORT>`.
|
|
7
|
+
* 3. Wait for the browser to redirect to `http://localhost:<PORT>?token=<JWT>`.
|
|
8
|
+
* 4. Save the token to `~/.spectral/config.json` as `userJwt`.
|
|
9
|
+
* 5. Shutdown the HTTP server and exit.
|
|
10
|
+
*
|
|
11
|
+
* On failure (timeout, network error, user closes browser), the server shuts
|
|
12
|
+
* down after a configurable timeout (default 120s).
|
|
13
|
+
*/
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
|
+
import pc from "picocolors";
|
|
16
|
+
import { DEFAULT_API_URL, getConfigFile, readConfig, writeConfig, } from "../config.js";
|
|
17
|
+
const DEFAULT_BACKEND_URL = "https://studio.aexol.ai";
|
|
18
|
+
const CALLBACK_TIMEOUT_MS = 120_000; // 2 minutes
|
|
19
|
+
/**
|
|
20
|
+
* Start a temporary HTTP server to receive the OAuth callback.
|
|
21
|
+
* Returns the port and a promise that resolves with the JWT token.
|
|
22
|
+
*/
|
|
23
|
+
function listenForCallback(timeoutMs) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const server = createServer();
|
|
26
|
+
const timeout = setTimeout(() => {
|
|
27
|
+
server.close();
|
|
28
|
+
reject(new Error(`Timed out after ${timeoutMs / 1000}s waiting for browser authorization.`));
|
|
29
|
+
}, timeoutMs);
|
|
30
|
+
server.on("request", (req, res) => {
|
|
31
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
32
|
+
const token = url.searchParams.get("token");
|
|
33
|
+
if (token) {
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
36
|
+
res.end("<html><body><h1>✓ Authorized</h1><p>You can close this tab and return to your terminal.</p></body></html>");
|
|
37
|
+
resolve({ token, server });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
41
|
+
res.end("Missing token parameter.");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
server.on("error", (err) => {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
server.close();
|
|
47
|
+
reject(new Error(`Failed to start local server: ${err.message}`));
|
|
48
|
+
});
|
|
49
|
+
server.listen(0, "127.0.0.1", () => {
|
|
50
|
+
// Port 0 lets the OS assign an ephemeral port — we read it from the server.
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Derive the landing base URL from the backend URL. */
|
|
55
|
+
function deriveLandingUrl(backendUrl) {
|
|
56
|
+
const env = process.env.SPECTRAL_LANDING_URL;
|
|
57
|
+
if (env)
|
|
58
|
+
return env;
|
|
59
|
+
// If using local dev backend, use local dev landing
|
|
60
|
+
if (backendUrl.includes("localhost") || backendUrl.includes("127.0.0.1")) {
|
|
61
|
+
return "http://localhost:3000";
|
|
62
|
+
}
|
|
63
|
+
return DEFAULT_BACKEND_URL;
|
|
64
|
+
}
|
|
65
|
+
export async function runLoginOAuth() {
|
|
66
|
+
process.stdout.write(pc.bold("Spectral login (OAuth)\n"));
|
|
67
|
+
process.stdout.write(pc.dim(`Opens a browser to authorize the CLI. The JWT is stored at ${getConfigFile()}.\n\n`));
|
|
68
|
+
// Load existing config to get backendUrl
|
|
69
|
+
const existingCfg = await readConfig();
|
|
70
|
+
const landingUrl = deriveLandingUrl(existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL);
|
|
71
|
+
// Start the callback listener FIRST (port must be known before opening the browser)
|
|
72
|
+
let server;
|
|
73
|
+
let token;
|
|
74
|
+
try {
|
|
75
|
+
const result = await listenForCallback(CALLBACK_TIMEOUT_MS);
|
|
76
|
+
server = result.server;
|
|
77
|
+
const addr = server.address();
|
|
78
|
+
const port = addr.port;
|
|
79
|
+
// Open browser to the CLI auth page
|
|
80
|
+
const authUrl = `${landingUrl}/cli-auth?port=${port}`;
|
|
81
|
+
process.stdout.write(pc.dim(`Opening browser: ${authUrl}\n`));
|
|
82
|
+
const { exec } = await import("node:child_process");
|
|
83
|
+
const platform = process.platform;
|
|
84
|
+
const openCmd = platform === "darwin"
|
|
85
|
+
? `open "${authUrl}"`
|
|
86
|
+
: platform === "win32"
|
|
87
|
+
? `start "" "${authUrl}"`
|
|
88
|
+
: `xdg-open "${authUrl}"`;
|
|
89
|
+
exec(openCmd, (err) => {
|
|
90
|
+
if (err) {
|
|
91
|
+
process.stderr.write(pc.yellow(`⚠ Could not open browser automatically. Please visit:\n ${authUrl}\n`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// Wait for the token
|
|
95
|
+
process.stdout.write(pc.dim("Waiting for authorization...\n"));
|
|
96
|
+
token = result.token;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
process.stderr.write(pc.red(`✗ ${msg}\n`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
server?.close();
|
|
105
|
+
}
|
|
106
|
+
// Save the token. Preserve existing config (especially teamApiKey).
|
|
107
|
+
const cfg = existingCfg ?? {
|
|
108
|
+
apiUrl: process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL,
|
|
109
|
+
teamApiKey: "",
|
|
110
|
+
};
|
|
111
|
+
cfg.userJwt = token;
|
|
112
|
+
await writeConfig(cfg);
|
|
113
|
+
process.stdout.write(pc.green("✓ CLI authorized.\n"));
|
|
114
|
+
process.stdout.write(pc.dim(`Saved user JWT to ${getConfigFile()}\n`));
|
|
115
|
+
process.stdout.write(pc.dim("Run `spectral serve` to register your machine with owner info.\n"));
|
|
116
|
+
}
|
package/dist/commands/serve.js
CHANGED
package/dist/config.js
CHANGED
|
@@ -59,7 +59,11 @@ export async function readConfig() {
|
|
|
59
59
|
if (typeof parsed.apiUrl === "string" &&
|
|
60
60
|
typeof parsed.teamApiKey === "string" &&
|
|
61
61
|
parsed.teamApiKey.length > 0) {
|
|
62
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
apiUrl: parsed.apiUrl,
|
|
64
|
+
teamApiKey: parsed.teamApiKey,
|
|
65
|
+
userJwt: typeof parsed.userJwt === "string" ? parsed.userJwt : undefined,
|
|
66
|
+
};
|
|
63
67
|
}
|
|
64
68
|
return null;
|
|
65
69
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function getAgentDir() {
|
|
4
|
+
const configured = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
5
|
+
if (!configured) {
|
|
6
|
+
return join(homedir(), ".pi", "agent");
|
|
7
|
+
}
|
|
8
|
+
if (configured === "~") {
|
|
9
|
+
return homedir();
|
|
10
|
+
}
|
|
11
|
+
if (configured.startsWith("~/")) {
|
|
12
|
+
return resolve(homedir(), configured.slice(2));
|
|
13
|
+
}
|
|
14
|
+
return resolve(configured);
|
|
15
|
+
}
|
|
16
|
+
export function getAgentPath(...segments) {
|
|
17
|
+
return join(getAgentDir(), ...segments);
|
|
18
|
+
}
|