@aexol/spectral 0.8.6 → 0.8.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.
Files changed (85) hide show
  1. package/dist/auth-helper.d.ts +49 -0
  2. package/dist/auth-helper.d.ts.map +1 -0
  3. package/dist/auth-helper.js +191 -0
  4. package/dist/commands/login-oauth.d.ts +2 -9
  5. package/dist/commands/login-oauth.d.ts.map +1 -1
  6. package/dist/commands/login-oauth.js +10 -105
  7. package/dist/commands/serve.d.ts.map +1 -1
  8. package/dist/commands/serve.js +68 -1
  9. package/dist/designer/systems.d.ts.map +1 -1
  10. package/dist/designer/systems.js +5 -0
  11. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  12. package/dist/extensions/aexol-mcp.js +33 -2
  13. package/dist/extensions/browser/browser-service.d.ts +104 -0
  14. package/dist/extensions/browser/browser-service.d.ts.map +1 -0
  15. package/dist/extensions/browser/browser-service.js +333 -0
  16. package/dist/extensions/browser/config.d.ts +24 -0
  17. package/dist/extensions/browser/config.d.ts.map +1 -0
  18. package/dist/extensions/browser/config.js +17 -0
  19. package/dist/extensions/browser/index.d.ts +35 -0
  20. package/dist/extensions/browser/index.d.ts.map +1 -0
  21. package/dist/extensions/browser/index.js +126 -0
  22. package/dist/extensions/browser/stream/signaling-client.d.ts +28 -0
  23. package/dist/extensions/browser/stream/signaling-client.d.ts.map +1 -0
  24. package/dist/extensions/browser/stream/signaling-client.js +69 -0
  25. package/dist/extensions/browser/stream/stream-injector.d.ts +30 -0
  26. package/dist/extensions/browser/stream/stream-injector.d.ts.map +1 -0
  27. package/dist/extensions/browser/stream/stream-injector.js +161 -0
  28. package/dist/extensions/browser/stream/stream-tool.d.ts +10 -0
  29. package/dist/extensions/browser/stream/stream-tool.d.ts.map +1 -0
  30. package/dist/extensions/browser/stream/stream-tool.js +182 -0
  31. package/dist/extensions/browser/tools/console.d.ts +7 -0
  32. package/dist/extensions/browser/tools/console.d.ts.map +1 -0
  33. package/dist/extensions/browser/tools/console.js +54 -0
  34. package/dist/extensions/browser/tools/core.d.ts +7 -0
  35. package/dist/extensions/browser/tools/core.d.ts.map +1 -0
  36. package/dist/extensions/browser/tools/core.js +115 -0
  37. package/dist/extensions/browser/tools/dialog.d.ts +11 -0
  38. package/dist/extensions/browser/tools/dialog.d.ts.map +1 -0
  39. package/dist/extensions/browser/tools/dialog.js +54 -0
  40. package/dist/extensions/browser/tools/drop.d.ts +7 -0
  41. package/dist/extensions/browser/tools/drop.d.ts.map +1 -0
  42. package/dist/extensions/browser/tools/drop.js +93 -0
  43. package/dist/extensions/browser/tools/evaluate.d.ts +7 -0
  44. package/dist/extensions/browser/tools/evaluate.d.ts.map +1 -0
  45. package/dist/extensions/browser/tools/evaluate.js +105 -0
  46. package/dist/extensions/browser/tools/input.d.ts +7 -0
  47. package/dist/extensions/browser/tools/input.d.ts.map +1 -0
  48. package/dist/extensions/browser/tools/input.js +290 -0
  49. package/dist/extensions/browser/tools/network.d.ts +7 -0
  50. package/dist/extensions/browser/tools/network.d.ts.map +1 -0
  51. package/dist/extensions/browser/tools/network.js +116 -0
  52. package/dist/extensions/browser/tools/screenshot.d.ts +7 -0
  53. package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -0
  54. package/dist/extensions/browser/tools/screenshot.js +153 -0
  55. package/dist/extensions/browser/tools/tabs.d.ts +7 -0
  56. package/dist/extensions/browser/tools/tabs.d.ts.map +1 -0
  57. package/dist/extensions/browser/tools/tabs.js +72 -0
  58. package/dist/extensions/browser/tools/wait.d.ts +7 -0
  59. package/dist/extensions/browser/tools/wait.d.ts.map +1 -0
  60. package/dist/extensions/browser/tools/wait.js +64 -0
  61. package/dist/extensions/kanban-bridge.d.ts.map +1 -1
  62. package/dist/extensions/kanban-bridge.js +70 -3
  63. package/dist/mcp/ui-server.d.ts.map +1 -1
  64. package/dist/mcp/ui-server.js +23 -10
  65. package/dist/relay/models-fetch.d.ts +1 -1
  66. package/dist/relay/models-fetch.js +4 -4
  67. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts +60 -0
  68. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -0
  69. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +108 -0
  70. package/dist/sdk/coding-agent/core/resource-loader.d.ts.map +1 -1
  71. package/dist/sdk/coding-agent/core/resource-loader.js +23 -1
  72. package/dist/sdk/coding-agent/core/settings-manager.d.ts +5 -0
  73. package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
  74. package/dist/sdk/coding-agent/core/settings-manager.js +12 -0
  75. package/dist/server/agent-bridge.d.ts +2 -2
  76. package/dist/server/agent-bridge.d.ts.map +1 -1
  77. package/dist/server/agent-bridge.js +1 -1
  78. package/dist/server/session-stream.d.ts.map +1 -1
  79. package/dist/server/session-stream.js +24 -5
  80. package/dist/server/sqlite-adapter.d.ts +41 -0
  81. package/dist/server/sqlite-adapter.d.ts.map +1 -0
  82. package/dist/server/sqlite-adapter.js +128 -0
  83. package/dist/server/storage.d.ts.map +1 -1
  84. package/dist/server/storage.js +2 -2
  85. 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
- * Flow:
5
- * 1. Start a temporary HTTP server on a random localhost port.
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;;;;;;;;;;;;GAYG;AAsEH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA4EnD"}
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
- * Flow:
5
- * 1. Start a temporary HTTP server on a random localhost port.
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 { DEFAULT_API_URL, getConfigFile, readConfig, writeConfig, } from "../config.js";
17
- const DEFAULT_BACKEND_URL = "https://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 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 get backendUrl
13
+ // Load existing config to pass apiUrl
67
14
  const existingCfg = await readConfig();
68
- const landingUrl = deriveLandingUrl(existingCfg?.apiUrl ?? process.env.SPECTRAL_MCP_URL ?? DEFAULT_API_URL);
69
- // Start the callback listener FIRST (port must be known before opening the browser)
70
- let server;
71
- let token;
72
- try {
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;AAiBH,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,CA4RlF;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
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"}
@@ -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;AA+ID,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"}
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"}
@@ -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,CA6PhF"}
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
- process.stderr.write("[aexol-mcp] Continuing without Aexol tools.\n");
136
- return;
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) {