@computesdk/workbench 3.1.4 → 3.1.6

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
@@ -316,6 +316,67 @@ BL_WORKSPACE=xxx
316
316
  - **Auto-create**: First command automatically creates a sandbox
317
317
  - **Stay in context**: Workbench maintains "current sandbox" - no IDs to track
318
318
 
319
+ ## Debugging SDK Tests
320
+
321
+ The workbench is a full Node.js REPL with the ComputeSDK pre-loaded. You can reproduce any SDK test by calling the same methods interactively.
322
+
323
+ ### SDK to Workbench Mapping
324
+
325
+ | SDK Test Code | Workbench Equivalent |
326
+ |---------------|---------------------|
327
+ | `sandbox.runCommand('echo hi')` | `runCommand('echo hi')` or `getInstance().runCommand(...)` |
328
+ | `sandbox.terminal.create()` | `terminal.create()` |
329
+ | `sandbox.filesystem.readFile(path)` | `filesystem.readFile(path)` |
330
+ | `sandbox.getInfo()` | `sandboxInfo()` |
331
+ | `sandbox.getUrl({ port })` | `getUrl({ port: 3000 })` |
332
+
333
+ ### Example: Reproducing a Failing Test
334
+
335
+ If this test fails:
336
+
337
+ ```typescript
338
+ // From provider-compatibility.test.ts
339
+ it('command with streaming callbacks', async () => {
340
+ let stdoutCalled = false;
341
+ const result = await sandbox.runCommand('echo "hello"', {
342
+ onStdout: () => { stdoutCalled = true; },
343
+ });
344
+ expect(stdoutCalled).toBe(true);
345
+ });
346
+ ```
347
+
348
+ Reproduce it in workbench:
349
+
350
+ ```javascript
351
+ > ls('/home') // Auto-creates sandbox
352
+ > const sandbox = getInstance()
353
+ > let stdoutCalled = false
354
+ > const result = await sandbox.runCommand('echo "hello"', {
355
+ onStdout: (data) => { console.log('STDOUT:', data); stdoutCalled = true }
356
+ })
357
+ > stdoutCalled // Should be true
358
+ > result
359
+ ```
360
+
361
+ ### Verbose Mode
362
+
363
+ Enable verbose mode to see full response objects and WebSocket debug info:
364
+
365
+ ```javascript
366
+ > verbose() // Toggle on - shows full results and WebSocket frames
367
+ > ls('/home')
368
+ > verbose() // Toggle off
369
+ ```
370
+
371
+ ### Direct Shell Commands
372
+
373
+ Prefix with `$` to run shell commands directly (bypasses `@computesdk/cmd`):
374
+
375
+ ```javascript
376
+ > $echo "hello" | tr 'a-z' 'A-Z'
377
+ > $for i in 1 2 3; do echo $i; done
378
+ ```
379
+
319
380
  ## License
320
381
 
321
382
  MIT
@@ -42,6 +42,7 @@ function createState() {
42
42
  useDirectMode: false,
43
43
  // Default to gateway mode
44
44
  verbose: false,
45
+ // Enabled automatically for local provider
45
46
  compute: null
46
47
  };
47
48
  }
@@ -118,21 +119,27 @@ __export(output_exports, {
118
119
  showInfo: () => showInfo,
119
120
  showWelcome: () => showWelcome
120
121
  });
