@lov3kaizen/agentsea-core 1.1.0 → 1.2.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.
package/dist/index.d.mts CHANGED
@@ -198,6 +198,8 @@ declare class AnthropicProvider implements LLMProvider {
198
198
  streamResponse(messages: Message[], config: ProviderConfig): AsyncIterable<LLMStreamChunk>;
199
199
  parseToolCalls(response: LLMResponse): ToolCall[];
200
200
  private convertMessages;
201
+ private ensureToolUse;
202
+ private toolResultBlock;
201
203
  }
202
204
 
203
205
  declare class OpenAIProvider implements LLMProvider {
package/dist/index.d.ts CHANGED
@@ -198,6 +198,8 @@ declare class AnthropicProvider implements LLMProvider {
198
198
  streamResponse(messages: Message[], config: ProviderConfig): AsyncIterable<LLMStreamChunk>;
199
199
  parseToolCalls(response: LLMResponse): ToolCall[];
200
200
  private convertMessages;
201
+ private ensureToolUse;
202
+ private toolResultBlock;
201
203
  }
202
204
 
203
205
  declare class OpenAIProvider implements LLMProvider {
package/dist/index.js CHANGED
@@ -743,7 +743,7 @@ var ToolRegistry = class {
743
743
  * Sleep helper
744
744
  */
745
745
  sleep(ms) {
746
- return new Promise((resolve) => setTimeout(resolve, ms));
746
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
747
747
  }
748
748
  };
749
749
 
@@ -778,7 +778,49 @@ var calculatorTool = {
778
778
  };
779
779
 
780
780
  // src/tools/built-in/http-request.tool.ts
781
+ var import_promises = require("dns/promises");
782
+ var import_net = require("net");
781
783
  var import_zod2 = require("zod");
784
+ function isPrivateIp(ip) {
785
+ const mapped = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
786
+ const addr = mapped ? mapped[1] : ip;
787
+ if ((0, import_net.isIP)(addr) === 4) {
788
+ const [a, b] = addr.split(".").map(Number);
789
+ return a === 0 || // 0.0.0.0/8
790
+ a === 10 || // 10.0.0.0/8 (private)
791
+ a === 127 || // 127.0.0.0/8 (loopback)
792
+ a === 169 && b === 254 || // 169.254.0.0/16 (link-local incl. cloud metadata)
793
+ a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 (private)
794
+ a === 192 && b === 168 || // 192.168.0.0/16 (private)
795
+ a === 100 && b >= 64 && b <= 127 || // 100.64.0.0/10 (CGNAT)
796
+ a >= 224;
797
+ }
798
+ const lower = ip.toLowerCase();
799
+ return lower === "::1" || // loopback
800
+ lower === "::" || // unspecified
801
+ lower.startsWith("fe80:") || // link-local
802
+ lower.startsWith("fc") || // unique-local fc00::/7
803
+ lower.startsWith("fd");
804
+ }
805
+ async function assertPublicUrl(rawUrl) {
806
+ let parsed;
807
+ try {
808
+ parsed = new URL(rawUrl);
809
+ } catch {
810
+ throw new Error("Invalid URL");
811
+ }
812
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
813
+ throw new Error(`Blocked URL scheme: ${parsed.protocol}`);
814
+ }
815
+ const host = parsed.hostname;
816
+ if (host === "localhost") {
817
+ throw new Error("Blocked request to localhost");
818
+ }
819
+ const candidates = (0, import_net.isIP)(host) ? [host] : (await (0, import_promises.lookup)(host, { all: true })).map((r) => r.address);
820
+ if (candidates.some(isPrivateIp)) {
821
+ throw new Error(`Blocked request to non-public address for host "${host}"`);
822
+ }
823
+ }
782
824
  var httpRequestTool = {
783
825
  name: "http_request",
784
826
  description: "Make HTTP requests to external APIs. Supports GET, POST, PUT, DELETE methods.",
@@ -790,6 +832,7 @@ var httpRequestTool = {
790
832
  timeout: import_zod2.z.number().optional().default(1e4).describe("Request timeout in milliseconds")
791
833
  }),
792
834
  execute: async (params) => {
835
+ await assertPublicUrl(params.url);
793
836
  const controller = new AbortController();
794
837
  const timeoutId = setTimeout(
795
838
  () => controller.abort(),
@@ -844,8 +887,21 @@ var httpRequestTool = {
844
887
 
845
888
  // src/tools/built-in/file-operations.tool.ts
846
889
  var import_fs = require("fs");
847
- var import_path = require("path");
890
+ var import_path2 = require("path");
848
891
  var import_zod3 = require("zod");
892
+
893
+ // src/tools/built-in/path-guard.ts
894
+ var import_path = require("path");
895
+ function resolveWithinRoot(inputPath) {
896
+ const root = (0, import_path.resolve)(process.env.AGENTSEA_FILE_ROOT || process.cwd());
897
+ const resolved = (0, import_path.resolve)(root, inputPath);
898
+ if (resolved !== root && !resolved.startsWith(root + import_path.sep)) {
899
+ throw new Error(`Path escapes the allowed root directory: ${inputPath}`);
900
+ }
901
+ return resolved;
902
+ }
903
+
904
+ // src/tools/built-in/file-operations.tool.ts
849
905
  var fileReadTool = {
850
906
  name: "file_read",
851
907
  description: "Read contents of a file from the file system",
@@ -855,11 +911,12 @@ var fileReadTool = {
855
911
  }),
856
912
  execute: async (params) => {
857
913
  try {
914
+ const safePath = resolveWithinRoot(params.path);
858
915
  const content = await import_fs.promises.readFile(
859
- params.path,
916
+ safePath,
860
917
  params.encoding
861
918
  );
862
- const stats = await import_fs.promises.stat(params.path);
919
+ const stats = await import_fs.promises.stat(safePath);
863
920
  return {
864
921
  content,
865
922
  size: stats.size,
@@ -885,23 +942,24 @@ var fileWriteTool = {
885
942
  }),
886
943
  execute: async (params) => {
887
944
  try {
945
+ const safePath = resolveWithinRoot(params.path);
888
946
  if (params.append) {
889
947
  await import_fs.promises.appendFile(
890
- params.path,
948
+ safePath,
891
949
  params.content,
892
950
  params.encoding
893
951
  );
894
952
  } else {
895
953
  await import_fs.promises.writeFile(
896
- params.path,
954
+ safePath,
897
955
  params.content,
898
956
  params.encoding
899
957
  );
900
958
  }
901
- const stats = await import_fs.promises.stat(params.path);
959
+ const stats = await import_fs.promises.stat(safePath);
902
960
  return {
903
961
  success: true,
904
- path: params.path,
962
+ path: safePath,
905
963
  size: stats.size,
906
964
  modified: stats.mtime
907
965
  };
@@ -922,10 +980,11 @@ var fileListTool = {
922
980
  }),
923
981
  execute: async (params) => {
924
982
  try {
925
- const items = await import_fs.promises.readdir(params.path, { withFileTypes: true });
983
+ const safePath = resolveWithinRoot(params.path);
984
+ const items = await import_fs.promises.readdir(safePath, { withFileTypes: true });
926
985
  const results = [];
927
986
  for (const item of items) {
928
- const fullPath = (0, import_path.join)(params.path, item.name);
987
+ const fullPath = (0, import_path2.join)(safePath, item.name);
929
988
  const stats = await import_fs.promises.stat(fullPath);
930
989
  results.push({
931
990
  name: item.name,
@@ -1444,7 +1503,7 @@ async function pollExecutionStatus(executionId, apiKey, baseUrl, maxAttempts = 3
1444
1503
  return execution;
1445
1504
  }
1446
1505
  if (attempt < maxAttempts - 1) {
1447
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1506
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
1448
1507
  }
1449
1508
  }
1450
1509
  throw new Error(
@@ -1459,10 +1518,10 @@ var MAX_OUTPUT_BYTES = 100 * 1024;
1459
1518
  var DEFAULT_TIMEOUT_MS = 3e4;
1460
1519
  var MAX_TIMEOUT_MS = 12e4;
1461
1520
  var DANGEROUS_PATTERNS = [
1462
- /\brm\s+-[^\s]*r[^\s]*f[^\s]*\s+\/\s*$/,
1463
- // rm -rf /
1464
- /\brm\s+-[^\s]*f[^\s]*r[^\s]*\s+\/\s*$/,
1465
- // rm -fr /
1521
+ /\brm\s+-[^\s]*r[^\s]*f[^\s]*\s+\/(\s|\*|$)/,
1522
+ // rm -rf / , / * , /...
1523
+ /\brm\s+-[^\s]*f[^\s]*r[^\s]*\s+\/(\s|\*|$)/,
1524
+ // rm -fr / , / * , /...
1466
1525
  /\bmkfs\b/,
1467
1526
  // mkfs (format disk)
1468
1527
  /:(){ :\|:& };:/,
@@ -1478,7 +1537,7 @@ var DANGEROUS_PATTERNS = [
1478
1537
  ];
1479
1538
  var shellExecuteTool = {
1480
1539
  name: "shell_execute",
1481
- description: "Execute a shell command and return stdout/stderr. Commands are checked against a safety blocklist. Non-zero exit codes return results (not errors) since tools like grep exit 1 on no matches.",
1540
+ description: "Execute a shell command and return stdout/stderr. A best-effort blocklist rejects a few obviously destructive commands, but this is NOT a security boundary \u2014 only enable this tool for trusted callers and prefer a sandboxed host. Non-zero exit codes return results (not errors) since tools like grep exit 1 on no matches.",
1482
1541
  parameters: import_zod7.z.object({
1483
1542
  command: import_zod7.z.string().describe("The shell command to execute"),
1484
1543
  cwd: import_zod7.z.string().optional().describe(
@@ -1555,13 +1614,14 @@ var codeEditTool = {
1555
1614
  }),
1556
1615
  execute: async (params) => {
1557
1616
  try {
1558
- const content = await import_fs2.promises.readFile(params.path, "utf8");
1617
+ const safePath = resolveWithinRoot(params.path);
1618
+ const content = await import_fs2.promises.readFile(safePath, "utf8");
1559
1619
  if (params.oldString === "") {
1560
1620
  const newContent2 = params.newString + content;
1561
- await import_fs2.promises.writeFile(params.path, newContent2, "utf8");
1621
+ await import_fs2.promises.writeFile(safePath, newContent2, "utf8");
1562
1622
  return {
1563
1623
  success: true,
1564
- path: params.path,
1624
+ path: safePath,
1565
1625
  replacements: 1,
1566
1626
  message: "Content inserted at beginning of file"
1567
1627
  };
@@ -1585,10 +1645,10 @@ var codeEditTool = {
1585
1645
  );
1586
1646
  }
1587
1647
  const newContent = content.split(params.oldString).join(params.newString);
1588
- await import_fs2.promises.writeFile(params.path, newContent, "utf8");
1648
+ await import_fs2.promises.writeFile(safePath, newContent, "utf8");
1589
1649
  return {
1590
1650
  success: true,
1591
- path: params.path,
1651
+ path: safePath,
1592
1652
  replacements: count,
1593
1653
  message: `Replaced ${count} occurrence(s)`
1594
1654
  };
@@ -1665,7 +1725,7 @@ var globTool = {
1665
1725
 
1666
1726
  // src/tools/built-in/grep.tool.ts
1667
1727
  var import_fs4 = require("fs");
1668
- var import_path2 = require("path");
1728
+ var import_path3 = require("path");
1669
1729
  var import_zod10 = require("zod");
1670
1730
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
1671
1731
  var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -1685,7 +1745,7 @@ async function walkDir(dir, includePattern) {
1685
1745
  for (const entry of entries) {
1686
1746
  if (DEFAULT_IGNORE_DIRS.has(entry.name)) continue;
1687
1747
  if (entry.name.startsWith(".") && entry.name !== ".env.example") continue;
1688
- const fullPath = (0, import_path2.join)(dir, entry.name);
1748
+ const fullPath = (0, import_path3.join)(dir, entry.name);
1689
1749
  if (entry.isDirectory()) {
1690
1750
  const subResults = await walkDir(fullPath, includePattern);
1691
1751
  results.push(...subResults);
@@ -1761,7 +1821,7 @@ var grepTool = {
1761
1821
  );
1762
1822
  for (const match of matches) {
1763
1823
  if (allMatches.length >= params.maxResults) break;
1764
- match.file = stat.isFile() ? match.file : (0, import_path2.relative)(searchPath, match.file);
1824
+ match.file = stat.isFile() ? match.file : (0, import_path3.relative)(searchPath, match.file);
1765
1825
  allMatches.push(match);
1766
1826
  }
1767
1827
  } catch {
@@ -1787,7 +1847,7 @@ var import_child_process2 = require("child_process");
1787
1847
  var import_zod11 = require("zod");
1788
1848
  var GIT_TIMEOUT_MS = 3e4;
1789
1849
  function gitExec(args, cwd) {
1790
- return (0, import_child_process2.execSync)(`git ${args}`, {
1850
+ return (0, import_child_process2.execFileSync)("git", args, {
1791
1851
  cwd: cwd || process.cwd(),
1792
1852
  timeout: GIT_TIMEOUT_MS,
1793
1853
  encoding: "utf8",
@@ -1802,8 +1862,8 @@ var gitStatusTool = {
1802
1862
  }),
1803
1863
  execute: (params) => {
1804
1864
  try {
1805
- const output = gitExec("status --porcelain", params.cwd);
1806
- const branch = gitExec("branch --show-current", params.cwd);
1865
+ const output = gitExec(["status", "--porcelain"], params.cwd);
1866
+ const branch = gitExec(["branch", "--show-current"], params.cwd);
1807
1867
  const staged = [];
1808
1868
  const unstaged = [];
1809
1869
  const untracked = [];
@@ -1845,9 +1905,9 @@ var gitDiffTool = {
1845
1905
  }),
1846
1906
  execute: (params) => {
1847
1907
  try {
1848
- let args = "diff";
1849
- if (params.staged) args += " --cached";
1850
- if (params.path) args += ` -- ${params.path}`;
1908
+ const args = ["diff"];
1909
+ if (params.staged) args.push("--cached");
1910
+ if (params.path) args.push("--", params.path);
1851
1911
  const output = gitExec(args, params.cwd);
1852
1912
  return Promise.resolve({
1853
1913
  diff: output,
@@ -1870,8 +1930,7 @@ var gitAddTool = {
1870
1930
  }),
1871
1931
  execute: (params) => {
1872
1932
  try {
1873
- const escapedPaths = params.paths.map((p) => `"${p}"`).join(" ");
1874
- gitExec(`add ${escapedPaths}`, params.cwd);
1933
+ gitExec(["add", "--", ...params.paths], params.cwd);
1875
1934
  return Promise.resolve({
1876
1935
  success: true,
1877
1936
  added: params.paths
@@ -1893,8 +1952,7 @@ var gitCommitTool = {
1893
1952
  }),
1894
1953
  execute: (params) => {
1895
1954
  try {
1896
- const safeMessage = params.message.replace(/'/g, "'\\''");
1897
- const output = gitExec(`commit -m '${safeMessage}'`, params.cwd);
1955
+ const output = gitExec(["commit", "-m", params.message], params.cwd);
1898
1956
  return Promise.resolve({
1899
1957
  success: true,
1900
1958
  output
@@ -1918,13 +1976,13 @@ var gitLogTool = {
1918
1976
  }),
1919
1977
  execute: (params) => {
1920
1978
  try {
1921
- let args = `log -${params.maxCount}`;
1979
+ const args = ["log", `-${params.maxCount}`];
1922
1980
  if (params.oneline) {
1923
- args += " --oneline";
1981
+ args.push("--oneline");
1924
1982
  } else {
1925
- args += " --format=%H%n%an%n%ae%n%ai%n%s%n---";
1983
+ args.push("--format=%H%n%an%n%ae%n%ai%n%s%n---");
1926
1984
  }
1927
- if (params.path) args += ` -- ${params.path}`;
1985
+ if (params.path) args.push("--", params.path);
1928
1986
  const output = gitExec(args, params.cwd);
1929
1987
  if (params.oneline) {
1930
1988
  const commits = output.split("\n").filter(Boolean).map((line) => {
@@ -1957,8 +2015,8 @@ var gitBranchTool = {
1957
2015
  try {
1958
2016
  switch (params.action) {
1959
2017
  case "list": {
1960
- const output = gitExec("branch -a", params.cwd);
1961
- const current = gitExec("branch --show-current", params.cwd);
2018
+ const output = gitExec(["branch", "-a"], params.cwd);
2019
+ const current = gitExec(["branch", "--show-current"], params.cwd);
1962
2020
  const branches = output.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s+/, "").trim());
1963
2021
  return Promise.resolve({ branches, current });
1964
2022
  }
@@ -1966,14 +2024,20 @@ var gitBranchTool = {
1966
2024
  if (!params.name) {
1967
2025
  throw new Error("Branch name is required for create action");
1968
2026
  }
1969
- gitExec(`branch ${params.name}`, params.cwd);
2027
+ if (params.name.startsWith("-")) {
2028
+ throw new Error("Invalid branch name");
2029
+ }
2030
+ gitExec(["branch", params.name], params.cwd);
1970
2031
  return Promise.resolve({ success: true, created: params.name });
1971
2032
  }
1972
2033
  case "switch": {
1973
2034
  if (!params.name) {
1974
2035
  throw new Error("Branch name is required for switch action");
1975
2036
  }
1976
- gitExec(`checkout ${params.name}`, params.cwd);
2037
+ if (params.name.startsWith("-")) {
2038
+ throw new Error("Invalid branch name");
2039
+ }
2040
+ gitExec(["checkout", params.name], params.cwd);
1977
2041
  return Promise.resolve({ success: true, switched: params.name });
1978
2042
  }
1979
2043
  }
@@ -2278,17 +2342,28 @@ var AnthropicProvider = class {
2278
2342
  continue;
2279
2343
  }
2280
2344
  if (message.role === "tool") {
2281
- const lastMessage = converted[converted.length - 1];
2282
- if (lastMessage && lastMessage.role === "assistant") {
2283
- const content = lastMessage.content;
2284
- if (Array.isArray(content)) {
2285
- content.push({
2286
- type: "tool_result",
2287
- tool_use_id: message.toolCallId || "",
2288
- content: message.content
2289
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2290
- });
2345
+ const toolUseId = message.toolCallId || "";
2346
+ const resultBlock = this.toolResultBlock(toolUseId, message.content);
2347
+ const prev = converted[converted.length - 1];
2348
+ if (prev && prev.role === "user" && Array.isArray(prev.content) && prev.content.some((b) => b.type === "tool_result")) {
2349
+ const assistant = converted[converted.length - 2];
2350
+ if (assistant && assistant.role === "assistant") {
2351
+ assistant.content = this.ensureToolUse(
2352
+ assistant.content,
2353
+ toolUseId,
2354
+ message.name
2355
+ );
2291
2356
  }
2357
+ prev.content.push(resultBlock);
2358
+ } else if (prev && prev.role === "assistant") {
2359
+ prev.content = this.ensureToolUse(
2360
+ prev.content,
2361
+ toolUseId,
2362
+ message.name
2363
+ );
2364
+ converted.push({ role: "user", content: [resultBlock] });
2365
+ } else {
2366
+ converted.push({ role: "user", content: [resultBlock] });
2292
2367
  }
2293
2368
  continue;
2294
2369
  }
@@ -2299,6 +2374,20 @@ var AnthropicProvider = class {
2299
2374
  }
2300
2375
  return converted;
2301
2376
  }
2377
+ /**
2378
+ * Ensure an assistant message's content is a block array that includes a
2379
+ * `tool_use` block with the given id, backfilling one if missing.
2380
+ */
2381
+ ensureToolUse(content, id, name) {
2382
+ const blocks = typeof content === "string" ? content ? [{ type: "text", text: content }] : [] : [...content];
2383
+ if (!blocks.some((b) => b.type === "tool_use" && b.id === id)) {
2384
+ blocks.push({ type: "tool_use", id, name: name || "tool", input: {} });
2385
+ }
2386
+ return blocks;
2387
+ }
2388
+ toolResultBlock(toolUseId, content) {
2389
+ return { type: "tool_result", tool_use_id: toolUseId, content };
2390
+ }
2302
2391
  };
2303
2392
 
2304
2393
  // src/providers/openai.ts
@@ -3560,7 +3649,7 @@ var Workflow = class {
3560
3649
  if (attempt < maxAttempts && initialDelayMs > 0) {
3561
3650
  const raw = backoff === "linear" ? initialDelayMs * attempt : initialDelayMs * 2 ** (attempt - 1);
3562
3651
  await new Promise(
3563
- (resolve) => setTimeout(resolve, Math.min(raw, maxDelayMs))
3652
+ (resolve2) => setTimeout(resolve2, Math.min(raw, maxDelayMs))
3564
3653
  );
3565
3654
  }
3566
3655
  }
@@ -4152,7 +4241,7 @@ var RateLimiter = class {
4152
4241
  * Sleep helper
4153
4242
  */
4154
4243
  sleep(ms) {
4155
- return new Promise((resolve) => setTimeout(resolve, ms));
4244
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4156
4245
  }
4157
4246
  };
4158
4247
  var SlidingWindowRateLimiter = class {
@@ -4384,7 +4473,7 @@ var StdioTransport = class extends import_events.EventEmitter {
4384
4473
  connected = false;
4385
4474
  buffer = "";
4386
4475
  async connect() {
4387
- return new Promise((resolve, reject) => {
4476
+ return new Promise((resolve2, reject) => {
4388
4477
  try {
4389
4478
  this.process = (0, import_child_process3.spawn)(this.command, this.args, {
4390
4479
  env: { ...process.env, ...this.env },
@@ -4407,7 +4496,7 @@ var StdioTransport = class extends import_events.EventEmitter {
4407
4496
  });
4408
4497
  this.connected = true;
4409
4498
  this.emit("connect");
4410
- resolve();
4499
+ resolve2();
4411
4500
  } catch (error) {
4412
4501
  reject(error);
4413
4502
  }
@@ -4455,7 +4544,7 @@ var SSETransport = class extends import_events.EventEmitter {
4455
4544
  eventSource = null;
4456
4545
  connected = false;
4457
4546
  async connect() {
4458
- return new Promise((resolve, reject) => {
4547
+ return new Promise((resolve2, reject) => {
4459
4548
  try {
4460
4549
  if (typeof EventSource === "undefined") {
4461
4550
  throw new Error(
@@ -4466,7 +4555,7 @@ var SSETransport = class extends import_events.EventEmitter {
4466
4555
  this.eventSource.onopen = () => {
4467
4556
  this.connected = true;
4468
4557
  this.emit("connect");
4469
- resolve();
4558
+ resolve2();
4470
4559
  };
4471
4560
  this.eventSource.onmessage = (event) => {
4472
4561
  try {
@@ -4677,9 +4766,9 @@ var MCPClient = class extends import_events2.EventEmitter {
4677
4766
  if (!this.transport || !this.transport.isConnected()) {
4678
4767
  throw new Error("Not connected to MCP server");
4679
4768
  }
4680
- return new Promise((resolve, reject) => {
4769
+ return new Promise((resolve2, reject) => {
4681
4770
  const id = message.id;
4682
- this.pendingRequests.set(id, { resolve, reject });
4771
+ this.pendingRequests.set(id, { resolve: resolve2, reject });
4683
4772
  try {
4684
4773
  this.transport.send(message);
4685
4774
  } catch (error) {
@@ -5814,7 +5903,7 @@ Respond naturally while ensuring you gather the required information.`;
5814
5903
 
5815
5904
  // src/voice/voice-agent.ts
5816
5905
  var import_fs5 = require("fs");
5817
- var import_path3 = require("path");
5906
+ var import_path4 = require("path");
5818
5907
  var VoiceAgent = class {
5819
5908
  agent;
5820
5909
  sttProvider;
@@ -5978,10 +6067,10 @@ var VoiceAgent = class {
5978
6067
  exportConversation(outputDir) {
5979
6068
  for (let i = 0; i < this.conversationHistory.length; i++) {
5980
6069
  const message = this.conversationHistory[i];
5981
- const textPath = (0, import_path3.join)(outputDir, `${i}-${message.role}.txt`);
6070
+ const textPath = (0, import_path4.join)(outputDir, `${i}-${message.role}.txt`);
5982
6071
  (0, import_fs5.writeFileSync)(textPath, message.text);
5983
6072
  if (message.audio) {
5984
- const audioPath = (0, import_path3.join)(outputDir, `${i}-${message.role}.mp3`);
6073
+ const audioPath = (0, import_path4.join)(outputDir, `${i}-${message.role}.mp3`);
5985
6074
  (0, import_fs5.writeFileSync)(audioPath, message.audio);
5986
6075
  }
5987
6076
  }
@@ -6202,9 +6291,9 @@ var OpenAIWhisperProvider = class {
6202
6291
  var import_child_process4 = require("child_process");
6203
6292
  var import_fs7 = require("fs");
6204
6293
  var import_os = require("os");
6205
- var import_path4 = require("path");
6294
+ var import_path5 = require("path");
6206
6295
  var import_util = require("util");
6207
- var execAsync = (0, import_util.promisify)(import_child_process4.exec);
6296
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
6208
6297
  var LocalWhisperProvider = class {
6209
6298
  whisperPath;
6210
6299
  modelPath;
@@ -6220,7 +6309,7 @@ var LocalWhisperProvider = class {
6220
6309
  let isTemporary = false;
6221
6310
  try {
6222
6311
  if (Buffer.isBuffer(audio)) {
6223
- audioPath = (0, import_path4.join)((0, import_os.tmpdir)(), `audio-${Date.now()}.wav`);
6312
+ audioPath = (0, import_path5.join)((0, import_os.tmpdir)(), `audio-${Date.now()}.wav`);
6224
6313
  (0, import_fs7.writeFileSync)(audioPath, audio);
6225
6314
  isTemporary = true;
6226
6315
  } else {
@@ -6230,16 +6319,23 @@ var LocalWhisperProvider = class {
6230
6319
  throw new Error(`Audio file not found: ${audioPath}`);
6231
6320
  }
6232
6321
  const model = config?.model || "base";
6233
- const language = config?.language ? `--language ${config.language}` : "";
6322
+ const languageArgs = config?.language ? ["--language", config.language] : [];
6234
6323
  const outputFormat = config?.responseFormat || "txt";
6235
- let command;
6324
+ let args;
6236
6325
  if (this.whisperPath.includes("whisper.cpp")) {
6237
6326
  const modelFile = this.modelPath || `ggml-${model}.bin`;
6238
- command = `${this.whisperPath} -m ${modelFile} ${language} -otxt "${audioPath}"`;
6327
+ args = ["-m", modelFile, ...languageArgs, "-otxt", audioPath];
6239
6328
  } else {
6240
- command = `${this.whisperPath} "${audioPath}" --model ${model} ${language} --output_format ${outputFormat}`;
6241
- }
6242
- const { stdout } = await execAsync(command, {
6329
+ args = [
6330
+ audioPath,
6331
+ "--model",
6332
+ model,
6333
+ ...languageArgs,
6334
+ "--output_format",
6335
+ outputFormat
6336
+ ];
6337
+ }
6338
+ const { stdout } = await execFileAsync(this.whisperPath, args, {
6243
6339
  maxBuffer: 10 * 1024 * 1024
6244
6340
  // 10MB buffer
6245
6341
  });
@@ -6281,7 +6377,7 @@ var LocalWhisperProvider = class {
6281
6377
  */
6282
6378
  async isInstalled() {
6283
6379
  try {
6284
- await execAsync(`${this.whisperPath} --help`);
6380
+ await execFileAsync(this.whisperPath, ["--help"]);
6285
6381
  return true;
6286
6382
  } catch {
6287
6383
  return false;
@@ -6587,9 +6683,9 @@ var ElevenLabsTTSProvider = class {
6587
6683
  var import_child_process5 = require("child_process");
6588
6684
  var import_fs8 = require("fs");
6589
6685
  var import_os2 = require("os");
6590
- var import_path5 = require("path");
6686
+ var import_path6 = require("path");
6591
6687
  var import_util2 = require("util");
6592
- var execAsync2 = (0, import_util2.promisify)(import_child_process5.exec);
6688
+ var execFileAsync2 = (0, import_util2.promisify)(import_child_process5.execFile);
6593
6689
  var PiperTTSProvider = class {
6594
6690
  piperPath;
6595
6691
  modelPath;
@@ -6603,26 +6699,40 @@ var PiperTTSProvider = class {
6603
6699
  * Synthesize text to speech
6604
6700
  */
6605
6701
  async synthesize(text, config) {
6702
+ const outputPath = (0, import_path6.join)((0, import_os2.tmpdir)(), `speech-${Date.now()}.wav`);
6606
6703
  try {
6607
- const outputPath = (0, import_path5.join)((0, import_os2.tmpdir)(), `speech-${Date.now()}.wav`);
6608
6704
  const model = this.modelPath || config?.model;
6609
6705
  if (!model) {
6610
6706
  throw new Error("Model path is required for Piper TTS");
6611
6707
  }
6612
6708
  const modelConfig = this.configPath || model.replace(".onnx", ".json");
6613
- const textPath = (0, import_path5.join)((0, import_os2.tmpdir)(), `text-${Date.now()}.txt`);
6614
- (0, import_fs8.writeFileSync)(textPath, text, "utf-8");
6615
- const command = `${this.piperPath} --model ${model} --config ${modelConfig} --output_file ${outputPath} < ${textPath}`;
6616
- await execAsync2(command, {
6617
- maxBuffer: 50 * 1024 * 1024
6618
- // 50MB buffer
6709
+ const args = [
6710
+ "--model",
6711
+ model,
6712
+ "--config",
6713
+ modelConfig,
6714
+ "--output_file",
6715
+ outputPath
6716
+ ];
6717
+ await new Promise((resolve2, reject) => {
6718
+ const child = (0, import_child_process5.spawn)(this.piperPath, args, {
6719
+ stdio: ["pipe", "ignore", "pipe"]
6720
+ });
6721
+ let stderr = "";
6722
+ child.stderr?.on("data", (chunk) => {
6723
+ stderr += String(chunk);
6724
+ });
6725
+ child.on("error", reject);
6726
+ child.on("close", (code) => {
6727
+ if (code === 0) resolve2();
6728
+ else reject(new Error(`piper exited with code ${code}: ${stderr}`));
6729
+ });
6730
+ child.stdin?.end(text);
6619
6731
  });
6620
6732
  if (!(0, import_fs8.existsSync)(outputPath)) {
6621
6733
  throw new Error("Piper failed to generate audio file");
6622
6734
  }
6623
6735
  const audio = (0, import_fs8.readFileSync)(outputPath);
6624
- (0, import_fs8.unlinkSync)(outputPath);
6625
- (0, import_fs8.unlinkSync)(textPath);
6626
6736
  return {
6627
6737
  audio,
6628
6738
  format: "wav",
@@ -6632,6 +6742,10 @@ var PiperTTSProvider = class {
6632
6742
  throw new Error(
6633
6743
  `Piper TTS synthesis failed: ${error instanceof Error ? error.message : String(error)}`
6634
6744
  );
6745
+ } finally {
6746
+ if ((0, import_fs8.existsSync)(outputPath)) {
6747
+ (0, import_fs8.unlinkSync)(outputPath);
6748
+ }
6635
6749
  }
6636
6750
  }
6637
6751
  /**
@@ -6645,7 +6759,7 @@ var PiperTTSProvider = class {
6645
6759
  */
6646
6760
  async isInstalled() {
6647
6761
  try {
6648
- await execAsync2(`${this.piperPath} --version`);
6762
+ await execFileAsync2(this.piperPath, ["--version"]);
6649
6763
  return true;
6650
6764
  } catch {
6651
6765
  return false;
package/dist/index.mjs CHANGED
@@ -731,7 +731,7 @@ var ToolRegistry = class {
731
731
  * Sleep helper
732
732
  */
733
733
  sleep(ms) {
734
- return new Promise((resolve) => setTimeout(resolve, ms));
734
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
735
735
  }
736
736
  };
737
737
 
@@ -766,7 +766,49 @@ var calculatorTool = {
766
766
  };
767
767
 
768
768
  // src/tools/built-in/http-request.tool.ts
769
+ import { lookup } from "dns/promises";
770
+ import { isIP } from "net";
769
771
  import { z as z2 } from "zod";
772
+ function isPrivateIp(ip) {
773
+ const mapped = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
774
+ const addr = mapped ? mapped[1] : ip;
775
+ if (isIP(addr) === 4) {
776
+ const [a, b] = addr.split(".").map(Number);
777
+ return a === 0 || // 0.0.0.0/8
778
+ a === 10 || // 10.0.0.0/8 (private)
779
+ a === 127 || // 127.0.0.0/8 (loopback)
780
+ a === 169 && b === 254 || // 169.254.0.0/16 (link-local incl. cloud metadata)
781
+ a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 (private)
782
+ a === 192 && b === 168 || // 192.168.0.0/16 (private)
783
+ a === 100 && b >= 64 && b <= 127 || // 100.64.0.0/10 (CGNAT)
784
+ a >= 224;
785
+ }
786
+ const lower = ip.toLowerCase();
787
+ return lower === "::1" || // loopback
788
+ lower === "::" || // unspecified
789
+ lower.startsWith("fe80:") || // link-local
790
+ lower.startsWith("fc") || // unique-local fc00::/7
791
+ lower.startsWith("fd");
792
+ }
793
+ async function assertPublicUrl(rawUrl) {
794
+ let parsed;
795
+ try {
796
+ parsed = new URL(rawUrl);
797
+ } catch {
798
+ throw new Error("Invalid URL");
799
+ }
800
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
801
+ throw new Error(`Blocked URL scheme: ${parsed.protocol}`);
802
+ }
803
+ const host = parsed.hostname;
804
+ if (host === "localhost") {
805
+ throw new Error("Blocked request to localhost");
806
+ }
807
+ const candidates = isIP(host) ? [host] : (await lookup(host, { all: true })).map((r) => r.address);
808
+ if (candidates.some(isPrivateIp)) {
809
+ throw new Error(`Blocked request to non-public address for host "${host}"`);
810
+ }
811
+ }
770
812
  var httpRequestTool = {
771
813
  name: "http_request",
772
814
  description: "Make HTTP requests to external APIs. Supports GET, POST, PUT, DELETE methods.",
@@ -778,6 +820,7 @@ var httpRequestTool = {
778
820
  timeout: z2.number().optional().default(1e4).describe("Request timeout in milliseconds")
779
821
  }),
780
822
  execute: async (params) => {
823
+ await assertPublicUrl(params.url);
781
824
  const controller = new AbortController();
782
825
  const timeoutId = setTimeout(
783
826
  () => controller.abort(),
@@ -834,6 +877,19 @@ var httpRequestTool = {
834
877
  import { promises as fs } from "fs";
835
878
  import { join } from "path";
836
879
  import { z as z3 } from "zod";
880
+
881
+ // src/tools/built-in/path-guard.ts
882
+ import { resolve, sep } from "path";
883
+ function resolveWithinRoot(inputPath) {
884
+ const root = resolve(process.env.AGENTSEA_FILE_ROOT || process.cwd());
885
+ const resolved = resolve(root, inputPath);
886
+ if (resolved !== root && !resolved.startsWith(root + sep)) {
887
+ throw new Error(`Path escapes the allowed root directory: ${inputPath}`);
888
+ }
889
+ return resolved;
890
+ }
891
+
892
+ // src/tools/built-in/file-operations.tool.ts
837
893
  var fileReadTool = {
838
894
  name: "file_read",
839
895
  description: "Read contents of a file from the file system",
@@ -843,11 +899,12 @@ var fileReadTool = {
843
899
  }),
844
900
  execute: async (params) => {
845
901
  try {
902
+ const safePath = resolveWithinRoot(params.path);
846
903
  const content = await fs.readFile(
847
- params.path,
904
+ safePath,
848
905
  params.encoding
849
906
  );
850
- const stats = await fs.stat(params.path);
907
+ const stats = await fs.stat(safePath);
851
908
  return {
852
909
  content,
853
910
  size: stats.size,
@@ -873,23 +930,24 @@ var fileWriteTool = {
873
930
  }),
874
931
  execute: async (params) => {
875
932
  try {
933
+ const safePath = resolveWithinRoot(params.path);
876
934
  if (params.append) {
877
935
  await fs.appendFile(
878
- params.path,
936
+ safePath,
879
937
  params.content,
880
938
  params.encoding
881
939
  );
882
940
  } else {
883
941
  await fs.writeFile(
884
- params.path,
942
+ safePath,
885
943
  params.content,
886
944
  params.encoding
887
945
  );
888
946
  }
889
- const stats = await fs.stat(params.path);
947
+ const stats = await fs.stat(safePath);
890
948
  return {
891
949
  success: true,
892
- path: params.path,
950
+ path: safePath,
893
951
  size: stats.size,
894
952
  modified: stats.mtime
895
953
  };
@@ -910,10 +968,11 @@ var fileListTool = {
910
968
  }),
911
969
  execute: async (params) => {
912
970
  try {
913
- const items = await fs.readdir(params.path, { withFileTypes: true });
971
+ const safePath = resolveWithinRoot(params.path);
972
+ const items = await fs.readdir(safePath, { withFileTypes: true });
914
973
  const results = [];
915
974
  for (const item of items) {
916
- const fullPath = join(params.path, item.name);
975
+ const fullPath = join(safePath, item.name);
917
976
  const stats = await fs.stat(fullPath);
918
977
  results.push({
919
978
  name: item.name,
@@ -1432,7 +1491,7 @@ async function pollExecutionStatus(executionId, apiKey, baseUrl, maxAttempts = 3
1432
1491
  return execution;
1433
1492
  }
1434
1493
  if (attempt < maxAttempts - 1) {
1435
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
1494
+ await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
1436
1495
  }
1437
1496
  }
1438
1497
  throw new Error(
@@ -1447,10 +1506,10 @@ var MAX_OUTPUT_BYTES = 100 * 1024;
1447
1506
  var DEFAULT_TIMEOUT_MS = 3e4;
1448
1507
  var MAX_TIMEOUT_MS = 12e4;
1449
1508
  var DANGEROUS_PATTERNS = [
1450
- /\brm\s+-[^\s]*r[^\s]*f[^\s]*\s+\/\s*$/,
1451
- // rm -rf /
1452
- /\brm\s+-[^\s]*f[^\s]*r[^\s]*\s+\/\s*$/,
1453
- // rm -fr /
1509
+ /\brm\s+-[^\s]*r[^\s]*f[^\s]*\s+\/(\s|\*|$)/,
1510
+ // rm -rf / , / * , /...
1511
+ /\brm\s+-[^\s]*f[^\s]*r[^\s]*\s+\/(\s|\*|$)/,
1512
+ // rm -fr / , / * , /...
1454
1513
  /\bmkfs\b/,
1455
1514
  // mkfs (format disk)
1456
1515
  /:(){ :\|:& };:/,
@@ -1466,7 +1525,7 @@ var DANGEROUS_PATTERNS = [
1466
1525
  ];
1467
1526
  var shellExecuteTool = {
1468
1527
  name: "shell_execute",
1469
- description: "Execute a shell command and return stdout/stderr. Commands are checked against a safety blocklist. Non-zero exit codes return results (not errors) since tools like grep exit 1 on no matches.",
1528
+ description: "Execute a shell command and return stdout/stderr. A best-effort blocklist rejects a few obviously destructive commands, but this is NOT a security boundary \u2014 only enable this tool for trusted callers and prefer a sandboxed host. Non-zero exit codes return results (not errors) since tools like grep exit 1 on no matches.",
1470
1529
  parameters: z7.object({
1471
1530
  command: z7.string().describe("The shell command to execute"),
1472
1531
  cwd: z7.string().optional().describe(
@@ -1543,13 +1602,14 @@ var codeEditTool = {
1543
1602
  }),
1544
1603
  execute: async (params) => {
1545
1604
  try {
1546
- const content = await fs2.readFile(params.path, "utf8");
1605
+ const safePath = resolveWithinRoot(params.path);
1606
+ const content = await fs2.readFile(safePath, "utf8");
1547
1607
  if (params.oldString === "") {
1548
1608
  const newContent2 = params.newString + content;
1549
- await fs2.writeFile(params.path, newContent2, "utf8");
1609
+ await fs2.writeFile(safePath, newContent2, "utf8");
1550
1610
  return {
1551
1611
  success: true,
1552
- path: params.path,
1612
+ path: safePath,
1553
1613
  replacements: 1,
1554
1614
  message: "Content inserted at beginning of file"
1555
1615
  };
@@ -1573,10 +1633,10 @@ var codeEditTool = {
1573
1633
  );
1574
1634
  }
1575
1635
  const newContent = content.split(params.oldString).join(params.newString);
1576
- await fs2.writeFile(params.path, newContent, "utf8");
1636
+ await fs2.writeFile(safePath, newContent, "utf8");
1577
1637
  return {
1578
1638
  success: true,
1579
- path: params.path,
1639
+ path: safePath,
1580
1640
  replacements: count,
1581
1641
  message: `Replaced ${count} occurrence(s)`
1582
1642
  };
@@ -1771,11 +1831,11 @@ var grepTool = {
1771
1831
  };
1772
1832
 
1773
1833
  // src/tools/built-in/git.tool.ts
1774
- import { execSync as execSync2 } from "child_process";
1834
+ import { execFileSync } from "child_process";
1775
1835
  import { z as z11 } from "zod";
1776
1836
  var GIT_TIMEOUT_MS = 3e4;
1777
1837
  function gitExec(args, cwd) {
1778
- return execSync2(`git ${args}`, {
1838
+ return execFileSync("git", args, {
1779
1839
  cwd: cwd || process.cwd(),
1780
1840
  timeout: GIT_TIMEOUT_MS,
1781
1841
  encoding: "utf8",
@@ -1790,8 +1850,8 @@ var gitStatusTool = {
1790
1850
  }),
1791
1851
  execute: (params) => {
1792
1852
  try {
1793
- const output = gitExec("status --porcelain", params.cwd);
1794
- const branch = gitExec("branch --show-current", params.cwd);
1853
+ const output = gitExec(["status", "--porcelain"], params.cwd);
1854
+ const branch = gitExec(["branch", "--show-current"], params.cwd);
1795
1855
  const staged = [];
1796
1856
  const unstaged = [];
1797
1857
  const untracked = [];
@@ -1833,9 +1893,9 @@ var gitDiffTool = {
1833
1893
  }),
1834
1894
  execute: (params) => {
1835
1895
  try {
1836
- let args = "diff";
1837
- if (params.staged) args += " --cached";
1838
- if (params.path) args += ` -- ${params.path}`;
1896
+ const args = ["diff"];
1897
+ if (params.staged) args.push("--cached");
1898
+ if (params.path) args.push("--", params.path);
1839
1899
  const output = gitExec(args, params.cwd);
1840
1900
  return Promise.resolve({
1841
1901
  diff: output,
@@ -1858,8 +1918,7 @@ var gitAddTool = {
1858
1918
  }),
1859
1919
  execute: (params) => {
1860
1920
  try {
1861
- const escapedPaths = params.paths.map((p) => `"${p}"`).join(" ");
1862
- gitExec(`add ${escapedPaths}`, params.cwd);
1921
+ gitExec(["add", "--", ...params.paths], params.cwd);
1863
1922
  return Promise.resolve({
1864
1923
  success: true,
1865
1924
  added: params.paths
@@ -1881,8 +1940,7 @@ var gitCommitTool = {
1881
1940
  }),
1882
1941
  execute: (params) => {
1883
1942
  try {
1884
- const safeMessage = params.message.replace(/'/g, "'\\''");
1885
- const output = gitExec(`commit -m '${safeMessage}'`, params.cwd);
1943
+ const output = gitExec(["commit", "-m", params.message], params.cwd);
1886
1944
  return Promise.resolve({
1887
1945
  success: true,
1888
1946
  output
@@ -1906,13 +1964,13 @@ var gitLogTool = {
1906
1964
  }),
1907
1965
  execute: (params) => {
1908
1966
  try {
1909
- let args = `log -${params.maxCount}`;
1967
+ const args = ["log", `-${params.maxCount}`];
1910
1968
  if (params.oneline) {
1911
- args += " --oneline";
1969
+ args.push("--oneline");
1912
1970
  } else {
1913
- args += " --format=%H%n%an%n%ae%n%ai%n%s%n---";
1971
+ args.push("--format=%H%n%an%n%ae%n%ai%n%s%n---");
1914
1972
  }
1915
- if (params.path) args += ` -- ${params.path}`;
1973
+ if (params.path) args.push("--", params.path);
1916
1974
  const output = gitExec(args, params.cwd);
1917
1975
  if (params.oneline) {
1918
1976
  const commits = output.split("\n").filter(Boolean).map((line) => {
@@ -1945,8 +2003,8 @@ var gitBranchTool = {
1945
2003
  try {
1946
2004
  switch (params.action) {
1947
2005
  case "list": {
1948
- const output = gitExec("branch -a", params.cwd);
1949
- const current = gitExec("branch --show-current", params.cwd);
2006
+ const output = gitExec(["branch", "-a"], params.cwd);
2007
+ const current = gitExec(["branch", "--show-current"], params.cwd);
1950
2008
  const branches = output.split("\n").filter(Boolean).map((b) => b.replace(/^\*?\s+/, "").trim());
1951
2009
  return Promise.resolve({ branches, current });
1952
2010
  }
@@ -1954,14 +2012,20 @@ var gitBranchTool = {
1954
2012
  if (!params.name) {
1955
2013
  throw new Error("Branch name is required for create action");
1956
2014
  }
1957
- gitExec(`branch ${params.name}`, params.cwd);
2015
+ if (params.name.startsWith("-")) {
2016
+ throw new Error("Invalid branch name");
2017
+ }
2018
+ gitExec(["branch", params.name], params.cwd);
1958
2019
  return Promise.resolve({ success: true, created: params.name });
1959
2020
  }
1960
2021
  case "switch": {
1961
2022
  if (!params.name) {
1962
2023
  throw new Error("Branch name is required for switch action");
1963
2024
  }
1964
- gitExec(`checkout ${params.name}`, params.cwd);
2025
+ if (params.name.startsWith("-")) {
2026
+ throw new Error("Invalid branch name");
2027
+ }
2028
+ gitExec(["checkout", params.name], params.cwd);
1965
2029
  return Promise.resolve({ success: true, switched: params.name });
1966
2030
  }
1967
2031
  }
@@ -2266,17 +2330,28 @@ var AnthropicProvider = class {
2266
2330
  continue;
2267
2331
  }
2268
2332
  if (message.role === "tool") {
2269
- const lastMessage = converted[converted.length - 1];
2270
- if (lastMessage && lastMessage.role === "assistant") {
2271
- const content = lastMessage.content;
2272
- if (Array.isArray(content)) {
2273
- content.push({
2274
- type: "tool_result",
2275
- tool_use_id: message.toolCallId || "",
2276
- content: message.content
2277
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2278
- });
2333
+ const toolUseId = message.toolCallId || "";
2334
+ const resultBlock = this.toolResultBlock(toolUseId, message.content);
2335
+ const prev = converted[converted.length - 1];
2336
+ if (prev && prev.role === "user" && Array.isArray(prev.content) && prev.content.some((b) => b.type === "tool_result")) {
2337
+ const assistant = converted[converted.length - 2];
2338
+ if (assistant && assistant.role === "assistant") {
2339
+ assistant.content = this.ensureToolUse(
2340
+ assistant.content,
2341
+ toolUseId,
2342
+ message.name
2343
+ );
2279
2344
  }
2345
+ prev.content.push(resultBlock);
2346
+ } else if (prev && prev.role === "assistant") {
2347
+ prev.content = this.ensureToolUse(
2348
+ prev.content,
2349
+ toolUseId,
2350
+ message.name
2351
+ );
2352
+ converted.push({ role: "user", content: [resultBlock] });
2353
+ } else {
2354
+ converted.push({ role: "user", content: [resultBlock] });
2280
2355
  }
2281
2356
  continue;
2282
2357
  }
@@ -2287,6 +2362,20 @@ var AnthropicProvider = class {
2287
2362
  }
2288
2363
  return converted;
2289
2364
  }
2365
+ /**
2366
+ * Ensure an assistant message's content is a block array that includes a
2367
+ * `tool_use` block with the given id, backfilling one if missing.
2368
+ */
2369
+ ensureToolUse(content, id, name) {
2370
+ const blocks = typeof content === "string" ? content ? [{ type: "text", text: content }] : [] : [...content];
2371
+ if (!blocks.some((b) => b.type === "tool_use" && b.id === id)) {
2372
+ blocks.push({ type: "tool_use", id, name: name || "tool", input: {} });
2373
+ }
2374
+ return blocks;
2375
+ }
2376
+ toolResultBlock(toolUseId, content) {
2377
+ return { type: "tool_result", tool_use_id: toolUseId, content };
2378
+ }
2290
2379
  };
2291
2380
 
2292
2381
  // src/providers/openai.ts
@@ -3550,7 +3639,7 @@ var Workflow = class {
3550
3639
  if (attempt < maxAttempts && initialDelayMs > 0) {
3551
3640
  const raw = backoff === "linear" ? initialDelayMs * attempt : initialDelayMs * 2 ** (attempt - 1);
3552
3641
  await new Promise(
3553
- (resolve) => setTimeout(resolve, Math.min(raw, maxDelayMs))
3642
+ (resolve2) => setTimeout(resolve2, Math.min(raw, maxDelayMs))
3554
3643
  );
3555
3644
  }
3556
3645
  }
@@ -4142,7 +4231,7 @@ var RateLimiter = class {
4142
4231
  * Sleep helper
4143
4232
  */
4144
4233
  sleep(ms) {
4145
- return new Promise((resolve) => setTimeout(resolve, ms));
4234
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4146
4235
  }
4147
4236
  };
4148
4237
  var SlidingWindowRateLimiter = class {
@@ -4374,7 +4463,7 @@ var StdioTransport = class extends EventEmitter {
4374
4463
  connected = false;
4375
4464
  buffer = "";
4376
4465
  async connect() {
4377
- return new Promise((resolve, reject) => {
4466
+ return new Promise((resolve2, reject) => {
4378
4467
  try {
4379
4468
  this.process = spawn(this.command, this.args, {
4380
4469
  env: { ...process.env, ...this.env },
@@ -4397,7 +4486,7 @@ var StdioTransport = class extends EventEmitter {
4397
4486
  });
4398
4487
  this.connected = true;
4399
4488
  this.emit("connect");
4400
- resolve();
4489
+ resolve2();
4401
4490
  } catch (error) {
4402
4491
  reject(error);
4403
4492
  }
@@ -4445,7 +4534,7 @@ var SSETransport = class extends EventEmitter {
4445
4534
  eventSource = null;
4446
4535
  connected = false;
4447
4536
  async connect() {
4448
- return new Promise((resolve, reject) => {
4537
+ return new Promise((resolve2, reject) => {
4449
4538
  try {
4450
4539
  if (typeof EventSource === "undefined") {
4451
4540
  throw new Error(
@@ -4456,7 +4545,7 @@ var SSETransport = class extends EventEmitter {
4456
4545
  this.eventSource.onopen = () => {
4457
4546
  this.connected = true;
4458
4547
  this.emit("connect");
4459
- resolve();
4548
+ resolve2();
4460
4549
  };
4461
4550
  this.eventSource.onmessage = (event) => {
4462
4551
  try {
@@ -4667,9 +4756,9 @@ var MCPClient = class extends EventEmitter2 {
4667
4756
  if (!this.transport || !this.transport.isConnected()) {
4668
4757
  throw new Error("Not connected to MCP server");
4669
4758
  }
4670
- return new Promise((resolve, reject) => {
4759
+ return new Promise((resolve2, reject) => {
4671
4760
  const id = message.id;
4672
- this.pendingRequests.set(id, { resolve, reject });
4761
+ this.pendingRequests.set(id, { resolve: resolve2, reject });
4673
4762
  try {
4674
4763
  this.transport.send(message);
4675
4764
  } catch (error) {
@@ -6189,12 +6278,12 @@ var OpenAIWhisperProvider = class {
6189
6278
  };
6190
6279
 
6191
6280
  // src/voice/stt/local-whisper.ts
6192
- import { exec } from "child_process";
6281
+ import { execFile } from "child_process";
6193
6282
  import { writeFileSync as writeFileSync2, unlinkSync, existsSync } from "fs";
6194
6283
  import { tmpdir } from "os";
6195
6284
  import { join as join4 } from "path";
6196
6285
  import { promisify } from "util";
6197
- var execAsync = promisify(exec);
6286
+ var execFileAsync = promisify(execFile);
6198
6287
  var LocalWhisperProvider = class {
6199
6288
  whisperPath;
6200
6289
  modelPath;
@@ -6220,16 +6309,23 @@ var LocalWhisperProvider = class {
6220
6309
  throw new Error(`Audio file not found: ${audioPath}`);
6221
6310
  }
6222
6311
  const model = config?.model || "base";
6223
- const language = config?.language ? `--language ${config.language}` : "";
6312
+ const languageArgs = config?.language ? ["--language", config.language] : [];
6224
6313
  const outputFormat = config?.responseFormat || "txt";
6225
- let command;
6314
+ let args;
6226
6315
  if (this.whisperPath.includes("whisper.cpp")) {
6227
6316
  const modelFile = this.modelPath || `ggml-${model}.bin`;
6228
- command = `${this.whisperPath} -m ${modelFile} ${language} -otxt "${audioPath}"`;
6317
+ args = ["-m", modelFile, ...languageArgs, "-otxt", audioPath];
6229
6318
  } else {
6230
- command = `${this.whisperPath} "${audioPath}" --model ${model} ${language} --output_format ${outputFormat}`;
6231
- }
6232
- const { stdout } = await execAsync(command, {
6319
+ args = [
6320
+ audioPath,
6321
+ "--model",
6322
+ model,
6323
+ ...languageArgs,
6324
+ "--output_format",
6325
+ outputFormat
6326
+ ];
6327
+ }
6328
+ const { stdout } = await execFileAsync(this.whisperPath, args, {
6233
6329
  maxBuffer: 10 * 1024 * 1024
6234
6330
  // 10MB buffer
6235
6331
  });
@@ -6271,7 +6367,7 @@ var LocalWhisperProvider = class {
6271
6367
  */
6272
6368
  async isInstalled() {
6273
6369
  try {
6274
- await execAsync(`${this.whisperPath} --help`);
6370
+ await execFileAsync(this.whisperPath, ["--help"]);
6275
6371
  return true;
6276
6372
  } catch {
6277
6373
  return false;
@@ -6574,12 +6670,12 @@ var ElevenLabsTTSProvider = class {
6574
6670
  };
6575
6671
 
6576
6672
  // src/voice/tts/piper-tts.ts
6577
- import { exec as exec2 } from "child_process";
6578
- import { writeFileSync as writeFileSync3, readFileSync, unlinkSync as unlinkSync2, existsSync as existsSync2 } from "fs";
6673
+ import { execFile as execFile2, spawn as spawn2 } from "child_process";
6674
+ import { readFileSync, unlinkSync as unlinkSync2, existsSync as existsSync2 } from "fs";
6579
6675
  import { tmpdir as tmpdir2 } from "os";
6580
6676
  import { join as join5 } from "path";
6581
6677
  import { promisify as promisify2 } from "util";
6582
- var execAsync2 = promisify2(exec2);
6678
+ var execFileAsync2 = promisify2(execFile2);
6583
6679
  var PiperTTSProvider = class {
6584
6680
  piperPath;
6585
6681
  modelPath;
@@ -6593,26 +6689,40 @@ var PiperTTSProvider = class {
6593
6689
  * Synthesize text to speech
6594
6690
  */
6595
6691
  async synthesize(text, config) {
6692
+ const outputPath = join5(tmpdir2(), `speech-${Date.now()}.wav`);
6596
6693
  try {
6597
- const outputPath = join5(tmpdir2(), `speech-${Date.now()}.wav`);
6598
6694
  const model = this.modelPath || config?.model;
6599
6695
  if (!model) {
6600
6696
  throw new Error("Model path is required for Piper TTS");
6601
6697
  }
6602
6698
  const modelConfig = this.configPath || model.replace(".onnx", ".json");
6603
- const textPath = join5(tmpdir2(), `text-${Date.now()}.txt`);
6604
- writeFileSync3(textPath, text, "utf-8");
6605
- const command = `${this.piperPath} --model ${model} --config ${modelConfig} --output_file ${outputPath} < ${textPath}`;
6606
- await execAsync2(command, {
6607
- maxBuffer: 50 * 1024 * 1024
6608
- // 50MB buffer
6699
+ const args = [
6700
+ "--model",
6701
+ model,
6702
+ "--config",
6703
+ modelConfig,
6704
+ "--output_file",
6705
+ outputPath
6706
+ ];
6707
+ await new Promise((resolve2, reject) => {
6708
+ const child = spawn2(this.piperPath, args, {
6709
+ stdio: ["pipe", "ignore", "pipe"]
6710
+ });
6711
+ let stderr = "";
6712
+ child.stderr?.on("data", (chunk) => {
6713
+ stderr += String(chunk);
6714
+ });
6715
+ child.on("error", reject);
6716
+ child.on("close", (code) => {
6717
+ if (code === 0) resolve2();
6718
+ else reject(new Error(`piper exited with code ${code}: ${stderr}`));
6719
+ });
6720
+ child.stdin?.end(text);
6609
6721
  });
6610
6722
  if (!existsSync2(outputPath)) {
6611
6723
  throw new Error("Piper failed to generate audio file");
6612
6724
  }
6613
6725
  const audio = readFileSync(outputPath);
6614
- unlinkSync2(outputPath);
6615
- unlinkSync2(textPath);
6616
6726
  return {
6617
6727
  audio,
6618
6728
  format: "wav",
@@ -6622,6 +6732,10 @@ var PiperTTSProvider = class {
6622
6732
  throw new Error(
6623
6733
  `Piper TTS synthesis failed: ${error instanceof Error ? error.message : String(error)}`
6624
6734
  );
6735
+ } finally {
6736
+ if (existsSync2(outputPath)) {
6737
+ unlinkSync2(outputPath);
6738
+ }
6625
6739
  }
6626
6740
  }
6627
6741
  /**
@@ -6635,7 +6749,7 @@ var PiperTTSProvider = class {
6635
6749
  */
6636
6750
  async isInstalled() {
6637
6751
  try {
6638
- await execAsync2(`${this.piperPath} --version`);
6752
+ await execFileAsync2(this.piperPath, ["--version"]);
6639
6753
  return true;
6640
6754
  } catch {
6641
6755
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lov3kaizen/agentsea-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AgentSea - Unite and orchestrate AI agents. A production-ready ADK for building agentic AI applications with multi-provider support.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "winston": "^3.19.0",
26
26
  "ioredis": "^5.11.1",
27
27
  "marked": "^12.0.2",
28
- "@lov3kaizen/agentsea-types": "1.1.0"
28
+ "@lov3kaizen/agentsea-types": "1.2.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.19.0",