tarsk 0.5.34 → 0.5.36

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 (82) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +1283 -790
  3. package/dist/public/assets/account-view-BB-ze5ek.js +1 -0
  4. package/dist/public/assets/api-DzFQRz0u.js +1 -0
  5. package/dist/public/assets/browser-tab-B3bYbBws.js +1 -0
  6. package/dist/public/assets/commit-dialog-CJRMd4oA.js +1 -0
  7. package/dist/public/assets/context-menu-86TNA1D0.js +1 -0
  8. package/dist/public/assets/create-repo-dialog-nLnVNRqq.js +1 -0
  9. package/dist/public/assets/{dialogs-config-Ct6i8Iij.js → dialogs-config-I0_cSU2z.js} +14 -14
  10. package/dist/public/assets/diff-view-B8vWYa_2.js +3 -0
  11. package/dist/public/assets/explorer-tab-view-BGWQPnkw.js +2 -0
  12. package/dist/public/assets/explorer-tree-DrLoiF9t.js +1 -0
  13. package/dist/public/assets/explorer-view-Clm-RCAB.js +1 -0
  14. package/dist/public/assets/git-history-dialog-CcdMtW0o.js +1 -0
  15. package/dist/public/assets/git-ops-button-D9m2nmhO.js +2 -0
  16. package/dist/public/assets/history-view-Be-1C1tJ.js +7 -0
  17. package/dist/public/assets/index-DVXbmgHY.js +65 -0
  18. package/dist/public/assets/index-jIBJk8xl.css +1 -0
  19. package/dist/public/assets/mcp-server-card-bNNoXKsW.js +1 -0
  20. package/dist/public/assets/merged-pr-dialog-Fj8II9Bt.js +1 -0
  21. package/dist/public/assets/onboarding-CgP4bJ5F.js +1 -0
  22. package/dist/public/assets/project-settings-view-DixzP7KH.js +1 -0
  23. package/dist/public/assets/providers-list-view-BzSUZZyq.js +1 -0
  24. package/dist/public/assets/pull-request-dialog-ChuHHY3J.js +1 -0
  25. package/dist/public/assets/{pull-with-changes-dialog-DN0kE6os.js → pull-with-changes-dialog-Ck6aK6Q7.js} +1 -1
  26. package/dist/public/assets/push-before-pr-dialog-CbtHXjDH.js +1 -0
  27. package/dist/public/assets/radio-group-ZjNXbvW6.js +1 -0
  28. package/dist/public/assets/react-vendor-Do-APsQ6.js +16 -0
  29. package/dist/public/assets/settings-general-view-FwrdJbys.js +1 -0
  30. package/dist/public/assets/{settings-instructions-view-DULFg6dU.js → settings-instructions-view-BHzMtSvN.js} +1 -1
  31. package/dist/public/assets/settings-list-BCoqXa_X.js +1 -0
  32. package/dist/public/assets/settings-mcp-servers-view-DBD-NhPx.js +5 -0
  33. package/dist/public/assets/{settings-models-skeleton-CN2JkqZh.js → settings-models-skeleton-DsWpzWOw.js} +1 -1
  34. package/dist/public/assets/settings-models-view-vgV5YHRx.js +1 -0
  35. package/dist/public/assets/settings-rules-view-DcSXrmaq.js +8 -0
  36. package/dist/public/assets/settings-skills-view-DVGGs16n.js +2 -0
  37. package/dist/public/assets/settings-slash-commands-view-BsJw1F4p.js +1 -0
  38. package/dist/public/assets/settings-subagents-view-BgWhQ9bf.js +2 -0
  39. package/dist/public/assets/{settings-system-prompt-view-BNM_15ry.js → settings-system-prompt-view-DAAbMucD.js} +1 -1
  40. package/dist/public/assets/settings-view-DLzrbu69.js +2 -0
  41. package/dist/public/assets/skeleton-iEZ-dj1s.js +1 -0
  42. package/dist/public/assets/terminal-panel-C5qj1ctH.js +2 -0
  43. package/dist/public/assets/{ui-components-Di-9yNr4.js → ui-components-BN8dp4Bi.js} +1 -1
  44. package/dist/public/assets/{utils-DDDR42pW.js → utils-CU0c_yCM.js} +1 -1
  45. package/dist/public/assets/{whisper-wasm-Cq5Ofwpd.js → whisper-wasm-EGutPGND.js} +2 -2
  46. package/dist/public/index.html +12 -9
  47. package/package.json +1 -1
  48. package/dist/public/assets/account-view-Ds03urJE.js +0 -1
  49. package/dist/public/assets/api-BHfaVqlR.js +0 -1
  50. package/dist/public/assets/browser-tab-Qs8cnQkf.js +0 -1
  51. package/dist/public/assets/commit-dialog-D4suPxEe.js +0 -1
  52. package/dist/public/assets/context-menu-S8y0c1Fx.js +0 -1
  53. package/dist/public/assets/create-repo-dialog-DPKIg1mY.js +0 -1
  54. package/dist/public/assets/diff-view-rKDEnBLg.js +0 -3
  55. package/dist/public/assets/explorer-tab-view-JbLbPVAn.js +0 -2
  56. package/dist/public/assets/explorer-tree-Dq44yA1j.js +0 -1
  57. package/dist/public/assets/explorer-view-BMrlX2hb.js +0 -1
  58. package/dist/public/assets/git-history-dialog-CDwIqSZQ.js +0 -1
  59. package/dist/public/assets/git-ops-button-B6rEla5u.js +0 -2
  60. package/dist/public/assets/history-view-CFot099O.js +0 -7
  61. package/dist/public/assets/index-6DhhjIBL.js +0 -65
  62. package/dist/public/assets/index-sEZ3wJXu.css +0 -1
  63. package/dist/public/assets/mcp-server-card-DQY1RUPX.js +0 -1
  64. package/dist/public/assets/merged-pr-dialog-H_fuSmiF.js +0 -1
  65. package/dist/public/assets/onboarding-DHCR18iU.js +0 -1
  66. package/dist/public/assets/project-settings-view-CiefnK73.js +0 -1
  67. package/dist/public/assets/providers-list-view-DVopskGo.js +0 -1
  68. package/dist/public/assets/pull-request-dialog-Csql6eob.js +0 -1
  69. package/dist/public/assets/push-before-pr-dialog-IhdlLvWt.js +0 -1
  70. package/dist/public/assets/radio-group-BaKkAEZR.js +0 -1
  71. package/dist/public/assets/react-vendor-CjZZeCRj.js +0 -16
  72. package/dist/public/assets/settings-general-view-BMTxN35t.js +0 -1
  73. package/dist/public/assets/settings-list-Bdeuh4bJ.js +0 -1
  74. package/dist/public/assets/settings-mcp-servers-view-CRCtWjP2.js +0 -5
  75. package/dist/public/assets/settings-models-view-DosllhVB.js +0 -1
  76. package/dist/public/assets/settings-rules-view-BVWuGrnB.js +0 -8
  77. package/dist/public/assets/settings-skills-view-DYtcAu9-.js +0 -2
  78. package/dist/public/assets/settings-slash-commands-view-DFyL1W2n.js +0 -1
  79. package/dist/public/assets/settings-subagents-view-aCbK95Sk.js +0 -2
  80. package/dist/public/assets/settings-view-D5GE_CYR.js +0 -2
  81. package/dist/public/assets/skeleton-pSKdmWyJ.js +0 -1
  82. package/dist/public/assets/terminal-panel-Ds1tCgQA.js +0 -2
package/dist/index.js CHANGED
@@ -191,18 +191,18 @@ __export(database_exports, {
191
191
  initializeSchema: () => initializeSchema
192
192
  });
193
193
  import { createClient } from "@libsql/client";
194
- import { join as join3 } from "path";
195
- import { homedir as homedir3 } from "os";
194
+ import { join as join2 } from "path";
195
+ import { homedir as homedir2 } from "os";
196
196
  import { mkdirSync } from "fs";
197
197
  function getDatabasePath() {
198
- const appSupportDir = join3(homedir3(), "Library", "Application Support", "Tarsk");
199
- const dataDir = join3(appSupportDir, "data");
200
- return join3(dataDir, "tarsk.db");
198
+ const appSupportDir = join2(homedir2(), "Library", "Application Support", "Tarsk");
199
+ const dataDir = join2(appSupportDir, "data");
200
+ return join2(dataDir, "tarsk.db");
201
201
  }
202
202
  async function initializeDatabase() {
203
203
  const dbPath = getDatabasePath();
204
204
  try {
205
- const dataDir = join3(homedir3(), "Library", "Application Support", "Tarsk", "data");
205
+ const dataDir = join2(homedir2(), "Library", "Application Support", "Tarsk", "data");
206
206
  mkdirSync(dataDir, { recursive: true });
207
207
  const db = createClient({
208
208
  url: `file:${dbPath}`
@@ -900,6 +900,27 @@ async function runMigrations(db) {
900
900
  CREATE INDEX idx_project_scripts_projectId ON project_scripts(projectId)
901
901
  `);
902
902
  }
903
+ const projectEnvVarsExists = await db.execute(
904
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='project_env_vars'`
905
+ );
906
+ if (projectEnvVarsExists.rows.length === 0) {
907
+ tarskDebugLog("[db] Running migration: Creating project_env_vars table");
908
+ await db.execute(`
909
+ CREATE TABLE project_env_vars (
910
+ id TEXT PRIMARY KEY,
911
+ projectId TEXT NOT NULL,
912
+ name TEXT NOT NULL,
913
+ encryptedValue TEXT NOT NULL,
914
+ createdAt TEXT NOT NULL,
915
+ updatedAt TEXT NOT NULL,
916
+ FOREIGN KEY (projectId) REFERENCES projects(id) ON DELETE CASCADE,
917
+ UNIQUE(projectId, name)
918
+ )
919
+ `);
920
+ await db.execute(`
921
+ CREATE INDEX idx_project_env_vars_projectId ON project_env_vars(projectId)
922
+ `);
923
+ }
903
924
  } catch (error) {
904
925
  console.error("Failed to run migrations:", error);
905
926
  throw error;
@@ -927,7 +948,7 @@ var init_database = __esm({
927
948
  });
928
949
 
929
950
  // src/server.ts
930
- import { serve } from "@hono/node-server";
951
+ import { createAdaptorServer } from "@hono/node-server";
931
952
  import { serveStatic } from "@hono/node-server/serve-static";
932
953
 
933
954
  // ../shared/dist/programs.js
@@ -1554,6 +1575,59 @@ function isSameFetchHost(a, b) {
1554
1575
  return normalizeFetchHostname(a) === normalizeFetchHostname(b);
1555
1576
  }
1556
1577
 
1578
+ // ../shared/dist/explorer-media.js
1579
+ var EXPLORER_IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
1580
+ "png",
1581
+ "jpg",
1582
+ "jpeg",
1583
+ "gif",
1584
+ "webp",
1585
+ "svg",
1586
+ "ico",
1587
+ "bmp",
1588
+ "tiff",
1589
+ "tif"
1590
+ ]);
1591
+ var EXPLORER_VIDEO_EXTENSIONS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "avi"]);
1592
+ var EXPLORER_MEDIA_MIME_TYPES = {
1593
+ png: "image/png",
1594
+ jpg: "image/jpeg",
1595
+ jpeg: "image/jpeg",
1596
+ gif: "image/gif",
1597
+ webp: "image/webp",
1598
+ svg: "image/svg+xml",
1599
+ ico: "image/x-icon",
1600
+ bmp: "image/bmp",
1601
+ tiff: "image/tiff",
1602
+ tif: "image/tiff",
1603
+ mp4: "video/mp4",
1604
+ webm: "video/webm",
1605
+ mov: "video/quicktime",
1606
+ avi: "video/x-msvideo"
1607
+ };
1608
+ function getFileExtension(filename) {
1609
+ const trimmed = filename.trim();
1610
+ const basename5 = trimmed.split(/[/\\]/).pop() ?? trimmed;
1611
+ const dotIndex = basename5.lastIndexOf(".");
1612
+ if (dotIndex <= 0 || dotIndex === basename5.length - 1) {
1613
+ return "";
1614
+ }
1615
+ return basename5.slice(dotIndex + 1).toLowerCase();
1616
+ }
1617
+ function getExplorerFileMediaType(path7, name) {
1618
+ const ext = getFileExtension(path7) || (name ? getFileExtension(name) : "");
1619
+ if (!ext) {
1620
+ return null;
1621
+ }
1622
+ if (EXPLORER_IMAGE_EXTENSIONS.has(ext)) {
1623
+ return "image";
1624
+ }
1625
+ if (EXPLORER_VIDEO_EXTENSIONS.has(ext)) {
1626
+ return "video";
1627
+ }
1628
+ return null;
1629
+ }
1630
+
1557
1631
  // src/server.ts
1558
1632
  import fs3 from "fs";
1559
1633
  import { Hono as Hono26 } from "hono";
@@ -1580,10 +1654,10 @@ function createMcpThinkingEvent(content) {
1580
1654
  // src/tools/bash.ts
1581
1655
  init_tarsk_debug();
1582
1656
  init_utils();
1583
- import { randomBytes } from "node:crypto";
1657
+ import { randomBytes as randomBytes2 } from "node:crypto";
1584
1658
  import { createWriteStream, existsSync as existsSync3 } from "node:fs";
1585
1659
  import { tmpdir } from "node:os";
1586
- import { join as join2 } from "node:path";
1660
+ import { join as join3 } from "node:path";
1587
1661
  import { Type } from "@sinclair/typebox";
1588
1662
 
1589
1663
  // src/tools/shell.ts
@@ -1718,172 +1792,649 @@ function killProcessTree(pid) {
1718
1792
  }
1719
1793
  }
1720
1794
 
