@townco/agent 0.1.54 → 0.1.56

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 (37) hide show
  1. package/dist/acp-server/adapter.d.ts +1 -0
  2. package/dist/acp-server/adapter.js +9 -1
  3. package/dist/acp-server/http.js +75 -6
  4. package/dist/acp-server/session-storage.d.ts +14 -0
  5. package/dist/acp-server/session-storage.js +47 -0
  6. package/dist/definition/index.d.ts +9 -0
  7. package/dist/definition/index.js +9 -0
  8. package/dist/index.js +1 -1
  9. package/dist/logger.d.ts +26 -0
  10. package/dist/logger.js +43 -0
  11. package/dist/runner/agent-runner.d.ts +5 -1
  12. package/dist/runner/agent-runner.js +2 -1
  13. package/dist/runner/hooks/executor.js +1 -1
  14. package/dist/runner/hooks/loader.js +1 -1
  15. package/dist/runner/hooks/predefined/compaction-tool.js +1 -1
  16. package/dist/runner/hooks/predefined/tool-response-compactor.js +1 -1
  17. package/dist/runner/langchain/index.js +19 -7
  18. package/dist/runner/langchain/model-factory.d.ts +2 -0
  19. package/dist/runner/langchain/model-factory.js +20 -1
  20. package/dist/runner/langchain/tools/browser.d.ts +100 -0
  21. package/dist/runner/langchain/tools/browser.js +412 -0
  22. package/dist/runner/langchain/tools/port-utils.d.ts +8 -0
  23. package/dist/runner/langchain/tools/port-utils.js +35 -0
  24. package/dist/runner/langchain/tools/subagent.js +230 -127
  25. package/dist/runner/tools.d.ts +2 -2
  26. package/dist/runner/tools.js +1 -0
  27. package/dist/scaffold/index.js +7 -1
  28. package/dist/scaffold/templates/dot-claude/CLAUDE-append.md +2 -0
  29. package/dist/storage/index.js +1 -1
  30. package/dist/telemetry/index.d.ts +5 -0
  31. package/dist/telemetry/index.js +10 -0
  32. package/dist/templates/index.d.ts +2 -0
  33. package/dist/templates/index.js +30 -8
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/index.ts +1 -1
  36. package/package.json +11 -6
  37. package/templates/index.ts +40 -8
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ interface BrowserNavigateResult {
3
+ success: boolean;
4
+ url?: string;
5
+ title?: string;
6
+ liveViewUrl?: string;
7
+ error?: string;
8
+ }
9
+ interface BrowserScreenshotResult {
10
+ success: boolean;
11
+ screenshotPath?: string;
12
+ liveViewUrl?: string;
13
+ error?: string;
14
+ }
15
+ interface BrowserExtractResult {
16
+ success: boolean;
17
+ content?: string;
18
+ url?: string;
19
+ title?: string;
20
+ liveViewUrl?: string;
21
+ error?: string;
22
+ }
23
+ interface BrowserClickResult {
24
+ success: boolean;
25
+ message?: string;
26
+ liveViewUrl?: string;
27
+ error?: string;
28
+ }
29
+ interface BrowserTypeResult {
30
+ success: boolean;
31
+ message?: string;
32
+ liveViewUrl?: string;
33
+ error?: string;
34
+ }
35
+ interface BrowserCloseResult {
36
+ success: boolean;
37
+ message?: string;
38
+ error?: string;
39
+ }
40
+ export declare function makeBrowserTools(): readonly [import("langchain").DynamicStructuredTool<z.ZodObject<{
41
+ url: z.ZodString;
42
+ waitUntil: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
43
+ load: "load";
44
+ domcontentloaded: "domcontentloaded";
45
+ networkidle: "networkidle";
46
+ }>>>;
47
+ }, z.core.$strip>, {
48
+ url: string;
49
+ waitUntil: "load" | "domcontentloaded" | "networkidle";
50
+ }, {
51
+ url: string;
52
+ waitUntil?: "load" | "domcontentloaded" | "networkidle" | undefined;
53
+ }, BrowserNavigateResult>, import("langchain").DynamicStructuredTool<z.ZodObject<{
54
+ fullPage: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
55
+ selector: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strip>, {
57
+ fullPage: boolean;
58
+ selector?: string | undefined;
59
+ }, {
60
+ fullPage?: boolean | undefined;
61
+ selector?: string | undefined;
62
+ }, BrowserScreenshotResult>, import("langchain").DynamicStructuredTool<z.ZodObject<{
63
+ selector: z.ZodOptional<z.ZodString>;
64
+ extractType: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
65
+ text: "text";
66
+ html: "html";
67
+ }>>>;
68
+ }, z.core.$strip>, {
69
+ extractType: "text" | "html";
70
+ selector?: string | undefined;
71
+ }, {
72
+ selector?: string | undefined;
73
+ extractType?: "text" | "html" | undefined;
74
+ }, BrowserExtractResult>, import("langchain").DynamicStructuredTool<z.ZodObject<{
75
+ selector: z.ZodString;
76
+ button: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
77
+ left: "left";
78
+ right: "right";
79
+ middle: "middle";
80
+ }>>>;
81
+ }, z.core.$strip>, {
82
+ selector: string;
83
+ button: "left" | "right" | "middle";
84
+ }, {
85
+ selector: string;
86
+ button?: "left" | "right" | "middle" | undefined;
87
+ }, BrowserClickResult>, import("langchain").DynamicStructuredTool<z.ZodObject<{
88
+ selector: z.ZodString;
89
+ text: z.ZodString;
90
+ pressEnter: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
91
+ }, z.core.$strip>, {
92
+ selector: string;
93
+ text: string;
94
+ pressEnter: boolean;
95
+ }, {
96
+ selector: string;
97
+ text: string;
98
+ pressEnter?: boolean | undefined;
99
+ }, BrowserTypeResult>, import("langchain").DynamicStructuredTool<z.ZodObject<{}, z.core.$strip>, Record<string, never>, Record<string, never>, BrowserCloseResult>];
100
+ export {};
@@ -0,0 +1,412 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import Kernel from "@onkernel/sdk";
4
+ import { tool } from "langchain";
5
+ import { z } from "zod";
6
+ let _kernelClient = null;
7
+ let _sessionId = null;
8
+ let _liveViewUrl = null;
9
+ function getKernelClient() {
10
+ if (_kernelClient) {
11
+ return _kernelClient;
12
+ }
13
+ const apiKey = process.env.KERNEL_API_KEY;
14
+ if (!apiKey) {
15
+ throw new Error("KERNEL_API_KEY environment variable is required to use the browser tool. " +
16
+ "Please set it to your Kernel API key from https://onkernel.com");
17
+ }
18
+ _kernelClient = new Kernel({ apiKey });
19
+ return _kernelClient;
20
+ }
21
+ async function ensureBrowserSession() {
22
+ if (_sessionId && _liveViewUrl) {
23
+ return { sessionId: _sessionId, liveViewUrl: _liveViewUrl };
24
+ }
25
+ const kernel = getKernelClient();
26
+ let kernelBrowser;
27
+ try {
28
+ kernelBrowser = await kernel.browsers.create();
29
+ }
30
+ catch (error) {
31
+ const msg = error instanceof Error ? error.message : String(error);
32
+ throw new Error(`Failed to create Kernel browser. Check KERNEL_API_KEY is valid. Error: ${msg}`);
33
+ }
34
+ _sessionId = kernelBrowser.session_id;
35
+ // biome-ignore lint/suspicious/noExplicitAny: browser_live_view_url not yet typed in @onkernel/sdk
36
+ _liveViewUrl = kernelBrowser.browser_live_view_url;
37
+ if (!_sessionId) {
38
+ throw new Error("Kernel browser created but no session_id returned.");
39
+ }
40
+ if (!_liveViewUrl) {
41
+ throw new Error("Kernel browser created but no browser_live_view_url returned.");
42
+ }
43
+ return { sessionId: _sessionId, liveViewUrl: _liveViewUrl };
44
+ }
45
+ // biome-ignore lint/suspicious/noExplicitAny: Kernel SDK playwright.execute not yet typed
46
+ async function executePlaywright(code) {
47
+ const kernel = getKernelClient();
48
+ const { sessionId } = await ensureBrowserSession();
49
+ // biome-ignore lint/suspicious/noExplicitAny: playwright.execute not yet typed in @onkernel/sdk
50
+ const response = await kernel.browsers.playwright.execute(sessionId, {
51
+ code,
52
+ timeout_sec: 60,
53
+ });
54
+ return response;
55
+ }
56
+ export function makeBrowserTools() {
57
+ // Browser Navigate tool
58
+ const browserNavigate = tool(async ({ url, waitUntil = "domcontentloaded", }) => {
59
+ try {
60
+ const { liveViewUrl } = await ensureBrowserSession();
61
+ const code = `
62
+ await page.goto('${url.replace(/'/g, "\\'")}', {
63
+ waitUntil: '${waitUntil}',
64
+ timeout: 30000
65
+ });
66
+ return { url: page.url(), title: await page.title() };
67
+ `;
68
+ const response = await executePlaywright(code);
69
+ return {
70
+ success: true,
71
+ url: response.result?.url,
72
+ title: response.result?.title,
73
+ liveViewUrl,
74
+ };
75
+ }
76
+ catch (error) {
77
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
78
+ return {
79
+ success: false,
80
+ error: `Navigation failed: ${errorMessage}`,
81
+ ...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
82
+ };
83
+ }
84
+ }, {
85
+ name: "BrowserNavigate",
86
+ description: "Navigate a cloud browser to a specified URL using Kernel's browser-as-a-service.\n" +
87
+ "- Opens web pages in a cloud-hosted browser\n" +
88
+ "- Returns the final URL (after redirects), page title, and a liveViewUrl\n" +
89
+ "- The liveViewUrl allows viewing the browser session in real-time\n" +
90
+ "- Browser session persists across tool calls\n" +
91
+ "\n" +
92
+ "Usage notes:\n" +
93
+ " - First call creates a new browser session\n" +
94
+ " - Subsequent calls reuse the same browser\n" +
95
+ " - Share the liveViewUrl with users so they can watch the browser\n" +
96
+ " - Use BrowserClose when done to release resources\n",
97
+ schema: z.object({
98
+ url: z.string().url().describe("The URL to navigate to"),
99
+ waitUntil: z
100
+ .enum(["load", "domcontentloaded", "networkidle"])
101
+ .optional()
102
+ .default("domcontentloaded")
103
+ .describe("When to consider navigation complete"),
104
+ }),
105
+ });
106
+ browserNavigate.prettyName = "Browser Navigate";
107
+ browserNavigate.icon = "Globe";
108
+ // Browser Screenshot tool
109
+ const browserScreenshot = tool(async ({ fullPage = false, selector, }) => {
110
+ try {
111
+ const { liveViewUrl } = await ensureBrowserSession();
112
+ let code;
113
+ if (selector) {
114
+ code = `
115
+ const element = await page.$('${selector.replace(/'/g, "\\'")}');
116
+ if (!element) {
117
+ return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
118
+ }
119
+ const screenshot = await element.screenshot({ type: 'png' });
120
+ return { screenshot: screenshot.toString('base64') };
121
+ `;
122
+ }
123
+ else {
124
+ code = `
125
+ const screenshot = await page.screenshot({ type: 'png', fullPage: ${fullPage} });
126
+ return { screenshot: screenshot.toString('base64') };
127
+ `;
128
+ }
129
+ const response = await executePlaywright(code);
130
+ if (response.result?.error) {
131
+ return {
132
+ success: false,
133
+ error: response.result.error,
134
+ liveViewUrl,
135
+ };
136
+ }
137
+ // Save screenshot to local file instead of returning base64
138
+ const screenshotsDir = join(process.cwd(), "screenshots");
139
+ await mkdir(screenshotsDir, { recursive: true });
140
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
141
+ const filename = `screenshot-${timestamp}.png`;
142
+ const filepath = join(screenshotsDir, filename);
143
+ const imageBuffer = Buffer.from(response.result?.screenshot, "base64");
144
+ await Bun.write(filepath, imageBuffer);
145
+ return {
146
+ success: true,
147
+ screenshotPath: filepath,
148
+ liveViewUrl,
149
+ };
150
+ }
151
+ catch (error) {
152
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
153
+ return {
154
+ success: false,
155
+ error: `Screenshot failed: ${errorMessage}`,
156
+ ...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
157
+ };
158
+ }
159
+ }, {
160
+ name: "BrowserScreenshot",
161
+ description: "Take a screenshot of the current browser page or a specific element.\n" +
162
+ "- Captures the current viewport or full page\n" +
163
+ "- Can target specific elements using CSS selectors\n" +
164
+ "- Saves the screenshot to a local file and returns the file path\n" +
165
+ "\n" +
166
+ "Usage notes:\n" +
167
+ " - Use fullPage=true to capture the entire scrollable page\n" +
168
+ " - Use selector to capture a specific element\n" +
169
+ " - Requires an active browser session (call BrowserNavigate first)\n" +
170
+ " - Screenshots are saved in the 'screenshots' folder in the current working directory\n",
171
+ schema: z.object({
172
+ fullPage: z
173
+ .boolean()
174
+ .optional()
175
+ .default(false)
176
+ .describe("Whether to capture the full scrollable page"),
177
+ selector: z
178
+ .string()
179
+ .optional()
180
+ .describe("CSS selector of element to screenshot (captures whole page if not provided)"),
181
+ }),
182
+ });
183
+ browserScreenshot.prettyName = "Browser Screenshot";
184
+ browserScreenshot.icon = "Camera";
185
+ // Browser Extract Content tool
186
+ const browserExtract = tool(async ({ selector, extractType = "text", }) => {
187
+ try {
188
+ const { liveViewUrl } = await ensureBrowserSession();
189
+ let code;
190
+ if (selector) {
191
+ if (extractType === "html") {
192
+ code = `
193
+ const element = await page.$('${selector.replace(/'/g, "\\'")}');
194
+ if (!element) {
195
+ return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
196
+ }
197
+ const content = await element.innerHTML();
198
+ return { content, url: page.url(), title: await page.title() };
199
+ `;
200
+ }
201
+ else {
202
+ code = `
203
+ const element = await page.$('${selector.replace(/'/g, "\\'")}');
204
+ if (!element) {
205
+ return { error: 'Element not found: ${selector.replace(/'/g, "\\'")}' };
206
+ }
207
+ const content = await element.innerText();
208
+ return { content, url: page.url(), title: await page.title() };
209
+ `;
210
+ }
211
+ }
212
+ else {
213
+ if (extractType === "html") {
214
+ code = `
215
+ const content = await page.content();
216
+ return { content, url: page.url(), title: await page.title() };
217
+ `;
218
+ }
219
+ else {
220
+ code = `
221
+ const content = await page.innerText('body');
222
+ return { content, url: page.url(), title: await page.title() };
223
+ `;
224
+ }
225
+ }
226
+ const response = await executePlaywright(code);
227
+ if (response.result?.error) {
228
+ return {
229
+ success: false,
230
+ error: response.result.error,
231
+ liveViewUrl,
232
+ };
233
+ }
234
+ return {
235
+ success: true,
236
+ content: response.result?.content,
237
+ url: response.result?.url,
238
+ title: response.result?.title,
239
+ liveViewUrl,
240
+ };
241
+ }
242
+ catch (error) {
243
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
244
+ return {
245
+ success: false,
246
+ error: `Content extraction failed: ${errorMessage}`,
247
+ ...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
248
+ };
249
+ }
250
+ }, {
251
+ name: "BrowserExtract",
252
+ description: "Extract text or HTML content from the current page or a specific element.\n" +
253
+ "- Gets readable text content from web pages\n" +
254
+ "- Can extract HTML for structured data\n" +
255
+ "- Supports CSS selectors for targeting specific elements\n" +
256
+ "\n" +
257
+ "Usage notes:\n" +
258
+ " - Default extracts text from the entire page body\n" +
259
+ " - Use selector to target specific page sections\n" +
260
+ " - Use extractType='html' when you need the raw HTML structure\n",
261
+ schema: z.object({
262
+ selector: z
263
+ .string()
264
+ .optional()
265
+ .describe("CSS selector of element to extract from (extracts from body if not provided)"),
266
+ extractType: z
267
+ .enum(["text", "html"])
268
+ .optional()
269
+ .default("text")
270
+ .describe("Type of content to extract"),
271
+ }),
272
+ });
273
+ browserExtract.prettyName = "Browser Extract";
274
+ browserExtract.icon = "FileText";
275
+ // Browser Click tool
276
+ const browserClick = tool(async ({ selector, button = "left" }) => {
277
+ try {
278
+ const { liveViewUrl } = await ensureBrowserSession();
279
+ const code = `
280
+ await page.click('${selector.replace(/'/g, "\\'")}', {
281
+ button: '${button}',
282
+ timeout: 10000,
283
+ });
284
+ await page.waitForTimeout(500);
285
+ return { success: true };
286
+ `;
287
+ await executePlaywright(code);
288
+ return {
289
+ success: true,
290
+ message: `Clicked element: ${selector}`,
291
+ liveViewUrl,
292
+ };
293
+ }
294
+ catch (error) {
295
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
296
+ return {
297
+ success: false,
298
+ error: `Click failed: ${errorMessage}`,
299
+ ...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
300
+ };
301
+ }
302
+ }, {
303
+ name: "BrowserClick",
304
+ description: "Click on an element in the current browser page.\n" +
305
+ "- Supports left, right, and middle mouse buttons\n" +
306
+ "- Waits for the element to be visible before clicking\n" +
307
+ "- Useful for interacting with buttons, links, and other clickable elements\n" +
308
+ "\n" +
309
+ "Usage notes:\n" +
310
+ " - Provide a CSS selector to identify the target element\n" +
311
+ " - The tool waits briefly after clicking for page updates\n",
312
+ schema: z.object({
313
+ selector: z.string().describe("CSS selector of the element to click"),
314
+ button: z
315
+ .enum(["left", "right", "middle"])
316
+ .optional()
317
+ .default("left")
318
+ .describe("Which mouse button to use"),
319
+ }),
320
+ });
321
+ browserClick.prettyName = "Browser Click";
322
+ browserClick.icon = "MousePointer";
323
+ // Browser Type tool
324
+ const browserType = tool(async ({ selector, text, pressEnter = false, }) => {
325
+ try {
326
+ const { liveViewUrl } = await ensureBrowserSession();
327
+ const code = `
328
+ await page.fill('${selector.replace(/'/g, "\\'")}', '${text.replace(/'/g, "\\'")}', { timeout: 10000 });
329
+ ${pressEnter ? `await page.press('${selector.replace(/'/g, "\\'")}', 'Enter'); await page.waitForTimeout(500);` : ""}
330
+ return { success: true };
331
+ `;
332
+ await executePlaywright(code);
333
+ return {
334
+ success: true,
335
+ message: `Typed text into element: ${selector}${pressEnter ? " (pressed Enter)" : ""}`,
336
+ liveViewUrl,
337
+ };
338
+ }
339
+ catch (error) {
340
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
341
+ return {
342
+ success: false,
343
+ error: `Type failed: ${errorMessage}`,
344
+ ...(_liveViewUrl ? { liveViewUrl: _liveViewUrl } : {}),
345
+ };
346
+ }
347
+ }, {
348
+ name: "BrowserType",
349
+ description: "Type text into an input field in the current browser page.\n" +
350
+ "- Fills text into input fields, textareas, and contenteditable elements\n" +
351
+ "- Can optionally press Enter after typing (useful for search forms)\n" +
352
+ "- Clears existing content before typing\n" +
353
+ "\n" +
354
+ "Usage notes:\n" +
355
+ " - Provide a CSS selector to identify the input element\n" +
356
+ " - Set pressEnter=true to submit forms after typing\n",
357
+ schema: z.object({
358
+ selector: z.string().describe("CSS selector of the input element"),
359
+ text: z.string().describe("The text to type into the element"),
360
+ pressEnter: z
361
+ .boolean()
362
+ .optional()
363
+ .default(false)
364
+ .describe("Whether to press Enter after typing"),
365
+ }),
366
+ });
367
+ browserType.prettyName = "Browser Type";
368
+ browserType.icon = "Type";
369
+ // Browser Close tool
370
+ const browserClose = tool(async () => {
371
+ try {
372
+ if (_sessionId) {
373
+ const kernel = getKernelClient();
374
+ await kernel.browsers.deleteByID(_sessionId);
375
+ }
376
+ _sessionId = null;
377
+ _liveViewUrl = null;
378
+ return {
379
+ success: true,
380
+ message: "Browser session closed successfully",
381
+ };
382
+ }
383
+ catch (error) {
384
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
385
+ return {
386
+ success: false,
387
+ error: `Close failed: ${errorMessage}`,
388
+ };
389
+ }
390
+ }, {
391
+ name: "BrowserClose",
392
+ description: "Close the current browser session and release cloud resources.\n" +
393
+ "- Terminates the active browser connection\n" +
394
+ "- Frees up cloud browser resources\n" +
395
+ "- Call this when done with browser automation\n" +
396
+ "\n" +
397
+ "Usage notes:\n" +
398
+ " - A new browser session will be created on the next BrowserNavigate call\n" +
399
+ " - Calling this when no browser is active has no effect\n",
400
+ schema: z.object({}),
401
+ });
402
+ browserClose.prettyName = "Browser Close";
403
+ browserClose.icon = "X";
404
+ return [
405
+ browserNavigate,
406
+ browserScreenshot,
407
+ browserExtract,
408
+ browserClick,
409
+ browserType,
410
+ browserClose,
411
+ ];
412
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Check if a port is available
3
+ */
4
+ export declare function isPortAvailable(port: number): Promise<boolean>;
5
+ /**
6
+ * Find the next available port starting from the given port
7
+ */
8
+ export declare function findAvailablePort(startPort: number, maxAttempts?: number): Promise<number>;
@@ -0,0 +1,35 @@
1
+ import { createServer } from "node:net";
2
+ /**
3
+ * Check if a port is available
4
+ */
5
+ export async function isPortAvailable(port) {
6
+ return new Promise((resolve) => {
7
+ const server = createServer();
8
+ server.once("error", (err) => {
9
+ if (err.code === "EADDRINUSE") {
10
+ resolve(false);
11
+ }
12
+ else {
13
+ resolve(false);
14
+ }
15
+ });
16
+ server.once("listening", () => {
17
+ server.close();
18
+ resolve(true);
19
+ });
20
+ server.listen(port);
21
+ });
22
+ }
23
+ /**
24
+ * Find the next available port starting from the given port
25
+ */
26
+ export async function findAvailablePort(startPort, maxAttempts = 100) {
27
+ for (let i = 0; i < maxAttempts; i++) {
28
+ const port = startPort + i;
29
+ const available = await isPortAvailable(port);
30
+ if (available) {
31
+ return port;
32
+ }
33
+ }
34
+ throw new Error(`Could not find an available port between ${startPort} and ${startPort + maxAttempts - 1}`);
35
+ }