@causyne/agent 1.10.13 → 1.10.17

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 (59) hide show
  1. package/docs/ARCHITECTURE.md +101 -17
  2. package/docs/SECURITY_REVIEW.md +13 -1
  3. package/docs/THIRD_PARTY_NOTICES.md +43 -10
  4. package/package.json +10 -4
  5. package/src/agentLoop.js +25 -3
  6. package/src/agentProfiles.js +135 -0
  7. package/src/agentProfiles.ts +143 -0
  8. package/src/approvalGrants.js +17 -35
  9. package/src/approvalGrants.ts +54 -0
  10. package/src/auditLog.d.ts +3 -0
  11. package/src/browser/camofoxRuntime.js +93 -0
  12. package/src/browser/camofoxRuntime.ts +112 -0
  13. package/src/browser/causyneBrowser.js +495 -0
  14. package/src/browser/causyneBrowser.ts +594 -0
  15. package/src/browser/searchProviders.js +156 -0
  16. package/src/browser/searchProviders.ts +156 -0
  17. package/src/browser/snapshotWindow.js +28 -0
  18. package/src/browser/snapshotWindow.ts +40 -0
  19. package/src/browser/urlPolicy.js +89 -0
  20. package/src/browser/urlPolicy.ts +89 -0
  21. package/src/checkpoints.js +19 -6
  22. package/src/commands.js +37 -8
  23. package/src/config.d.ts +2 -0
  24. package/src/hooks.js +121 -107
  25. package/src/hooks.ts +193 -0
  26. package/src/mcp/mcpClient.js +520 -132
  27. package/src/mcp/mcpClient.ts +584 -0
  28. package/src/mcp/mcpRegistry.js +399 -185
  29. package/src/mcp/mcpRegistry.ts +451 -0
  30. package/src/planCache.js +473 -450
  31. package/src/planCache.ts +840 -0
  32. package/src/plugins/pluginManager.js +40 -5
  33. package/src/plugins/pluginSkills.js +84 -0
  34. package/src/plugins/pluginSkills.ts +106 -0
  35. package/src/provisioning/capabilityProvisioner.d.ts +10 -0
  36. package/src/provisioning/capabilityProvisioner.js +5 -0
  37. package/src/provisioning/capabilityTools.js +2 -1
  38. package/src/sandbox.js +32 -2
  39. package/src/sessionJournal.js +471 -0
  40. package/src/sessionJournal.ts +496 -0
  41. package/src/sessionStore.js +3 -279
  42. package/src/skillsManager.js +18 -0
  43. package/src/subagentJournal.js +215 -0
  44. package/src/subagentJournal.ts +254 -0
  45. package/src/taskShape.js +179 -198
  46. package/src/taskShape.ts +259 -0
  47. package/src/tools/browserAutomator.js +45 -241
  48. package/src/tools/browserAutomator.ts +57 -0
  49. package/src/tools/manageLocalAiDependencies.js +1 -1
  50. package/src/tools/subagentOrchestrator.js +73 -12
  51. package/src/tools/webFetch.js +183 -68
  52. package/src/tools/webFetch.ts +185 -0
  53. package/src/tools/webSearch.js +50 -75
  54. package/src/tools/webSearch.ts +56 -0
  55. package/src/types/contracts.js +8 -0
  56. package/src/types/contracts.ts +263 -0
  57. package/src/utils/paths.d.ts +3 -0
  58. package/src/version.d.ts +1 -0
  59. package/src/mcp/jsonRpc.js +0 -165