1721
- // src/tools/truncate.ts
1722
- var DEFAULT_MAX_LINES = 2e3;
1723
- var DEFAULT_MAX_BYTES = 50 * 1024;
1724
- var GREP_MAX_LINE_LENGTH = 500;
1725
- function formatSize(bytes) {
1726
- if (bytes < 1024) {
1727
- return `${bytes}B`;
1728
- } else if (bytes < 1024 * 1024) {
1729
- return `${(bytes / 1024).toFixed(1)}KB`;
1730
- } else {
1731
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1732
- }
1733
- }
1734
- function truncateHead(content, options = {}) {
1735
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
1736
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
1737
- const totalBytes = Buffer.byteLength(content, "utf-8");
1738
- const lines = content.split("\n");
1739
- const totalLines = lines.length;
1740
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
1741
- return {
1742
- content,
1743
- truncated: false,
1744
- truncatedBy: null,
1745
- totalLines,
1746
- totalBytes,
1747
- outputLines: totalLines,
1748
- outputBytes: totalBytes,
1749
- lastLinePartial: false,
1750
- firstLineExceedsLimit: false,
1751
- maxLines,
1752
- maxBytes
1753
- };
1754
- }
1755
- const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
1756
- if (firstLineBytes > maxBytes) {
1757
- return {
1758
- content: "",
1759
- truncated: true,
1760
- truncatedBy: "bytes",
1761
- totalLines,
1762
- totalBytes,
1763
- outputLines: 0,
1764
- outputBytes: 0,
1765
- lastLinePartial: false,
1766
- firstLineExceedsLimit: true,
1767
- maxLines,
1768
- maxBytes
1769
- };
1770
- }
1771
- const outputLinesArr = [];
1772
- let outputBytesCount = 0;
1773
- let truncatedBy = "lines";
1774
- for (let i = 0; i < lines.length && i < maxLines; i++) {
1775
- const line = lines[i];
1776
- const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
1777
- if (outputBytesCount + lineBytes > maxBytes) {
1778
- truncatedBy = "bytes";
1779
- break;
1795
+ // src/features/projects/project-env-vars.ts
1796
+ init_database();
1797
+
1798
+ // src/core/crypto.ts
1799
+ init_database();
1800
+ import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto";
1801
+ import { networkInterfaces, hostname } from "os";
1802
+ var ALGORITHM = "aes-256-gcm";
1803
+ var IV_LENGTH = 16;
1804
+ function getMachineKey() {
1805
+ try {
1806
+ const interfaces = networkInterfaces();
1807
+ let macAddress = "";
1808
+ for (const name of Object.keys(interfaces)) {
1809
+ const iface = interfaces[name];
1810
+ if (iface) {
1811
+ for (const addr of iface) {
1812
+ if (addr.mac && addr.mac !== "00:00:00:00:00:00" && !addr.internal) {
1813
+ macAddress = addr.mac;
1814
+ break;
1815
+ }
1816
+ }
1817
+ if (macAddress) break;
1818
+ }
1780
1819
  }
1781
- outputLinesArr.push(line);
1782
- outputBytesCount += lineBytes;
1783
- }
1784
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
1785
- truncatedBy = "lines";
1820
+ const host = hostname();
1821
+ const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
1822
+ const machineId = `${macAddress}-${host}-${homeDir}`;
1823
+ return createHash("sha256").update(machineId).digest();
1824
+ } catch {
1825
+ return Buffer.from("Invalid", "utf8");
1786
1826
  }
1787
- const outputContent = outputLinesArr.join("\n");
1788
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
1789
- return {
1790
- content: outputContent,
1791
- truncated: true,
1792
- truncatedBy,
1793
- totalLines,
1794
- totalBytes,
1795
- outputLines: outputLinesArr.length,
1796
- outputBytes: finalOutputBytes,
1797
- lastLinePartial: false,
1798
- firstLineExceedsLimit: false,
1799
- maxLines,
1800
- maxBytes
1801
- };
1802
1827
  }
1803
- function truncateTail(content, options = {}) {
1804
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
1805
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
1806
- const totalBytes = Buffer.byteLength(content, "utf-8");
1807
- const lines = content.split("\n");
1808
- const totalLines = lines.length;
1809
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
1810
- return {
1811
- content,
1812
- truncated: false,
1813
- truncatedBy: null,
1814
- totalLines,
1815
- totalBytes,
1816
- outputLines: totalLines,
1817
- outputBytes: totalBytes,
1818
- lastLinePartial: false,
1819
- firstLineExceedsLimit: false,
1820
- maxLines,
1821
- maxBytes
1822
- };
1823
- }
1824
- const outputLinesArr = [];
1825
- let outputBytesCount = 0;
1826
- let truncatedBy = "lines";
1827
- let lastLinePartial = false;
1828
- for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
1829
- const line = lines[i];
1830
- const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
1831
- if (outputBytesCount + lineBytes > maxBytes) {
1832
- truncatedBy = "bytes";
1833
- if (outputLinesArr.length === 0) {
1834
- const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
1835
- outputLinesArr.unshift(truncatedLine);
1836
- outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
1837
- lastLinePartial = true;
1828
+ async function getEncryptionKey() {
1829
+ try {
1830
+ const db = await getDatabase();
1831
+ const result = await db.execute({
1832
+ sql: "SELECT value FROM state WHERE key = ?",
1833
+ args: ["encryption_key"]
1834
+ });
1835
+ if (result.rows.length > 0 && result.rows[0].value) {
1836
+ const storedValue = result.rows[0].value;
1837
+ try {
1838
+ const parsed = JSON.parse(storedValue);
1839
+ return Buffer.from(parsed, "base64");
1840
+ } catch {
1841
+ return Buffer.from(storedValue, "base64");
1838
1842
  }
1839
- break;
1840
1843
  }
1841
- outputLinesArr.unshift(line);
1842
- outputBytesCount += lineBytes;
1843
- }
1844
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
1845
- truncatedBy = "lines";
1844
+ const machineKey = getMachineKey();
1845
+ await db.execute({
1846
+ sql: "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
1847
+ args: ["encryption_key", machineKey.toString("base64")]
1848
+ });
1849
+ return machineKey;
1850
+ } catch {
1851
+ return getMachineKey();
1846
1852
  }
1847
- const outputContent = outputLinesArr.join("\n");
1848
- const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
1849
- return {
1850
- content: outputContent,
1851
- truncated: true,
1852
- truncatedBy,
1853
- totalLines,
1854
- totalBytes,
1855
- outputLines: outputLinesArr.length,
1856
- outputBytes: finalOutputBytes,
1857
- lastLinePartial,
1858
- firstLineExceedsLimit: false,
1859
- maxLines,
1860
- maxBytes
1861
- };
1862
1853
  }
1863
- function truncateStringToBytesFromEnd(str, maxBytes) {
1864
- const buf = Buffer.from(str, "utf-8");
1865
- if (buf.length <= maxBytes) {
1866
- return str;
1867
- }
1868
- let start = buf.length - maxBytes;
1869
- while (start < buf.length && (buf[start] & 192) === 128) {
1870
- start++;
1854
+ async function encrypt(plaintext) {
1855
+ if (!plaintext) {
1856
+ return "";
1871
1857
  }
1872
- return buf.slice(start).toString("utf-8");
1858
+ const key = await getEncryptionKey();
1859
+ const iv = randomBytes(IV_LENGTH);
1860
+ const cipher = createCipheriv(ALGORITHM, key, iv);
1861
+ let encrypted = cipher.update(plaintext, "utf8", "base64");
1862
+ encrypted += cipher.final("base64");
1863
+ const authTag = cipher.getAuthTag();
1864
+ const result = `${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`;
1865
+ return result;
1873
1866
  }
1874
- function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
1875
- if (line.length <= maxChars) {
1876
- return { text: line, wasTruncated: false };
1867
+ async function decrypt(encryptedData) {
1868
+ if (!encryptedData) {
1869
+ return "";
1877
1870
  }
1878
- return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
1879
- }
1880
-
1881
- // src/tools/bash.ts
1882
- function getTempFilePath() {
1883
- return join2(tmpdir(), `tarsk-bash-${randomBytes(8).toString("hex")}.log`);
1884
- }
1885
- var bashSchema = Type.Object({
1886
- command: Type.String({ description: "Bash command to execute" }),
1871
+ try {
1872
+ const parts = encryptedData.split(":");
1873
+ if (parts.length !== 3) {
1874
+ throw new Error("Invalid encrypted data format");
1875
+ }
1876
+ const [ivBase64, authTagBase64, encrypted] = parts;
1877
+ const key = await getEncryptionKey();
1878
+ const iv = Buffer.from(ivBase64, "base64");
1879
+ const authTag = Buffer.from(authTagBase64, "base64");
1880
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
1881
+ decipher.setAuthTag(authTag);
1882
+ let decrypted = decipher.update(encrypted, "base64", "utf8");
1883
+ decrypted += decipher.final("utf8");
1884
+ return decrypted;
1885
+ } catch (error) {
1886
+ throw new Error(
1887
+ `Decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`
1888
+ );
1889
+ }
1890
+ }
1891
+ function isEncrypted(value) {
1892
+ if (!value) {
1893
+ return false;
1894
+ }
1895
+ const parts = value.split(":");
1896
+ if (parts.length !== 3) {
1897
+ return false;
1898
+ }
1899
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
1900
+ return parts.every((part) => base64Regex.test(part));
1901
+ }
1902
+
1903
+ // src/database/database.encryption.ts
1904
+ async function encryptProviderKey(key) {
1905
+ if (!key) {
1906
+ return "";
1907
+ }
1908
+ return await encrypt(key);
1909
+ }
1910
+ async function decryptProviderKey(encrypted) {
1911
+ if (!encrypted) {
1912
+ return "";
1913
+ }
1914
+ try {
1915
+ return await decrypt(encrypted);
1916
+ } catch (error) {
1917
+ console.error("Failed to decrypt provider key:", error);
1918
+ throw error;
1919
+ }
1920
+ }
1921
+ function isEncryptedKey(value) {
1922
+ return isEncrypted(value);
1923
+ }
1924
+
1925
+ // src/features/projects/project-env-vars.database.ts
1926
+ import { randomUUID } from "node:crypto";
1927
+ async function listProjectEnvVarNames(db, projectId) {
1928
+ const result = await db.execute(
1929
+ `SELECT name FROM project_env_vars WHERE projectId = ? ORDER BY name ASC`,
1930
+ [projectId]
1931
+ );
1932
+ return result.rows;
1933
+ }
1934
+ async function listProjectEnvVars(db, projectId) {
1935
+ const result = await db.execute(
1936
+ `SELECT id, projectId, name, encryptedValue, createdAt, updatedAt
1937
+ FROM project_env_vars WHERE projectId = ? ORDER BY name ASC`,
1938
+ [projectId]
1939
+ );
1940
+ return result.rows;
1941
+ }
1942
+ async function upsertProjectEnvVar(db, projectId, name, value) {
1943
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1944
+ const encryptedValue = await encryptProviderKey(value);
1945
+ const existing = await db.execute(
1946
+ `SELECT id FROM project_env_vars WHERE projectId = ? AND name = ?`,
1947
+ [projectId, name]
1948
+ );
1949
+ if (existing.rows.length > 0) {
1950
+ await db.execute(
1951
+ `UPDATE project_env_vars SET encryptedValue = ?, updatedAt = ? WHERE projectId = ? AND name = ?`,
1952
+ [encryptedValue, now, projectId, name]
1953
+ );
1954
+ return;
1955
+ }
1956
+ await db.execute(
1957
+ `INSERT INTO project_env_vars (id, projectId, name, encryptedValue, createdAt, updatedAt)
1958
+ VALUES (?, ?, ?, ?, ?, ?)`,
1959
+ [randomUUID(), projectId, name, encryptedValue, now, now]
1960
+ );
1961
+ }
1962
+ async function deleteProjectEnvVarsNotIn(db, projectId, names) {
1963
+ if (names.length === 0) {
1964
+ await db.execute(`DELETE FROM project_env_vars WHERE projectId = ?`, [projectId]);
1965
+ return;
1966
+ }
1967
+ const placeholders = names.map(() => "?").join(", ");
1968
+ await db.execute(
1969
+ `DELETE FROM project_env_vars WHERE projectId = ? AND name NOT IN (${placeholders})`,
1970
+ [projectId, ...names]
1971
+ );
1972
+ }
1973
+
1974
+ // src/features/projects/project-env-vars.ts
1975
+ function isValidProjectEnvVarName(name) {
1976
+ return /^[A-Z_][A-Z0-9_]*$/.test(name);
1977
+ }
1978
+ async function getDecryptedProjectEnvVars(projectId) {
1979
+ const db = await getDatabase();
1980
+ const rows = await listProjectEnvVars(db, projectId);
1981
+ const env = {};
1982
+ for (const row of rows) {
1983
+ env[row.name] = await decryptProviderKey(row.encryptedValue);
1984
+ }
1985
+ return env;
1986
+ }
1987
+ function mergeEnvVars(baseEnv, projectEnv) {
1988
+ if (!projectEnv || Object.keys(projectEnv).length === 0) {
1989
+ return baseEnv;
1990
+ }
1991
+ return { ...baseEnv, ...projectEnv };
1992
+ }
1993
+
1994
+ // src/features/shell-permissions/command-pattern.ts
1995
+ var ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=/;
1996
+ function stripLeadingEnvAssignments(command) {
1997
+ let remaining = command.trim();
1998
+ while (remaining.length > 0) {
1999
+ const tokenMatch = remaining.match(/^(\S+)\s*/);
2000
+ if (!tokenMatch) {
2001
+ break;
2002
+ }
2003
+ const token = tokenMatch[1];
2004
+ if (!ENV_ASSIGNMENT_PATTERN.test(token)) {
2005
+ break;
2006
+ }
2007
+ remaining = remaining.slice(tokenMatch[0].length).trimStart();
2008
+ }
2009
+ return remaining;
2010
+ }
2011
+ function tokenizeCommand(command) {
2012
+ const tokens = [];
2013
+ let current = "";
2014
+ let quote = null;
2015
+ let escaped = false;
2016
+ for (const char of command) {
2017
+ if (escaped) {
2018
+ current += char;
2019
+ escaped = false;
2020
+ continue;
2021
+ }
2022
+ if (char === "\\") {
2023
+ escaped = true;
2024
+ continue;
2025
+ }
2026
+ if (quote) {
2027
+ if (char === quote) {
2028
+ quote = null;
2029
+ } else {
2030
+ current += char;
2031
+ }
2032
+ continue;
2033
+ }
2034
+ if (char === "'" || char === '"') {
2035
+ quote = char;
2036
+ continue;
2037
+ }
2038
+ if (/\s/.test(char)) {
2039
+ if (current.length > 0) {
2040
+ tokens.push(current);
2041
+ current = "";
2042
+ }
2043
+ continue;
2044
+ }
2045
+ current += char;
2046
+ }
2047
+ if (current.length > 0) {
2048
+ tokens.push(current);
2049
+ }
2050
+ return tokens;
2051
+ }
2052
+ function getCommandTokens(command) {
2053
+ return tokenizeCommand(stripLeadingEnvAssignments(command));
2054
+ }
2055
+ function buildWildcardPattern(tokens, wildcardTokenCount) {
2056
+ const prefix = tokens.slice(0, wildcardTokenCount);
2057
+ if (prefix.length === 0) {
2058
+ return commandToPatternString(tokens);
2059
+ }
2060
+ return `${prefix.join(" ")} *`;
2061
+ }
2062
+ function commandToPatternString(tokens) {
2063
+ return tokens.join(" ");
2064
+ }
2065
+ function buildWildcardOptions(command) {
2066
+ const tokens = getCommandTokens(command);
2067
+ const options = [];
2068
+ if (tokens.length >= 1) {
2069
+ const oneTokenPattern = buildWildcardPattern(tokens, 1);
2070
+ options.push(
2071
+ {
2072
+ label: `Allow ${oneTokenPattern} (this session)`,
2073
+ pattern: oneTokenPattern,
2074
+ scope: "session"
2075
+ },
2076
+ {
2077
+ label: `Allow ${oneTokenPattern} (always for this project)`,
2078
+ pattern: oneTokenPattern,
2079
+ scope: "project"
2080
+ }
2081
+ );
2082
+ }
2083
+ if (tokens.length >= 2) {
2084
+ const twoTokenPattern = buildWildcardPattern(tokens, 2);
2085
+ options.push(
2086
+ {
2087
+ label: `Allow ${twoTokenPattern} (this session)`,
2088
+ pattern: twoTokenPattern,
2089
+ scope: "session"
2090
+ },
2091
+ {
2092
+ label: `Allow ${twoTokenPattern} (always for this project)`,
2093
+ pattern: twoTokenPattern,
2094
+ scope: "project"
2095
+ }
2096
+ );
2097
+ }
2098
+ return options;
2099
+ }
2100
+ function parsePattern(pattern) {
2101
+ const trimmed = pattern.trim();
2102
+ if (trimmed.endsWith(" *")) {
2103
+ return {
2104
+ tokens: tokenizeCommand(trimmed.slice(0, -2).trim()),
2105
+ hasWildcard: true
2106
+ };
2107
+ }
2108
+ return {
2109
+ tokens: tokenizeCommand(trimmed),
2110
+ hasWildcard: false
2111
+ };
2112
+ }
2113
+ function commandMatchesPattern(command, pattern) {
2114
+ const commandTokens = getCommandTokens(command);
2115
+ const parsedPattern = parsePattern(pattern);
2116
+ if (!parsedPattern.hasWildcard) {
2117
+ return command.trim() === pattern.trim();
2118
+ }
2119
+ if (parsedPattern.tokens.length === 0) {
2120
+ return false;
2121
+ }
2122
+ if (commandTokens.length < parsedPattern.tokens.length) {
2123
+ return false;
2124
+ }
2125
+ for (let index = 0; index < parsedPattern.tokens.length; index++) {
2126
+ if (commandTokens[index] !== parsedPattern.tokens[index]) {
2127
+ return false;
2128
+ }
2129
+ }
2130
+ return true;
2131
+ }
2132
+ function commandMatchesAnyPattern(command, patterns) {
2133
+ return patterns.some((pattern) => commandMatchesPattern(command, pattern));
2134
+ }
2135
+
2136
+ // src/tools/path-utils.ts
2137
+ import { accessSync, constants } from "node:fs";
2138
+ import * as os from "node:os";
2139
+ import { isAbsolute, resolve as resolvePath, relative } from "node:path";
2140
+ var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
2141
+ var NARROW_NO_BREAK_SPACE = "\u202F";
2142
+ function normalizeUnicodeSpaces(str) {
2143
+ return str.replace(UNICODE_SPACES, " ");
2144
+ }
2145
+ function tryMacOSScreenshotPath(filePath) {
2146
+ return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
2147
+ }
2148
+ function tryNFDVariant(filePath) {
2149
+ return filePath.normalize("NFD");
2150
+ }
2151
+ function tryCurlyQuoteVariant(filePath) {
2152
+ return filePath.replace(/'/g, "\u2019");
2153
+ }
2154
+ function fileExists(filePath) {
2155
+ try {
2156
+ accessSync(filePath, constants.F_OK);
2157
+ return true;
2158
+ } catch {
2159
+ return false;
2160
+ }
2161
+ }
2162
+ function normalizeAtPrefix(filePath) {
2163
+ return filePath.startsWith("@") ? filePath.slice(1) : filePath;
2164
+ }
2165
+ function expandPath(filePath) {
2166
+ const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
2167
+ if (normalized === "~") {
2168
+ return os.homedir();
2169
+ }
2170
+ if (normalized.startsWith("~/")) {
2171
+ return os.homedir() + normalized.slice(1);
2172
+ }
2173
+ return normalized;
2174
+ }
2175
+ function resolveToCwd(filePath, cwd) {
2176
+ const expanded = expandPath(filePath);
2177
+ if (isAbsolute(expanded)) {
2178
+ return expanded;
2179
+ }
2180
+ return resolvePath(cwd, expanded);
2181
+ }
2182
+ function validatePathWithinCwd(resolvedPath2, cwd) {
2183
+ const relativePath = relative(cwd, resolvedPath2);
2184
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2185
+ throw new Error(
2186
+ `Access denied: Path is outside the allowed directory. Requested: ${resolvedPath2}, Allowed root: ${cwd}`
2187
+ );
2188
+ }
2189
+ }
2190
+ function resolveReadPath(filePath, cwd) {
2191
+ const resolved = resolveToCwd(filePath, cwd);
2192
+ if (fileExists(resolved)) {
2193
+ return resolved;
2194
+ }
2195
+ const amPmVariant = tryMacOSScreenshotPath(resolved);
2196
+ if (amPmVariant !== resolved && fileExists(amPmVariant)) {
2197
+ return amPmVariant;
2198
+ }
2199
+ const nfdVariant = tryNFDVariant(resolved);
2200
+ if (nfdVariant !== resolved && fileExists(nfdVariant)) {
2201
+ return nfdVariant;
2202
+ }
2203
+ const curlyVariant = tryCurlyQuoteVariant(resolved);
2204
+ if (curlyVariant !== resolved && fileExists(curlyVariant)) {
2205
+ return curlyVariant;
2206
+ }
2207
+ const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
2208
+ if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
2209
+ return nfdCurlyVariant;
2210
+ }
2211
+ return resolved;
2212
+ }
2213
+
2214
+ // src/tools/bash-command-guard.ts
2215
+ var BROAD_SEARCH_ROOTS = /* @__PURE__ */ new Set(["/", "~"]);
2216
+ var FIND_OPTIONS_WITH_ARG = /* @__PURE__ */ new Set([
2217
+ "-name",
2218
+ "-iname",
2219
+ "-path",
2220
+ "-ipath",
2221
+ "-wholename",
2222
+ "-iwholename",
2223
+ "-perm",
2224
+ "-user",
2225
+ "-group",
2226
+ "-size",
2227
+ "-type",
2228
+ "-maxdepth",
2229
+ "-mindepth",
2230
+ "-mtime",
2231
+ "-atime",
2232
+ "-ctime",
2233
+ "-newer",
2234
+ "-anewer",
2235
+ "-cnewer"
2236
+ ]);
2237
+ function getFindSearchRoot(tokens) {
2238
+ for (let i = 1; i < tokens.length; i++) {
2239
+ const token = tokens[i];
2240
+ if (!token.startsWith("-")) {
2241
+ return token;
2242
+ }
2243
+ if (FIND_OPTIONS_WITH_ARG.has(token)) {
2244
+ i++;
2245
+ }
2246
+ }
2247
+ return void 0;
2248
+ }
2249
+ function validateBashFilesystemSearch(command, cwd) {
2250
+ const tokens = getCommandTokens(command);
2251
+ if (tokens[0] !== "find") {
2252
+ return;
2253
+ }
2254
+ const searchRoot = getFindSearchRoot(tokens);
2255
+ if (!searchRoot) {
2256
+ return;
2257
+ }
2258
+ if (BROAD_SEARCH_ROOTS.has(searchRoot) || searchRoot === "$HOME" || searchRoot.startsWith("~/")) {
2259
+ throw new Error(
2260
+ 'Searching outside the project directory is not allowed. Use the find tool with a glob pattern (e.g. **/filename.png), or run find . -name "filename" from the project root.'
2261
+ );
2262
+ }
2263
+ try {
2264
+ validatePathWithinCwd(resolveToCwd(searchRoot, cwd), cwd);
2265
+ } catch {
2266
+ throw new Error(
2267
+ "find search path must stay within the project directory. Use the find tool with a glob pattern instead."
2268
+ );
2269
+ }
2270
+ }
2271
+
2272
+ // src/tools/truncate.ts
2273
+ var DEFAULT_MAX_LINES = 2e3;
2274
+ var DEFAULT_MAX_BYTES = 50 * 1024;
2275
+ var GREP_MAX_LINE_LENGTH = 500;
2276
+ function formatSize(bytes) {
2277
+ if (bytes < 1024) {
2278
+ return `${bytes}B`;
2279
+ } else if (bytes < 1024 * 1024) {
2280
+ return `${(bytes / 1024).toFixed(1)}KB`;
2281
+ } else {
2282
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2283
+ }
2284
+ }
2285
+ function truncateHead(content, options = {}) {
2286
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
2287
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
2288
+ const totalBytes = Buffer.byteLength(content, "utf-8");
2289
+ const lines = content.split("\n");
2290
+ const totalLines = lines.length;
2291
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
2292
+ return {
2293
+ content,
2294
+ truncated: false,
2295
+ truncatedBy: null,
2296
+ totalLines,
2297
+ totalBytes,
2298
+ outputLines: totalLines,
2299
+ outputBytes: totalBytes,
2300
+ lastLinePartial: false,
2301
+ firstLineExceedsLimit: false,
2302
+ maxLines,
2303
+ maxBytes
2304
+ };
2305
+ }
2306
+ const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
2307
+ if (firstLineBytes > maxBytes) {
2308
+ return {
2309
+ content: "",
2310
+ truncated: true,
2311
+ truncatedBy: "bytes",
2312
+ totalLines,
2313
+ totalBytes,
2314
+ outputLines: 0,
2315
+ outputBytes: 0,
2316
+ lastLinePartial: false,
2317
+ firstLineExceedsLimit: true,
2318
+ maxLines,
2319
+ maxBytes
2320
+ };
2321
+ }
2322
+ const outputLinesArr = [];
2323
+ let outputBytesCount = 0;
2324
+ let truncatedBy = "lines";
2325
+ for (let i = 0; i < lines.length && i < maxLines; i++) {
2326
+ const line = lines[i];
2327
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
2328
+ if (outputBytesCount + lineBytes > maxBytes) {
2329
+ truncatedBy = "bytes";
2330
+ break;
2331
+ }
2332
+ outputLinesArr.push(line);
2333
+ outputBytesCount += lineBytes;
2334
+ }
2335
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
2336
+ truncatedBy = "lines";
2337
+ }
2338
+ const outputContent = outputLinesArr.join("\n");
2339
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
2340
+ return {
2341
+ content: outputContent,
2342
+ truncated: true,
2343
+ truncatedBy,
2344
+ totalLines,
2345
+ totalBytes,
2346
+ outputLines: outputLinesArr.length,
2347
+ outputBytes: finalOutputBytes,
2348
+ lastLinePartial: false,
2349
+ firstLineExceedsLimit: false,
2350
+ maxLines,
2351
+ maxBytes
2352
+ };
2353
+ }
2354
+ function truncateTail(content, options = {}) {
2355
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
2356
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
2357
+ const totalBytes = Buffer.byteLength(content, "utf-8");
2358
+ const lines = content.split("\n");
2359
+ const totalLines = lines.length;
2360
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
2361
+ return {
2362
+ content,
2363
+ truncated: false,
2364
+ truncatedBy: null,
2365
+ totalLines,
2366
+ totalBytes,
2367
+ outputLines: totalLines,
2368
+ outputBytes: totalBytes,
2369
+ lastLinePartial: false,
2370
+ firstLineExceedsLimit: false,
2371
+ maxLines,
2372
+ maxBytes
2373
+ };
2374
+ }
2375
+ const outputLinesArr = [];
2376
+ let outputBytesCount = 0;
2377
+ let truncatedBy = "lines";
2378
+ let lastLinePartial = false;
2379
+ for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
2380
+ const line = lines[i];
2381
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0);
2382
+ if (outputBytesCount + lineBytes > maxBytes) {
2383
+ truncatedBy = "bytes";
2384
+ if (outputLinesArr.length === 0) {
2385
+ const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
2386
+ outputLinesArr.unshift(truncatedLine);
2387
+ outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
2388
+ lastLinePartial = true;
2389
+ }
2390
+ break;
2391
+ }
2392
+ outputLinesArr.unshift(line);
2393
+ outputBytesCount += lineBytes;
2394
+ }
2395
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
2396
+ truncatedBy = "lines";
2397
+ }
2398
+ const outputContent = outputLinesArr.join("\n");
2399
+ const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
2400
+ return {
2401
+ content: outputContent,
2402
+ truncated: true,
2403
+ truncatedBy,
2404
+ totalLines,
2405
+ totalBytes,
2406
+ outputLines: outputLinesArr.length,
2407
+ outputBytes: finalOutputBytes,
2408
+ lastLinePartial,
2409
+ firstLineExceedsLimit: false,
2410
+ maxLines,
2411
+ maxBytes
2412
+ };
2413
+ }
2414
+ function truncateStringToBytesFromEnd(str, maxBytes) {
2415
+ const buf = Buffer.from(str, "utf-8");
2416
+ if (buf.length <= maxBytes) {
2417
+ return str;
2418
+ }
2419
+ let start = buf.length - maxBytes;
2420
+ while (start < buf.length && (buf[start] & 192) === 128) {
2421
+ start++;
2422
+ }
2423
+ return buf.slice(start).toString("utf-8");
2424
+ }
2425
+ function truncateLine(line, maxChars = GREP_MAX_LINE_LENGTH) {
2426
+ if (line.length <= maxChars) {
2427
+ return { text: line, wasTruncated: false };
2428
+ }
2429
+ return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
2430
+ }
2431
+
2432
+ // src/tools/bash.ts
2433
+ function getTempFilePath() {
2434
+ return join3(tmpdir(), `tarsk-bash-${randomBytes2(8).toString("hex")}.log`);
2435
+ }
2436
+ var bashSchema = Type.Object({
2437
+ command: Type.String({ description: "Bash command to execute" }),
1887
2438
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional)" }))
1888
2439
  });
