@aexol/spectral 0.8.7 → 0.8.10
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/auth-helper.d.ts +49 -0
- package/dist/auth-helper.d.ts.map +1 -0
- package/dist/auth-helper.js +191 -0
- package/dist/commands/login-oauth.d.ts +2 -9
- package/dist/commands/login-oauth.d.ts.map +1 -1
- package/dist/commands/login-oauth.js +10 -105
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +68 -1
- package/dist/designer/systems.d.ts.map +1 -1
- package/dist/designer/systems.js +5 -0
- package/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +33 -2
- package/dist/extensions/browser/browser-service.d.ts +104 -0
- package/dist/extensions/browser/browser-service.d.ts.map +1 -0
- package/dist/extensions/browser/browser-service.js +333 -0
- package/dist/extensions/browser/config.d.ts +24 -0
- package/dist/extensions/browser/config.d.ts.map +1 -0
- package/dist/extensions/browser/config.js +17 -0
- package/dist/extensions/browser/index.d.ts +35 -0
- package/dist/extensions/browser/index.d.ts.map +1 -0
- package/dist/extensions/browser/index.js +126 -0
- package/dist/extensions/browser/stream/signaling-client.d.ts +28 -0
- package/dist/extensions/browser/stream/signaling-client.d.ts.map +1 -0
- package/dist/extensions/browser/stream/signaling-client.js +69 -0
- package/dist/extensions/browser/stream/stream-injector.d.ts +30 -0
- package/dist/extensions/browser/stream/stream-injector.d.ts.map +1 -0
- package/dist/extensions/browser/stream/stream-injector.js +161 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
- package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -0
- package/dist/extensions/browser/stream/stream-tool.js +182 -0
- package/dist/extensions/browser/tools/console.d.ts +7 -0
- package/dist/extensions/browser/tools/console.d.ts.map +1 -0
- package/dist/extensions/browser/tools/console.js +54 -0
- package/dist/extensions/browser/tools/core.d.ts +7 -0
- package/dist/extensions/browser/tools/core.d.ts.map +1 -0
- package/dist/extensions/browser/tools/core.js +115 -0
- package/dist/extensions/browser/tools/dialog.d.ts +11 -0
- package/dist/extensions/browser/tools/dialog.d.ts.map +1 -0
- package/dist/extensions/browser/tools/dialog.js +54 -0
- package/dist/extensions/browser/tools/drop.d.ts +7 -0
- package/dist/extensions/browser/tools/drop.d.ts.map +1 -0
- package/dist/extensions/browser/tools/drop.js +93 -0
- package/dist/extensions/browser/tools/evaluate.d.ts +7 -0
- package/dist/extensions/browser/tools/evaluate.d.ts.map +1 -0
- package/dist/extensions/browser/tools/evaluate.js +105 -0
- package/dist/extensions/browser/tools/input.d.ts +7 -0
- package/dist/extensions/browser/tools/input.d.ts.map +1 -0
- package/dist/extensions/browser/tools/input.js +290 -0
- package/dist/extensions/browser/tools/network.d.ts +7 -0
- package/dist/extensions/browser/tools/network.d.ts.map +1 -0
- package/dist/extensions/browser/tools/network.js +116 -0
- package/dist/extensions/browser/tools/screenshot.d.ts +7 -0
- package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -0
- package/dist/extensions/browser/tools/screenshot.js +153 -0
- package/dist/extensions/browser/tools/tabs.d.ts +7 -0
- package/dist/extensions/browser/tools/tabs.d.ts.map +1 -0
- package/dist/extensions/browser/tools/tabs.js +72 -0
- package/dist/extensions/browser/tools/wait.d.ts +7 -0
- package/dist/extensions/browser/tools/wait.d.ts.map +1 -0
- package/dist/extensions/browser/tools/wait.js +64 -0
- package/dist/extensions/kanban-bridge.d.ts.map +1 -1
- package/dist/extensions/kanban-bridge.js +70 -3
- package/dist/mcp/ui-server.d.ts.map +1 -1
- package/dist/mcp/ui-server.js +23 -10
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts +60 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +108 -0
- package/dist/sdk/coding-agent/core/resource-loader.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/resource-loader.js +23 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +5 -0
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +12 -0
- package/dist/server/sqlite-adapter.d.ts +41 -0
- package/dist/server/sqlite-adapter.d.ts.map +1 -0
- package/dist/server/sqlite-adapter.js +128 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +2 -2
- package/package.json +5 -2
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared authentication helpers for native extensions and CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `runOAuthFlow()` — opens browser for OAuth, returns JWT token
|
|
6
|
+
* - `ensureAuthenticated()` — checks token validity, triggers OAuth re-login on 401/403
|
|
7
|
+
* - `isAuthError()` — checks if an error indicates auth failure
|
|
8
|
+
*
|
|
9
|
+
* Extensions (aexol-mcp, kanban-bridge) call ensureAuthenticated() when they
|
|
10
|
+
* detect an auth failure so the user can re-authenticate without dropping
|
|
11
|
+
* out to the terminal and running `spectral login` manually.
|
|
12
|
+
*/
|
|
13
|
+
import { type SpectralConfig } from "./config.js";
|
|
14
|
+
export interface EnsureAuthResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
config: SpectralConfig | null;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OAuthResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
token?: string;
|
|
22
|
+
reason?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Run the OAuth browser flow to obtain a fresh JWT token.
|
|
26
|
+
*
|
|
27
|
+
* Opens the user's browser to the Aexol Studio CLI auth page. The landing
|
|
28
|
+
* app redirects back to localhost with a JWT in the query string.
|
|
29
|
+
*
|
|
30
|
+
* Returns `{ ok: true, token }` on success, `{ ok: false, reason }` on failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runOAuthFlow(apiUrl?: string, timeoutMs?: number): Promise<OAuthResult>;
|
|
33
|
+
/** Check if an error indicates an authentication failure (401 or 403). */
|
|
34
|
+
export declare function isAuthError(err: unknown): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the user is authenticated.
|
|
37
|
+
*
|
|
38
|
+
* Call this when a tool or extension encounters an auth error (401/403).
|
|
39
|
+
* It verifies the current token is valid by pinging the backend. If the
|
|
40
|
+
* token is expired or missing, it triggers the OAuth browser flow.
|
|
41
|
+
*
|
|
42
|
+
* Only attempts OAuth re-login when stdout is a TTY (interactive session).
|
|
43
|
+
* In headless/CI environments, it returns `{ ok: false }` immediately.
|
|
44
|
+
*
|
|
45
|
+
* Returns `{ ok: true, config }` with fresh config on success,
|
|
46
|
+
* or `{ ok: false, reason }` when auth could not be established.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ensureAuthenticated(): Promise<EnsureAuthResult>;
|
|
49
|
+
//# sourceMappingURL=auth-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-helper.d.ts","sourceRoot":"","sources":["../src/auth-helper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAOrB,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,MAAM,CAAC,EAAE,MAAM,EACf,SAAS,GAAE,MAAyB,GACnC,OAAO,CAAC,WAAW,CAAC,CAyDtB;AAMD,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAUjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA6CrE"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared authentication helpers for native extensions and CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - `runOAuthFlow()` — opens browser for OAuth, returns JWT token
|
|
6
|
+
* - `ensureAuthenticated()` — checks token validity, triggers OAuth re-login on 401/403
|
|
7
|
+
* - `isAuthError()` — checks if an error indicates auth failure
|
|
8
|
+
*
|
|
9
|
+
* Extensions (aexol-mcp, kanban-bridge) call ensureAuthenticated() when they
|
|
10
|
+
* detect an auth failure so the user can re-authenticate without dropping
|
|
11
|
+
* out to the terminal and running `spectral login` manually.
|
|
12
|
+
*/
|
|
13
|
+
import { createServer } from "node:http";
|
|
14
|
+
import { exec } from "node:child_process";
|
|
15
|
+
import pc from "picocolors";
|
|
16
|
+
import { DEFAULT_API_URL, readConfig, writeConfig, } from "./config.js";
|
|
17
|
+
import { AexolMcpClient, AexolMcpError } from "./mcp-client.js";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constants
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const DEFAULT_BACKEND_URL = "https://aexol.ai";
|
|
22
|
+
const OAUTH_TIMEOUT_MS = 120_000; // 2 minutes
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// OAuth flow
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Derive the landing base URL from the backend URL. */
|
|
27
|
+
function deriveLandingUrl(backendUrl) {
|
|
28
|
+
const env = process.env.SPECTRAL_LANDING_URL;
|
|
29
|
+
if (env)
|
|
30
|
+
return env;
|
|
31
|
+
if (backendUrl.includes("localhost") || backendUrl.includes("127.0.0.1")) {
|
|
32
|
+
return "http://localhost:3000";
|
|
33
|
+
}
|
|
34
|
+
return DEFAULT_BACKEND_URL;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Start a temporary HTTP server to receive the OAuth callback.
|
|
38
|
+
* Returns the server and a promise that resolves with the JWT token.
|
|
39
|
+
*/
|
|
40
|
+
function listenForCallback(timeoutMs) {
|
|
41
|
+
const server = createServer();
|
|
42
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
43
|
+
const timeout = setTimeout(() => {
|
|
44
|
+
server.close();
|
|
45
|
+
reject(new Error(`Timed out after ${timeoutMs / 1000}s waiting for browser authorization.`));
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
server.on("request", (req, res) => {
|
|
48
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
49
|
+
const token = url.searchParams.get("token");
|
|
50
|
+
if (token) {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
53
|
+
res.end("<html><body><h1>✓ Authorized</h1><p>You can close this tab and return to your terminal.</p></body></html>");
|
|
54
|
+
resolve(token);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
58
|
+
res.end("Missing token parameter.");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
server.on("error", (err) => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
server.close();
|
|
64
|
+
reject(new Error(`Failed to start local server: ${err.message}`));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
return { server, tokenPromise };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Run the OAuth browser flow to obtain a fresh JWT token.
|
|
71
|
+
*
|
|
72
|
+
* Opens the user's browser to the Aexol Studio CLI auth page. The landing
|
|
73
|
+
* app redirects back to localhost with a JWT in the query string.
|
|
74
|
+
*
|
|
75
|
+
* Returns `{ ok: true, token }` on success, `{ ok: false, reason }` on failure.
|
|
76
|
+
*/
|
|
77
|
+
export async function runOAuthFlow(apiUrl, timeoutMs = OAUTH_TIMEOUT_MS) {
|
|
78
|
+
// Load existing config to preserve teamApiKey
|
|
79
|
+
const existingCfg = await readConfig();
|
|
80
|
+
const resolvedApiUrl = apiUrl ?? existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL;
|
|
81
|
+
const landingUrl = deriveLandingUrl(resolvedApiUrl);
|
|
82
|
+
let server;
|
|
83
|
+
try {
|
|
84
|
+
const { server: srv, tokenPromise } = listenForCallback(timeoutMs);
|
|
85
|
+
server = srv;
|
|
86
|
+
// Start listening on a random port
|
|
87
|
+
server.listen(0, "127.0.0.1");
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
server.once("listening", resolve);
|
|
90
|
+
server.once("error", reject);
|
|
91
|
+
});
|
|
92
|
+
const addr = server.address();
|
|
93
|
+
const port = addr.port;
|
|
94
|
+
// Open browser to the CLI auth page
|
|
95
|
+
const authUrl = `${landingUrl}/cli-auth?port=${port}`;
|
|
96
|
+
const openCmd = process.platform === "darwin"
|
|
97
|
+
? `open "${authUrl}"`
|
|
98
|
+
: process.platform === "win32"
|
|
99
|
+
? `start "" "${authUrl}"`
|
|
100
|
+
: `xdg-open "${authUrl}"`;
|
|
101
|
+
exec(openCmd, (err) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
process.stderr.write(pc.yellow(`⚠ Could not open browser automatically. Please visit:\n ${authUrl}\n`));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const token = await tokenPromise;
|
|
107
|
+
// Save the token, preserving existing config (especially teamApiKey).
|
|
108
|
+
const cfg = existingCfg ?? {
|
|
109
|
+
apiUrl: resolvedApiUrl,
|
|
110
|
+
teamApiKey: "",
|
|
111
|
+
};
|
|
112
|
+
cfg.userJwt = token;
|
|
113
|
+
await writeConfig(cfg);
|
|
114
|
+
return { ok: true, token };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
return { ok: false, reason: msg };
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
server?.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Auth helpers
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/** Check if an error indicates an authentication failure (401 or 403). */
|
|
128
|
+
export function isAuthError(err) {
|
|
129
|
+
if (err instanceof AexolMcpError) {
|
|
130
|
+
return err.status === 401 || err.status === 403;
|
|
131
|
+
}
|
|
132
|
+
// Generic HTTP errors from fetch
|
|
133
|
+
if (err instanceof Error && "status" in err) {
|
|
134
|
+
const status = err.status;
|
|
135
|
+
return status === 401 || status === 403;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Ensure the user is authenticated.
|
|
141
|
+
*
|
|
142
|
+
* Call this when a tool or extension encounters an auth error (401/403).
|
|
143
|
+
* It verifies the current token is valid by pinging the backend. If the
|
|
144
|
+
* token is expired or missing, it triggers the OAuth browser flow.
|
|
145
|
+
*
|
|
146
|
+
* Only attempts OAuth re-login when stdout is a TTY (interactive session).
|
|
147
|
+
* In headless/CI environments, it returns `{ ok: false }` immediately.
|
|
148
|
+
*
|
|
149
|
+
* Returns `{ ok: true, config }` with fresh config on success,
|
|
150
|
+
* or `{ ok: false, reason }` when auth could not be established.
|
|
151
|
+
*/
|
|
152
|
+
export async function ensureAuthenticated() {
|
|
153
|
+
// Check if we're in an interactive session where browser OAuth is possible
|
|
154
|
+
if (!process.stdout.isTTY) {
|
|
155
|
+
return { ok: false, config: null, reason: "Not an interactive terminal — cannot open browser for OAuth." };
|
|
156
|
+
}
|
|
157
|
+
const existingCfg = await readConfig();
|
|
158
|
+
const apiUrl = existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL;
|
|
159
|
+
const token = existingCfg?.teamApiKey || existingCfg?.userJwt || "";
|
|
160
|
+
// If we have a token, verify it still works
|
|
161
|
+
if (token) {
|
|
162
|
+
try {
|
|
163
|
+
const client = new AexolMcpClient(apiUrl, token);
|
|
164
|
+
await client.listTools();
|
|
165
|
+
// Token is still valid
|
|
166
|
+
return { ok: true, config: existingCfg };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
if (!isAuthError(err)) {
|
|
170
|
+
// Non-auth error (network, server down) — don't re-auth, just report
|
|
171
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
172
|
+
return { ok: false, config: existingCfg, reason: `Backend unreachable: ${msg}` };
|
|
173
|
+
}
|
|
174
|
+
// Token is invalid/expired — fall through to OAuth
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// No valid token — run OAuth flow
|
|
178
|
+
process.stderr.write(pc.yellow("\n🔐 Authentication required. Opening browser for Aexol Studio login...\n"));
|
|
179
|
+
const result = await runOAuthFlow(apiUrl);
|
|
180
|
+
if (!result.ok) {
|
|
181
|
+
process.stderr.write(pc.red(`✗ Authentication failed: ${result.reason}\n`));
|
|
182
|
+
return { ok: false, config: existingCfg, reason: result.reason };
|
|
183
|
+
}
|
|
184
|
+
// Re-read config after OAuth saved it
|
|
185
|
+
const freshCfg = await readConfig();
|
|
186
|
+
if (!freshCfg || !freshCfg.userJwt) {
|
|
187
|
+
return { ok: false, config: existingCfg, reason: "OAuth completed but config was not saved correctly." };
|
|
188
|
+
}
|
|
189
|
+
process.stderr.write(pc.green("✓ Authenticated.\n"));
|
|
190
|
+
return { ok: true, config: freshCfg };
|
|
191
|
+
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth login for CLI machine ownership (called from interactive `spectral login`).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2. Open the browser to `https://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).
|
|
4
|
+
* Thin wrapper around the shared `runOAuthFlow()` in auth-helper.ts, adding
|
|
5
|
+
* interactive messaging specific to the `spectral login` command.
|
|
13
6
|
*/
|
|
14
7
|
export declare function runLoginOAuth(): Promise<void>;
|
|
15
8
|
//# sourceMappingURL=login-oauth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../src/commands/login-oauth.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../src/commands/login-oauth.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBnD"}
|
|
@@ -1,119 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth login for CLI machine ownership (called from interactive `spectral login`).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2. Open the browser to `https://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).
|
|
4
|
+
* Thin wrapper around the shared `runOAuthFlow()` in auth-helper.ts, adding
|
|
5
|
+
* interactive messaging specific to the `spectral login` command.
|
|
13
6
|
*/
|
|
14
|
-
import { createServer } from "node:http";
|
|
15
7
|
import pc from "picocolors";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const CALLBACK_TIMEOUT_MS = 120_000; // 2 minutes
|
|
19
|
-
/**
|
|
20
|
-
* Start a temporary HTTP server to receive the OAuth callback.
|
|
21
|
-
* Returns the server and a promise that resolves with the JWT token.
|
|
22
|
-
*/
|
|
23
|
-
function listenForCallback(timeoutMs) {
|
|
24
|
-
const server = createServer();
|
|
25
|
-
const tokenPromise = new Promise((resolve, reject) => {
|
|
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);
|
|
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
|
-
});
|
|
50
|
-
return { server, tokenPromise };
|
|
51
|
-
}
|
|
52
|
-
/** Derive the landing base URL from the backend URL. */
|
|
53
|
-
function deriveLandingUrl(backendUrl) {
|
|
54
|
-
const env = process.env.SPECTRAL_LANDING_URL;
|
|
55
|
-
if (env)
|
|
56
|
-
return env;
|
|
57
|
-
// If using local dev backend, use local dev landing
|
|
58
|
-
if (backendUrl.includes("localhost") || backendUrl.includes("127.0.0.1")) {
|
|
59
|
-
return "http://localhost:3000";
|
|
60
|
-
}
|
|
61
|
-
return DEFAULT_BACKEND_URL;
|
|
62
|
-
}
|
|
8
|
+
import { getConfigFile, readConfig } from "../config.js";
|
|
9
|
+
import { runOAuthFlow } from "../auth-helper.js";
|
|
63
10
|
export async function runLoginOAuth() {
|
|
64
11
|
process.stdout.write(pc.bold("Spectral login (OAuth)\n"));
|
|
65
12
|
process.stdout.write(pc.dim(`Opens a browser to authorize the CLI. The JWT is stored at ${getConfigFile()}.\n\n`));
|
|
66
|
-
// Load existing config to
|
|
13
|
+
// Load existing config to pass apiUrl
|
|
67
14
|
const existingCfg = await readConfig();
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const { server: srv, tokenPromise } = listenForCallback(CALLBACK_TIMEOUT_MS);
|
|
74
|
-
server = srv;
|
|
75
|
-
// Start listening on a random port, then wait for it to be ready
|
|
76
|
-
server.listen(0, "127.0.0.1");
|
|
77
|
-
await new Promise((resolve, reject) => {
|
|
78
|
-
server.once("listening", resolve);
|
|
79
|
-
server.once("error", reject);
|
|
80
|
-
});
|
|
81
|
-
const addr = server.address();
|
|
82
|
-
const port = addr.port;
|
|
83
|
-
// Open browser to the CLI auth page
|
|
84
|
-
const authUrl = `${landingUrl}/cli-auth?port=${port}`;
|
|
85
|
-
process.stdout.write(pc.dim(`Opening browser: ${authUrl}\n`));
|
|
86
|
-
const { exec } = await import("node:child_process");
|
|
87
|
-
const platform = process.platform;
|
|
88
|
-
const openCmd = platform === "darwin"
|
|
89
|
-
? `open "${authUrl}"`
|
|
90
|
-
: platform === "win32"
|
|
91
|
-
? `start "" "${authUrl}"`
|
|
92
|
-
: `xdg-open "${authUrl}"`;
|
|
93
|
-
exec(openCmd, (err) => {
|
|
94
|
-
if (err) {
|
|
95
|
-
process.stderr.write(pc.yellow(`⚠ Could not open browser automatically. Please visit:\n ${authUrl}\n`));
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
// Wait for the token
|
|
99
|
-
process.stdout.write(pc.dim("Waiting for authorization...\n"));
|
|
100
|
-
token = await tokenPromise;
|
|
101
|
-
}
|
|
102
|
-
catch (err) {
|
|
103
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
104
|
-
process.stderr.write(pc.red(`✗ ${msg}\n`));
|
|
15
|
+
const apiUrl = existingCfg?.apiUrl ?? undefined;
|
|
16
|
+
process.stdout.write(pc.dim("Waiting for authorization...\n"));
|
|
17
|
+
const result = await runOAuthFlow(apiUrl);
|
|
18
|
+
if (!result.ok) {
|
|
19
|
+
process.stderr.write(pc.red(`✗ ${result.reason}\n`));
|
|
105
20
|
process.exit(1);
|
|
106
21
|
}
|
|
107
|
-
finally {
|
|
108
|
-
server?.close();
|
|
109
|
-
}
|
|
110
|
-
// Save the token. Preserve existing config (especially teamApiKey).
|
|
111
|
-
const cfg = existingCfg ?? {
|
|
112
|
-
apiUrl: process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL,
|
|
113
|
-
teamApiKey: "",
|
|
114
|
-
};
|
|
115
|
-
cfg.userJwt = token;
|
|
116
|
-
await writeConfig(cfg);
|
|
117
22
|
process.stdout.write(pc.green("✓ CLI authorized.\n"));
|
|
118
23
|
process.stdout.write(pc.dim(`Saved user JWT to ${getConfigFile()}\n`));
|
|
119
24
|
process.stdout.write(pc.dim("Run `spectral serve` to register your machine with owner info.\n"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAerE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CA0SlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -36,6 +36,9 @@ import { homedir, hostname } from "node:os";
|
|
|
36
36
|
import { dirname, join, resolve } from "node:path";
|
|
37
37
|
import { fileURLToPath } from "node:url";
|
|
38
38
|
import { getConfigDir } from "../config.js";
|
|
39
|
+
import { NATIVE_EXTENSIONS, buildNativeExtensionsInfo } from "../sdk/coding-agent/core/extensions/native-extensions.js";
|
|
40
|
+
import { SettingsManager } from "../sdk/coding-agent/core/settings-manager.js";
|
|
41
|
+
import { getAgentDir } from "../sdk/coding-agent/config.js";
|
|
39
42
|
import { requireLogin } from "../preflight.js";
|
|
40
43
|
import { RelayClient } from "../relay/client.js";
|
|
41
44
|
import { detachAllSubscribers, handleAutoResearchFrame, handleCancelTurn, handleClientMessage, handleRestRequest, handleSubscribe, } from "../relay/dispatcher.js";
|
|
@@ -270,6 +273,15 @@ export async function runServe(opts = {}) {
|
|
|
270
273
|
});
|
|
271
274
|
return;
|
|
272
275
|
}
|
|
276
|
+
if (frame.kind === "config_patch") {
|
|
277
|
+
handleConfigPatch(frame, {
|
|
278
|
+
cwd,
|
|
279
|
+
agentDir: getAgentDir(),
|
|
280
|
+
relay,
|
|
281
|
+
machineId: registration.record.machineId,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
273
285
|
// Other frames (error, machine_disconnected addressed to us, etc.)
|
|
274
286
|
// are ignored at this layer. Future batches may surface them in
|
|
275
287
|
// structured logs.
|
|
@@ -281,8 +293,13 @@ export async function runServe(opts = {}) {
|
|
|
281
293
|
relay.on("close", () => {
|
|
282
294
|
detachAllSubscribers(manager, subscribers);
|
|
283
295
|
});
|
|
296
|
+
// Send native extensions info when the relay connects
|
|
297
|
+
relay.on("welcome", () => {
|
|
298
|
+
if (!silent)
|
|
299
|
+
process.stdout.write("✓ Relay connected\n");
|
|
300
|
+
broadcastNativeExtensionsInfo(cwd, relay, registration.record.machineId);
|
|
301
|
+
});
|
|
284
302
|
if (!silent) {
|
|
285
|
-
relay.on("welcome", () => process.stdout.write("✓ Relay connected\n"));
|
|
286
303
|
relay.on("close", ({ code, reason }) => {
|
|
287
304
|
process.stdout.write(`Relay closed (${code}${reason ? `: ${reason}` : ""}); will reconnect…\n`);
|
|
288
305
|
});
|
|
@@ -376,6 +393,56 @@ export async function runServe(opts = {}) {
|
|
|
376
393
|
close,
|
|
377
394
|
};
|
|
378
395
|
}
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Native extension settings helpers
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
function broadcastNativeExtensionsInfo(cwd, relay, machineId) {
|
|
400
|
+
const settingsManager = SettingsManager.create(cwd, getAgentDir());
|
|
401
|
+
settingsManager.reload().then(() => {
|
|
402
|
+
const settings = settingsManager.getSettings();
|
|
403
|
+
const disabledNative = new Set(settings.disabledNativeExtensions ?? []);
|
|
404
|
+
const extensions = buildNativeExtensionsInfo(disabledNative);
|
|
405
|
+
relay.send({
|
|
406
|
+
kind: "native_extensions_info",
|
|
407
|
+
machineId,
|
|
408
|
+
extensions,
|
|
409
|
+
});
|
|
410
|
+
}).catch((err) => {
|
|
411
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
412
|
+
process.stderr.write(`[serve] Failed to broadcast native extensions info: ${msg}\n`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async function handleConfigPatch(frame, deps) {
|
|
416
|
+
const settingsManager = SettingsManager.create(deps.cwd, deps.agentDir);
|
|
417
|
+
await settingsManager.reload();
|
|
418
|
+
const current = settingsManager.getSettings();
|
|
419
|
+
const disabledNative = new Set(current.disabledNativeExtensions ?? []);
|
|
420
|
+
for (const patch of frame.patches) {
|
|
421
|
+
if (patch.path === "disabledNativeExtensions" && typeof patch.value === "string") {
|
|
422
|
+
if (patch.op === "add") {
|
|
423
|
+
// Validate: only known native extension ids
|
|
424
|
+
if (!NATIVE_EXTENSIONS.some((e) => e.id === patch.value)) {
|
|
425
|
+
process.stderr.write(`[serve] Unknown native extension id in config_patch: ${patch.value}\n`);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
disabledNative.add(patch.value);
|
|
429
|
+
}
|
|
430
|
+
else if (patch.op === "remove") {
|
|
431
|
+
disabledNative.delete(patch.value);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Persist to settings.json
|
|
436
|
+
settingsManager.setDisabledNativeExtensions([...disabledNative]);
|
|
437
|
+
await settingsManager.flush();
|
|
438
|
+
// Broadcast updated state back to browsers
|
|
439
|
+
const extensions = buildNativeExtensionsInfo(disabledNative);
|
|
440
|
+
deps.relay.send({
|
|
441
|
+
kind: "native_extensions_info",
|
|
442
|
+
machineId: deps.machineId,
|
|
443
|
+
extensions,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
379
446
|
/**
|
|
380
447
|
* CLI entry point used by `cli.ts`. Surfaces the deprecation warning for
|
|
381
448
|
* `--port`/`SPECTRAL_PORT` once, then defers to `runServe`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"systems.d.ts","sourceRoot":"","sources":["../../src/designer/systems.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,cAAc,EAAE,MAAM,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"systems.d.ts","sourceRoot":"","sources":["../../src/designer/systems.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAYH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,cAAc,EAAE,MAAM,CAAC;CACxB;AAoJD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAajE;AAMD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CA0B/D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CA0C1C"}
|
package/dist/designer/systems.js
CHANGED
|
@@ -26,6 +26,11 @@ function discoverDataDir() {
|
|
|
26
26
|
const prodPath = resolve(__dirname, "data", "systems");
|
|
27
27
|
if (existsSync(prodPath))
|
|
28
28
|
return prodPath;
|
|
29
|
+
// Standalone binary: check relative to the executable (Bun compile)
|
|
30
|
+
const execDir = dirname(process.execPath);
|
|
31
|
+
const standalonePath = resolve(execDir, "data", "systems");
|
|
32
|
+
if (existsSync(standalonePath))
|
|
33
|
+
return standalonePath;
|
|
29
34
|
// Both dev (src/designer/) and production (dist/designer/) layouts resolve the same way
|
|
30
35
|
return prodPath;
|
|
31
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aexol-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/aexol-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AAqFjF,wBAA8B,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"aexol-mcp.d.ts","sourceRoot":"","sources":["../../src/extensions/aexol-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,8BAA8B,CAAC;AAqFjF,wBAA8B,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyRhF"}
|
|
@@ -132,8 +132,39 @@ export default async function aexolMcpExtension(ext) {
|
|
|
132
132
|
catch (err) {
|
|
133
133
|
const msg = err instanceof AexolMcpError || err instanceof Error ? err.message : String(err);
|
|
134
134
|
process.stderr.write(`[aexol-mcp] Failed to fetch tool catalog from ${apiUrl}: ${msg}\n`);
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// Attempt auto-reauth via OAuth browser flow on auth errors
|
|
136
|
+
if (err instanceof AexolMcpError && (err.status === 401 || err.status === 403)) {
|
|
137
|
+
const { ensureAuthenticated } = await import("../auth-helper.js");
|
|
138
|
+
const authResult = await ensureAuthenticated();
|
|
139
|
+
if (authResult.ok && authResult.config) {
|
|
140
|
+
const newToken = authResult.config.teamApiKey || authResult.config.userJwt || "";
|
|
141
|
+
if (newToken && newToken !== token) {
|
|
142
|
+
process.stderr.write("[aexol-mcp] Re-authenticated — retrying tool catalog fetch...\n");
|
|
143
|
+
const newClient = new AexolMcpClient(apiUrl, newToken);
|
|
144
|
+
try {
|
|
145
|
+
tools = await newClient.listTools();
|
|
146
|
+
}
|
|
147
|
+
catch (retryErr) {
|
|
148
|
+
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
149
|
+
process.stderr.write(`[aexol-mcp] Retry also failed: ${retryMsg}\n`);
|
|
150
|
+
process.stderr.write("[aexol-mcp] Continuing without Aexol tools.\n");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
process.stderr.write("[aexol-mcp] Re-authentication did not produce a new token. Continuing without Aexol tools.\n");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
process.stderr.write(`[aexol-mcp] Re-authentication failed: ${authResult.reason ?? "unknown"}. Continuing without Aexol tools.\n`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
process.stderr.write("[aexol-mcp] Continuing without Aexol tools.\n");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
137
168
|
}
|
|
138
169
|
let registered = 0;
|
|
139
170
|
for (const tool of tools) {
|