openwork 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,7 @@ Or configure them in-app via the settings panel.
43
43
  | --------- | ----------------------------------------------------------------- |
44
44
  | Anthropic | Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5, Claude Opus 4.1, Claude Sonnet 4 |
45
45
  | OpenAI | GPT-5.2, GPT-5.1, o3, o3 Mini, o4 Mini, o1, GPT-4.1, GPT-4o |
46
+ | Google | Gemini 3 Pro Preview, Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini 2.5 Flash Lite |
46
47
 
47
48
  ## Contributing
48
49
 
package/out/main/index.js CHANGED
@@ -10,6 +10,7 @@ const fs = require("fs");
10
10
  const os = require("os");
11
11
  const anthropic = require("@langchain/anthropic");
12
12
  const openai = require("@langchain/openai");
13
+ const googleGenai = require("@langchain/google-genai");
13
14
  const initSqlJs = require("sql.js");
14
15
  const langgraphCheckpoint = require("@langchain/langgraph-checkpoint");
15
16
  const node_child_process = require("node:child_process");
@@ -104,7 +105,8 @@ const OPENWORK_DIR = path.join(os.homedir(), ".openwork");
104
105
  const ENV_FILE = path.join(OPENWORK_DIR, ".env");
105
106
  const ENV_VAR_NAMES = {
106
107
  anthropic: "ANTHROPIC_API_KEY",
107
- openai: "OPENAI_API_KEY"
108
+ openai: "OPENAI_API_KEY",
109
+ google: "GOOGLE_API_KEY"
108
110
  };