1889
2440
  var defaultBashOperations = {
@@ -1946,15 +2497,17 @@ function createBashTool(cwd, options) {
1946
2497
  const ops = options?.operations ?? defaultBashOperations;
1947
2498
  const commandPrefix = options?.commandPrefix;
1948
2499
  const shellPermissionGate = options?.shellPermissionGate;
2500
+ const projectEnv = options?.projectEnv;
1949
2501
  return {
1950
2502
  name: "bash",
1951
2503
  label: "bash",
1952
- description: `Execute a bash command in the current working directory. Output truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. Optionally provide a timeout in seconds.`,
2504
+ description: `Execute a bash command in the current working directory. Do not search the filesystem from / or ~; use the find tool for file discovery. Output truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB. Optionally provide a timeout in seconds.`,
1953
2505
  parameters: bashSchema,
1954
2506
  execute: async (toolCallId, { command, timeout }, signal, onUpdate) => {
1955
2507
  const resolvedCommand = commandPrefix ? `${commandPrefix}
1956
2508
  ${command}` : command;
1957
2509
  tarskDebugLog(`[ai] bash-start: ${resolvedCommand}`);
2510
+ validateBashFilesystemSearch(resolvedCommand, cwd);
1958
2511
  if (shellPermissionGate) {
1959
2512
  await shellPermissionGate.ensureAllowed(toolCallId, resolvedCommand, "bash", signal);
1960
2513
  }
@@ -1991,7 +2544,12 @@ ${command}` : command;
1991
2544
  });
1992
2545
  }
1993
2546
  };
1994
- ops.exec(resolvedCommand, cwd, { onData: handleData, signal, timeout, env: getShellEnv() }).then(({ exitCode }) => {
2547
+ ops.exec(resolvedCommand, cwd, {
2548
+ onData: handleData,
2549
+ signal,
2550
+ timeout,
2551
+ env: mergeEnvVars(getShellEnv(), projectEnv)
2552
+ }).then(({ exitCode }) => {
1995
2553
  tarskDebugLog(`[ai] bash-end: ${resolvedCommand}`);
1996
2554
  if (tempFileStream) tempFileStream.end();
1997
2555
  const fullOutput = Buffer.concat(chunks).toString("utf-8");
@@ -2101,157 +2659,79 @@ function fuzzyFindText(content, oldText) {
2101
2659
  found: true,
2102
2660
  index: fuzzyIndex,
2103
2661
  matchLength: fuzzyOldText.length,
2104
- usedFuzzyMatch: true,
2105
- contentForReplacement: fuzzyContent
2106
- };
2107
- }
2108
- function stripBom(content) {
2109
- return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
2110
- }
2111
- function generateDiffString(oldContent, newContent, contextLines = 4) {
2112
- const parts = Diff.diffLines(oldContent, newContent);
2113
- const output = [];
2114
- const oldLines = oldContent.split("\n");
2115
- const newLines = newContent.split("\n");
2116
- const maxLineNum = Math.max(oldLines.length, newLines.length);
2117
- const lineNumWidth = String(maxLineNum).length;
2118
- let oldLineNum = 1;
2119
- let newLineNum = 1;
2120
- let lastWasChange = false;
2121
- let firstChangedLine;
2122
- for (let i = 0; i < parts.length; i++) {
2123
- const part = parts[i];
2124
- const raw = part.value.split("\n");
2125
- if (raw[raw.length - 1] === "") {
2126
- raw.pop();
2127
- }
2128
- if (part.added || part.removed) {
2129
- firstChangedLine ??= newLineNum;
2130
- for (const line of raw) {
2131
- if (part.added) {
2132
- output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
2133
- newLineNum++;
2134
- } else {
2135
- output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
2136
- oldLineNum++;
2137
- }
2138
- }
2139
- lastWasChange = true;
2140
- } else {
2141
- const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
2142
- if (lastWasChange || nextPartIsChange) {
2143
- let linesToShow = raw;
2144
- let skipStart = 0;
2145
- let skipEnd = 0;
2146
- if (!lastWasChange) {
2147
- skipStart = Math.max(0, raw.length - contextLines);
2148
- linesToShow = raw.slice(skipStart);
2149
- }
2150
- if (!nextPartIsChange && linesToShow.length > contextLines) {
2151
- skipEnd = linesToShow.length - contextLines;
2152
- linesToShow = linesToShow.slice(0, contextLines);
2153
- }
2154
- if (skipStart > 0) {
2155
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
2156
- oldLineNum += skipStart;
2157
- newLineNum += skipStart;
2158
- }
2159
- for (const line of linesToShow) {
2160
- output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
2161
- oldLineNum++;
2162
- newLineNum++;
2163
- }
2164
- if (skipEnd > 0) {
2165
- output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
2166
- oldLineNum += skipEnd;
2167
- newLineNum += skipEnd;
2168
- }
2169
- } else {
2170
- oldLineNum += raw.length;
2171
- newLineNum += raw.length;
2172
- }
2173
- lastWasChange = false;
2174
- }
2175
- }
2176
- return { diff: output.join("\n"), firstChangedLine };
2177
- }
2178
-
2179
- // src/tools/path-utils.ts
2180
- import { accessSync, constants } from "node:fs";
2181
- import * as os from "node:os";
2182
- import { isAbsolute, resolve as resolvePath, relative } from "node:path";
2183
- var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
2184
- var NARROW_NO_BREAK_SPACE = "\u202F";
2185
- function normalizeUnicodeSpaces(str) {
2186
- return str.replace(UNICODE_SPACES, " ");
2187
- }
2188
- function tryMacOSScreenshotPath(filePath) {
2189
- return filePath.replace(/ (AM|PM)\./g, `${NARROW_NO_BREAK_SPACE}$1.`);
2190
- }
2191
- function tryNFDVariant(filePath) {
2192
- return filePath.normalize("NFD");
2193
- }
2194
- function tryCurlyQuoteVariant(filePath) {
2195
- return filePath.replace(/'/g, "\u2019");
2196
- }
2197
- function fileExists(filePath) {
2198
- try {
2199
- accessSync(filePath, constants.F_OK);
2200
- return true;
2201
- } catch {
2202
- return false;
2203
- }
2204
- }
2205
- function normalizeAtPrefix(filePath) {
2206
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
2207
- }
2208
- function expandPath(filePath) {
2209
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
2210
- if (normalized === "~") {
2211
- return os.homedir();
2212
- }
2213
- if (normalized.startsWith("~/")) {
2214
- return os.homedir() + normalized.slice(1);
2215
- }
2216
- return normalized;
2217
- }
2218
- function resolveToCwd(filePath, cwd) {
2219
- const expanded = expandPath(filePath);
2220
- if (isAbsolute(expanded)) {
2221
- return expanded;
2222
- }
2223
- return resolvePath(cwd, expanded);
2224
- }
2225
- function validatePathWithinCwd(resolvedPath2, cwd) {
2226
- const relativePath = relative(cwd, resolvedPath2);
2227
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
2228
- throw new Error(
2229
- `Access denied: Path is outside the allowed directory. Requested: ${resolvedPath2}, Allowed root: ${cwd}`
2230
- );
2231
- }
2232
- }
2233
- function resolveReadPath(filePath, cwd) {
2234
- const resolved = resolveToCwd(filePath, cwd);
2235
- if (fileExists(resolved)) {
2236
- return resolved;
2237
- }
2238
- const amPmVariant = tryMacOSScreenshotPath(resolved);
2239
- if (amPmVariant !== resolved && fileExists(amPmVariant)) {
2240
- return amPmVariant;
2241
- }
2242
- const nfdVariant = tryNFDVariant(resolved);
2243
- if (nfdVariant !== resolved && fileExists(nfdVariant)) {
2244
- return nfdVariant;
2245
- }
2246
- const curlyVariant = tryCurlyQuoteVariant(resolved);
2247
- if (curlyVariant !== resolved && fileExists(curlyVariant)) {
2248
- return curlyVariant;
2249
- }
2250
- const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
2251
- if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
2252
- return nfdCurlyVariant;
2662
+ usedFuzzyMatch: true,
2663
+ contentForReplacement: fuzzyContent
2664
+ };
2665
+ }
2666
+ function stripBom(content) {
2667
+ return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
2668
+ }
2669
+ function generateDiffString(oldContent, newContent, contextLines = 4) {
2670
+ const parts = Diff.diffLines(oldContent, newContent);
2671
+ const output = [];
2672
+ const oldLines = oldContent.split("\n");
2673
+ const newLines = newContent.split("\n");
2674
+ const maxLineNum = Math.max(oldLines.length, newLines.length);
2675
+ const lineNumWidth = String(maxLineNum).length;
2676
+ let oldLineNum = 1;
2677
+ let newLineNum = 1;
2678
+ let lastWasChange = false;
2679
+ let firstChangedLine;
2680
+ for (let i = 0; i < parts.length; i++) {
2681
+ const part = parts[i];
2682
+ const raw = part.value.split("\n");
2683
+ if (raw[raw.length - 1] === "") {
2684
+ raw.pop();
2685
+ }
2686
+ if (part.added || part.removed) {
2687
+ firstChangedLine ??= newLineNum;
2688
+ for (const line of raw) {
2689
+ if (part.added) {
2690
+ output.push(`+${String(newLineNum).padStart(lineNumWidth, " ")} ${line}`);
2691
+ newLineNum++;
2692
+ } else {
2693
+ output.push(`-${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
2694
+ oldLineNum++;
2695
+ }
2696
+ }
2697
+ lastWasChange = true;
2698
+ } else {
2699
+ const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
2700
+ if (lastWasChange || nextPartIsChange) {
2701
+ let linesToShow = raw;
2702
+ let skipStart = 0;
2703
+ let skipEnd = 0;
2704
+ if (!lastWasChange) {
2705
+ skipStart = Math.max(0, raw.length - contextLines);
2706
+ linesToShow = raw.slice(skipStart);
2707
+ }
2708
+ if (!nextPartIsChange && linesToShow.length > contextLines) {
2709
+ skipEnd = linesToShow.length - contextLines;
2710
+ linesToShow = linesToShow.slice(0, contextLines);
2711
+ }
2712
+ if (skipStart > 0) {
2713
+ output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
2714
+ oldLineNum += skipStart;
2715
+ newLineNum += skipStart;
2716
+ }
2717
+ for (const line of linesToShow) {
2718
+ output.push(` ${String(oldLineNum).padStart(lineNumWidth, " ")} ${line}`);
2719
+ oldLineNum++;
2720
+ newLineNum++;
2721
+ }
2722
+ if (skipEnd > 0) {
2723
+ output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
2724
+ oldLineNum += skipEnd;
2725
+ newLineNum += skipEnd;
2726
+ }
2727
+ } else {
2728
+ oldLineNum += raw.length;
2729
+ newLineNum += raw.length;
2730
+ }
2731
+ lastWasChange = false;
2732
+ }
2253
2733
  }
2254
- return resolved;
2734
+ return { diff: output.join("\n"), firstChangedLine };
2255
2735
  }