121
- function showWelcome(availableProviders, currentProvider, useDirectMode) {
122
+ function showWelcome(availableProviders, currentProvider, useDirectMode, localDaemonRunning = false) {
122
123
  console.log(c.bold(c.cyan("\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")));
123
124
  console.log(c.bold(c.cyan("\u2551 ComputeSDK Workbench \u2551")));
124
125
  console.log(c.bold(c.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n")));
125
126
  console.log(c.dim("Prompt shows connection status: > (disconnected) or provider:sandbox> (connected)\n"));
127
+ if (localDaemonRunning) {
128
+ console.log(c.green("Local daemon detected - auto-connecting...\n"));
129
+ }
126
130
  if (availableProviders.length > 0) {
127
131
  const backendProviders = availableProviders.filter((p) => p !== "gateway");
128
132
  console.log(`Providers available: ${backendProviders.join(", ")}`);
129
133
  if (currentProvider) {
130
- if (useDirectMode) {
131
- console.log(`Current provider: ${c.green(currentProvider)} (\u{1F517} direct mode)
134
+ if (currentProvider === "local") {
135
+ console.log(`Current provider: ${c.green("local")} (local daemon)
136
+ `);
137
+ } else if (useDirectMode) {
138
+ console.log(`Current provider: ${c.green(currentProvider)} (direct mode)
132
139
  `);
133
140
  } else {
134
141
  const backendProvider = currentProvider === "gateway" ? backendProviders[0] || "auto" : currentProvider;
135
- console.log(`Current provider: ${c.green(backendProvider)} (\u{1F310} via gateway)
142
+ console.log(`Current provider: ${c.green(backendProvider)} (via gateway)
136
143
  `);
137
144
  }
138
145
  } else {
@@ -141,7 +148,7 @@ ${c.dim('Tip: Use "provider <name>" to select a provider')}
141
148
  `);
142
149
  }
143
150
  } else {
144
- console.log(c.yellow("\u26A0\uFE0F No providers detected.\n"));
151
+ console.log(c.yellow("No providers detected.\n"));
145
152
  console.log("To get started:");
146
153
  console.log(" 1. Copy .env.example to .env");
147
154
  console.log(" 2. Add your provider credentials");
@@ -204,10 +211,14 @@ ${c.bold("Provider Modes:")}
204
211
  ${c.bold("Sandbox Management:")}
205
212
  ${c.cyan("restart")} Restart current sandbox
206
213
  ${c.cyan("destroy")} Destroy current sandbox
207
- ${c.cyan("connect <url> [token]")} Connect to existing sandbox via URL
208
- ${c.dim("Example: connect https://sandbox-123.localhost:8080")}
209
- ${c.dim("Example: connect https://sandbox-123.localhost:8080 your_token")}
210
214
  ${c.cyan("info")} Show sandbox info (provider, uptime)
215
+ ${c.cyan("connect <url> [token]")} Connect to sandbox via URL
216
+
217
+ ${c.bold("Local Daemon:")}
218
+ ${c.cyan("provider local")} Connect to local daemon's main sandbox
219
+ ${c.cyan("provider local list")} List local sandboxes
220
+ ${c.cyan("provider local <subdomain>")} Connect to specific local sandbox
221
+ ${c.dim("Example: provider local separate-snail-qkktux")}
211
222
 
212
223
  ${c.bold("Environment:")}
213
224
  ${c.cyan("env")} Show environment/credentials status
@@ -262,6 +273,24 @@ ${c.bold("Running Commands:")}
262
273
  ${c.cyan("sandboxInfo()")} ${c.dim("// Get sandbox details")}
263
274
  ${c.cyan("getInstance()")} ${c.dim("// Get native instance")}
264
275
 
276
+ ${c.dim("Terminal (PTY & Exec):")}
277
+ ${c.cyan("terminal.create({ pty: true })")} ${c.dim("// Create PTY terminal")}
278
+ ${c.cyan("terminal.create({ pty: false })")} ${c.dim("// Create exec terminal")}
279
+ ${c.cyan("terminal.list()")} ${c.dim("// List all terminals")}
280
+ ${c.cyan("terminal.retrieve(id)")} ${c.dim("// Get terminal by ID")}
281
+ ${c.cyan("terminal.destroy(id)")} ${c.dim("// Close terminal by ID")}
282
+
283
+ ${c.dim("PTY Terminal:")}
284
+ ${c.cyan("term = terminal.create({ pty: true })")}
285
+ ${c.cyan('term.on("output", (data) => console.log(data))')}
286
+ ${c.cyan('term.write("echo hello\\n")')}
287
+ ${c.cyan("term.destroy()")}
288
+
289
+ ${c.dim("Exec Terminal:")}
290
+ ${c.cyan("exec = terminal.create({ pty: false })")}
291
+ ${c.cyan('cmd = exec.command.run("ls -la")')}
292
+ ${c.cyan("cmd.stdout")} ${c.dim("// view output")}
293
+
265
294
  ${c.dim('Note: No need to use "await" - promises are auto-awaited!')}
266
295
 
267
296
  ${c.dim("Compute CLI:")}
@@ -566,12 +595,15 @@ var commands_exports = {};
566
595
  __export(commands_exports, {
567
596
  cleanupOnExit: () => cleanupOnExit,
568
597
  confirmSandboxSwitch: () => confirmSandboxSwitch,
598
+ connectToLocal: () => connectToLocal,
569
599
  connectToSandbox: () => connectToSandbox,
570
600
  createSandbox: () => createSandbox,
571
601
  defineProviderCommand: () => defineProviderCommand,
572
602
  destroySandbox: () => destroySandbox,
573
603
  ensureSandbox: () => ensureSandbox,
574
604
  getComputeInstance: () => getComputeInstance,
605
+ isLocalDaemonRunning: () => isLocalDaemonRunning,
606
+ listLocalSandboxes: () => listLocalSandboxes,
575
607
  restartSandbox: () => restartSandbox,
576
608
  runCommand: () => runCommand,
577
609
  showMode: () => showMode,
@@ -582,31 +614,58 @@ __export(commands_exports, {
582
614
  });
583
615
  import { createCompute } from "@computesdk/provider";
584
616
  import { escapeArgs } from "@computesdk/cmd";
585
- import * as readline from "readline";
586
- async function confirm(question, defaultYes = false) {
587
- return new Promise((resolve) => {
588
- const rl = readline.createInterface({
589
- input: process.stdin,
590
- output: process.stdout
591
- });
617
+ async function confirm(question, defaultYes = false, _state) {
618
+ const promptSuffix = defaultYes ? "(Y/n)" : "(y/N)";
619
+ process.stdout.write(`${question} ${promptSuffix}: `);
620
+ if (process.stdin.isPaused()) {
592
621
  process.stdin.resume();
593
- const promptSuffix = defaultYes ? "(Y/n)" : "(y/N)";
594
- rl.question(`${question} ${promptSuffix}: `, (answer) => {
595
- rl.close();
596
- const trimmed = answer.trim().toLowerCase();
597
- if (trimmed === "") {
622
+ }
623
+ return new Promise((resolve) => {
624
+ const wasRaw = process.stdin.isRaw;
625
+ if (process.stdin.isTTY) {
626
+ process.stdin.setRawMode(true);
627
+ }
628
+ const cleanup = (restoreRaw) => {
629
+ process.stdin.removeListener("data", onData);
630
+ if (process.stdin.isTTY && restoreRaw) {
631
+ process.stdin.setRawMode(wasRaw || false);
632
+ }
633
+ };
634
+ const onData = (key) => {
635
+ const char = key.toString();
636
+ if (char === "") {
637
+ process.stdout.write("^C\n");
638
+ cleanup(true);
639
+ resolve(false);
640
+ return;
641
+ }
642
+ if (char === "\r" || char === "\n") {
643
+ process.stdout.write(defaultYes ? "Y\n" : "N\n");
644
+ cleanup(true);
598
645
  resolve(defaultYes);
599
- } else {
600
- resolve(trimmed === "y" || trimmed === "yes");
646
+ return;
601
647
  }
602
- });
648
+ if (char === "y" || char === "Y") {
649
+ process.stdout.write("y\n");
650
+ cleanup(true);
651
+ resolve(true);
652
+ return;
653
+ }
654
+ if (char === "n" || char === "N") {
655
+ process.stdout.write("n\n");
656
+ cleanup(true);
657
+ resolve(false);
658
+ return;
659
+ }
660
+ };
661
+ process.stdin.on("data", onData);
603
662
  });
604
663
  }
605
664
  async function confirmSandboxSwitch(state) {
606
665
  if (!hasSandbox(state)) {
607
666
  return true;
608
667
  }
609
- return await confirm("Switch to new sandbox?", true);
668
+ return await confirm("Switch to new sandbox?", true, state);
610
669
  }
611
670
  async function ensureSandbox(state) {
612
671
  if (hasSandbox(state)) {
@@ -768,6 +827,14 @@ function isStaleConnectionError(error) {
768
827
  return stalePhrases.some((phrase) => message.includes(phrase));
769
828
  }
770
829
  async function switchProvider(state, mode, providerName) {
830
+ if (mode === "local") {
831
+ if (providerName === "list") {
832
+ await listLocalSandboxes();
833
+ return;
834
+ }
835
+ await connectToLocal(state, providerName);
836
+ return;
837
+ }
771
838
  let useDirect = false;
772
839
  let actualProvider = mode;
773
840
  if (mode === "direct") {
@@ -792,7 +859,7 @@ async function switchProvider(state, mode, providerName) {
792
859
  }
793
860
  if (!isValidProvider(actualProvider)) {
794
861
  logError(`Unknown provider: ${actualProvider}`);
795
- console.log(`Available providers: e2b, railway, daytona, modal, runloop, vercel, cloudflare, codesandbox, blaxel`);
862
+ console.log(`Available providers: e2b, railway, daytona, modal, runloop, vercel, cloudflare, codesandbox, blaxel, local`);
796
863
  return;
797
864
  }
798
865
  if (!useDirect && !isProviderReady("gateway")) {
@@ -806,7 +873,7 @@ async function switchProvider(state, mode, providerName) {
806
873
  return;
807
874
  }
808
875
  if (hasSandbox(state)) {
809
- const shouldDestroy = await confirm("Destroy current sandbox?");
876
+ const shouldDestroy = await confirm("Destroy current sandbox?", false, state);
810
877
  if (shouldDestroy) {
811
878
  await destroySandbox(state);
812
879
  state.currentProvider = actualProvider;
@@ -829,7 +896,7 @@ function defineProviderCommand(state) {
829
896
  return async function provider(mode, providerName) {
830
897
  if (!mode) {
831
898
  if (state.currentProvider) {
832
- const modeStr = state.useDirectMode ? "direct" : "via gateway";
899
+ const modeStr = state.useDirectMode ? "direct" : state.currentProvider === "local" ? "local daemon" : "via gateway";
833
900
  console.log(`
834
901
  Current provider: ${c.green(state.currentProvider)} (${modeStr})
835
902
  `);
@@ -907,8 +974,8 @@ async function connectToSandbox(state, sandboxUrl, token) {
907
974
  }
908
975
  const cleanUrl = sandboxUrl.replace(/\/$/, "");
909
976
  if (hasSandbox(state)) {
910
- const shouldDestroy = await confirm("Disconnect from current sandbox?");
911
- if (!shouldDestroy) {
977
+ const shouldDisconnect = await confirm("Disconnect from current sandbox?", false, state);
978
+ if (!shouldDisconnect) {
912
979
  logWarning("Keeping current sandbox. Connection cancelled.");
913
980
  return;
914
981
  }
@@ -954,6 +1021,125 @@ async function connectToSandbox(state, sandboxUrl, token) {
954
1021
  throw error;
955
1022
  }
956
1023
  }
1024
+ async function readLocalConfig() {
1025
+ const os2 = await import("os");
1026
+ const fs2 = await import("fs/promises");
1027
+ const path4 = await import("path");
1028
+ const configPath = path4.join(os2.homedir(), ".compute", "config.json");
1029
+ try {
1030
+ const content = await fs2.readFile(configPath, "utf-8");
1031
+ return JSON.parse(content);
1032
+ } catch {
1033
+ return null;
1034
+ }
1035
+ }
1036
+ async function isLocalDaemonRunning() {
1037
+ const os2 = await import("os");
1038
+ const fs2 = await import("fs/promises");
1039
+ const path4 = await import("path");
1040
+ const pidPath = path4.join(os2.homedir(), ".compute", "compute.pid");
1041
+ try {
1042
+ const pidContent = await fs2.readFile(pidPath, "utf-8");
1043
+ const pid = parseInt(pidContent.trim(), 10);
1044
+ process.kill(pid, 0);
1045
+ return true;
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ }
1050
+ async function listLocalSandboxes() {
1051
+ const config2 = await readLocalConfig();
1052
+ if (!config2) {
1053
+ logError("No local daemon config found at ~/.compute/config.json");
1054
+ console.log(c.dim('Run "compute start" to start the local daemon'));
1055
+ return;
1056
+ }
1057
+ const isRunning = await isLocalDaemonRunning();
1058
+ console.log("");
1059
+ console.log(c.bold("Local Daemon Status:"), isRunning ? c.green("Running") : c.red("Stopped"));
1060
+ console.log("");
1061
+ if (!config2.sandboxes || config2.sandboxes.length === 0) {
1062
+ console.log(c.dim("No sandboxes found"));
1063
+ return;
1064
+ }
1065
+ console.log(c.bold("Sandboxes:"));
1066
+ for (const sandbox of config2.sandboxes) {
1067
+ const isMain = sandbox.subdomain === config2.main_subdomain;
1068
+ const mainLabel = isMain ? c.green(" (main)") : "";
1069
+ console.log(` ${c.cyan(sandbox.subdomain)}${mainLabel}`);
1070
+ console.log(c.dim(` https://${sandbox.subdomain}.sandbox.computesdk.com`));
1071
+ }
1072
+ console.log("");
1073
+ console.log(c.dim(`Connect with: local ${config2.main_subdomain}`));
1074
+ console.log("");
1075
+ }
1076
+ async function connectToLocal(state, subdomain) {
1077
+ const config2 = await readLocalConfig();
1078
+ if (!config2) {
1079
+ logError("No local daemon config found at ~/.compute/config.json");
1080
+ console.log(c.dim('Run "compute start" to start the local daemon'));
1081
+ return;
1082
+ }
1083
+ const isRunning = await isLocalDaemonRunning();
1084
+ if (!isRunning) {
1085
+ logError("Local daemon is not running");
1086
+ console.log(c.dim('Run "compute start" to start the local daemon'));
1087
+ return;
1088
+ }
1089
+ const targetSubdomain = subdomain || config2.main_subdomain;
1090
+ const sandbox = config2.sandboxes.find((s) => s.subdomain === targetSubdomain);
1091
+ if (!sandbox) {
1092
+ logError(`Sandbox "${targetSubdomain}" not found`);
1093
+ console.log(c.dim('Run "local list" to see available sandboxes'));
1094
+ return;
1095
+ }
1096
+ const sandboxUrl = `https://${targetSubdomain}.sandbox.computesdk.com`;
1097
+ const token = config2.access_token;
1098
+ if (hasSandbox(state)) {
1099
+ const shouldDisconnect = await confirm("Disconnect from current sandbox?", false, state);
1100
+ if (!shouldDisconnect) {
1101
+ logWarning("Keeping current sandbox. Connection cancelled.");
1102
+ return;
1103
+ }
1104
+ clearSandbox(state);
1105
+ }
1106
+ const spinner = new Spinner(`Connecting to local sandbox ${targetSubdomain}...`).start();
1107
+ const startTime = Date.now();
1108
+ try {
1109
+ const { Sandbox } = await import("computesdk");
1110
+ let WebSocket;
1111
+ try {
1112
+ const wsModule = await import("ws");
1113
+ WebSocket = wsModule.default;
1114
+ } catch {
1115
+ spinner.fail('Failed to import "ws" module');
1116
+ logError("Please install ws: pnpm add ws");
1117
+ throw new Error('Missing "ws" dependency');
1118
+ }
1119
+ const sandboxInstance = new Sandbox({
1120
+ sandboxUrl,
1121
+ sandboxId: targetSubdomain,
1122
+ provider: "local",
1123
+ token,
1124
+ WebSocket
1125
+ });
1126
+ const info = await sandboxInstance.getInfo();
1127
+ const duration = Date.now() - startTime;
1128
+ setSandbox(state, sandboxInstance, "local");
1129
+ state.verbose = true;
1130
+ spinner.succeed(`Connected to local sandbox ${c.dim(`(${formatDuration(duration)})`)}`);
1131
+ console.log(c.dim(`Sandbox: ${targetSubdomain}`));
1132
+ console.log(c.dim(`URL: ${sandboxUrl}`));
1133
+ console.log(c.dim(`Verbose mode: enabled (for debugging)`));
1134
+ } catch (error) {
1135
+ const duration = Date.now() - startTime;
1136
+ spinner.fail(`Failed to connect ${c.dim(`(${formatDuration(duration)})`)}`);
1137
+ if (error instanceof Error) {
1138
+ logError(`Error: ${error.message}`);
1139
+ }
1140
+ throw error;
1141
+ }
1142
+ }
957
1143
  async function cleanupOnExit(state, replServer) {
958
1144
  if (!hasSandbox(state)) {
959
1145
  return;
@@ -966,7 +1152,7 @@ async function cleanupOnExit(state, replServer) {
966
1152
  logWarning("Disconnecting from external sandbox (not destroying).");
967
1153
  return;
968
1154
  }
969
- const shouldDestroy = await confirm("Destroy active sandbox?");
1155
+ const shouldDestroy = await confirm("Destroy active sandbox?", false, state);
970
1156
  if (shouldDestroy) {
971
1157
  await destroySandbox(state);
972
1158
  } else {
@@ -1360,6 +1546,59 @@ function injectWorkbenchCommands(replServer, state) {
1360
1546
  };
1361
1547
  }
1362
1548
  };
1549
+ replServer.context.terminal = {
1550
+ get create() {
1551
+ return async (options) => {
1552
+ const sandbox = state.currentSandbox;
1553
+ if (!sandbox) {
1554
+ throw new Error("No active sandbox. Run a command to auto-create one.");
1555
+ }
1556
+ const term = await sandbox.terminal.create(options);
1557
+ if (state.verbose) {
1558
+ if (term._ws) {
1559
+ term._ws.config.debug = true;
1560
+ }
1561
+ term.on("output", (data) => {
1562
+ console.log("[terminal:output]", JSON.stringify(data));
1563
+ });
1564
+ term.on("error", (error) => {
1565
+ console.log("[terminal:error]", error);
1566
+ });
1567
+ term.on("destroyed", () => {
1568
+ console.log("[terminal:destroyed]");
1569
+ });
1570
+ }
1571
+ return term;
1572
+ };
1573
+ },
1574
+ get list() {
1575
+ return async () => {
1576
+ const sandbox = state.currentSandbox;
1577
+ if (!sandbox) {
1578
+ throw new Error("No active sandbox. Run a command to auto-create one.");
1579
+ }
1580
+ return sandbox.terminal.list();
1581
+ };
1582
+ },
1583
+ get retrieve() {
1584
+ return async (id) => {
1585
+ const sandbox = state.currentSandbox;
1586
+ if (!sandbox) {
1587
+ throw new Error("No active sandbox. Run a command to auto-create one.");
1588
+ }
1589
+ return sandbox.terminal.retrieve(id);
1590
+ };
1591
+ },
1592
+ get destroy() {
1593
+ return async (id) => {
1594
+ const sandbox = state.currentSandbox;
1595
+ if (!sandbox) {
1596
+ throw new Error("No active sandbox. Run a command to auto-create one.");
1597
+ }
1598
+ return sandbox.terminal.destroy(id);
1599
+ };
1600
+ }
1601
+ };
1363
1602
  replServer.context.getInstance = () => {
1364
1603
  const sandbox = state.currentSandbox;
1365
1604
  if (!sandbox) {
@@ -1373,6 +1612,13 @@ function setupSmartEvaluator(replServer, state) {
1373
1612
  const workbenchCommands = /* @__PURE__ */ new Set(["help", "providers", "info", "env", "restart", "destroy", "mode", "verbose", "sandboxInfo", "connect"]);
1374
1613
  replServer.eval = function(cmd2, context, filename, callback) {
1375
1614
  const trimmedCmd = cmd2.trim();
1615
+ const providerLocalMatch = trimmedCmd.match(/^provider\s+local(?:\s+(\S+))?$/);
1616
+ if (providerLocalMatch) {
1617
+ const arg = providerLocalMatch[1];
1618
+ const providerCmd = arg ? `await provider('local', '${arg}')` : `await provider('local')`;
1619
+ originalEval.call(this, providerCmd, context, filename, callback);
1620
+ return;
1621
+ }
1376
1622
  const providerMatch = trimmedCmd.match(/^provider(?:\s+(direct|gateway))?\s+(\w+)$/);
1377
1623
  if (providerMatch) {
1378
1624
  const mode = providerMatch[1] || null;
@@ -1445,8 +1691,8 @@ function setupSmartEvaluator(replServer, state) {
1445
1691
  function setupAutocomplete(replServer, state) {
1446
1692
  const originalCompleter = replServer.completer;
1447
1693
  const workbenchCommands = {
1448
- "provider": [...PROVIDER_NAMES],
1449
- // Use actual provider names from config
1694
+ "provider": [...PROVIDER_NAMES, "local"],
1695
+ // Include 'local' as a provider option
1450
1696
  "mode": ["gateway", "direct"],
1451
1697
  "providers": [],
1452
1698
  "restart": [],
@@ -1551,22 +1797,38 @@ init_providers();
1551
1797
  init_commands();
1552
1798
  async function startWorkbench() {
1553
1799
  const state = createState();
1800
+ const localDaemonRunning = await isLocalDaemonRunning();
1554
1801
  state.availableProviders = getAvailableProviders();
1802
+ if (localDaemonRunning) {
1803
+ state.availableProviders.push("local");
1804
+ }
1555
1805
  const detectedProvider = autoDetectProvider();
1556
- const hasGateway = state.availableProviders.includes("gateway");
1557
- const backendProviders = state.availableProviders.filter((p) => p !== "gateway");
1558
- if (hasGateway && backendProviders.length > 0) {
1559
- state.currentProvider = backendProviders[0] || "e2b";
1806
+ if (localDaemonRunning) {
1807
+ state.currentProvider = "local";
1560
1808
  state.useDirectMode = false;
1561
- } else if (backendProviders.length > 0) {
1562
- state.currentProvider = backendProviders[0];
1563
- state.useDirectMode = true;
1809
+ state.verbose = true;
1564
1810
  } else {
1565
- state.currentProvider = detectedProvider;
1566
- state.useDirectMode = false;
1811
+ const hasGateway = state.availableProviders.includes("gateway");
1812
+ const backendProviders = state.availableProviders.filter((p) => p !== "gateway" && p !== "local");
1813
+ if (hasGateway && backendProviders.length > 0) {
1814
+ state.currentProvider = backendProviders[0] || "e2b";
1815
+ state.useDirectMode = false;
1816
+ } else if (backendProviders.length > 0) {
1817
+ state.currentProvider = backendProviders[0];
1818
+ state.useDirectMode = true;
1819
+ } else {
1820
+ state.currentProvider = detectedProvider;
1821
+ state.useDirectMode = false;
1822
+ }
1567
1823
  }
1568
- showWelcome(state.availableProviders, state.currentProvider, state.useDirectMode);
1824
+ showWelcome(state.availableProviders, state.currentProvider, state.useDirectMode, localDaemonRunning);
1569
1825
  const replServer = createREPL(state);
1826
+ if (localDaemonRunning) {
1827
+ try {
1828
+ await connectToLocal(state);
1829
+ } catch {
1830
+ }
1831
+ }
1570
1832
  replServer.on("exit", async () => {
1571
1833
  await cleanupOnExit(state, replServer);
1572
1834
  process.exit(0);