@@ -1,82 +1,57 @@
1
+ import { BrowserCapabilityError, causyneBrowser } from '../browser/causyneBrowser.js';
2
+ import { searchPublicWeb } from '../browser/searchProviders.js';
3
+ function renderResults(query, results) {
4
+ const lines = [`Search results for "${query}"`, ''];
5
+ for (const [index, result] of results.entries()) {
6
+ lines.push(`${index + 1}. ${result.title}`, ` URL: ${result.url}`);
7
+ if (result.snippet)
8
+ lines.push(` ${result.snippet}`);
9
+ lines.push('');
10
+ }
11
+ lines.push(`Search engine used: ${results[0]?.engine ?? 'unknown'}`);
12
+ return lines.join('\n').trim();
13
+ }
1
14
  export const webSearch = {
2
- schema: {
3
- name: 'web_search',
4
- description: 'High-Precision SERP Router. Searches the web and extracts top organic results.',
5
- parameters: {
6
- type: 'object',
7
- properties: {
8
- query: {
9
- type: 'string',
10
- description: 'The search query.'
15
+ schema: {
16
+ name: 'web_search',
17
+ description: 'Search the public web through lightweight providers, with the optional Causyne Camoufox browser as a rendered-page fallback. No browser binary is downloaded automatically.',
18
+ parameters: {
19
+ type: 'object',
20
+ properties: {
21
+ query: { type: 'string', description: 'Search query, 1-1000 characters.' }
22
+ },
23
+ required: ['query'],
24
+ additionalProperties: false
11
25
  }
12
- },
13
- required: ['query']
14
- }
15
- },
16
-
17
- execute: async (args) => {
18
- try {
19
- // Phase 7 AGI Upgrade: Zero-Dependency Native Fetch Scraper
20
- // Skipping Google's aggressive 429 Blocks by utilizing Yahoo Search HTML parsing.
21
- const url = 'https://search.yahoo.com/search?p=' + encodeURIComponent(args.query);
22
-
23
- const response = await fetch(url, {
24
- headers: {
25
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
26
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
27
- 'Accept-Language': 'en-US,en;q=0.5'
26
+ },
27
+ async execute(args) {
28
+ const query = String(args.query ?? '').trim();
29
+ if (!query || query.length > 1_000)
30
+ return { success: false, error: 'Search query must contain 1-1000 characters' };
31
+ let lightweightFailure = '';
32
+ try {
33
+ const results = await searchPublicWeb(query);
34
+ return { success: true, output: renderResults(query, results), results };
35
+ }
36
+ catch (error) {
37
+ lightweightFailure = error instanceof Error ? error.message : String(error);
28
38
  }
29
- });
30
-
31
- if (!response.ok) {
32
- throw new Error(`HTTP Error ${response.status}`);
33
- }
34
-
35
- const html = await response.text();
36
- const results = [];
37
-
38
- // Robust Regex to extract Yahoo organic results
39
- const regex = /<div class=\"compTitle[^>]*>.*?<h3[^>]*>.*?<a[^>]*href=\"([^\"]+)\"[^>]*>(.*?)<\/a>.*?<\/h3>.*?<div class=\"compText[^>]*>(.*?)<\/div>/gs;
40
-
41
- let match;
42
- while ((match = regex.exec(html)) !== null && results.length < 8) {
43
- let link = match[1];
44
-
45
- // Yahoo heavily redirects via RU=..., decode it to get the raw clean URL
46
- if (link.includes('RU=')) {
47
- const ruMatch = link.match(/RU=([^/]+)\//);
48
- if (ruMatch) link = decodeURIComponent(ruMatch[1]);
39
+ try {
40
+ const results = await causyneBrowser().search(query);
41
+ return { success: true, output: renderResults(query, results), results };
49
42
  }
50
-
51
- // Skip sponsored Bing/Yahoo Ads
52
- if (link.includes('bing.com/aclick') || link.includes('yahoo.com/yhs/')) {
53
- continue;
43
+ catch (error) {
44
+ if (error instanceof BrowserCapabilityError) {
45
+ return {
46
+ success: false,
47
+ error: `${lightweightFailure}; rendered browser fallback unavailable: ${error.message}`,
48
+ requiresConsent: error.requiresConsent
49
+ };
50
+ }
51
+ return {
52
+ success: false,
53
+ error: `Web search failed after lightweight and rendered providers: ${lightweightFailure}; ${error instanceof Error ? error.message : String(error)}`
54
+ };
54
55
  }
55
-
56
- results.push({
57
- url: link,
58
- title: match[2].replace(/<[^>]+>/g, '').trim(),
59
- snippet: match[3].replace(/<[^>]+>/g, '').trim()
60
- });
61
- }
62
-
63
- if (results.length === 0) {
64
- return { success: true, output: "No organic results found. Try using web.fetch on known direct domains." };
65
- }
66
-
67
- let outputString = `🔍 SERP Results for "${args.query}":\n\n`;
68
- outputString += `[TOP WEB RESULTS]\n`;
69
- results.forEach((r, idx) => {
70
- outputString += `${idx + 1}. ${r.title}\n URL: ${r.url}\n Snippet: ${r.snippet}\n\n`;
71
- });
72
-
73
- return { success: true, output: outputString.trim() };
74
-
75
- } catch (error) {
76
- return {
77
- success: false,
78
- error: `Search Engine Blocked Request: ${error.message}. Do NOT guess random API URLs. Instead, use the web_fetch tool directly on known sites (e.g. finance.yahoo.com, coinmarketcap.com, wikipedia.org).`
79
- };
80
56
  }
81
- }
82
57
  };
@@ -0,0 +1,56 @@
1
+ import { BrowserCapabilityError, causyneBrowser } from '../browser/causyneBrowser.js';
2
+ import { searchPublicWeb, type SearchResult } from '../browser/searchProviders.js';
3
+
4
+ function renderResults(query: string, results: SearchResult[]): string {
5
+ const lines = [`Search results for "${query}"`, ''];
6
+ for (const [index, result] of results.entries()) {
7
+ lines.push(`${index + 1}. ${result.title}`, ` URL: ${result.url}`);
8
+ if (result.snippet) lines.push(` ${result.snippet}`);
9
+ lines.push('');
10
+ }
11
+ lines.push(`Search engine used: ${results[0]?.engine ?? 'unknown'}`);
12
+ return lines.join('\n').trim();
13
+ }
14
+
15
+ export const webSearch = {
16
+ schema: {
17
+ name: 'web_search',
18
+ description: 'Search the public web through lightweight providers, with the optional Causyne Camoufox browser as a rendered-page fallback. No browser binary is downloaded automatically.',
19
+ parameters: {
20
+ type: 'object',
21
+ properties: {
22
+ query: { type: 'string', description: 'Search query, 1-1000 characters.' }
23
+ },
24
+ required: ['query'],
25
+ additionalProperties: false
26
+ }
27
+ },
28
+
29
+ async execute(args: { query?: string }) {
30
+ const query = String(args.query ?? '').trim();
31
+ if (!query || query.length > 1_000) return { success: false, error: 'Search query must contain 1-1000 characters' };
32
+ let lightweightFailure = '';
33
+ try {
34
+ const results = await searchPublicWeb(query);
35
+ return { success: true, output: renderResults(query, results), results };
36
+ } catch (error) {
37
+ lightweightFailure = error instanceof Error ? error.message : String(error);
38
+ }
39
+ try {
40
+ const results = await causyneBrowser().search(query);
41
+ return { success: true, output: renderResults(query, results), results };
42
+ } catch (error) {
43
+ if (error instanceof BrowserCapabilityError) {
44
+ return {
45
+ success: false,
46
+ error: `${lightweightFailure}; rendered browser fallback unavailable: ${error.message}`,
47
+ requiresConsent: error.requiresConsent
48
+ };
49
+ }
50
+ return {
51
+ success: false,
52
+ error: `Web search failed after lightweight and rendered providers: ${lightweightFailure}; ${error instanceof Error ? error.message : String(error)}`
53
+ };
54
+ }
55
+ }
56
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared compile-time contracts for the incremental TypeScript migration.
3
+ *
4
+ * This module deliberately exports types only. Runtime data entering from a
5
+ * model, MCP server, plugin, config file, subprocess, or filesystem still has
6
+ * to be validated before it is treated as one of these shapes.
7
+ */
8
+ export {};
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Shared compile-time contracts for the incremental TypeScript migration.
3
+ *
4
+ * This module deliberately exports types only. Runtime data entering from a
5
+ * model, MCP server, plugin, config file, subprocess, or filesystem still has
6
+ * to be validated before it is treated as one of these shapes.
7
+ */
8
+
9
+ export type JsonPrimitive = string | number | boolean | null;
10
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
11
+ export type JsonObject = { [key: string]: JsonValue };
12
+
13
+ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
14
+
15
+ export interface TextContentBlock {
16
+ type: 'text';
17
+ text: string;
18
+ }
19
+
20
+ export interface ImageContentBlock {
21
+ type: 'image_url';
22
+ image_url: {
23
+ url: string;
24
+ detail?: 'auto' | 'low' | 'high';
25
+ };
26
+ }
27
+
28
+ export interface AudioContentBlock {
29
+ type: 'input_audio';
30
+ input_audio: {
31
+ data: string;
32
+ format: string;
33
+ };
34
+ }
35
+
36
+ export type MessageContent = string | Array<TextContentBlock | ImageContentBlock | AudioContentBlock | JsonObject>;
37
+
38
+ export interface AgentMessage {
39
+ role: MessageRole;
40
+ content: MessageContent;
41
+ name?: string;
42
+ tool_call_id?: string;
43
+ cacheAnchor?: string;
44
+ spillPath?: string;
45
+ }
46
+
47
+ export interface TokenUsage {
48
+ promptTokens: number;
49
+ completionTokens: number;
50
+ totalTokens?: number;
51
+ }
52
+
53
+ export interface ProviderResponse {
54
+ content: string;
55
+ usage: TokenUsage;
56
+ reasoning?: string;
57
+ toolCalls?: ToolCall[];
58
+ }
59
+
60
+ export interface ProviderChatOptions {
61
+ temperature?: number;
62
+ maxTokens?: number;
63
+ jsonMode?: boolean;
64
+ tools?: ToolSchema[];
65
+ signal?: AbortSignal;
66
+ }
67
+
68
+ export interface JsonSchema {
69
+ type?: string | string[];
70
+ description?: string;
71
+ properties?: Record<string, JsonSchema>;
72
+ required?: string[];
73
+ items?: JsonSchema;
74
+ enum?: JsonPrimitive[];
75
+ additionalProperties?: boolean | JsonSchema;
76
+ }
77
+
78
+ export interface ToolSchema {
79
+ name: string;
80
+ description: string;
81
+ parameters: JsonSchema;
82
+ }
83
+
84
+ export interface ToolCall {
85
+ id?: string;
86
+ name: string;
87
+ arguments: JsonObject;
88
+ }
89
+
90
+ export interface ToolExecutionContext {
91
+ workspace: string;
92
+ signal?: AbortSignal;
93
+ taskId?: string;
94
+ sessionId?: string;
95
+ }
96
+
97
+ export interface ToolSuccess<T extends JsonValue = JsonValue> {
98
+ success: true;
99
+ result: T;
100
+ metadata?: JsonObject;
101
+ }
102
+
103
+ export interface ToolFailure {
104
+ success: false;
105
+ error: string;
106
+ code?: string;
107
+ metadata?: JsonObject;
108
+ }
109
+
110
+ export type ToolResult<T extends JsonValue = JsonValue> = ToolSuccess<T> | ToolFailure;
111
+
112
+ export interface ToolModule<TArguments extends JsonObject = JsonObject, TResult extends JsonValue = JsonValue> {
113
+ schema: ToolSchema;
114
+ execute(arguments_: TArguments, context?: ToolExecutionContext): Promise<ToolResult<TResult>> | ToolResult<TResult>;
115
+ }
116
+
117
+ export type RiskLevel = 'safe' | 'moderate' | 'dangerous' | 'extreme' | 'forbidden';
118
+
119
+ export interface SandboxDecision {
120
+ allowed: boolean;
121
+ risk: RiskLevel;
122
+ reason?: string;
123
+ }
124
+
125
+ export type ApprovalChoice = 'deny' | 'allow_once' | 'allow_always';
126
+
127
+ export interface ApprovalDecision {
128
+ choice: ApprovalChoice;
129
+ toolName: string;
130
+ reason?: string;
131
+ }
132
+
133
+ export type HookEvent = 'PreToolUse' | 'PostToolUse';
134
+
135
+ export interface HookRule {
136
+ event: HookEvent;
137
+ command: string[];
138
+ name?: string;
139
+ tool?: string;
140
+ tools?: string[];
141
+ timeoutMs?: number;
142
+ enabled?: boolean;
143
+ }
144
+
145
+ export interface HookDecision {
146
+ denied: boolean;
147
+ messages: string[];
148
+ reason?: string;
149
+ }
150
+
151
+ export type PlanFailureClass =
152
+ | 'not_found'
153
+ | 'permission'
154
+ | 'timeout'
155
+ | 'network'
156
+ | 'rate_limited'
157
+ | 'invalid_args'
158
+ | 'syntax'
159
+ | 'exit_nonzero'
160
+ | 'failed';
161
+
162
+ export interface PlanStep {
163
+ tool: string;
164
+ note: string;
165
+ ok: boolean;
166
+ why?: PlanFailureClass;
167
+ }
168
+
169
+ export interface PlanTemplate {
170
+ id: string;
171
+ archetype: string;
172
+ keywords: string[];
173
+ steps: PlanStep[];
174
+ uses: number;
175
+ wins: number;
176
+ createdAt: number;
177
+ updatedAt: number;
178
+ }
179
+
180
+ export interface PlanMatch {
181
+ template: PlanTemplate;
182
+ similarity: number;
183
+ matchType: 'exact' | 'fuzzy';
184
+ telemetry: {
185
+ keywordCount: number;
186
+ lookupMs: number;
187
+ };
188
+ }
189
+
190
+ export type PluginCapability = 'workspace:read' | 'workspace:write' | 'network' | 'subprocess';
191
+
192
+ export interface PluginToolManifest {
193
+ name: string;
194
+ description: string;
195
+ parameters: JsonSchema;
196
+ entry: string;
197
+ export?: string;
198
+ capabilities?: PluginCapability[];
199
+ timeoutMs?: number;
200
+ }
201
+
202
+ export interface PluginSkillManifest {
203
+ id: string;
204
+ name?: string;
205
+ description: string;
206
+ file: string;
207
+ }
208
+
209
+ export interface PluginManifest {
210
+ schemaVersion: 1;
211
+ name: string;
212
+ version: string;
213
+ description: string;
214
+ capabilities?: PluginCapability[];
215
+ tools?: PluginToolManifest[];
216
+ skills?: PluginSkillManifest[];
217
+ }
218
+
219
+ export interface AgentProfileManifest {
220
+ schemaVersion: 1;
221
+ name: string;
222
+ description: string;
223
+ instructions: string;
224
+ toolScope: string[];
225
+ maxSteps?: number;
226
+ maxTokens?: number;
227
+ }
228
+
229
+ export interface TaskPhase {
230
+ name: string;
231
+ tools: string[];
232
+ note: string;
233
+ }
234
+
235
+ export interface TaskPackScore {
236
+ name: string;
237
+ score: number;
238
+ }
239
+
240
+ export type TaskConfidence = 'low' | 'medium' | 'high';
241
+
242
+ export interface WorkspaceContext {
243
+ framework?: string;
244
+ projectType?: string;
245
+ designSystem?: string;
246
+ keyFiles?: string[];
247
+ }
248
+
249
+ export interface TaskShape {
250
+ archetype: string;
251
+ archetypeLabel: string;
252
+ packs: TaskPackScore[];
253
+ phases: TaskPhase[];
254
+ confidence: TaskConfidence;
255
+ }
256
+
257
+ export type AgentEvent =
258
+ | { type: 'model.started'; provider: string; model: string }
259
+ | { type: 'model.delta'; text: string }
260
+ | { type: 'tool.requested'; call: ToolCall }
261
+ | { type: 'tool.completed'; callId?: string; result: ToolResult }
262
+ | { type: 'approval.required'; toolName: string; risk: RiskLevel; reason?: string }
263
+ | { type: 'session.notice'; message: string };
@@ -0,0 +1,3 @@
1
+ /** Runtime declarations for the retained JavaScript path boundary. */
2
+ export function resolveUserPath(input: string): string;
3
+ export function causyneHome(): string;
@@ -0,0 +1 @@
1
+ export function getPackageVersion(): string;
@@ -1,165 +0,0 @@
1
-
2
-
3
- import { spawn } from 'child_process';
4
-
5
- const DEFAULT_TIMEOUT_MS = 30000;
6
-
7
- /**
8
- * A JSON-RPC channel over a child process's stdin/stdout.
9
- *
10
- * MCP frames stdio traffic as newline-delimited JSON. stderr is deliberately
11
- * NOT parsed — servers use it for human-readable logging, and treating it as
12
- * protocol is how a chatty server becomes a hang.
13
- */
14
- export class StdioChannel {
15
- constructor({ command, args = [], env = {}, cwd }) {
16
- this.command = command;
17
- this.args = args;
18
- this.env = env;
19
- this.cwd = cwd;
20
- this.child = null;
21
- this.buffer = '';
22
- this.pending = new Map();
23
- this.nextId = 1;
24
- this.notificationHandler = null;
25
- this.stderrTail = [];
26
- }
27
-
28
- async open() {
29
- this.child = spawn(this.command, this.args, {
30
- cwd: this.cwd || process.cwd(),
31
- env: { ...process.env, ...this.env },
32
- stdio: ['pipe', 'pipe', 'pipe']
33
- });
34
-
35
- this.child.stdout.setEncoding('utf-8');
36
- this.child.stdout.on('data', chunk => this.#ingest(chunk));
37
-
38
- // Kept only so a failure can be explained. Bounded, because a server that
39
-
40
- this.child.stderr.setEncoding('utf-8');
41
- this.child.stderr.on('data', chunk => {
42
- this.stderrTail.push(String(chunk));
43
- if (this.stderrTail.length > 20) this.stderrTail.shift();
44
- });
45
-
46
- this.child.on('exit', code => this.#failAll(new Error(`server exited (code ${code})${this.lastError()}`)));
47
- this.child.on('error', err => this.#failAll(err));
48
-
49
- // Surface a command that does not exist as an error rather than a timeout.
50
- await new Promise((resolve, reject) => {
51
- const onError = (err) => reject(err);
52
- this.child.once('error', onError);
53
- setTimeout(() => { this.child.removeListener('error', onError); resolve(); }, 150);
54
- });
55
- }
56
-
57
- lastError() {
58
- const tail = this.stderrTail.join('').trim();
59
- return tail ? `: ${tail.slice(-400)}` : '';
60
- }
61
-
62
- #ingest(chunk) {
63
- this.buffer += chunk;
64
- let newline;
65
- while ((newline = this.buffer.indexOf('\n')) !== -1) {
66
- const line = this.buffer.slice(0, newline).trim();
67
- this.buffer = this.buffer.slice(newline + 1);
68
- if (!line) continue;
69
- let frame;
70
- try { frame = JSON.parse(line); } catch (e) { continue; }
71
- this.#dispatch(frame);
72
- }
73
- }
74
-
75
- #dispatch(frame) {
76
- if (frame.id !== undefined && this.pending.has(frame.id)) {
77
- const { resolve, reject, timer } = this.pending.get(frame.id);
78
- this.pending.delete(frame.id);
79
- clearTimeout(timer);
80
- if (frame.error) reject(new Error(frame.error.message || 'JSON-RPC error'));
81
- else resolve(frame.result);
82
- return;
83
- }
84
- if (frame.method && this.notificationHandler) {
85
- try { this.notificationHandler(frame); } catch (e) {}
86
- }
87
- }
88
-
89
- #failAll(error) {
90
- for (const [, { reject, timer }] of this.pending) {
91
- clearTimeout(timer);
92
- reject(error);
93
- }
94
- this.pending.clear();
95
- }
96
-
97
- send(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
98
- if (!this.child || this.child.killed) return Promise.reject(new Error('channel is closed'));
99
- const id = this.nextId++;
100
- const frame = { jsonrpc: '2.0', id, method, params: params ?? {} };
101
-
102
- return new Promise((resolve, reject) => {
103
- const timer = setTimeout(() => {
104
- this.pending.delete(id);
105
- reject(new Error(`${method} timed out after ${timeoutMs} ms`));
106
- }, timeoutMs);
107
- this.pending.set(id, { resolve, reject, timer });
108
- try {
109
- this.child.stdin.write(JSON.stringify(frame) + '\n');
110
- } catch (e) {
111
- clearTimeout(timer);
112
- this.pending.delete(id);
113
- reject(e);
114
- }
115
- });
116
- }
117
-
118
- notify(method, params) {
119
- if (!this.child || this.child.killed) return;
120
- try {
121
- this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params: params ?? {} }) + '\n');
122
- } catch (e) {}
123
- }
124
-
125
- close() {
126
- this.#failAll(new Error('channel closed'));
127
- try { if (this.child) this.child.kill('SIGTERM'); } catch (e) {}
128
- this.child = null;
129
- }
130
- }
131
-
132
- /** A JSON-RPC channel over HTTP POST. One request, one response, no session. */
133
- export class HttpChannel {
134
- constructor({ url, headers = {} }) {
135
- this.url = url;
136
- this.headers = headers;
137
- this.nextId = 1;
138
- this.notificationHandler = null;
139
- }
140
-
141
- async open() {
142
- // Nothing to open. Validate the URL now so a typo fails at connect time
143
- // rather than on the first tool call.
144
- const parsed = new URL(this.url);
145
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
146
- throw new Error(`unsupported transport protocol: ${parsed.protocol}`);
147
- }
148
- }
149
-
150
- async send(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
151
- const response = await fetch(this.url, {
152
- method: 'POST',
153
- headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...this.headers },
154
- body: JSON.stringify({ jsonrpc: '2.0', id: this.nextId++, method, params: params ?? {} }),
155
- signal: AbortSignal.timeout(timeoutMs)
156
- });
157
- if (!response.ok) throw new Error(`HTTP ${response.status} from ${this.url}`);
158
- const frame = await response.json();
159
- if (frame.error) throw new Error(frame.error.message || 'JSON-RPC error');
160
- return frame.result;
161
- }
162
-
163
- notify() { /* HTTP transport has no notification channel */ }
164
- close() { /* nothing to close */ }
165
- }