2256
2736
 
2257
2737
  // src/tools/tool-helpers.ts
@@ -4360,7 +4840,7 @@ import { Type as Type13 } from "@sinclair/typebox";
4360
4840
 
4361
4841
  // src/features/todos/todos.database.ts
4362
4842
  init_database();
4363
- import { randomUUID } from "crypto";
4843
+ import { randomUUID as randomUUID2 } from "crypto";
4364
4844
  async function fetchAllTodos(db, threadId) {
4365
4845
  const result = await db.execute({
4366
4846
  sql: `SELECT id, threadId, description, status, assignedTo, passes, createdAt, updatedAt
@@ -4376,7 +4856,7 @@ async function addTodos(threadId, descriptions, assignedTo = "agent") {
4376
4856
  const db = await getDatabase();
4377
4857
  const baseTime = Date.now();
4378
4858
  for (let i = 0; i < descriptions.length; i++) {
4379
- const id = randomUUID();
4859
+ const id = randomUUID2();
4380
4860
  const ts = new Date(baseTime + i).toISOString();
4381
4861
  await db.execute({
4382
4862
  sql: `INSERT INTO todos (id, threadId, description, status, assignedTo, passes, createdAt, updatedAt)
@@ -4850,7 +5330,7 @@ function describeMCPServerConnection(config) {
4850
5330
  }
4851
5331
  return "(invalid config)";
4852
5332
  }
4853
- function createMCPClientTransport(config) {
5333
+ function createMCPClientTransport(config, additionalEnv) {
4854
5334
  if (isRemoteMCPServer(config)) {
4855
5335
  const url = new URL(config.url.trim());
4856
5336
  const transport = resolveRemoteTransport(config, url);
@@ -4869,7 +5349,7 @@ function createMCPClientTransport(config) {
4869
5349
  return new StdioClientTransport({
4870
5350
  command: config.command,
4871
5351
  args: config.args,
4872
- env: config.env
5352
+ env: additionalEnv && config.env ? { ...additionalEnv, ...config.env } : additionalEnv ?? config.env
4873
5353
  });
4874
5354
  }
4875
5355
 
@@ -4946,6 +5426,10 @@ var MCPManagerImpl = class {
4946
5426
  configCache = /* @__PURE__ */ new Map();
4947
5427
  serverInstances = /* @__PURE__ */ new Map();
4948
5428
  toolWrappers = /* @__PURE__ */ new Map();
5429
+ projectEnv;
5430
+ constructor(options) {
5431
+ this.projectEnv = options?.projectEnv;
5432
+ }
4949
5433
  /**
4950
5434
  * Load MCP configuration from file
4951
5435
  */
@@ -5078,7 +5562,7 @@ var MCPManagerImpl = class {
5078
5562
  const connection = describeMCPServerConnection(serverConfig);
5079
5563
  logMCPConnecting(serverName, connection, options);
5080
5564
  tarskDebugLog(`[MCP] Connecting to server ${serverName}: ${connection}`);
5081
- const transport = createMCPClientTransport(serverConfig);
5565
+ const transport = createMCPClientTransport(serverConfig, this.projectEnv);
5082
5566
  const client = new Client(
5083
5567
  {
5084
5568
  name: "tarsk-cli",
@@ -5199,7 +5683,7 @@ var MCPManagerImpl = class {
5199
5683
 
5200
5684
  // src/tools/mcp-tools.ts
5201
5685
  async function createMCPTools(projectPath, options) {
5202
- const mcpManager = new MCPManagerImpl();
5686
+ const mcpManager = new MCPManagerImpl({ projectEnv: options?.projectEnv });
5203
5687
  const logOptions = options?.onMcpStatus ? { onStatus: options.onMcpStatus } : void 0;
5204
5688
  try {
5205
5689
  const mcpTools = await mcpManager.createToolWrappers(projectPath, logOptions);
@@ -5223,6 +5707,10 @@ async function createMCPTools(projectPath, options) {
5223
5707
  return [];
5224
5708
  }
5225
5709
  }
5710
+ function clearMCPToolCache(projectPath) {
5711
+ const mcpManager = new MCPManagerImpl();
5712
+ mcpManager.clearCache(projectPath);
5713
+ }
5226
5714
 
5227
5715
  // src/tools/generate-image.ts
5228
5716
  import { Type as Type14 } from "@sinclair/typebox";
@@ -7584,7 +8072,7 @@ function buildCrossHostRedirectResult(redirectUrl) {
7584
8072
  // src/features/web-fetch/web-fetch.client.ts
7585
8073
  import { mkdir, writeFile as writeFile2 } from "fs/promises";
7586
8074
  import { join as join11, extname as extname3 } from "path";
7587
- import { createHash } from "crypto";
8075
+ import { createHash as createHash2 } from "crypto";
7588
8076
 
7589
8077
  // src/features/web-fetch/web-fetch-cache.ts
7590
8078
  var WebFetchCache = class {
@@ -7690,7 +8178,7 @@ async function readResponseBody(response, signal) {
7690
8178
  async function saveBinaryContent(body, contentType, url) {
7691
8179
  const downloadsDir = join11(getDataDir(), "fetch-downloads");
7692
8180
  await mkdir(downloadsDir, { recursive: true });
7693
- const hash = createHash("sha256").update(url).digest("hex").slice(0, 12);
8181
+ const hash = createHash2("sha256").update(url).digest("hex").slice(0, 12);
7694
8182
  let extension = extname3(new URL(url).pathname);
7695
8183
  if (!extension && contentType) {
7696
8184
  if (contentType.includes("pdf")) extension = ".pdf";
@@ -8218,7 +8706,7 @@ function isSessionDomainAllowed(projectId, hostname2) {
8218
8706
  // src/features/web-fetch-permissions/web-fetch-permission.gate.ts
8219
8707
  var WEB_FETCH_PERMISSION_SKIP_MESSAGE_PREFIX = "User declined to fetch from this domain:";
8220
8708
  var pendingPermissions = /* @__PURE__ */ new Map();
8221
- function buildWildcardOptions(hostname2) {
8709
+ function buildWildcardOptions2(hostname2) {
8222
8710
  return [
8223
8711
  {
8224
8712
  label: `Always allow ${hostname2} (this project)`,
@@ -8271,7 +8759,7 @@ var WebFetchPermissionGate = class {
8271
8759
  toolCallId,
8272
8760
  hostname: normalized,
8273
8761
  url,
8274
- wildcardOptions: buildWildcardOptions(normalized)
8762
+ wildcardOptions: buildWildcardOptions2(normalized)
8275
8763
  };
8276
8764
  this.config.onPermissionRequired?.(request);
8277
8765
  return new Promise((resolve6, reject) => {
@@ -8415,7 +8903,8 @@ function applyCodeSearchInvalidationHooks(tools, threadId) {
8415
8903
  async function createCodingTools(cwd, options) {
8416
8904
  const deferOption = options?.deferTools ?? true;
8417
8905
  const shellPermissionGate = options?.shellPermissionGate;
8418
- const bashOptions = shellPermissionGate ? { ...options?.bash, shellPermissionGate } : options?.bash;
8906
+ const projectEnv = options?.projectId ? await getDecryptedProjectEnvVars(options.projectId) : void 0;
8907
+ const bashOptions = shellPermissionGate || projectEnv ? { ...options?.bash, shellPermissionGate, projectEnv } : options?.bash;
8419
8908
  const coreTools = filterEnabledTools([
8420
8909
  createReadTool(cwd, options?.read),
8421
8910
  createBashTool(cwd, bashOptions),
@@ -8449,7 +8938,7 @@ async function createCodingTools(cwd, options) {
8449
8938
  if (options?.loadMcpTools !== false && isToolEnabled("mcp")) {
8450
8939
  try {
8451
8940
  mcpTools = filterEnabledTools(
8452
- await createMCPTools(cwd, { onMcpStatus: options?.onMcpStatus })
8941
+ await createMCPTools(cwd, { onMcpStatus: options?.onMcpStatus, projectEnv })
8453
8942
  );
8454
8943
  } catch (error) {
8455
8944
  console.warn(`[Tools] Failed to load MCP tools for ${cwd}:`, error);
@@ -8586,6 +9075,16 @@ var EventQueue = class {
8586
9075
  errorOccurred = false;
8587
9076
  toolCallCount = 0;
8588
9077
  lastErrorMessage;
9078
+ setLastErrorMessage(message) {
9079
+ if (typeof message !== "string") {
9080
+ return;
9081
+ }
9082
+ const trimmedMessage = message.trim();
9083
+ if (!trimmedMessage) {
9084
+ return;
9085
+ }
9086
+ this.lastErrorMessage = trimmedMessage;
9087
+ }
8589
9088
  /**
8590
9089
  * Processes Pi Agent events and transforms them into AgentEvent format
8591
9090
  */
@@ -8691,7 +9190,7 @@ var EventQueue = class {
8691
9190
  const messages = event.messages;
8692
9191
  const lastAssistant = [...messages].reverse().find((m) => "role" in m && m.role === "assistant");
8693
9192
  if (lastAssistant && "errorMessage" in lastAssistant && typeof lastAssistant.errorMessage === "string") {
8694
- this.lastErrorMessage = lastAssistant.errorMessage;
9193
+ this.setLastErrorMessage(lastAssistant.errorMessage);
8695
9194
  }
8696
9195
  if (lastAssistant) {
8697
9196
  const extracted = extractTextFromAssistantMessage(lastAssistant);
@@ -9646,231 +10145,89 @@ async function loadPromptSections(threadPath, tools, skills, planMode, agents, d
9646
10145
  }
9647
10146
  function formatSkillsSection(skills) {
9648
10147
  if (!skills || skills.length === 0) {
9649
- return "";
9650
- }
9651
- let skillsSection = "\n\n# Available Skills\n\n";
9652
- skillsSection += "The following skills are available for this session. Use them to enhance your capabilities:\n\n";
9653
- for (const skill of skills) {
9654
- skillsSection += `## ${skill.name}
9655
-
9656
- `;
9657
- skillsSection += `**Description**: ${skill.description}
9658
-
9659
- `;
9660
- if (skill.compatibility) {
9661
- skillsSection += `**Compatibility**: ${skill.compatibility}
9662
-
9663
- `;
9664
- }
9665
- skillsSection += skill.instructions;
9666
- skillsSection += "\n\n---\n\n";
9667
- }
9668
- tarskDebugLog(
9669
- `[ai] Injected ${skills.length} skill(s) into system prompt: ${skills.map((s) => s.name).join(", ")}`
9670
- );
9671
- return skillsSection;
9672
- }
9673
- function formatAgentsSection(agents) {
9674
- const invocable = agents.filter((a) => !a.disableModelInvocation);
9675
- if (invocable.length === 0) return "";
9676
- let section = "\n\n# Available Agents\n\nUse the `agent` tool to delegate focused subtasks to these specialized agents. Each agent runs in an isolated context and returns its result.\n\n";
9677
- for (const agent of invocable) {
9678
- section += `## ${agent.name}
9679
-
9680
- `;
9681
- section += `**Description**: ${agent.description}
9682
- `;
9683
- section += `**Invoke as**: \`agentName: "${agent.name}"\`
9684
-
9685
- `;
9686
- }
9687
- tarskDebugLog(
9688
- `[ai] Injected ${invocable.length} agent(s) into system prompt: ${invocable.map((a) => a.name).join(", ")}`
9689
- );
9690
- return section;
9691
- }
9692
- async function loadAgentSystemPrompt(threadPath, tools, skills, planMode, agents, deferredTools, ralphMode, overrideOptions) {
9693
- let prompt = resolveEffectiveSystemPrompt(buildDefaultPrompt(tools), overrideOptions);
9694
- const agentsContent = loadDeveloperContext(threadPath);
9695
- if (agentsContent) {
9696
- prompt += "\n\n# Developer Context\n\n" + agentsContent;
9697
- }
9698
- const rulesSection = await loadRulesSection(threadPath);
9699
- if (rulesSection) {
9700
- prompt += rulesSection;
9701
- }
9702
- const projectAnalysisSection = loadProjectAnalysisSection(threadPath);
9703
- if (projectAnalysisSection) {
9704
- prompt += projectAnalysisSection;
9705
- }
9706
- const devServerSection = await loadDevServerSection(threadPath);
9707
- if (devServerSection) {
9708
- prompt += devServerSection;
9709
- }
9710
- if (planMode) {
9711
- prompt += PLAN_MODE_INSTRUCTIONS;
9712
- }
9713
- if (ralphMode) {
9714
- prompt += RALPH_MODE_INSTRUCTIONS;
9715
- const progressContent = await readProgress(threadPath);
9716
- if (progressContent.trim()) {
9717
- prompt += "\n\n# Prior Learnings (from progress.txt)\n\n" + progressContent;
9718
- }
9719
- }
9720
- const skillsSection = formatSkillsSection(skills ?? []);
9721
- if (skillsSection) {
9722
- prompt += skillsSection;
9723
- }
9724
- const agentsSection = formatAgentsSection(agents ?? []);
9725
- if (agentsSection) {
9726
- prompt += agentsSection;
9727
- }
9728
- if (deferredTools && deferredTools.size > 0) {
9729
- prompt += formatDeferredToolsList(deferredTools);
9730
- }
9731
- return prompt;
9732
- }
9733
-
9734
- // src/features/shell-permissions/command-pattern.ts
9735
- var ENV_ASSIGNMENT_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=/;
9736
- function stripLeadingEnvAssignments(command) {
9737
- let remaining = command.trim();
9738
- while (remaining.length > 0) {
9739
- const tokenMatch = remaining.match(/^(\S+)\s*/);
9740
- if (!tokenMatch) {
9741
- break;
9742
- }
9743
- const token = tokenMatch[1];
9744
- if (!ENV_ASSIGNMENT_PATTERN.test(token)) {
9745
- break;
9746
- }
9747
- remaining = remaining.slice(tokenMatch[0].length).trimStart();
9748
- }
9749
- return remaining;
9750
- }
9751
- function tokenizeCommand(command) {
9752
- const tokens = [];
9753
- let current = "";
9754
- let quote = null;
9755
- let escaped = false;
9756
- for (const char of command) {
9757
- if (escaped) {
9758
- current += char;
9759
- escaped = false;
9760
- continue;
9761
- }
9762
- if (char === "\\") {
9763
- escaped = true;
9764
- continue;
9765
- }
9766
- if (quote) {
9767
- if (char === quote) {
9768
- quote = null;
9769
- } else {
9770
- current += char;
9771
- }
9772
- continue;
9773
- }
9774
- if (char === "'" || char === '"') {
9775
- quote = char;
9776
- continue;
9777
- }
9778
- if (/\s/.test(char)) {
9779
- if (current.length > 0) {
9780
- tokens.push(current);
9781
- current = "";
9782
- }
9783
- continue;
9784
- }
9785
- current += char;
9786
- }
9787
- if (current.length > 0) {
9788
- tokens.push(current);
9789
- }
9790
- return tokens;
9791
- }
9792
- function getCommandTokens(command) {
9793
- return tokenizeCommand(stripLeadingEnvAssignments(command));
9794
- }
9795
- function buildWildcardPattern(tokens, wildcardTokenCount) {
9796
- const prefix = tokens.slice(0, wildcardTokenCount);
9797
- if (prefix.length === 0) {
9798
- return commandToPatternString(tokens);
9799
- }
9800
- return `${prefix.join(" ")} *`;
9801
- }
9802
- function commandToPatternString(tokens) {
9803
- return tokens.join(" ");
9804
- }
9805
- function buildWildcardOptions2(command) {
9806
- const tokens = getCommandTokens(command);
9807
- const options = [];
9808
- if (tokens.length >= 1) {
9809
- const oneTokenPattern = buildWildcardPattern(tokens, 1);
9810
- options.push(
9811
- {
9812
- label: `Allow ${oneTokenPattern} (this session)`,
9813
- pattern: oneTokenPattern,
9814
- scope: "session"
9815
- },
9816
- {
9817
- label: `Allow ${oneTokenPattern} (always for this project)`,
9818
- pattern: oneTokenPattern,
9819
- scope: "project"
9820
- }
9821
- );
10148
+ return "";
9822
10149
  }
9823
- if (tokens.length >= 2) {
9824
- const twoTokenPattern = buildWildcardPattern(tokens, 2);
9825
- options.push(
9826
- {
9827
- label: `Allow ${twoTokenPattern} (this session)`,
9828
- pattern: twoTokenPattern,
9829
- scope: "session"
9830
- },
9831
- {
9832
- label: `Allow ${twoTokenPattern} (always for this project)`,
9833
- pattern: twoTokenPattern,
9834
- scope: "project"
9835
- }
9836
- );
10150
+ let skillsSection = "\n\n# Available Skills\n\n";
10151
+ skillsSection += "The following skills are available for this session. Use them to enhance your capabilities:\n\n";
10152
+ for (const skill of skills) {
10153
+ skillsSection += `## ${skill.name}
10154
+
10155
+ `;
10156
+ skillsSection += `**Description**: ${skill.description}
10157
+
10158
+ `;
10159
+ if (skill.compatibility) {
10160
+ skillsSection += `**Compatibility**: ${skill.compatibility}
10161
+
10162
+ `;
10163
+ }
10164
+ skillsSection += skill.instructions;
10165
+ skillsSection += "\n\n---\n\n";
9837
10166
  }
9838
- return options;
10167
+ tarskDebugLog(
10168
+ `[ai] Injected ${skills.length} skill(s) into system prompt: ${skills.map((s) => s.name).join(", ")}`
10169
+ );
10170
+ return skillsSection;
9839
10171
  }
9840
- function parsePattern(pattern) {
9841
- const trimmed = pattern.trim();
9842
- if (trimmed.endsWith(" *")) {
9843
- return {
9844
- tokens: tokenizeCommand(trimmed.slice(0, -2).trim()),
9845
- hasWildcard: true
9846
- };
10172
+ function formatAgentsSection(agents) {
10173
+ const invocable = agents.filter((a) => !a.disableModelInvocation);
10174
+ if (invocable.length === 0) return "";
10175
+ let section = "\n\n# Available Agents\n\nUse the `agent` tool to delegate focused subtasks to these specialized agents. Each agent runs in an isolated context and returns its result.\n\n";
10176
+ for (const agent of invocable) {
10177
+ section += `## ${agent.name}
10178
+
10179
+ `;
10180
+ section += `**Description**: ${agent.description}
10181
+ `;
10182
+ section += `**Invoke as**: \`agentName: "${agent.name}"\`
10183
+
10184
+ `;
9847
10185
  }
9848
- return {
9849
- tokens: tokenizeCommand(trimmed),
9850
- hasWildcard: false
9851
- };
10186
+ tarskDebugLog(
10187
+ `[ai] Injected ${invocable.length} agent(s) into system prompt: ${invocable.map((a) => a.name).join(", ")}`
10188
+ );
10189
+ return section;
9852
10190
  }
9853
- function commandMatchesPattern(command, pattern) {
9854
- const commandTokens = getCommandTokens(command);
9855
- const parsedPattern = parsePattern(pattern);
9856
- if (!parsedPattern.hasWildcard) {
9857
- return command.trim() === pattern.trim();
10191
+ async function loadAgentSystemPrompt(threadPath, tools, skills, planMode, agents, deferredTools, ralphMode, overrideOptions) {
10192
+ let prompt = resolveEffectiveSystemPrompt(buildDefaultPrompt(tools), overrideOptions);
10193
+ const agentsContent = loadDeveloperContext(threadPath);
10194
+ if (agentsContent) {
10195
+ prompt += "\n\n# Developer Context\n\n" + agentsContent;
9858
10196
  }
9859
- if (parsedPattern.tokens.length === 0) {
9860
- return false;
10197
+ const rulesSection = await loadRulesSection(threadPath);
10198
+ if (rulesSection) {
10199
+ prompt += rulesSection;
9861
10200
  }
9862
- if (commandTokens.length < parsedPattern.tokens.length) {
9863
- return false;
10201
+ const projectAnalysisSection = loadProjectAnalysisSection(threadPath);
10202
+ if (projectAnalysisSection) {
10203
+ prompt += projectAnalysisSection;
9864
10204
  }
9865
- for (let index = 0; index < parsedPattern.tokens.length; index++) {
9866
- if (commandTokens[index] !== parsedPattern.tokens[index]) {
9867
- return false;
10205
+ const devServerSection = await loadDevServerSection(threadPath);
10206
+ if (devServerSection) {
10207
+ prompt += devServerSection;
10208
+ }
10209
+ if (planMode) {
10210
+ prompt += PLAN_MODE_INSTRUCTIONS;
10211
+ }
10212
+ if (ralphMode) {
10213
+ prompt += RALPH_MODE_INSTRUCTIONS;
10214
+ const progressContent = await readProgress(threadPath);
10215
+ if (progressContent.trim()) {
10216
+ prompt += "\n\n# Prior Learnings (from progress.txt)\n\n" + progressContent;
9868
10217
  }
9869
10218
  }
9870
- return true;
9871
- }
9872
- function commandMatchesAnyPattern(command, patterns) {
9873
- return patterns.some((pattern) => commandMatchesPattern(command, pattern));
10219
+ const skillsSection = formatSkillsSection(skills ?? []);
10220
+ if (skillsSection) {
10221
+ prompt += skillsSection;
10222
+ }
10223
+ const agentsSection = formatAgentsSection(agents ?? []);
10224
+ if (agentsSection) {
10225
+ prompt += agentsSection;
10226
+ }
10227
+ if (deferredTools && deferredTools.size > 0) {
10228
+ prompt += formatDeferredToolsList(deferredTools);
10229
+ }
10230
+ return prompt;
9874
10231
  }
9875
10232
 
9876
10233
  // src/features/shell-permissions/shell-permission.store.ts
@@ -9946,7 +10303,7 @@ var ShellPermissionGate = class {
9946
10303
  toolCallId,
9947
10304
  command: command.trim(),
9948
10305
  source,
9949
- wildcardOptions: buildWildcardOptions2(command)
10306
+ wildcardOptions: buildWildcardOptions(command)
9950
10307
  };
9951
10308
  this.config.onPermissionRequired?.(request);
9952
10309
  return new Promise((resolve6, reject) => {
@@ -10219,6 +10576,7 @@ var PiExecutorImpl = class {
10219
10576
  skills: context.skills,
10220
10577
  threadId: context.threadId,
10221
10578
  threadPath: cwd,
10579
+ projectId: context.projectId,
10222
10580
  metadataManager: this.metadataManager,
10223
10581
  shellPermissionGate,
10224
10582
  webFetchPermissionGate
@@ -10368,6 +10726,7 @@ ${userPrompt}`;
10368
10726
  eventQueue.notify();
10369
10727
  }).catch((err) => {
10370
10728
  const errMessage = err instanceof Error ? err.message : String(err);
10729
+ eventQueue.setLastErrorMessage(errMessage);
10371
10730
  console.error(`[ai] Agent prompt failed:`, {
10372
10731
  request: promptRequest,
10373
10732
  error: serializeLogValue(err),
@@ -10428,7 +10787,7 @@ ${userPrompt}`;
10428
10787
  console.log("[ai] \u26A0\uFE0F Agent ended with no text content and no tool calls.");
10429
10788
  const upstreamError = eventQueue.lastErrorMessage ? { message: eventQueue.lastErrorMessage } : void 0;
10430
10789
  const noResponseMessage = `The model ${model} from ${providerName} did not provide a response to your prompt. Check your API key and balance and try again.`;
10431
- const content = upstreamError ? `${noResponseMessage} ${upstreamError.message}` : noResponseMessage;
10790
+ const content = upstreamError?.message ?? noResponseMessage;
10432
10791
  console.error("[ai] Agent ended without text content:", {
10433
10792
  request: promptRequest,
10434
10793
  upstreamError,
@@ -10440,7 +10799,7 @@ ${userPrompt}`;
10440
10799
  content,
10441
10800
  error: {
10442
10801
  code: "NO_CONTENT_NO_TOOLS",
10443
- message: `The model ${model} from ${providerName} did not provide a response to your prompt.`,
10802
+ message: upstreamError?.message ?? noResponseMessage,
10444
10803
  details: upstreamError
10445
10804
  }
10446
10805
  };
@@ -10610,7 +10969,7 @@ async function readEnvFile() {
10610
10969
  import { Hono } from "hono";
10611
10970
 
10612
10971
  // src/agent/agent-run-guard.ts
10613
- import { randomUUID as randomUUID2 } from "crypto";
10972
+ import { randomUUID as randomUUID3 } from "crypto";
10614
10973
  var DEFAULT_LONG_RUNNING_TURN_LIMIT = 50;
10615
10974
  var LONG_RUNNING_CONFIRMATION_QUESTION = "The agent has been working for a long time. Do you want to continue?";
10616
10975
  var LONG_RUNNING_CONFIRMATION_OPTIONS = ["Yes", "No"];
@@ -10652,7 +11011,7 @@ function shouldPromptForLongRunningConfirmation(state) {
10652
11011
  return state.turnCount >= state.maximumTurnCount;
10653
11012
  }
10654
11013
  function createLongRunningConfirmationRequest(maximumTurnCount) {
10655
- const toolCallId = `long-running-confirmation-${randomUUID2()}`;
11014
+ const toolCallId = `long-running-confirmation-${randomUUID3()}`;
10656
11015
  const event = {
10657
11016
  type: "long_running_confirmation",
10658
11017
  prompt: {
@@ -11028,7 +11387,7 @@ function createWebFetchPermissionRoutes() {
11028
11387
  import { Hono as Hono4 } from "hono";
11029
11388
 
11030
11389
  // src/features/chat/chat-post.route.ts
11031
- import { randomUUID as randomUUID5 } from "crypto";
11390
+ import { randomUUID as randomUUID6 } from "crypto";
11032
11391
 
11033
11392
  // src/features/skills/skills.manager.ts
11034
11393
  import { readdir as readdir4, readFile as readFile6 } from "fs/promises";
@@ -11602,7 +11961,7 @@ import { join as join17 } from "path";
11602
11961
 
11603
11962
  // src/features/project-todos/project-todos.database.ts
11604
11963
  init_database();
11605
- import { randomUUID as randomUUID3 } from "crypto";
11964
+ import { randomUUID as randomUUID4 } from "crypto";
11606
11965
  function serializeTodo(row) {
11607
11966
  return {
11608
11967
  ...row,
@@ -11627,7 +11986,7 @@ async function getTodoById(db, todoId) {
11627
11986
  }
11628
11987
  async function insertTodo(projectId, title, description) {
11629
11988
  const db = await getDatabase();
11630
- const id = randomUUID3();
11989
+ const id = randomUUID4();
11631
11990
  const now = (/* @__PURE__ */ new Date()).toISOString();
11632
11991
  await db.execute({
11633
11992
  sql: `INSERT INTO project_todos (id, projectId, title, description, status, working, createdAt, updatedAt)
@@ -11708,155 +12067,48 @@ async function getGitStatusCache(db, threadId) {
11708
12067
  return null;
11709
12068
  }
11710
12069
  }
11711
- async function setGitStatusCache(db, threadId, data) {
11712
- await db.execute(
11713
- "INSERT OR REPLACE INTO git_status_cache (threadId, data, cachedAt) VALUES (?, ?, ?)",
11714
- [threadId, JSON.stringify(data), (/* @__PURE__ */ new Date()).toISOString()]
11715
- );
11716
- }
11717
- async function getGitStatusCacheAnyAge(db, threadId) {
11718
- const result = await db.execute("SELECT data FROM git_status_cache WHERE threadId = ?", [
11719
- threadId
11720
- ]);
11721
- const row = result.rows[0];
11722
- if (!row) return null;
11723
- try {
11724
- return JSON.parse(row.data);
11725
- } catch {
11726
- return null;
11727
- }
11728
- }
11729
- async function getGitStatusCacheBulk(db, threadIds) {
11730
- if (threadIds.length === 0) return /* @__PURE__ */ new Map();
11731
- const placeholders = threadIds.map(() => "?").join(", ");
11732
- const result = await db.execute(
11733
- `SELECT threadId, data FROM git_status_cache WHERE threadId IN (${placeholders})`,
11734
- threadIds
11735
- );
11736
- const map = /* @__PURE__ */ new Map();
11737
- for (const row of result.rows) {
11738
- const r = row;
11739
- try {
11740
- map.set(r.threadId, JSON.parse(r.data));
11741
- } catch {
11742
- }
11743
- }
11744
- return map;
11745
- }
11746
- async function invalidateGitStatusCache(db, threadId) {
11747
- await db.execute("DELETE FROM git_status_cache WHERE threadId = ?", [threadId]);
11748
- }
11749
-
11750
- // src/features/account/account-get-info.route.ts
11751
- init_database();
11752
- import { randomUUID as randomUUID4 } from "crypto";
11753
-
11754
- // src/core/crypto.ts
11755
- init_database();
11756
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash2 } from "crypto";
11757
- import { networkInterfaces, hostname } from "os";
11758
- var ALGORITHM = "aes-256-gcm";
11759
- var IV_LENGTH = 16;
11760
- function getMachineKey() {
11761
- try {
11762
- const interfaces = networkInterfaces();
11763
- let macAddress = "";
11764
- for (const name of Object.keys(interfaces)) {
11765
- const iface = interfaces[name];
11766
- if (iface) {
11767
- for (const addr of iface) {
11768
- if (addr.mac && addr.mac !== "00:00:00:00:00:00" && !addr.internal) {
11769
- macAddress = addr.mac;
11770
- break;
11771
- }
11772
- }
11773
- if (macAddress) break;
11774
- }
11775
- }
11776
- const host = hostname();
11777
- const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
11778
- const machineId = `${macAddress}-${host}-${homeDir}`;
11779
- return createHash2("sha256").update(machineId).digest();
11780
- } catch {
11781
- return Buffer.from("Invalid", "utf8");
11782
- }
11783
- }
11784
- async function getEncryptionKey() {
11785
- try {
11786
- const db = await getDatabase();
11787
- const result = await db.execute({
11788
- sql: "SELECT value FROM state WHERE key = ?",
11789
- args: ["encryption_key"]
11790
- });
11791
- if (result.rows.length > 0 && result.rows[0].value) {
11792
- const storedValue = result.rows[0].value;
11793
- try {
11794
- const parsed = JSON.parse(storedValue);
11795
- return Buffer.from(parsed, "base64");
11796
- } catch {
11797
- return Buffer.from(storedValue, "base64");
11798
- }
11799
- }
11800
- const machineKey = getMachineKey();
11801
- await db.execute({
11802
- sql: "INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)",
11803
- args: ["encryption_key", machineKey.toString("base64")]
11804
- });
11805
- return machineKey;
11806
- } catch {
11807
- return getMachineKey();
11808
- }
11809
- }
11810
- async function encrypt(plaintext) {
11811
- if (!plaintext) {
11812
- return "";
11813
- }
11814
- const key = await getEncryptionKey();
11815
- const iv = randomBytes2(IV_LENGTH);
11816
- const cipher = createCipheriv(ALGORITHM, key, iv);
11817
- let encrypted = cipher.update(plaintext, "utf8", "base64");
11818
- encrypted += cipher.final("base64");
11819
- const authTag = cipher.getAuthTag();
11820
- const result = `${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`;
11821
- return result;
11822
- }
11823
- async function decrypt(encryptedData) {
11824
- if (!encryptedData) {
11825
- return "";
11826
- }
11827
- try {
11828
- const parts = encryptedData.split(":");
11829
- if (parts.length !== 3) {
11830
- throw new Error("Invalid encrypted data format");
11831
- }
11832
- const [ivBase64, authTagBase64, encrypted] = parts;
11833
- const key = await getEncryptionKey();
11834
- const iv = Buffer.from(ivBase64, "base64");
11835
- const authTag = Buffer.from(authTagBase64, "base64");
11836
- const decipher = createDecipheriv(ALGORITHM, key, iv);
11837
- decipher.setAuthTag(authTag);
11838
- let decrypted = decipher.update(encrypted, "base64", "utf8");
11839
- decrypted += decipher.final("utf8");
11840
- return decrypted;
11841
- } catch (error) {
11842
- throw new Error(
11843
- `Decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`
11844
- );
11845
- }
11846
- }
11847
- function isEncrypted(value) {
11848
- if (!value) {
11849
- return false;
12070
+ async function setGitStatusCache(db, threadId, data) {
12071
+ await db.execute(
12072
+ "INSERT OR REPLACE INTO git_status_cache (threadId, data, cachedAt) VALUES (?, ?, ?)",
12073
+ [threadId, JSON.stringify(data), (/* @__PURE__ */ new Date()).toISOString()]
12074
+ );
12075
+ }
12076
+ async function getGitStatusCacheAnyAge(db, threadId) {
12077
+ const result = await db.execute("SELECT data FROM git_status_cache WHERE threadId = ?", [
12078
+ threadId
12079
+ ]);
12080
+ const row = result.rows[0];
12081
+ if (!row) return null;
12082
+ try {
12083
+ return JSON.parse(row.data);
12084
+ } catch {
12085
+ return null;
11850
12086
  }
11851
- const parts = value.split(":");
11852
- if (parts.length !== 3) {
11853
- return false;
12087
+ }
12088
+ async function getGitStatusCacheBulk(db, threadIds) {
12089
+ if (threadIds.length === 0) return /* @__PURE__ */ new Map();
12090
+ const placeholders = threadIds.map(() => "?").join(", ");
12091
+ const result = await db.execute(
12092
+ `SELECT threadId, data FROM git_status_cache WHERE threadId IN (${placeholders})`,
12093
+ threadIds
12094
+ );
12095
+ const map = /* @__PURE__ */ new Map();
12096
+ for (const row of result.rows) {
12097
+ const r = row;
12098
+ try {
12099
+ map.set(r.threadId, JSON.parse(r.data));
12100
+ } catch {
12101
+ }
11854
12102
  }
11855
- const base64Regex = /^[A-Za-z0-9+/]+=*$/;
11856
- return parts.every((part) => base64Regex.test(part));
12103
+ return map;
12104
+ }
12105
+ async function invalidateGitStatusCache(db, threadId) {
12106
+ await db.execute("DELETE FROM git_status_cache WHERE threadId = ?", [threadId]);
11857
12107
  }
11858
12108
 
11859
12109
  // src/features/account/account-get-info.route.ts
12110
+ init_database();
12111
+ import { randomUUID as randomUUID5 } from "crypto";
11860
12112
  var KEY_BALANCE = "PromptBalance";
11861
12113
  var KEY_VERIFICATION = "PromptBalanceVerification";
11862
12114
  var KEY_PLAN = "Plan";
@@ -11902,7 +12154,7 @@ async function getOrCreateClientReferenceId() {
11902
12154
  if (existing !== null && existing !== void 0) {
11903
12155
  return existing;
11904
12156
  }
11905
- const id = randomUUID4();
12157
+ const id = randomUUID5();
11906
12158
  await setState(db, KEY_CLIENT_REFERENCE_ID, id);
11907
12159
  return id;
11908
12160
  }
@@ -12101,6 +12353,7 @@ async function deleteProject(db, id) {
12101
12353
  );
12102
12354
  await db.execute("DELETE FROM threads WHERE projectId = ?", [id]);
12103
12355
  await db.execute("DELETE FROM project_todos WHERE projectId = ?", [id]);
12356
+ await db.execute("DELETE FROM project_env_vars WHERE projectId = ?", [id]);
12104
12357
  await db.execute("DELETE FROM projects WHERE id = ?", [id]);
12105
12358
  } finally {
12106
12359
  await db.execute("PRAGMA foreign_keys = ON");
@@ -12318,7 +12571,7 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
12318
12571
  }
12319
12572
  let conversationId = thread.currentConversationId;
12320
12573
  if (!conversationId) {
12321
- conversationId = randomUUID5();
12574
+ conversationId = randomUUID6();
12322
12575
  const db2 = await getDatabase();
12323
12576
  await db2.execute({
12324
12577
  sql: "DELETE FROM todos WHERE threadId = ?",
@@ -12628,7 +12881,7 @@ ${result.output}`;
12628
12881
  };
12629
12882
  processingStateManager.pushEvent(threadId, iterationEvent);
12630
12883
  yield iterationEvent;
12631
- const newConversationId = randomUUID5();
12884
+ const newConversationId = randomUUID6();
12632
12885
  await threadManager.updateThread(threadId, {
12633
12886
  currentConversationId: newConversationId
12634
12887
  });
@@ -13061,7 +13314,7 @@ ${summary}`;
13061
13314
  }
13062
13315
 
13063
13316
  // src/features/chat/chat-delete.route.ts
13064
- import { randomUUID as randomUUID6 } from "crypto";
13317
+ import { randomUUID as randomUUID7 } from "crypto";
13065
13318
  init_database();
13066
13319
  async function deleteChat(c, threadManager) {
13067
13320
  try {
@@ -13083,7 +13336,7 @@ async function deleteChat(c, threadManager) {
13083
13336
  sql: "DELETE FROM todos WHERE threadId = ?",
13084
13337
  args: [threadId]
13085
13338
  });
13086
- const newConversationId = randomUUID6();
13339
+ const newConversationId = randomUUID7();
13087
13340
  await threadManager.updateThread(threadId, { currentConversationId: newConversationId });
13088
13341
  return successResponse(
13089
13342
  c,
@@ -13207,10 +13460,10 @@ function createChatRoutes(threadManager, agentExecutor, conversationManager, pro
13207
13460
  init_database();
13208
13461
 
13209
13462
  // src/features/conversations/conversations.database.ts
13210
- import { randomUUID as randomUUID7 } from "crypto";
13463
+ import { randomUUID as randomUUID8 } from "crypto";
13211
13464
  async function insertMessage(db, threadId, conversationId, message, model, attachments, planMode, imageMode, ralphMode, checkpointRef) {
13212
13465
  try {
13213
- const messageId = randomUUID7();
13466
+ const messageId = randomUUID8();
13214
13467
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13215
13468
  await db.execute(
13216
13469
  `
@@ -14717,28 +14970,6 @@ function deserializeThread(row) {
14717
14970
  };
14718
14971
  }
14719
14972
 
14720
- // src/database/database.encryption.ts
14721
- async function encryptProviderKey(key) {
14722
- if (!key) {
14723
- return "";
14724
- }
14725
- return await encrypt(key);
14726
- }
14727
- async function decryptProviderKey(encrypted) {
14728
- if (!encrypted) {
14729
- return "";
14730
- }
14731
- try {
14732
- return await decrypt(encrypted);
14733
- } catch (error) {
14734
- console.error("Failed to decrypt provider key:", error);
14735
- throw error;
14736
- }
14737
- }
14738
- function isEncryptedKey(value) {
14739
- return isEncrypted(value);
14740
- }
14741
-
14742
14973
  // src/features/metadata/metadata.manager.ts
14743
14974
  init_tarsk_debug();
14744
14975
  function normalizeProviderSlug(provider) {
@@ -16815,6 +17046,111 @@ async function handleGetSystemPrompt(c, projectManager) {
16815
17046
  }
16816
17047
  }
16817
17048
 
17049
+ // src/features/projects/projects-env-vars.route.ts
17050
+ init_database();
17051
+ async function handleGetProjectEnvVars(c, projectManager) {
17052
+ try {
17053
+ const projectId = c.req.param("id");
17054
+ if (!projectId) {
17055
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project ID is required", 400);
17056
+ }
17057
+ const project = await projectManager.getProject(projectId);
17058
+ if (!project) {
17059
+ return errorResponse(c, ErrorCodes.PROJECT_NOT_FOUND, `Project not found: ${projectId}`, 404);
17060
+ }
17061
+ const db = await getDatabase();
17062
+ const rows = await listProjectEnvVarNames(db, projectId);
17063
+ const vars = rows.map((row) => ({
17064
+ name: row.name,
17065
+ hasValue: true
17066
+ }));
17067
+ return c.json({ vars });
17068
+ } catch (error) {
17069
+ return errorResponse(
17070
+ c,
17071
+ ErrorCodes.GET_PROJECT_ERROR,
17072
+ "Failed to get project environment variables",
17073
+ 500,
17074
+ error instanceof Error ? error.message : String(error)
17075
+ );
17076
+ }
17077
+ }
17078
+ async function handleSyncProjectEnvVars(c, projectManager) {
17079
+ try {
17080
+ const projectId = c.req.param("id");
17081
+ if (!projectId) {
17082
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Project ID is required", 400);
17083
+ }
17084
+ const project = await projectManager.getProject(projectId);
17085
+ if (!project) {
17086
+ return errorResponse(c, ErrorCodes.PROJECT_NOT_FOUND, `Project not found: ${projectId}`, 404);
17087
+ }
17088
+ const body = await c.req.json();
17089
+ if (!body.vars || !Array.isArray(body.vars)) {
17090
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "vars array is required", 400);
17091
+ }
17092
+ const seenNames = /* @__PURE__ */ new Set();
17093
+ const namesToKeep = [];
17094
+ for (const entry of body.vars) {
17095
+ if (!entry || typeof entry.name !== "string") {
17096
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Each variable must have a name", 400);
17097
+ }
17098
+ const name = entry.name.trim();
17099
+ if (!name) {
17100
+ return errorResponse(c, ErrorCodes.INVALID_REQUEST, "Variable name cannot be empty", 400);
17101
+ }
17102
+ if (!isValidProjectEnvVarName(name)) {
17103
+ return errorResponse(
17104
+ c,
17105
+ ErrorCodes.INVALID_REQUEST,
17106
+ `Invalid environment variable name: ${name}`,
17107
+ 400
17108
+ );
17109
+ }
17110
+ if (seenNames.has(name)) {
17111
+ return errorResponse(
17112
+ c,
17113
+ ErrorCodes.INVALID_REQUEST,
17114
+ `Duplicate environment variable name: ${name}`,
17115
+ 400
17116
+ );
17117
+ }
17118
+ seenNames.add(name);
17119
+ namesToKeep.push(name);
17120
+ }
17121
+ const db = await getDatabase();
17122
+ const existingRows = await listProjectEnvVarNames(db, projectId);
17123
+ const existingNames = new Set(existingRows.map((row) => row.name));
17124
+ for (const entry of body.vars) {
17125
+ const name = entry.name.trim();
17126
+ const value = typeof entry.value === "string" ? entry.value : "";
17127
+ if (value) {
17128
+ await upsertProjectEnvVar(db, projectId, name, value);
17129
+ continue;
17130
+ }
17131
+ if (!existingNames.has(name)) {
17132
+ return errorResponse(
17133
+ c,
17134
+ ErrorCodes.INVALID_REQUEST,
17135
+ `Value is required for new environment variable: ${name}`,
17136
+ 400
17137
+ );
17138
+ }
17139
+ }
17140
+ await deleteProjectEnvVarsNotIn(db, projectId, namesToKeep);
17141
+ clearMCPToolCache(project.path);
17142
+ return c.json({ success: true });
17143
+ } catch (error) {
17144
+ return errorResponse(
17145
+ c,
17146
+ ErrorCodes.UPDATE_PROJECT_ERROR,
17147
+ "Failed to save project environment variables",
17148
+ 500,
17149
+ error instanceof Error ? error.message : String(error)
17150
+ );
17151
+ }
17152
+ }
17153
+
16818
17154
  // src/features/projects/projects.routes.ts
16819
17155
  function createProjectRoutes(projectManager, threadManager) {
16820
17156
  const router = new Hono8();
@@ -16886,6 +17222,12 @@ function createProjectRoutes(projectManager, threadManager) {
16886
17222
  router.get("/:id/system-prompt", async (c) => {
16887
17223
  return handleGetSystemPrompt(c, projectManager);
16888
17224
  });
17225
+ router.get("/:id/env-vars", async (c) => {
17226
+ return handleGetProjectEnvVars(c, projectManager);
17227
+ });
17228
+ router.put("/:id/env-vars", async (c) => {
17229
+ return handleSyncProjectEnvVars(c, projectManager);
17230
+ });
16889
17231
  return router;
16890
17232
  }
16891
17233
 
@@ -17112,7 +17454,7 @@ var ProcessManager = class extends EventEmitter {
17112
17454
 
17113
17455
  // src/features/projects/projects.creator.ts
17114
17456
  init_utils();
17115
- import { randomUUID as randomUUID8 } from "crypto";
17457
+ import { randomUUID as randomUUID9 } from "crypto";
17116
17458
  import { basename as basename2, join as join23, relative as relative5 } from "path";
17117
17459
  import { access as access3, mkdir as mkdir4, rm as rm3, stat as stat3, readdir as readdir8 } from "fs/promises";
17118
17460
 
@@ -18298,16 +18640,25 @@ var ProjectCreator = class {
18298
18640
  }
18299
18641
  let initialThreadId;
18300
18642
  try {
18301
- const projectId = randomUUID8();
18643
+ const projectId = randomUUID9();
18302
18644
  const existingProjects = await this.metadataManager.loadProjects();
18303
18645
  const existingNames = existingProjects.map((p) => p.name);
18304
18646
  const projectName = this.ensureUniqueProjectName(
18305
18647
  this.deriveProjectName(gitUrl),
18306
18648
  existingNames
18307
18649
  );
18308
- initialThreadId = randomUUID8();
18650
+ initialThreadId = randomUUID9();
18309
18651
  const initialThreadTitle = generateRandomThreadName();
18310
- const firstThreadPath = this.generateThreadPath(projectId, initialThreadId);
18652
+ const baseFolderName = this.toFolderName(this.deriveProjectName(gitUrl));
18653
+ if (!baseFolderName) {
18654
+ yield {
18655
+ type: "error",
18656
+ message: "Git URL must produce a valid folder name"
18657
+ };
18658
+ return;
18659
+ }
18660
+ const uniqueFolderName = await this.ensureUniqueFolderName(baseFolderName);
18661
+ const firstThreadPath = join23(this.rootFolder, uniqueFolderName);
18311
18662
  this.processingStateManager?.setProcessing(initialThreadId);
18312
18663
  yield {
18313
18664
  type: "progress",
@@ -18445,8 +18796,8 @@ var ProjectCreator = class {
18445
18796
  };
18446
18797
  return;
18447
18798
  }
18448
- const projectId = randomUUID8();
18449
- const initialThreadId = randomUUID8();
18799
+ const projectId = randomUUID9();
18800
+ const initialThreadId = randomUUID9();
18450
18801
  const initialThreadTitle = generateRandomThreadName();
18451
18802
  this.processingStateManager?.setProcessing(initialThreadId);
18452
18803
  try {
@@ -18557,20 +18908,14 @@ var ProjectCreator = class {
18557
18908
  };
18558
18909
  return;
18559
18910
  }
18560
- const projectId = randomUUID8();
18561
- const initialThreadId = randomUUID8();
18911
+ const projectId = randomUUID9();
18912
+ const initialThreadId = randomUUID9();
18562
18913
  const initialThreadTitle = generateRandomThreadName();
18563
18914
  let folderPath;
18564
18915
  let projectMetadataSaved = false;
18565
18916
  this.processingStateManager?.setProcessing(initialThreadId);
18566
18917
  try {
18567
- const existingProjects = await this.metadataManager.loadProjects();
18568
- const existingNames = existingProjects.map((p) => p.name);
18569
- const uniqueProjectName = this.ensureUniqueProjectName(trimmedProjectName, existingNames);
18570
- const uniqueFolderName = this.toFolderName(uniqueProjectName);
18571
- if (!uniqueFolderName) {
18572
- throw new Error("Unique project name must produce a valid folder name");
18573
- }
18918
+ const uniqueFolderName = await this.ensureUniqueFolderName(folderName);
18574
18919
  folderPath = join23(this.rootFolder, uniqueFolderName);
18575
18920
  yield {
18576
18921
  type: "progress",
@@ -18582,7 +18927,7 @@ var ProjectCreator = class {
18582
18927
  await this.gitManager.initRepository(folderPath);
18583
18928
  const project = {
18584
18929
  id: projectId,
18585
- name: uniqueProjectName,
18930
+ name: trimmedProjectName,
18586
18931
  gitUrl: "",
18587
18932
  path: folderPath,
18588
18933
  createdAt: /* @__PURE__ */ new Date(),
@@ -18607,7 +18952,7 @@ var ProjectCreator = class {
18607
18952
  this.processingStateManager?.clearProcessing(initialThreadId);
18608
18953
  yield {
18609
18954
  type: "progress",
18610
- message: `Folder project "${uniqueProjectName}" created successfully`
18955
+ message: `Folder project "${trimmedProjectName}" created successfully`
18611
18956
  };
18612
18957
  yield {
18613
18958
  type: "complete",
@@ -18663,6 +19008,23 @@ var ProjectCreator = class {
18663
19008
  toFolderName(name) {
18664
19009
  return name.trim().toLowerCase().replace(/[\\/]+/g, " ").replace(/[^a-z0-9._ -]+/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
18665
19010
  }
19011
+ async folderExists(folderPath) {
19012
+ try {
19013
+ await stat3(folderPath);
19014
+ return true;
19015
+ } catch {
19016
+ return false;
19017
+ }
19018
+ }
19019
+ async ensureUniqueFolderName(baseFolderName) {
19020
+ let candidate = baseFolderName;
19021
+ let counter = 1;
19022
+ while (await this.folderExists(join23(this.rootFolder, candidate))) {
19023
+ candidate = `${baseFolderName}-${counter}`;
19024
+ counter++;
19025
+ }
19026
+ return candidate;
19027
+ }
18666
19028
  async *createProjectFromFolder() {
18667
19029
  await loadUtils();
18668
19030
  if (!Utils) {
@@ -18689,8 +19051,8 @@ var ProjectCreator = class {
18689
19051
  yield { type: "error", message: `Cannot access folder: ${folderPath}` };
18690
19052
  return;
18691
19053
  }
18692
- const projectId = randomUUID8();
18693
- const initialThreadId = randomUUID8();
19054
+ const projectId = randomUUID9();
19055
+ const initialThreadId = randomUUID9();
18694
19056
  const initialThreadTitle = generateRandomThreadName();
18695
19057
  this.processingStateManager?.setProcessing(initialThreadId);
18696
19058
  try {
@@ -20729,7 +21091,7 @@ function createRuleRoutes(router, projectManager) {
20729
21091
 
20730
21092
  // src/features/threads/threads.manager.ts
20731
21093
  init_utils();
20732
- import { randomUUID as randomUUID9 } from "crypto";
21094
+ import { randomUUID as randomUUID10 } from "crypto";
20733
21095
  import { join as join26 } from "path";
20734
21096
  import { execSync as execSync3 } from "child_process";
20735
21097
  import { rm as rm5, stat as stat4, mkdir as mkdir6 } from "fs/promises";
@@ -20796,7 +21158,7 @@ var ThreadManagerImpl = class {
20796
21158
  } catch {
20797
21159
  await mkdir6(project.path, { recursive: true });
20798
21160
  }
20799
- threadId = randomUUID9();
21161
+ threadId = randomUUID10();
20800
21162
  const threadPath = this.generateThreadPath(projectId, threadId);
20801
21163
  const existingThreads = await this.metadataManager.loadThreads();
20802
21164
  const existingTitles = new Set(
@@ -21354,7 +21716,7 @@ import { glob as glob2 } from "glob";
21354
21716
 
21355
21717
  // src/features/project-scripts/project-scripts.database.ts
21356
21718
  init_database();
21357
- import { randomUUID as randomUUID10 } from "crypto";
21719
+ import { randomUUID as randomUUID11 } from "crypto";
21358
21720
  async function getScriptsByProject(db, projectId) {
21359
21721
  const result = await db.execute({
21360
21722
  sql: `SELECT id, projectId, workspace, name, command, friendlyName, updatedAt
@@ -21371,7 +21733,7 @@ async function upsertProjectScripts(projectId, scripts) {
21371
21733
  sql: `INSERT OR REPLACE INTO project_scripts (id, projectId, workspace, name, command, friendlyName, updatedAt)
21372
21734
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
21373
21735
  args: [
21374
- randomUUID10(),
21736
+ randomUUID11(),
21375
21737
  projectId,
21376
21738
  script.workspace,
21377
21739
  script.name,
@@ -22153,22 +22515,7 @@ async function handleCreateExplorerFile(c, threadManager) {
22153
22515
  );
22154
22516
  }
22155
22517
  }
22156
- var MEDIA_MIME_TYPES = {
22157
- png: "image/png",
22158
- jpg: "image/jpeg",
22159
- jpeg: "image/jpeg",
22160
- gif: "image/gif",
22161
- webp: "image/webp",
22162
- svg: "image/svg+xml",
22163
- ico: "image/x-icon",
22164
- bmp: "image/bmp",
22165
- tiff: "image/tiff",
22166
- tif: "image/tiff",
22167
- mp4: "video/mp4",
22168
- webm: "video/webm",
22169
- mov: "video/quicktime",
22170
- avi: "video/x-msvideo"
22171
- };
22518
+ var MEDIA_MIME_TYPES = EXPLORER_MEDIA_MIME_TYPES;
22172
22519
  async function handleGetExplorerMedia(c, threadManager) {
22173
22520
  try {
22174
22521
  const threadId = c.req.param("id");
@@ -22195,10 +22542,15 @@ async function handleGetExplorerMedia(c, threadManager) {
22195
22542
  } catch {
22196
22543
  return c.json({ error: { code: "NOT_FOUND", message: `File not found: ${filePath}` } }, 404);
22197
22544
  }
22198
- const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
22545
+ const ext = getFileExtension(filePath);
22199
22546
  const contentType = MEDIA_MIME_TYPES[ext] ?? "application/octet-stream";
22200
22547
  const data = await readFile14(absPath);
22201
- return new Response(data, { headers: { "Content-Type": contentType } });
22548
+ return new Response(data, {
22549
+ headers: {
22550
+ "Content-Type": contentType,
22551
+ "Cross-Origin-Resource-Policy": "cross-origin"
22552
+ }
22553
+ });
22202
22554
  } catch (error) {
22203
22555
  return errorResponse(
22204
22556
  c,
@@ -22564,6 +22916,17 @@ Links to important documentation, tools, or references.
22564
22916
  if (fileStat.isDirectory()) {
22565
22917
  return c.json({ error: { code: "BAD_REQUEST", message: "Cannot read a directory" } }, 400);
22566
22918
  }
22919
+ if (getExplorerFileMediaType(filePath)) {
22920
+ return c.json(
22921
+ {
22922
+ error: {
22923
+ code: "BAD_REQUEST",
22924
+ message: "Binary media files cannot be read as text. Use the explorer media endpoint."
22925
+ }
22926
+ },
22927
+ 400
22928
+ );
22929
+ }
22567
22930
  const content = await readFile15(absPath, "utf-8");
22568
22931
  return c.json({ content, path: filePath });
22569
22932
  } catch (error) {
@@ -25910,11 +26273,81 @@ function createProjectTodosRoutes(metadataManager) {
25910
26273
  // src/features/account/account.routes.ts
25911
26274
  import { Hono as Hono19 } from "hono";
25912
26275
 
26276
+ // src/core/network-addresses.ts
26277
+ import { networkInterfaces as networkInterfaces2 } from "os";
26278
+ function isIPv4Address(family) {
26279
+ return family === "IPv4" || family === 4;
26280
+ }
26281
+ function getLocalNetworkAddresses() {
26282
+ const addresses = /* @__PURE__ */ new Set();
26283
+ for (const iface of Object.values(networkInterfaces2())) {
26284
+ if (!iface) {
26285
+ continue;
26286
+ }
26287
+ for (const config of iface) {
26288
+ if (config.internal || !isIPv4Address(config.family)) {
26289
+ continue;
26290
+ }
26291
+ addresses.add(config.address);
26292
+ }
26293
+ }
26294
+ return [...addresses].sort();
26295
+ }
26296
+
26297
+ // src/core/server-bind-mode-store.ts
26298
+ import { existsSync as existsSync23, mkdirSync as mkdirSync2, readFileSync as readFileSync8, writeFileSync } from "fs";
26299
+ import { join as join32 } from "path";
26300
+
26301
+ // src/core/server-bind-mode.ts
26302
+ var DEFAULT_SERVER_BIND_MODE = "local";
26303
+ function normalizeServerBindMode(value) {
26304
+ return value === "network" ? "network" : "local";
26305
+ }
26306
+ function getServerBindHostname(mode) {
26307
+ return mode === "network" ? "0.0.0.0" : void 0;
26308
+ }
26309
+
26310
+ // src/core/server-bind-mode-store.ts
26311
+ var SERVER_BIND_MODE_FILE = join32(DATA_DIR, "server-bind-mode.json");
26312
+ function hasServerBindModeFile() {
26313
+ return existsSync23(SERVER_BIND_MODE_FILE);
26314
+ }
26315
+ function readServerBindMode() {
26316
+ try {
26317
+ if (!existsSync23(SERVER_BIND_MODE_FILE)) {
26318
+ return DEFAULT_SERVER_BIND_MODE;
26319
+ }
26320
+ const raw = readFileSync8(SERVER_BIND_MODE_FILE, "utf8");
26321
+ const parsed = JSON.parse(raw);
26322
+ return normalizeServerBindMode(parsed.serverBindMode);
26323
+ } catch {
26324
+ return DEFAULT_SERVER_BIND_MODE;
26325
+ }
26326
+ }
26327
+ function writeServerBindMode(mode) {
26328
+ mkdirSync2(DATA_DIR, { recursive: true });
26329
+ writeFileSync(
26330
+ SERVER_BIND_MODE_FILE,
26331
+ `${JSON.stringify({ serverBindMode: mode }, null, 2)}
26332
+ `,
26333
+ "utf8"
26334
+ );
26335
+ }
26336
+
25913
26337
  // src/features/account/account-settings.route.ts
25914
26338
  init_database();
25915
26339
  var DEFAULT_MAXIMUM_TURN_COUNT = 50;
25916
26340
  var KEY_MAXIMUM_TURN_COUNT = "MaximumTurnCount";
25917
26341
  var KEY_USE_TOOLS_THROUGH_CODE = "UseToolsThroughCode";
26342
+ var LEGACY_KEY_SERVER_BIND_MODE = "ServerBindMode";
26343
+ function buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCode, serverBindMode) {
26344
+ return {
26345
+ maximumTurnCount: normalizeMaximumTurnCount(rawMaximumTurnCount),
26346
+ useToolsThroughCode: rawUseToolsThroughCode !== false,
26347
+ serverBindMode,
26348
+ networkAddresses: serverBindMode === "network" ? getLocalNetworkAddresses() : []
26349
+ };
26350
+ }
25918
26351
  function normalizeMaximumTurnCount(value) {
25919
26352
  if (typeof value !== "number" || !Number.isFinite(value)) {
25920
26353
  return DEFAULT_MAXIMUM_TURN_COUNT;
@@ -25925,14 +26358,27 @@ function normalizeMaximumTurnCount(value) {
25925
26358
  }
25926
26359
  return normalized;
25927
26360
  }
26361
+ async function getServerBindModeSetting() {
26362
+ if (hasServerBindModeFile()) {
26363
+ return readServerBindMode();
26364
+ }
26365
+ const db = await getDatabase();
26366
+ const legacyValue = await getState(db, LEGACY_KEY_SERVER_BIND_MODE);
26367
+ if (legacyValue === null) {
26368
+ return DEFAULT_SERVER_BIND_MODE;
26369
+ }
26370
+ const migratedMode = normalizeServerBindMode(legacyValue);
26371
+ writeServerBindMode(migratedMode);
26372
+ return migratedMode;
26373
+ }
25928
26374
  async function getAccountSettings(c) {
25929
26375
  const db = await getDatabase();
25930
26376
  const rawUseToolsThroughCode = await getState(db, KEY_USE_TOOLS_THROUGH_CODE);
25931
26377
  const rawMaximumTurnCount = await getState(db, KEY_MAXIMUM_TURN_COUNT);
25932
- return c.json({
25933
- maximumTurnCount: normalizeMaximumTurnCount(rawMaximumTurnCount),
25934
- useToolsThroughCode: rawUseToolsThroughCode !== false
25935
- });
26378
+ const serverBindMode = await getServerBindModeSetting();
26379
+ return c.json(
26380
+ buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCode, serverBindMode)
26381
+ );
25936
26382
  }
25937
26383
  async function updateAccountSettings(c) {
25938
26384
  const body = await c.req.json();
@@ -25943,12 +26389,15 @@ async function updateAccountSettings(c) {
25943
26389
  if (body.maximumTurnCount !== void 0) {
25944
26390
  await setState(db, KEY_MAXIMUM_TURN_COUNT, normalizeMaximumTurnCount(body.maximumTurnCount));
25945
26391
  }
26392
+ if (body.serverBindMode !== void 0) {
26393
+ writeServerBindMode(normalizeServerBindMode(body.serverBindMode));
26394
+ }
25946
26395
  const rawUseToolsThroughCode = await getState(db, KEY_USE_TOOLS_THROUGH_CODE);
25947
26396
  const rawMaximumTurnCount = await getState(db, KEY_MAXIMUM_TURN_COUNT);
25948
- return c.json({
25949
- maximumTurnCount: normalizeMaximumTurnCount(rawMaximumTurnCount),
25950
- useToolsThroughCode: rawUseToolsThroughCode !== false
25951
- });
26397
+ const serverBindMode = readServerBindMode();
26398
+ return c.json(
26399
+ buildAccountSettingsResponse(rawMaximumTurnCount, rawUseToolsThroughCode, serverBindMode)
26400
+ );
25952
26401
  }
25953
26402
 
25954
26403
  // src/features/account/account.routes.ts
@@ -26209,7 +26658,7 @@ async function clipboardWriteImage(c) {
26209
26658
  }
26210
26659
 
26211
26660
  // src/features/image/image-save.route.ts
26212
- import { join as join32 } from "path";
26661
+ import { join as join33 } from "path";
26213
26662
  var Utils5 = null;
26214
26663
  var utilsLoaded5 = false;
26215
26664
  async function loadUtils5() {
@@ -26246,7 +26695,7 @@ async function saveImage(c) {
26246
26695
  return c.json({ error: "imageUrl or imageData is required" }, 400);
26247
26696
  }
26248
26697
  const name = filename ?? `generated-image-${Date.now()}.png`;
26249
- const savePath = join32(Utils5.paths.downloads, name);
26698
+ const savePath = join33(Utils5.paths.downloads, name);
26250
26699
  await Bun.write(savePath, data);
26251
26700
  return c.json({ success: true, path: savePath });
26252
26701
  } catch (error) {
@@ -26452,7 +26901,7 @@ import { Hono as Hono25 } from "hono";
26452
26901
 
26453
26902
  // src/features/user-tasks/user-tasks.database.ts
26454
26903
  init_database();
26455
- import { randomUUID as randomUUID11 } from "crypto";
26904
+ import { randomUUID as randomUUID12 } from "crypto";
26456
26905
  async function getUserTasksByThread(db, threadId) {
26457
26906
  const result = await db.execute({
26458
26907
  sql: `SELECT id, threadId, content, createdAt, updatedAt
@@ -26465,7 +26914,7 @@ async function getUserTasksByThread(db, threadId) {
26465
26914
  }
26466
26915
  async function insertUserTask(threadId, content) {
26467
26916
  const db = await getDatabase();
26468
- const id = randomUUID11();
26917
+ const id = randomUUID12();
26469
26918
  const now = (/* @__PURE__ */ new Date()).toISOString();
26470
26919
  await db.execute({
26471
26920
  sql: `INSERT INTO user_tasks (id, threadId, content, createdAt, updatedAt)
@@ -26518,7 +26967,15 @@ function createUserTaskRoutes() {
26518
26967
  // src/server.ts
26519
26968
  var __filename = fileURLToPath2(import.meta.url);
26520
26969
  var __dirname = path5.dirname(__filename);
26970
+ var startPromise = null;
26521
26971
  async function startTarskServer(options) {
26972
+ if (startPromise) {
26973
+ return startPromise;
26974
+ }
26975
+ startPromise = startTarskServerInternal(options);
26976
+ return startPromise;
26977
+ }
26978
+ async function startTarskServerInternal(options) {
26522
26979
  const { isDebug: isDebug2, publicDir: publicDirOverride } = options;
26523
26980
  const port = isDebug2 ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
26524
26981
  const app = new Hono26();
@@ -26652,22 +27109,58 @@ async function startTarskServer(options) {
26652
27109
  };
26653
27110
  return c.json(errorResponse2, 404);
26654
27111
  });
27112
+ const serverBindMode = readServerBindMode();
27113
+ const hostname2 = getServerBindHostname(serverBindMode);
26655
27114
  const url = `http://localhost:${port}`;
26656
- process.stdout.write(`Tarsk started on ${url}
27115
+ const bound = await listenForTarskServer({
27116
+ fetch: app.fetch,
27117
+ hostname: hostname2,
27118
+ onListening: () => {
27119
+ process.stdout.write(`Tarsk started on ${url}
26657
27120
  `);
26658
- serve(
26659
- {
26660
- fetch: app.fetch,
26661
- port
26662
- },
26663
- () => {
27121
+ if (serverBindMode === "network") {
27122
+ for (const address of getLocalNetworkAddresses()) {
27123
+ process.stdout.write(` Network: http://${address}:${port}
27124
+ `);
27125
+ }
27126
+ }
26664
27127
  if (options.openBrowser) {
26665
27128
  open3(url).catch(() => {
26666
27129
  });
26667
27130
  }
27131
+ },
27132
+ port
27133
+ });
27134
+ if (!bound) {
27135
+ return { url, port, bound: false };
27136
+ }
27137
+ return { url, port, bound: true };
27138
+ }
27139
+ async function listenForTarskServer(options) {
27140
+ const server = createAdaptorServer({
27141
+ fetch: options.fetch,
27142
+ hostname: options.hostname,
27143
+ port: options.port
27144
+ });
27145
+ return new Promise((resolve6, reject) => {
27146
+ server.once("error", (error) => {
27147
+ if (error.code === "EADDRINUSE") {
27148
+ resolve6(false);
27149
+ return;
27150
+ }
27151
+ startPromise = null;
27152
+ reject(error);
27153
+ });
27154
+ function onListening() {
27155
+ options.onListening();
27156
+ resolve6(true);
26668
27157
  }
26669
- );
26670
- return { url, port };
27158
+ if (options.hostname) {
27159
+ server.listen(options.port, options.hostname, onListening);
27160
+ } else {
27161
+ server.listen(options.port, onListening);
27162
+ }
27163
+ });
26671
27164
  }
26672
27165
 
26673
27166
  // src/index.ts
@@ -26675,7 +27168,7 @@ import fs4 from "fs";
26675
27168
  import path6 from "path";
26676
27169
  var args = process.argv.slice(2);
26677
27170
  var isDebug = args.includes("--debug");
26678
- var shouldOpenBrowser = args.includes("--open");
27171
+ var isServerOnly = args.includes("--server");
26679
27172
  if (!isDebug) {
26680
27173
  console.log = () => {
26681
27174
  };
@@ -26709,5 +27202,5 @@ if (!isDebug) {
26709
27202
  var isDevelopment = process.env.MODE === "development";
26710
27203
  await startTarskServer({
26711
27204
  isDebug,
26712
- openBrowser: shouldOpenBrowser || !isDevelopment
27205
+ openBrowser: !isServerOnly && !isDevelopment
26713
27206
  });