@devang0907/agent-dev 0.1.3 → 0.1.5

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.
@@ -0,0 +1,229 @@
1
+ export const webSearchTool = {
2
+ name: "web_search",
3
+ description: "Search the internet for news, documentation, or current information",
4
+ parameters: {
5
+ type: "object",
6
+ properties: {
7
+ query: { type: "string", description: "Search query" },
8
+ },
9
+ required: ["query"],
10
+ additionalProperties: false,
11
+ },
12
+ };
13
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
14
+ const NEWS_QUERY_RE = /\b(news|headlines|breaking news|top\s+\d+\s+(news|stories|headlines)|\d+\s+news|today'?s?\s+(news|headlines|\d+\s+news))\b/i;
15
+ function isNewsQuery(query) {
16
+ return NEWS_QUERY_RE.test(query);
17
+ }
18
+ function headlineLimit(query) {
19
+ const top = query.match(/\btop\s+(\d+)\b/i);
20
+ if (top)
21
+ return Math.min(50, Math.max(1, parseInt(top[1], 10)));
22
+ const n = query.match(/\b(\d+)\s+news\b/i);
23
+ if (n)
24
+ return Math.min(50, Math.max(1, parseInt(n[1], 10)));
25
+ return 20;
26
+ }
27
+ function stripHtml(text) {
28
+ return decodeHtmlEntities(text.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
29
+ }
30
+ function decodeHtmlEntities(text) {
31
+ return text
32
+ .replace(/&amp;/g, "&")
33
+ .replace(/&lt;/g, "<")
34
+ .replace(/&gt;/g, ">")
35
+ .replace(/&quot;/g, '"')
36
+ .replace(/&#x27;/g, "'")
37
+ .replace(/&#39;/g, "'")
38
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
39
+ }
40
+ function extractXmlTag(block, tag) {
41
+ const cdata = block.match(new RegExp(`<${tag}><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/${tag}>`))?.[1];
42
+ if (cdata)
43
+ return cdata.trim();
44
+ return block.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`))?.[1]?.trim() ?? "";
45
+ }
46
+ function parseRssItems(xml, limit) {
47
+ const results = [];
48
+ const itemRe = /<item>([\s\S]*?)<\/item>/gi;
49
+ let match;
50
+ while ((match = itemRe.exec(xml)) !== null && results.length < limit) {
51
+ const block = match[1];
52
+ const title = stripHtml(extractXmlTag(block, "title"));
53
+ const url = extractXmlTag(block, "link");
54
+ const pubDate = extractXmlTag(block, "pubDate");
55
+ const snippet = stripHtml(extractXmlTag(block, "description")).slice(0, 200);
56
+ if (title && url) {
57
+ results.push({ title, url, snippet, pubDate });
58
+ }
59
+ }
60
+ return results;
61
+ }
62
+ async function fetchGoogleNewsHeadlines(limit) {
63
+ const url = "https://news.google.com/rss?hl=en-US&gl=US&ceid=US:en";
64
+ const res = await fetch(url, {
65
+ headers: { "User-Agent": USER_AGENT },
66
+ signal: AbortSignal.timeout(15000),
67
+ });
68
+ if (!res.ok)
69
+ throw new Error(`Google News RSS failed (${res.status})`);
70
+ return parseRssItems(await res.text(), limit);
71
+ }
72
+ async function fetchGoogleNewsSearch(query, limit) {
73
+ const url = `https://news.google.com/rss/search?q=${encodeURIComponent(query)}&hl=en-US&gl=US&ceid=US:en`;
74
+ const res = await fetch(url, {
75
+ headers: { "User-Agent": USER_AGENT },
76
+ signal: AbortSignal.timeout(15000),
77
+ });
78
+ if (!res.ok)
79
+ throw new Error(`Google News search RSS failed (${res.status})`);
80
+ return parseRssItems(await res.text(), limit);
81
+ }
82
+ function extractAttr(tag, attr) {
83
+ const re = new RegExp(`${attr}=['"]([^'"]*)['"]`, "i");
84
+ return tag.match(re)?.[1] ?? null;
85
+ }
86
+ function decodeDdgUrl(href) {
87
+ try {
88
+ const match = href.match(/uddg=([^&]+)/);
89
+ if (match)
90
+ return decodeURIComponent(match[1]);
91
+ if (href.startsWith("//"))
92
+ return `https:${href}`;
93
+ return href;
94
+ }
95
+ catch {
96
+ return href;
97
+ }
98
+ }
99
+ function isAdOrJunk(result) {
100
+ const url = result.url.toLowerCase();
101
+ return (url.includes("ad_provider") ||
102
+ url.includes("ad_domain") ||
103
+ url.includes("duckduckgo.com/y.") ||
104
+ url.length > 280);
105
+ }
106
+ function cleanResultUrl(url) {
107
+ try {
108
+ const parsed = new URL(url);
109
+ return `${parsed.origin}${parsed.pathname}`;
110
+ }
111
+ catch {
112
+ return url.length > 120 ? url.slice(0, 120) + "…" : url;
113
+ }
114
+ }
115
+ function parseTagsByClass(html, className) {
116
+ const re = new RegExp(`<a\\b[^>]*class=['"]${className}['"][^>]*>[\\s\\S]*?<\\/a>`, "gi");
117
+ return html.match(re) ?? [];
118
+ }
119
+ function parseDdgHtmlResults(html) {
120
+ const linkTags = parseTagsByClass(html, "result__a");
121
+ const snippetTags = parseTagsByClass(html, "result__snippet");
122
+ const results = [];
123
+ for (let i = 0; i < linkTags.length && results.length < 15; i++) {
124
+ const tag = linkTags[i];
125
+ const href = extractAttr(tag, "href");
126
+ const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
127
+ const snippetTag = snippetTags[i];
128
+ const snippet = snippetTag
129
+ ? stripHtml(snippetTag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""))
130
+ : "";
131
+ if (href && title && !isAdOrJunk({ title, url: decodeDdgUrl(href), snippet })) {
132
+ results.push({ title, url: cleanResultUrl(decodeDdgUrl(href)), snippet });
133
+ }
134
+ }
135
+ return results;
136
+ }
137
+ async function fetchDdgHtmlResults(query) {
138
+ const res = await fetch("https://html.duckduckgo.com/html/", {
139
+ method: "POST",
140
+ headers: {
141
+ "User-Agent": USER_AGENT,
142
+ "Content-Type": "application/x-www-form-urlencoded",
143
+ },
144
+ body: new URLSearchParams({ q: query, b: "", kl: "us-en" }),
145
+ signal: AbortSignal.timeout(15000),
146
+ });
147
+ if (!res.ok)
148
+ throw new Error(`DuckDuckGo html failed (${res.status})`);
149
+ return parseDdgHtmlResults(await res.text());
150
+ }
151
+ function dedupeResults(results) {
152
+ const seen = new Set();
153
+ return results.filter((r) => {
154
+ const key = r.title.toLowerCase().slice(0, 60);
155
+ if (seen.has(key))
156
+ return false;
157
+ seen.add(key);
158
+ return true;
159
+ });
160
+ }
161
+ async function fetchNewsResults(query) {
162
+ const limit = headlineLimit(query);
163
+ const generic = /\b(top\s+\d+|today'?s?\s+news|news of today|news today|latest news|breaking news)\b/i.test(query);
164
+ let results;
165
+ if (generic) {
166
+ results = await fetchGoogleNewsHeadlines(limit);
167
+ }
168
+ else {
169
+ results = await fetchGoogleNewsSearch(query, limit);
170
+ if (results.length < 5) {
171
+ results = [...results, ...(await fetchGoogleNewsHeadlines(limit))];
172
+ }
173
+ }
174
+ return dedupeResults(results).slice(0, limit);
175
+ }
176
+ async function fetchWebResults(query) {
177
+ return dedupeResults(await fetchDdgHtmlResults(query)).slice(0, 12);
178
+ }
179
+ function formatNewsResults(query, items) {
180
+ const lines = [
181
+ `Headlines for: ${query}`,
182
+ `Source: Google News (US, ${new Date().toISOString().slice(0, 10)})`,
183
+ "",
184
+ ];
185
+ if (items.length === 0) {
186
+ lines.push("No headlines found.");
187
+ return lines.join("\n");
188
+ }
189
+ for (const [i, item] of items.entries()) {
190
+ lines.push(`${i + 1}. ${item.title}`);
191
+ if (item.pubDate)
192
+ lines.push(` ${item.pubDate}`);
193
+ }
194
+ lines.push("");
195
+ lines.push("Reply with a numbered list using these exact headline titles. Add a one-line note only if a headline is unclear.");
196
+ return lines.join("\n");
197
+ }
198
+ function formatWebResults(query, web) {
199
+ const lines = [`Search results for: ${query}`, ""];
200
+ if (web.length === 0) {
201
+ lines.push("No results found.");
202
+ return lines.join("\n");
203
+ }
204
+ for (const [i, r] of web.entries()) {
205
+ lines.push(`${i + 1}. ${r.title}`);
206
+ lines.push(` ${r.url}`);
207
+ if (r.snippet)
208
+ lines.push(` ${r.snippet.slice(0, 160)}`);
209
+ }
210
+ lines.push("");
211
+ lines.push("Summarize the key findings for the user concisely.");
212
+ return lines.join("\n");
213
+ }
214
+ export async function executeWebSearch(args) {
215
+ const query = args.query?.trim();
216
+ if (!query)
217
+ return "Error: query is required";
218
+ try {
219
+ if (isNewsQuery(query)) {
220
+ const headlines = await fetchNewsResults(query);
221
+ return formatNewsResults(query, headlines);
222
+ }
223
+ const web = await fetchWebResults(query);
224
+ return formatWebResults(query, web);
225
+ }
226
+ catch (err) {
227
+ return `Error: search failed — ${err instanceof Error ? err.message : String(err)}`;
228
+ }
229
+ }
@@ -0,0 +1 @@
1
+ export declare function executeShellCommand(command: string, workdir: string): Promise<string>;
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform as osPlatform } from "node:os";
3
+ import { getShellConfig, normalizeCommand } from "../platform.js";
4
+ const DEFAULT_TIMEOUT_MS = 120_000;
5
+ const INSTALL_TIMEOUT_MS = 300_000;
6
+ const DEV_SERVER_BOOT_MS = 20_000;
7
+ const DEV_SERVER_RE = /\b(npm run dev|npm start|yarn dev|pnpm dev|next dev|nuxt dev|vite(\s|$)|react-scripts start)\b/i;
8
+ const INSTALL_RE = /\b(npm install|npm ci|yarn install|pnpm install|npx create-|npm audit)\b/i;
9
+ const READY_RE = /https?:\/\/(?:localhost|127\.0\.0\.1):\d+|localhost:\d+|Local:\s*https?:\/\/[^\s]+|ready in \d|started server on|compiled successfully/i;
10
+ const backgroundProcesses = new Map();
11
+ function getCommandTimeout(command) {
12
+ return INSTALL_RE.test(command) ? INSTALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
13
+ }
14
+ function isDevServerCommand(command) {
15
+ return DEV_SERVER_RE.test(command) || /\brun dev\b/i.test(command);
16
+ }
17
+ function extractUrl(output) {
18
+ const http = output.match(/https?:\/\/(?:localhost|127\.0\.0\.1):\d+/i);
19
+ if (http)
20
+ return http[0];
21
+ const local = output.match(/localhost:\d+/i);
22
+ if (local)
23
+ return `http://${local[0]}`;
24
+ return "http://localhost:3000";
25
+ }
26
+ function killHint(pid) {
27
+ return osPlatform() === "win32"
28
+ ? `Stop: taskkill /PID ${pid} /F`
29
+ : `Stop: kill ${pid}`;
30
+ }
31
+ async function runDevServerInBackground(command, workdir, shell) {
32
+ const normalized = normalizeCommand(command, shell);
33
+ const args = [...shell.args, normalized];
34
+ return new Promise((resolve) => {
35
+ let output = "";
36
+ let settled = false;
37
+ const finish = (message) => {
38
+ if (settled)
39
+ return;
40
+ settled = true;
41
+ resolve(message);
42
+ };
43
+ const child = spawn(shell.executable, args, {
44
+ cwd: workdir,
45
+ detached: true,
46
+ stdio: ["ignore", "pipe", "pipe"],
47
+ env: { ...process.env, FORCE_COLOR: "0", BROWSER: "none" },
48
+ windowsHide: true,
49
+ });
50
+ if (!child.pid) {
51
+ finish("Error: failed to start dev server process");
52
+ return;
53
+ }
54
+ const pid = child.pid;
55
+ backgroundProcesses.set(pid, child);
56
+ child.on("exit", () => {
57
+ backgroundProcesses.delete(pid);
58
+ });
59
+ const onData = (chunk) => {
60
+ output += String(chunk);
61
+ if (READY_RE.test(output)) {
62
+ clearTimeout(bootTimer);
63
+ child.stdout?.off("data", onData);
64
+ child.stderr?.off("data", onData);
65
+ child.unref();
66
+ const url = extractUrl(output);
67
+ finish(`Dev server started in background (PID ${pid}).\nOpen ${url}\n${killHint(pid)}`);
68
+ }
69
+ };
70
+ child.stdout?.on("data", onData);
71
+ child.stderr?.on("data", onData);
72
+ child.on("error", (err) => {
73
+ backgroundProcesses.delete(pid);
74
+ finish(`Error: ${err.message}`);
75
+ });
76
+ const bootTimer = setTimeout(() => {
77
+ child.stdout?.off("data", onData);
78
+ child.stderr?.off("data", onData);
79
+ child.unref();
80
+ const url = extractUrl(output);
81
+ finish(`Dev server starting in background (PID ${pid}).\nLikely URL: ${url}\n${killHint(pid)}`);
82
+ }, DEV_SERVER_BOOT_MS);
83
+ });
84
+ }
85
+ export async function executeShellCommand(command, workdir) {
86
+ const shell = getShellConfig();
87
+ const normalized = normalizeCommand(command, shell);
88
+ if (isDevServerCommand(normalized)) {
89
+ return runDevServerInBackground(normalized, workdir, shell);
90
+ }
91
+ const timeoutMs = getCommandTimeout(normalized);
92
+ const args = [...shell.args, normalized];
93
+ return new Promise((resolve) => {
94
+ let stdout = "";
95
+ let stderr = "";
96
+ const child = spawn(shell.executable, args, {
97
+ cwd: workdir,
98
+ env: {
99
+ ...process.env,
100
+ CI: "1",
101
+ FORCE_COLOR: "0",
102
+ npm_config_yes: "true",
103
+ },
104
+ windowsHide: true,
105
+ });
106
+ const timer = setTimeout(() => {
107
+ child.kill();
108
+ resolve(`Error: command timed out after ${timeoutMs / 1000}s\n${combineOutput(stdout, stderr)}`.trim());
109
+ }, timeoutMs);
110
+ child.stdout?.on("data", (chunk) => {
111
+ stdout += String(chunk);
112
+ });
113
+ child.stderr?.on("data", (chunk) => {
114
+ stderr += String(chunk);
115
+ });
116
+ child.on("error", (err) => {
117
+ clearTimeout(timer);
118
+ resolve(`Error: ${err.message}`);
119
+ });
120
+ child.on("close", (code) => {
121
+ clearTimeout(timer);
122
+ const out = combineOutput(stdout, stderr);
123
+ if (code === 0)
124
+ return resolve(out.trim() || "(no output)");
125
+ resolve(out.trim() ? `${out.trim()}\n(exit ${code})` : `Error: command failed (exit ${code})`);
126
+ });
127
+ });
128
+ }
129
+ function combineOutput(stdout, stderr) {
130
+ return stdout + (stderr ? (stdout ? `\n${stderr}` : stderr) : "");
131
+ }
@@ -1,5 +1,19 @@
1
1
  import chalk from "chalk";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { stdin as input, stdout as output } from "node:process";
2
4
  import { runAgentLoop } from "../agent/loop.js";
5
+ async function promptCommandApproval(request) {
6
+ console.log(chalk.yellow(`\nCommand approval required:`));
7
+ console.log(chalk.white(` ${request.command}`));
8
+ const rl = createInterface({ input, output });
9
+ try {
10
+ const answer = await rl.question(chalk.gray("Run? [y/N] "));
11
+ return /^y(es)?$/i.test(answer.trim());
12
+ }
13
+ finally {
14
+ rl.close();
15
+ }
16
+ }
3
17
  export async function runPrintMode(session, prompt) {
4
18
  const model = session.getModel();
5
19
  console.log(chalk.gray(`Model: ${model.provider}/${model.id}`));
@@ -11,6 +25,7 @@ export async function runPrintMode(session, prompt) {
11
25
  messages: [...prior, userMsg],
12
26
  settings: session.getSettings(),
13
27
  workdir: process.cwd(),
28
+ onPermissionRequest: promptCommandApproval,
14
29
  onEvent: (event) => {
15
30
  if (event.type === "text_delta") {
16
31
  process.stdout.write(event.delta);
@@ -3,6 +3,8 @@ import type { ChatContext, StreamEvent, ToolCall } from "./types.js";
3
3
  export declare function sanitizeToolParameters(params: Record<string, unknown>): Record<string, unknown>;
4
4
  export declare function toOpenAITools(tools: ChatContext["tools"]): OpenAI.Chat.ChatCompletionTool[];
5
5
  export declare function normalizeToolCalls(toolCalls: ToolCall[]): ToolCall[];
6
+ export declare function parseMalformedToolCalls(text: string): ToolCall[];
7
+ export declare function extractFailedGeneration(errorMessage: string): string | null;
6
8
  export declare function toOpenAIMessages(ctx: ChatContext): OpenAI.Chat.ChatCompletionMessageParam[];
7
9
  export declare function processOpenAIStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncGenerator<StreamEvent>;
8
10
  export declare function formatApiError(err: unknown): string;
@@ -22,6 +22,104 @@ export function normalizeToolCalls(toolCalls) {
22
22
  arguments: tc.arguments?.trim() || "{}",
23
23
  }));
24
24
  }
25
+ /** Recover tool calls Groq/Llama sometimes emit as malformed text instead of structured tool_calls. */
26
+ function unescapeJsonString(value) {
27
+ return value
28
+ .replace(/\\n/g, "\n")
29
+ .replace(/\\t/g, "\t")
30
+ .replace(/\\"/g, '"')
31
+ .replace(/\\\\/g, "\\");
32
+ }
33
+ function stripToolArgWrapper(raw) {
34
+ let body = raw.trim().replace(/^[\[\]=\s]+/, "");
35
+ if (body.startsWith("(") && body.endsWith(")")) {
36
+ body = body.slice(1, -1).trim();
37
+ }
38
+ return body;
39
+ }
40
+ function argsFromFunctionTail(tail) {
41
+ const jsonIdx = tail.indexOf("{");
42
+ if (jsonIdx < 0)
43
+ return null;
44
+ return parseToolArguments(tail.slice(jsonIdx));
45
+ }
46
+ function parseToolArguments(raw) {
47
+ const body = stripToolArgWrapper(raw);
48
+ if (!body.startsWith("{"))
49
+ return null;
50
+ try {
51
+ JSON.parse(body);
52
+ return body;
53
+ }
54
+ catch {
55
+ // Groq often truncates JSON — extract known fields.
56
+ }
57
+ const commandMatch = body.match(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/);
58
+ if (commandMatch) {
59
+ return JSON.stringify({ command: unescapeJsonString(commandMatch[1]) });
60
+ }
61
+ const truncatedCommand = body.match(/"command"\s*:\s*"([\s\S]+)$/);
62
+ if (truncatedCommand) {
63
+ const command = unescapeJsonString(truncatedCommand[1].replace(/\\+$/, ""));
64
+ return JSON.stringify({ command });
65
+ }
66
+ const queryMatch = body.match(/"query"\s*:\s*"((?:\\.|[^"\\])*)"/);
67
+ if (queryMatch) {
68
+ return JSON.stringify({ query: unescapeJsonString(queryMatch[1]) });
69
+ }
70
+ const truncatedQuery = body.match(/"query"\s*:\s*"([\s\S]+)$/);
71
+ if (truncatedQuery) {
72
+ const query = unescapeJsonString(truncatedQuery[1].replace(/\\+$/, ""));
73
+ return JSON.stringify({ query });
74
+ }
75
+ const pathMatch = body.match(/"path"\s*:\s*"((?:\\.|[^"\\])*)"/);
76
+ if (pathMatch) {
77
+ return JSON.stringify({ path: unescapeJsonString(pathMatch[1]) });
78
+ }
79
+ return null;
80
+ }
81
+ function pushRecovered(results, name, args) {
82
+ results.push({
83
+ id: `recovered_${Date.now()}_${results.length}`,
84
+ name,
85
+ arguments: args,
86
+ });
87
+ }
88
+ export function parseMalformedToolCalls(text) {
89
+ const results = [];
90
+ if (!text)
91
+ return results;
92
+ const tryAdd = (name, tail) => {
93
+ const args = argsFromFunctionTail(tail);
94
+ if (name && args)
95
+ pushRecovered(results, name, args);
96
+ };
97
+ const closedRe = /<function=([a-zA-Z0-9_]+)([\s\S]*?)<\/function>/gi;
98
+ let match;
99
+ while ((match = closedRe.exec(text)) !== null) {
100
+ tryAdd(match[1].trim(), match[2]);
101
+ }
102
+ if (results.length === 0) {
103
+ const truncatedRe = /<function=([a-zA-Z0-9_]+)([\s\S]+)/gi;
104
+ while ((match = truncatedRe.exec(text)) !== null) {
105
+ tryAdd(match[1].trim(), match[2]);
106
+ }
107
+ }
108
+ if (results.length === 0) {
109
+ const toolCallRe = /<tool_call>\s*([a-zA-Z0-9_]+)([\s\S]*?)<\/tool_call>/gi;
110
+ while ((match = toolCallRe.exec(text)) !== null) {
111
+ tryAdd(match[1].trim(), match[2]);
112
+ }
113
+ }
114
+ return results;
115
+ }
116
+ export function extractFailedGeneration(errorMessage) {
117
+ const marker = "Model output:";
118
+ const idx = errorMessage.indexOf(marker);
119
+ if (idx >= 0)
120
+ return errorMessage.slice(idx + marker.length).trim();
121
+ return null;
122
+ }
25
123
  export function toOpenAIMessages(ctx) {
26
124
  const msgs = [];
27
125
  if (ctx.systemPrompt) {
package/dist/ui/App.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import type { AgentSession } from "../agent/session.js";
3
3
  export interface DisplayMessage {
4
+ id: number;
4
5
  role: "user" | "assistant" | "tool";
5
6
  content: string;
6
7
  toolName?: string;