109
111
  function getOpenworkDir() {
110
112
  if (!fs.existsSync(OPENWORK_DIR)) {
@@ -115,8 +117,21 @@ function getOpenworkDir() {
115
117
  function getDbPath() {
116
118
  return path.join(getOpenworkDir(), "openwork.sqlite");
117
119
  }
118
- function getCheckpointDbPath() {
119
- return path.join(getOpenworkDir(), "langgraph.sqlite");
120
+ function getThreadCheckpointDir() {
121
+ const dir = path.join(getOpenworkDir(), "threads");
122
+ if (!fs.existsSync(dir)) {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ }
125
+ return dir;
126
+ }
127
+ function getThreadCheckpointPath(threadId) {
128
+ return path.join(getThreadCheckpointDir(), `${threadId}.sqlite`);
129
+ }
130
+ function deleteThreadCheckpoint(threadId) {
131
+ const path2 = getThreadCheckpointPath(threadId);
132
+ if (fs.existsSync(path2)) {
133
+ fs.unlinkSync(path2);
134
+ }
120
135
  }
121
136
  function getEnvFilePath() {
122
137
  return ENV_FILE;
@@ -175,7 +190,8 @@ const store = new Store({
175
190
  });
176
191
  const PROVIDERS = [
177
192
  { id: "anthropic", name: "Anthropic" },
178
- { id: "openai", name: "OpenAI" }
193
+ { id: "openai", name: "OpenAI" },
194
+ { id: "google", name: "Google" }
179
195
  ];
180
196
  const AVAILABLE_MODELS = [
181
197
  // Anthropic Claude 4.5 series (latest as of Jan 2026)
@@ -310,6 +326,39 @@ const AVAILABLE_MODELS = [
310
326
  model: "gpt-4o-mini",
311
327
  description: "Cost-efficient variant with faster response times",
312
328
  available: true
329
+ },
330
+ // Google Gemini models
331
+ {
332
+ id: "gemini-3-pro-preview",
333
+ name: "Gemini 3 Pro Preview",
334
+ provider: "google",
335
+ model: "gemini-3-pro-preview",
336
+ description: "State-of-the-art reasoning and multimodal understanding",
337
+ available: true
338
+ },
339
+ {
340
+ id: "gemini-2.5-pro",
341
+ name: "Gemini 2.5 Pro",
342
+ provider: "google",
343
+ model: "gemini-2.5-pro",
344
+ description: "High-capability model for complex reasoning and coding",
345
+ available: true
346
+ },
347
+ {
348
+ id: "gemini-2.5-flash",
349
+ name: "Gemini 2.5 Flash",
350
+ provider: "google",
351
+ model: "gemini-2.5-flash",
352
+ description: "Lightning-fast with balance of intelligence and latency",
353
+ available: true
354
+ },
355
+ {
356
+ id: "gemini-2.5-flash-lite",
357
+ name: "Gemini 2.5 Flash Lite",
358
+ provider: "google",
359
+ model: "gemini-2.5-flash-lite",
360
+ description: "Fast, low-cost, high-performance model",
361
+ available: true
313
362
  }
314
363
  ];
315
364
  function registerModelHandlers(ipcMain) {
@@ -495,6 +544,47 @@ function registerModelHandlers(ipcMain) {
495
544
  }
496
545
  }
497
546
  );
547
+ ipcMain.handle(
548
+ "workspace:readBinaryFile",
549
+ async (_event, { threadId, filePath }) => {
550
+ const { getThread: getThread2 } = await Promise.resolve().then(() => index);
551
+ const thread = getThread2(threadId);
552
+ const metadata = thread?.metadata ? JSON.parse(thread.metadata) : {};
553
+ const workspacePath = metadata.workspacePath;
554
+ if (!workspacePath) {
555
+ return {
556
+ success: false,
557
+ error: "No workspace folder linked"
558
+ };
559
+ }
560
+ try {
561
+ const relativePath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
562
+ const fullPath = path__namespace.join(workspacePath, relativePath);
563
+ const resolvedPath = path__namespace.resolve(fullPath);
564
+ const resolvedWorkspace = path__namespace.resolve(workspacePath);
565
+ if (!resolvedPath.startsWith(resolvedWorkspace)) {
566
+ return { success: false, error: "Access denied: path outside workspace" };
567
+ }
568
+ const stat = await fs__namespace$1.stat(fullPath);
569
+ if (stat.isDirectory()) {
570
+ return { success: false, error: "Cannot read directory as file" };
571
+ }
572
+ const buffer = await fs__namespace$1.readFile(fullPath);
573
+ const base64 = buffer.toString("base64");
574
+ return {
575
+ success: true,
576
+ content: base64,
577
+ size: stat.size,
578
+ modified_at: stat.mtime.toISOString()
579
+ };
580
+ } catch (e) {
581
+ return {
582
+ success: false,
583
+ error: e instanceof Error ? e.message : "Unknown error"
584
+ };
585
+ }
586
+ }
587
+ );
498
588
  }
499
589
  function getDefaultModel() {
500
590
  return store.get("defaultModel", "claude-sonnet-4-5-20250929");
@@ -516,8 +606,29 @@ class SqlJsSaver extends langgraphCheckpoint.BaseCheckpointSaver {
516
606
  if (this.db) return;
517
607
  const SQL = await initSqlJs();
518
608
  if (fs.existsSync(this.dbPath)) {
519
- const buffer = fs.readFileSync(this.dbPath);
520
- this.db = new SQL.Database(buffer);
609
+ const stats = fs.statSync(this.dbPath);
610
+ const MAX_DB_SIZE = 100 * 1024 * 1024;
611
+ if (stats.size > MAX_DB_SIZE) {
612
+ console.warn(
613
+ `[SqlJsSaver] Database file is too large (${Math.round(stats.size / 1024 / 1024)}MB). Creating fresh database to prevent memory issues.`
614
+ );
615
+ const backupPath = this.dbPath + ".bak." + Date.now();
616
+ try {
617
+ fs.renameSync(this.dbPath, backupPath);
618
+ console.log(`[SqlJsSaver] Old database backed up to: ${backupPath}`);
619
+ } catch (e) {
620
+ console.warn("[SqlJsSaver] Could not backup old database:", e);
621
+ try {
622
+ fs.unlinkSync(this.dbPath);
623
+ } catch (e2) {
624
+ console.error("[SqlJsSaver] Could not delete old database:", e2);
625
+ }
626
+ }
627
+ this.db = new SQL.Database();
628
+ } else {
629
+ const buffer = fs.readFileSync(this.dbPath);
630
+ this.db = new SQL.Database(buffer);
631
+ }
521
632
  } else {
522
633
  const dir = path.dirname(this.dbPath);
523
634
  if (!fs.existsSync(dir)) {
@@ -1075,14 +1186,24 @@ function getSystemPrompt(workspacePath) {
1075
1186
  `;
1076
1187
  return workingDirSection + BASE_SYSTEM_PROMPT;
1077
1188
  }
1078
- let checkpointer = null;
1079
- async function getCheckpointer() {
1189
+ const checkpointers = /* @__PURE__ */ new Map();
1190
+ async function getCheckpointer(threadId) {
1191
+ let checkpointer = checkpointers.get(threadId);
1080
1192
  if (!checkpointer) {
1081
- checkpointer = new SqlJsSaver(getCheckpointDbPath());
1193
+ const dbPath = getThreadCheckpointPath(threadId);
1194
+ checkpointer = new SqlJsSaver(dbPath);
1082
1195
  await checkpointer.initialize();
1196
+ checkpointers.set(threadId, checkpointer);
1083
1197
  }
1084
1198
  return checkpointer;
1085
1199
  }
1200
+ async function closeCheckpointer(threadId) {
1201
+ const checkpointer = checkpointers.get(threadId);
1202
+ if (checkpointer) {
1203
+ await checkpointer.close();
1204
+ checkpointers.delete(threadId);
1205
+ }
1206
+ }
1086
1207
  function getModelInstance(modelId) {
1087
1208
  const model = modelId || getDefaultModel();
1088
1209
  console.log("[Runtime] Using model:", model);
@@ -1106,22 +1227,36 @@ function getModelInstance(modelId) {
1106
1227
  model,
1107
1228
  openAIApiKey: apiKey
1108
1229
  });
1230
+ } else if (model.startsWith("gemini")) {
1231
+ const apiKey = getApiKey("google");
1232
+ console.log("[Runtime] Google API key present:", !!apiKey);
1233
+ if (!apiKey) {
1234
+ throw new Error("Google API key not configured");
1235
+ }
1236
+ return new googleGenai.ChatGoogleGenerativeAI({
1237
+ model,
1238
+ apiKey
1239
+ });
1109
1240
  }
1110
1241
  return model;
1111
1242
  }
1112
1243
  async function createAgentRuntime(options) {
1113
- const { modelId, workspacePath } = options;
1244
+ const { threadId, modelId, workspacePath } = options;
1245
+ if (!threadId) {
1246
+ throw new Error("Thread ID is required for checkpointing.");
1247
+ }
1114
1248
  if (!workspacePath) {
1115
1249
  throw new Error(
1116
1250
  "Workspace path is required. Please select a workspace folder before running the agent."
1117
1251
  );
1118
1252
  }
1119
1253
  console.log("[Runtime] Creating agent runtime...");
1254
+ console.log("[Runtime] Thread ID:", threadId);
1120
1255
  console.log("[Runtime] Workspace path:", workspacePath);
1121
1256
  const model = getModelInstance(modelId);
1122
1257
  console.log("[Runtime] Model instance created:", typeof model);
1123
- const checkpointer2 = await getCheckpointer();
1124
- console.log("[Runtime] Checkpointer ready");
1258
+ const checkpointer = await getCheckpointer(threadId);
1259
+ console.log("[Runtime] Checkpointer ready for thread:", threadId);
1125
1260
  const backend = new LocalSandbox({
1126
1261
  rootDir: workspacePath,
1127
1262
  virtualMode: false,
@@ -1144,7 +1279,7 @@ async function createAgentRuntime(options) {
1144
1279
  The workspace root is: ${workspacePath}`;
1145
1280
  const agent = deepagents.createDeepAgent({
1146
1281
  model,
1147
- checkpointer: checkpointer2,
1282
+ checkpointer,
1148
1283
  backend,
1149
1284
  systemPrompt,
1150
1285
  // Custom filesystem prompt for absolute paths (requires deepagents update)
@@ -1360,7 +1495,7 @@ function registerAgentHandlers(ipcMain) {
1360
1495
  });
1361
1496
  return;
1362
1497
  }
1363
- const agent = await createAgentRuntime({ workspacePath });
1498
+ const agent = await createAgentRuntime({ threadId, workspacePath });
1364
1499
  const humanMessage = new messages.HumanMessage(message);
1365
1500
  const stream = await agent.stream(
1366
1501
  { messages: [humanMessage] },
@@ -1380,13 +1515,18 @@ function registerAgentHandlers(ipcMain) {
1380
1515
  data: JSON.parse(JSON.stringify(data))
1381
1516
  });
1382
1517
  }
1383
- window.webContents.send(channel, { type: "done" });
1518
+ if (!abortController.signal.aborted) {
1519
+ window.webContents.send(channel, { type: "done" });
1520
+ }
1384
1521
  } catch (error) {
1385
- console.error("[Agent] Error:", error);
1386
- window.webContents.send(channel, {
1387
- type: "error",
1388
- error: error instanceof Error ? error.message : "Unknown error"
1389
- });
1522
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
1523
+ if (!isAbortError) {
1524
+ console.error("[Agent] Error:", error);
1525
+ window.webContents.send(channel, {
1526
+ type: "error",
1527
+ error: error instanceof Error ? error.message : "Unknown error"
1528
+ });
1529
+ }
1390
1530
  } finally {
1391
1531
  window.removeListener("closed", onWindowClosed);
1392
1532
  activeRuns.delete(threadId);
@@ -1424,7 +1564,7 @@ function registerAgentHandlers(ipcMain) {
1424
1564
  const abortController = new AbortController();
1425
1565
  activeRuns.set(threadId, abortController);
1426
1566
  try {
1427
- const agent = await createAgentRuntime({ workspacePath });
1567
+ const agent = await createAgentRuntime({ threadId, workspacePath });
1428
1568
  const config = {
1429
1569
  configurable: { thread_id: threadId },
1430
1570
  signal: abortController.signal,
@@ -1443,13 +1583,18 @@ function registerAgentHandlers(ipcMain) {
1443
1583
  data: JSON.parse(JSON.stringify(data))
1444
1584
  });
1445
1585
  }
1446
- window.webContents.send(channel, { type: "done" });
1586
+ if (!abortController.signal.aborted) {
1587
+ window.webContents.send(channel, { type: "done" });
1588
+ }
1447
1589
  } catch (error) {
1448
- console.error("[Agent] Resume error:", error);
1449
- window.webContents.send(channel, {
1450
- type: "error",
1451
- error: error instanceof Error ? error.message : "Unknown error"
1452
- });
1590
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
1591
+ if (!isAbortError) {
1592
+ console.error("[Agent] Resume error:", error);
1593
+ window.webContents.send(channel, {
1594
+ type: "error",
1595
+ error: error instanceof Error ? error.message : "Unknown error"
1596
+ });
1597
+ }
1453
1598
  } finally {
1454
1599
  activeRuns.delete(threadId);
1455
1600
  }
@@ -1482,7 +1627,7 @@ function registerAgentHandlers(ipcMain) {
1482
1627
  const abortController = new AbortController();
1483
1628
  activeRuns.set(threadId, abortController);
1484
1629
  try {
1485
- const agent = await createAgentRuntime({ workspacePath });
1630
+ const agent = await createAgentRuntime({ threadId, workspacePath });
1486
1631
  const config = {
1487
1632
  configurable: { thread_id: threadId },
1488
1633
  signal: abortController.signal,
@@ -1500,16 +1645,21 @@ function registerAgentHandlers(ipcMain) {
1500
1645
  data: JSON.parse(JSON.stringify(data))
1501
1646
  });
1502
1647
  }
1503
- window.webContents.send(channel, { type: "done" });
1648
+ if (!abortController.signal.aborted) {
1649
+ window.webContents.send(channel, { type: "done" });
1650
+ }
1504
1651
  } else if (decision.type === "reject") {
1505
1652
  window.webContents.send(channel, { type: "done" });
1506
1653
  }
1507
1654
  } catch (error) {
1508
- console.error("[Agent] Interrupt error:", error);
1509
- window.webContents.send(channel, {
1510
- type: "error",
1511
- error: error instanceof Error ? error.message : "Unknown error"
1512
- });
1655
+ const isAbortError = error instanceof Error && (error.name === "AbortError" || error.message.includes("aborted") || error.message.includes("Controller is already closed"));
1656
+ if (!isAbortError) {
1657
+ console.error("[Agent] Interrupt error:", error);
1658
+ window.webContents.send(channel, {
1659
+ type: "error",
1660
+ error: error instanceof Error ? error.message : "Unknown error"
1661
+ });
1662
+ }
1513
1663
  } finally {
1514
1664
  activeRuns.delete(threadId);
1515
1665
  }
@@ -1612,19 +1762,24 @@ function registerThreadHandlers(ipcMain) {
1612
1762
  deleteThread(threadId);
1613
1763
  console.log("[Threads] Deleted from metadata store");
1614
1764
  try {
1615
- const checkpointer2 = await getCheckpointer();
1616
- await checkpointer2.deleteThread(threadId);
1617
- console.log("[Threads] Deleted from checkpointer");
1765
+ await closeCheckpointer(threadId);
1766
+ console.log("[Threads] Closed checkpointer");
1767
+ } catch (e) {
1768
+ console.warn("[Threads] Failed to close checkpointer:", e);
1769
+ }
1770
+ try {
1771
+ deleteThreadCheckpoint(threadId);
1772
+ console.log("[Threads] Deleted checkpoint file");
1618
1773
  } catch (e) {
1619
- console.warn("[Threads] Failed to delete thread from checkpointer:", e);
1774
+ console.warn("[Threads] Failed to delete checkpoint file:", e);
1620
1775
  }
1621
1776
  });
1622
1777
  ipcMain.handle("threads:history", async (_event, threadId) => {
1623
1778
  try {
1624
- const checkpointer2 = await getCheckpointer();
1779
+ const checkpointer = await getCheckpointer(threadId);
1625
1780
  const history = [];
1626
1781
  const config = { configurable: { thread_id: threadId } };
1627
- for await (const checkpoint of checkpointer2.list(config, { limit: 50 })) {
1782
+ for await (const checkpoint of checkpointer.list(config, { limit: 50 })) {
1628
1783
  history.push(checkpoint);
1629
1784
  }
1630
1785
  return history;
@@ -1637,6 +1792,27 @@ function registerThreadHandlers(ipcMain) {
1637
1792
  return generateTitle(message);
1638
1793
  });
1639
1794
  }
1795
+ const originalConsoleError = console.error;
1796
+ console.error = (...args) => {
1797
+ const message = args.map((a) => String(a)).join(" ");
1798
+ if (message.includes("Controller is already closed") || message.includes("ERR_INVALID_STATE") || message.includes("StreamMessagesHandler") && message.includes("aborted")) {
1799
+ return;
1800
+ }
1801
+ originalConsoleError.apply(console, args);
1802
+ };
1803
+ process.on("uncaughtException", (error) => {
1804
+ if (error.message?.includes("Controller is already closed") || error.message?.includes("aborted")) {
1805
+ return;
1806
+ }
1807
+ originalConsoleError("Uncaught exception:", error);
1808
+ });
1809
+ process.on("unhandledRejection", (reason) => {
1810
+ const message = reason instanceof Error ? reason.message : String(reason);
1811
+ if (message?.includes("Controller is already closed") || message?.includes("aborted")) {
1812
+ return;
1813
+ }
1814
+ originalConsoleError("Unhandled rejection:", reason);
1815
+ });
1640
1816
  let mainWindow = null;
1641
1817
  const isDev = !electron.app.isPackaged;
1642
1818
  function createWindow() {
@@ -133,6 +133,9 @@ const api = {
133
133
  readFile: (threadId, filePath) => {
134
134
  return electron.ipcRenderer.invoke("workspace:readFile", { threadId, filePath });
135
135
  },
136
+ readBinaryFile: (threadId, filePath) => {
137
+ return electron.ipcRenderer.invoke("workspace:readBinaryFile", { threadId, filePath });
138
+ },
136
139
  // Listen for file changes in the workspace
137
140
  onFilesChanged: (callback) => {
138
141
  const handler = (_, data) => {