ashlrcode 1.0.0

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * MCP Client — connects to an MCP server via stdio transport.
3
+ *
4
+ * Spawns a child process and communicates via JSON-RPC over stdin/stdout.
5
+ */
6
+
7
+ import type {
8
+ MCPServerConfig,
9
+ MCPToolInfo,
10
+ MCPToolResult,
11
+ MCPInitializeResult,
12
+ JsonRpcRequest,
13
+ JsonRpcResponse,
14
+ } from "./types.ts";
15
+
16
+ export class MCPClient {
17
+ private proc: any = null;
18
+ private nextId = 1;
19
+ private pending = new Map<number, {
20
+ resolve: (value: unknown) => void;
21
+ reject: (error: Error) => void;
22
+ }>();
23
+ private rawBuffer = Buffer.alloc(0);
24
+ private serverInfo: MCPInitializeResult | null = null;
25
+ private _tools: MCPToolInfo[] = [];
26
+
27
+ constructor(
28
+ readonly name: string,
29
+ private config: MCPServerConfig
30
+ ) {}
31
+
32
+ get tools(): MCPToolInfo[] {
33
+ return this._tools;
34
+ }
35
+
36
+ get isConnected(): boolean {
37
+ return this.proc !== null;
38
+ }
39
+
40
+ async connect(): Promise<void> {
41
+ const env = { ...process.env, ...this.config.env };
42
+
43
+ this.proc = Bun.spawn([this.config.command, ...(this.config.args ?? [])], {
44
+ stdin: "pipe",
45
+ stdout: "pipe",
46
+ stderr: "pipe",
47
+ env,
48
+ });
49
+
50
+ // Read stdout in background
51
+ this.readLoop();
52
+
53
+ // Initialize
54
+ this.serverInfo = await this.request("initialize", {
55
+ protocolVersion: "2024-11-05",
56
+ capabilities: {},
57
+ clientInfo: { name: "ashlrcode", version: "0.7.0" },
58
+ }) as MCPInitializeResult;
59
+
60
+ // Send initialized notification
61
+ this.notify("notifications/initialized", {});
62
+
63
+ // List tools
64
+ const result = await this.request("tools/list", {}) as { tools: MCPToolInfo[] };
65
+ this._tools = result.tools ?? [];
66
+ }
67
+
68
+ async callTool(name: string, args: Record<string, unknown>): Promise<MCPToolResult> {
69
+ const result = await this.request("tools/call", {
70
+ name,
71
+ arguments: args,
72
+ });
73
+ return result as MCPToolResult;
74
+ }
75
+
76
+ async disconnect(): Promise<void> {
77
+ if (this.proc) {
78
+ try {
79
+ this.proc.stdin.end();
80
+ this.proc.kill();
81
+ } catch {
82
+ // Process may already be dead
83
+ }
84
+ this.proc = null;
85
+ }
86
+ // Reject pending requests
87
+ for (const [, pending] of this.pending) {
88
+ pending.reject(new Error("MCP client disconnected"));
89
+ }
90
+ this.pending.clear();
91
+ }
92
+
93
+ private async request(method: string, params: Record<string, unknown>): Promise<unknown> {
94
+ const id = this.nextId++;
95
+ const message: JsonRpcRequest = {
96
+ jsonrpc: "2.0",
97
+ id,
98
+ method,
99
+ params,
100
+ };
101
+
102
+ return new Promise((resolve, reject) => {
103
+ const timeout = setTimeout(() => {
104
+ this.pending.delete(id);
105
+ reject(new Error(`MCP request timeout: ${method}`));
106
+ }, 5_000); // 5s timeout (was 30s — don't block startup)
107
+
108
+ this.pending.set(id, {
109
+ resolve: (value) => { clearTimeout(timeout); resolve(value); },
110
+ reject: (err) => { clearTimeout(timeout); reject(err); },
111
+ });
112
+
113
+ this.send(message as unknown as Record<string, unknown>);
114
+ });
115
+ }
116
+
117
+ private notify(method: string, params: Record<string, unknown>): void {
118
+ this.send({ jsonrpc: "2.0", method, params });
119
+ }
120
+
121
+ private send(message: Record<string, unknown>): void {
122
+ if (!this.proc) throw new Error("MCP client not connected");
123
+ const json = JSON.stringify(message);
124
+ // Use Buffer.byteLength for spec-correct Content-Length (UTF-8 bytes)
125
+ // Write as Buffer to ensure byte-level consistency with the header
126
+ const header = `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n`;
127
+ const buf = Buffer.concat([Buffer.from(header), Buffer.from(json, "utf-8")]);
128
+ this.proc.stdin.write(buf);
129
+ }
130
+
131
+ private async readLoop(): Promise<void> {
132
+ if (!this.proc) return;
133
+
134
+ const reader = this.proc.stdout.getReader();
135
+
136
+ try {
137
+ while (true) {
138
+ const { done, value } = await reader.read();
139
+ if (done) break;
140
+ this.rawBuffer = Buffer.concat([this.rawBuffer, Buffer.from(value)]);
141
+ this.processBuffer();
142
+ }
143
+ } catch {
144
+ // Stream ended or errored
145
+ }
146
+
147
+ // Reject all pending requests when read loop exits (server crash/EOF)
148
+ for (const [id, pending] of this.pending) {
149
+ this.pending.delete(id);
150
+ pending.reject(new Error("MCP server connection closed"));
151
+ }
152
+ }
153
+
154
+ private processBuffer(): void {
155
+ while (true) {
156
+ // Look for Content-Length header in byte buffer
157
+ const separator = Buffer.from("\r\n\r\n");
158
+ const headerEnd = this.rawBuffer.indexOf(separator);
159
+ if (headerEnd === -1) break;
160
+
161
+ const header = this.rawBuffer.subarray(0, headerEnd).toString("utf-8");
162
+ const match = header.match(/Content-Length:\s*(\d+)/i);
163
+ if (!match) {
164
+ // Skip malformed header
165
+ this.rawBuffer = this.rawBuffer.subarray(headerEnd + 4);
166
+ continue;
167
+ }
168
+
169
+ const contentLength = parseInt(match[1]!, 10);
170
+ const bodyStart = headerEnd + 4;
171
+ const bodyEnd = bodyStart + contentLength;
172
+
173
+ if (this.rawBuffer.length < bodyEnd) break; // Need more data
174
+
175
+ const body = this.rawBuffer.subarray(bodyStart, bodyEnd).toString("utf-8");
176
+ this.rawBuffer = this.rawBuffer.subarray(bodyEnd);
177
+
178
+ try {
179
+ const message = JSON.parse(body) as JsonRpcResponse;
180
+ if ("id" in message && message.id !== undefined) {
181
+ const pending = this.pending.get(message.id);
182
+ if (pending) {
183
+ this.pending.delete(message.id);
184
+ if (message.error) {
185
+ pending.reject(new Error(`MCP error: ${message.error.message}`));
186
+ } else {
187
+ pending.resolve(message.result);
188
+ }
189
+ }
190
+ }
191
+ // Notifications (no id) are ignored for now
192
+ } catch {
193
+ // Malformed JSON, skip
194
+ }
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * MCP Connection Manager — manages multiple MCP server connections.
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import { MCPClient } from "./client.ts";
7
+ import type { MCPServerConfig, MCPToolInfo } from "./types.ts";
8
+ import { authorizeOAuth } from "./oauth.ts";
9
+ import type { OAuthConfig } from "./oauth.ts";
10
+
11
+ export class MCPManager {
12
+ private clients = new Map<string, MCPClient>();
13
+
14
+ /**
15
+ * Connect to all configured MCP servers.
16
+ */
17
+ async connectAll(
18
+ servers: Record<string, MCPServerConfig>
19
+ ): Promise<void> {
20
+ const entries = Object.entries(servers);
21
+ if (entries.length === 0) return;
22
+
23
+ const results = await Promise.allSettled(
24
+ entries.map(async ([name, config]) => {
25
+ // If OAuth is configured, authenticate before connecting
26
+ let effectiveConfig = config;
27
+ if (config.oauth) {
28
+ try {
29
+ const oauthConfig: OAuthConfig = {
30
+ ...config.oauth,
31
+ scopes: config.oauth.scopes ?? [],
32
+ };
33
+ const token = await authorizeOAuth(name, oauthConfig);
34
+ // Pass the Bearer token to the MCP server via environment
35
+ effectiveConfig = {
36
+ ...config,
37
+ env: {
38
+ ...config.env,
39
+ MCP_AUTH_TOKEN: token.accessToken,
40
+ MCP_AUTH_TYPE: token.tokenType,
41
+ },
42
+ };
43
+ } catch (err) {
44
+ console.log(
45
+ chalk.dim(
46
+ ` MCP: ${name} OAuth failed — ${err instanceof Error ? err.message : "unknown error"}`
47
+ )
48
+ );
49
+ return;
50
+ }
51
+ }
52
+
53
+ const client = new MCPClient(name, effectiveConfig);
54
+ try {
55
+ await client.connect();
56
+ this.clients.set(name, client);
57
+ console.log(
58
+ chalk.dim(
59
+ ` MCP: ${name} connected (${client.tools.length} tools)`
60
+ )
61
+ );
62
+ } catch {
63
+ // Silently skip failed MCP servers — they may not be running
64
+ }
65
+ })
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Get all discovered tools across all connected servers.
71
+ */
72
+ getAllTools(): Array<{ serverName: string; tool: MCPToolInfo }> {
73
+ const tools: Array<{ serverName: string; tool: MCPToolInfo }> = [];
74
+ for (const [name, client] of this.clients) {
75
+ for (const tool of client.tools) {
76
+ tools.push({ serverName: name, tool });
77
+ }
78
+ }
79
+ return tools;
80
+ }
81
+
82
+ /**
83
+ * Call a tool on a specific server.
84
+ */
85
+ async callTool(
86
+ serverName: string,
87
+ toolName: string,
88
+ args: Record<string, unknown>
89
+ ): Promise<string> {
90
+ const client = this.clients.get(serverName);
91
+ if (!client) {
92
+ return `MCP server "${serverName}" not connected`;
93
+ }
94
+
95
+ const result = await client.callTool(toolName, args);
96
+
97
+ // Extract text from result content blocks
98
+ const text = result.content
99
+ .map((c) => c.text ?? JSON.stringify(c))
100
+ .join("\n");
101
+
102
+ if (result.isError) {
103
+ return `MCP Error: ${text}`;
104
+ }
105
+
106
+ return text;
107
+ }
108
+
109
+ /**
110
+ * Disconnect all servers.
111
+ */
112
+ async disconnectAll(): Promise<void> {
113
+ for (const [, client] of this.clients) {
114
+ await client.disconnect();
115
+ }
116
+ this.clients.clear();
117
+ }
118
+
119
+ /**
120
+ * Get connected server names.
121
+ */
122
+ getServerNames(): string[] {
123
+ return Array.from(this.clients.keys());
124
+ }
125
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * MCP OAuth 2.0 — authentication flow for MCP servers.
3
+ * Supports authorization code flow with PKCE, token refresh, and API key fallback.
4
+ */
5
+
6
+ import { createServer } from "http";
7
+ import { randomBytes, createHash } from "crypto";
8
+ import { readFile, writeFile, mkdir, unlink } from "fs/promises";
9
+ import { existsSync } from "fs";
10
+ import { join } from "path";
11
+ import { getConfigDir } from "../config/settings.ts";
12
+
13
+ export interface OAuthConfig {
14
+ authorizationUrl: string;
15
+ tokenUrl: string;
16
+ clientId: string;
17
+ clientSecret?: string;
18
+ scopes: string[];
19
+ redirectPort?: number; // Default: 8742
20
+ }
21
+
22
+ export interface TokenSet {
23
+ accessToken: string;
24
+ refreshToken?: string;
25
+ expiresAt: number; // Unix timestamp ms
26
+ tokenType: string;
27
+ scope?: string;
28
+ }
29
+
30
+ function getTokenCachePath(serverId: string): string {
31
+ return join(getConfigDir(), "mcp-tokens", `${serverId}.json`);
32
+ }
33
+
34
+ /** Load cached tokens for an MCP server */
35
+ export async function loadCachedToken(
36
+ serverId: string
37
+ ): Promise<TokenSet | null> {
38
+ const path = getTokenCachePath(serverId);
39
+ if (!existsSync(path)) return null;
40
+ try {
41
+ const raw = await readFile(path, "utf-8");
42
+ const token = JSON.parse(raw) as TokenSet;
43
+ // If expired and no refresh token, discard
44
+ if (token.expiresAt && Date.now() > token.expiresAt - 60_000) {
45
+ if (token.refreshToken) return token; // Can still refresh
46
+ return null;
47
+ }
48
+ return token;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /** Save tokens to cache */
55
+ async function saveToken(serverId: string, token: TokenSet): Promise<void> {
56
+ const dir = join(getConfigDir(), "mcp-tokens");
57
+ await mkdir(dir, { recursive: true });
58
+ await writeFile(
59
+ getTokenCachePath(serverId),
60
+ JSON.stringify(token, null, 2),
61
+ { encoding: "utf-8", mode: 0o600 }
62
+ );
63
+ }
64
+
65
+ /** Generate PKCE code verifier and challenge (S256) */
66
+ function generatePKCE(): { verifier: string; challenge: string } {
67
+ const verifier = randomBytes(32).toString("base64url");
68
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
69
+ return { verifier, challenge };
70
+ }
71
+
72
+ /** Refresh an expired token */
73
+ async function refreshAccessToken(
74
+ config: OAuthConfig,
75
+ refreshTokenStr: string
76
+ ): Promise<TokenSet> {
77
+ const response = await fetch(config.tokenUrl, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
80
+ body: new URLSearchParams({
81
+ grant_type: "refresh_token",
82
+ refresh_token: refreshTokenStr,
83
+ client_id: config.clientId,
84
+ ...(config.clientSecret
85
+ ? { client_secret: config.clientSecret }
86
+ : {}),
87
+ }),
88
+ });
89
+
90
+ if (!response.ok) {
91
+ throw new Error(`Token refresh failed: ${response.status}`);
92
+ }
93
+
94
+ const data = (await response.json()) as Record<string, unknown>;
95
+ return {
96
+ accessToken: data.access_token as string,
97
+ refreshToken:
98
+ (data.refresh_token as string | undefined) ?? refreshTokenStr,
99
+ expiresAt:
100
+ Date.now() + ((data.expires_in as number | undefined) ?? 3600) * 1000,
101
+ tokenType: (data.token_type as string | undefined) ?? "Bearer",
102
+ scope: data.scope as string | undefined,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Run OAuth 2.0 authorization code flow with PKCE.
108
+ * Opens browser for auth, listens on localhost for redirect.
109
+ * Returns a valid TokenSet (from cache, refresh, or fresh authorization).
110
+ */
111
+ export async function authorizeOAuth(
112
+ serverId: string,
113
+ config: OAuthConfig
114
+ ): Promise<TokenSet> {
115
+ // 1. Check cache — return immediately if still valid
116
+ const cached = await loadCachedToken(serverId);
117
+ if (cached && Date.now() < cached.expiresAt - 60_000) return cached;
118
+
119
+ // 2. Try refresh if we have a refresh token
120
+ if (cached?.refreshToken) {
121
+ try {
122
+ const refreshed = await refreshAccessToken(config, cached.refreshToken);
123
+ await saveToken(serverId, refreshed);
124
+ return refreshed;
125
+ } catch {
126
+ // Refresh failed — fall through to full authorization
127
+ }
128
+ }
129
+
130
+ // 3. Full authorization code flow with PKCE
131
+ const port = config.redirectPort ?? 8742;
132
+ const redirectUri = `http://localhost:${port}/callback`;
133
+ const { verifier, challenge } = generatePKCE();
134
+ const state = randomBytes(16).toString("hex");
135
+
136
+ const params = new URLSearchParams({
137
+ response_type: "code",
138
+ client_id: config.clientId,
139
+ redirect_uri: redirectUri,
140
+ scope: config.scopes.join(" "),
141
+ state,
142
+ code_challenge: challenge,
143
+ code_challenge_method: "S256",
144
+ });
145
+ const authUrl = `${config.authorizationUrl}?${params}`;
146
+
147
+ // Open browser (platform-aware)
148
+ const openCmd =
149
+ process.platform === "darwin"
150
+ ? "open"
151
+ : process.platform === "win32"
152
+ ? "start"
153
+ : "xdg-open";
154
+ Bun.spawn([openCmd, authUrl], { stdout: "pipe", stderr: "pipe" });
155
+
156
+ console.log(`\n 🔐 Opening browser for authorization...`);
157
+ console.log(` If browser doesn't open, visit: ${authUrl}\n`);
158
+
159
+ // Listen for the OAuth callback
160
+ const code = await new Promise<string>((resolve, reject) => {
161
+ const timeout = setTimeout(() => {
162
+ server.close();
163
+ reject(new Error("OAuth timeout — no callback received within 120s"));
164
+ }, 120_000);
165
+
166
+ const server = createServer((req, res) => {
167
+ const url = new URL(req.url!, `http://localhost:${port}`);
168
+
169
+ if (url.pathname !== "/callback") {
170
+ res.writeHead(404);
171
+ res.end();
172
+ return;
173
+ }
174
+
175
+ const returnedState = url.searchParams.get("state");
176
+ const returnedCode = url.searchParams.get("code");
177
+ const error = url.searchParams.get("error");
178
+
179
+ if (error) {
180
+ res.writeHead(200, { "Content-Type": "text/html" });
181
+ res.end(
182
+ "<h1>Authorization Failed</h1><p>You can close this window.</p>"
183
+ );
184
+ clearTimeout(timeout);
185
+ server.close();
186
+ reject(new Error(`OAuth error: ${error}`));
187
+ return;
188
+ }
189
+
190
+ if (returnedState !== state || !returnedCode) {
191
+ res.writeHead(400);
192
+ res.end("Invalid state or missing code");
193
+ clearTimeout(timeout);
194
+ server.close();
195
+ reject(new Error("Invalid OAuth state or missing code"));
196
+ return;
197
+ }
198
+
199
+ res.writeHead(200, { "Content-Type": "text/html" });
200
+ res.end(
201
+ "<h1>Authorization Complete</h1><p>You can close this window and return to AshlrCode.</p>"
202
+ );
203
+ clearTimeout(timeout);
204
+ server.close();
205
+ resolve(returnedCode);
206
+ });
207
+
208
+ server.listen(port);
209
+ });
210
+
211
+ // 4. Exchange authorization code for tokens
212
+ const tokenResponse = await fetch(config.tokenUrl, {
213
+ method: "POST",
214
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
215
+ body: new URLSearchParams({
216
+ grant_type: "authorization_code",
217
+ code,
218
+ redirect_uri: redirectUri,
219
+ client_id: config.clientId,
220
+ ...(config.clientSecret
221
+ ? { client_secret: config.clientSecret }
222
+ : {}),
223
+ code_verifier: verifier,
224
+ }),
225
+ });
226
+
227
+ if (!tokenResponse.ok) {
228
+ throw new Error(`Token exchange failed: ${tokenResponse.status}`);
229
+ }
230
+
231
+ const data = (await tokenResponse.json()) as Record<string, unknown>;
232
+ const token: TokenSet = {
233
+ accessToken: data.access_token as string,
234
+ refreshToken: data.refresh_token as string | undefined,
235
+ expiresAt:
236
+ Date.now() + ((data.expires_in as number | undefined) ?? 3600) * 1000,
237
+ tokenType: (data.token_type as string | undefined) ?? "Bearer",
238
+ scope: data.scope as string | undefined,
239
+ };
240
+
241
+ await saveToken(serverId, token);
242
+ console.log(" ✓ Authorization successful\n");
243
+ return token;
244
+ }
245
+
246
+ /** Revoke / delete cached token for a server */
247
+ export async function revokeToken(serverId: string): Promise<void> {
248
+ const path = getTokenCachePath(serverId);
249
+ if (existsSync(path)) {
250
+ await unlink(path);
251
+ }
252
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * MCP (Model Context Protocol) types — JSON-RPC over stdio.
3
+ */
4
+
5
+ export interface MCPServerConfig {
6
+ command: string;
7
+ args?: string[];
8
+ env?: Record<string, string>;
9
+ oauth?: {
10
+ authorizationUrl: string;
11
+ tokenUrl: string;
12
+ clientId: string;
13
+ clientSecret?: string;
14
+ scopes: string[];
15
+ };
16
+ }
17
+
18
+ // JSON-RPC 2.0 message types
19
+ export interface JsonRpcRequest {
20
+ jsonrpc: "2.0";
21
+ id: number;
22
+ method: string;
23
+ params?: Record<string, unknown>;
24
+ }
25
+
26
+ export interface JsonRpcResponse {
27
+ jsonrpc: "2.0";
28
+ id: number;
29
+ result?: unknown;
30
+ error?: { code: number; message: string; data?: unknown };
31
+ }
32
+
33
+ export interface JsonRpcNotification {
34
+ jsonrpc: "2.0";
35
+ method: string;
36
+ params?: Record<string, unknown>;
37
+ }
38
+
39
+ // MCP-specific types
40
+ export interface MCPToolInfo {
41
+ name: string;
42
+ description?: string;
43
+ inputSchema: Record<string, unknown>;
44
+ }
45
+
46
+ export interface MCPToolResult {
47
+ content: Array<{ type: string; text?: string }>;
48
+ isError?: boolean;
49
+ }
50
+
51
+ export interface MCPServerCapabilities {
52
+ tools?: Record<string, unknown>;
53
+ resources?: Record<string, unknown>;
54
+ prompts?: Record<string, unknown>;
55
+ }
56
+
57
+ export interface MCPInitializeResult {
58
+ protocolVersion: string;
59
+ capabilities: MCPServerCapabilities;
60
+ serverInfo: { name: string; version?: string };
61
+ }