@hasna/sandboxes 0.1.29 → 0.1.30

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.
@@ -12443,6 +12443,17 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (6);
12443
12443
  `
12444
12444
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
12445
12445
  INSERT OR IGNORE INTO _migrations (id) VALUES (7);
12446
+ `,
12447
+ `
12448
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC);
12449
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC);
12450
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC);
12451
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC);
12452
+ CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC);
12453
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC);
12454
+ CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC);
12455
+ CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC);
12456
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
12446
12457
  `
12447
12458
  ];
12448
12459
  var db = null;
@@ -12473,16 +12484,26 @@ function uuid() {
12473
12484
  function now() {
12474
12485
  return new Date().toISOString().replace("T", " ").replace("Z", "");
12475
12486
  }
12487
+ var PARTIAL_ID_TABLES = new Set([
12488
+ "agents",
12489
+ "projects",
12490
+ "sandbox_sessions",
12491
+ "sandboxes",
12492
+ "snapshots",
12493
+ "templates",
12494
+ "webhooks"
12495
+ ]);
12476
12496
  function resolvePartialId(table, partialId) {
12497
+ if (!PARTIAL_ID_TABLES.has(table)) {
12498
+ throw new Error(`Invalid partial-id table: ${table}`);
12499
+ }
12477
12500
  const database = getDatabase();
12478
- const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
12479
- if (rows.length === 1)
12480
- return rows[0].id;
12481
- if (rows.length === 0)
12482
- return null;
12483
- const exact = rows.find((r) => r.id === partialId);
12501
+ const exact = database.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
12484
12502
  if (exact)
12485
12503
  return exact.id;
12504
+ const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? ORDER BY id LIMIT 2`).all(`${partialId}%`);
12505
+ if (rows.length === 1)
12506
+ return rows[0].id;
12486
12507
  return null;
12487
12508
  }
12488
12509
 
@@ -12707,7 +12728,7 @@ function listEvents(opts) {
12707
12728
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
12708
12729
  const limit = opts?.limit ?? 100;
12709
12730
  const offset = opts?.offset ?? 0;
12710
- const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
12731
+ const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC, rowid ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
12711
12732
  return rows.map(rowToEvent);
12712
12733
  }
12713
12734
 
@@ -16586,7 +16607,7 @@ class JSONSchemaGenerator {
16586
16607
  if (val === undefined) {
16587
16608
  if (this.unrepresentable === "throw") {
16588
16609
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
16589
- } else {}
16610
+ }
16590
16611
  } else if (typeof val === "bigint") {
16591
16612
  if (this.unrepresentable === "throw") {
16592
16613
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -27035,55 +27056,80 @@ function getBuiltinImageSetupScript(image) {
27035
27056
  return BUILTIN_IMAGES[image]?.setup_script;
27036
27057
  }
27037
27058
 
27038
- // src/db/cloud-config.ts
27059
+ // src/db/storage-config.ts
27039
27060
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
27040
27061
  import { homedir } from "os";
27041
27062
  import { join as join4 } from "path";
27042
- var CONFIG_PATH = join4(homedir(), ".hasna", "sandboxes", "cloud", "config.json");
27043
- function isMode(value) {
27044
- return value === "local" || value === "hybrid" || value === "cloud";
27063
+ var STORAGE_CONFIG_PATH = join4(homedir(), ".hasna", "sandboxes", "storage", "config.json");
27064
+ var SANDBOXES_STORAGE_ENV = "HASNA_SANDBOXES_DATABASE_URL";
27065
+ var SANDBOXES_STORAGE_FALLBACK_ENV = "SANDBOXES_DATABASE_URL";
27066
+ var SANDBOXES_STORAGE_MODE_ENV = "HASNA_SANDBOXES_STORAGE_MODE";
27067
+ var SANDBOXES_STORAGE_MODE_FALLBACK_ENV = "SANDBOXES_STORAGE_MODE";
27068
+ var STORAGE_DATABASE_ENV = [SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV];
27069
+ function readEnv(name) {
27070
+ const value = process.env[name]?.trim();
27071
+ return value || undefined;
27072
+ }
27073
+ function normalizeStorageMode(value) {
27074
+ const normalized = value?.trim().toLowerCase();
27075
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
27076
+ return normalized;
27077
+ return;
27078
+ }
27079
+ function getStorageDatabaseEnvName() {
27080
+ for (const name of STORAGE_DATABASE_ENV) {
27081
+ if (readEnv(name))
27082
+ return name;
27083
+ }
27084
+ return null;
27085
+ }
27086
+ function getStorageDatabaseEnv() {
27087
+ const name = getStorageDatabaseEnvName();
27088
+ return name ? { name } : null;
27045
27089
  }
27046
- function envConnectionString() {
27047
- return process.env["HASNA_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["OPEN_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["SANDBOXES_CLOUD_DATABASE_URL"];
27090
+ function getStorageDatabaseUrl() {
27091
+ const env = getStorageDatabaseEnv();
27092
+ return env ? readEnv(env.name) : undefined;
27048
27093
  }
27049
- function getCloudConfig() {
27094
+ function getStorageConfig() {
27050
27095
  const config2 = {
27051
27096
  mode: "local",
27052
27097
  rds: {
27053
27098
  host: "",
27054
27099
  port: 5432,
27055
27100
  username: "",
27056
- password_env: "SANDBOXES_CLOUD_DATABASE_PASSWORD",
27101
+ password_env: "SANDBOXES_DATABASE_PASSWORD",
27057
27102
  ssl: true
27058
27103
  }
27059
27104
  };
27060
- if (existsSync4(CONFIG_PATH)) {
27105
+ if (existsSync4(STORAGE_CONFIG_PATH)) {
27061
27106
  try {
27062
- const raw = JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
27063
- config2.mode = raw.mode ?? config2.mode;
27107
+ const raw = JSON.parse(readFileSync3(STORAGE_CONFIG_PATH, "utf-8"));
27108
+ config2.mode = normalizeStorageMode(raw.mode) ?? config2.mode;
27064
27109
  config2.rds = { ...config2.rds, ...raw.rds ?? {} };
27065
27110
  } catch {}
27066
27111
  }
27067
- const modeOverride = process.env["HASNA_SANDBOXES_CLOUD_MODE"] ?? process.env["OPEN_SANDBOXES_CLOUD_MODE"] ?? process.env["SANDBOXES_CLOUD_MODE"];
27068
- if (isMode(modeOverride)) {
27069
- config2.mode = modeOverride;
27070
- } else if (envConnectionString() && config2.mode === "local") {
27112
+ const modeOverride = readEnv(SANDBOXES_STORAGE_MODE_ENV) ?? readEnv(SANDBOXES_STORAGE_MODE_FALLBACK_ENV);
27113
+ const mode = normalizeStorageMode(modeOverride);
27114
+ if (mode) {
27115
+ config2.mode = mode;
27116
+ } else if (getStorageDatabaseUrl() && config2.mode === "local") {
27071
27117
  config2.mode = "hybrid";
27072
27118
  }
27073
27119
  return config2;
27074
27120
  }
27075
- function getConnectionString(dbName = "sandboxes") {
27076
- const direct = envConnectionString();
27121
+ function getStorageConnectionString(dbName = "sandboxes") {
27122
+ const direct = getStorageDatabaseUrl();
27077
27123
  if (direct)
27078
27124
  return direct;
27079
- const config2 = getCloudConfig();
27125
+ const config2 = getStorageConfig();
27080
27126
  const { host, port, username, password_env, ssl } = config2.rds;
27081
27127
  if (!host || !username) {
27082
- throw new Error("Cloud database is not configured. Set HASNA_SANDBOXES_CLOUD_DATABASE_URL or configure ~/.hasna/sandboxes/cloud/config.json.");
27128
+ throw new Error("Storage database is not configured. Set HASNA_SANDBOXES_DATABASE_URL or configure ~/.hasna/sandboxes/storage/config.json.");
27083
27129
  }
27084
27130
  const password = process.env[password_env];
27085
27131
  if (!password) {
27086
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
27132
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
27087
27133
  }
27088
27134
  const sslParam = ssl ? "?sslmode=require" : "";
27089
27135
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
@@ -27179,6 +27225,9 @@ var PG_MIGRATIONS = [
27179
27225
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_status ON sandboxes(status)`,
27180
27226
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider ON sandboxes(provider)`,
27181
27227
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_project ON sandboxes(project_id)`,
27228
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC)`,
27229
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC)`,
27230
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC)`,
27182
27231
  `CREATE TABLE IF NOT EXISTS sandbox_sessions (
27183
27232
  id TEXT PRIMARY KEY,
27184
27233
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -27192,6 +27241,9 @@ var PG_MIGRATIONS = [
27192
27241
  )`,
27193
27242
  `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox ON sandbox_sessions(sandbox_id)`,
27194
27243
  `CREATE INDEX IF NOT EXISTS idx_sessions_status ON sandbox_sessions(status)`,
27244
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC)`,
27245
+ `CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC)`,
27246
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC)`,
27195
27247
  `CREATE TABLE IF NOT EXISTS sandbox_events (
27196
27248
  id TEXT PRIMARY KEY,
27197
27249
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -27203,6 +27255,8 @@ var PG_MIGRATIONS = [
27203
27255
  `CREATE INDEX IF NOT EXISTS idx_events_sandbox ON sandbox_events(sandbox_id)`,
27204
27256
  `CREATE INDEX IF NOT EXISTS idx_events_session ON sandbox_events(session_id)`,
27205
27257
  `CREATE INDEX IF NOT EXISTS idx_events_type ON sandbox_events(type)`,
27258
+ `CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC)`,
27259
+ `CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC)`,
27206
27260
  `CREATE TABLE IF NOT EXISTS webhooks (
27207
27261
  id TEXT PRIMARY KEY,
27208
27262
  url TEXT NOT NULL,
@@ -27253,8 +27307,8 @@ var PG_MIGRATIONS = [
27253
27307
  `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
27254
27308
  ];
27255
27309
 
27256
- // src/db/cloud-sync.ts
27257
- var CLOUD_TABLES = [
27310
+ // src/db/storage-sync.ts
27311
+ var STORAGE_TABLES = [
27258
27312
  "projects",
27259
27313
  "agents",
27260
27314
  "sandboxes",
@@ -27348,21 +27402,26 @@ function upsertSqlite(db2, table, rows) {
27348
27402
  }
27349
27403
  return written;
27350
27404
  }
27351
- async function getCloudPg() {
27352
- return new PgAdapterAsync(getConnectionString("sandboxes"));
27405
+ async function getStoragePg() {
27406
+ return new PgAdapterAsync(getStorageConnectionString("sandboxes"));
27353
27407
  }
27354
- async function runCloudMigrations(remote) {
27408
+ async function runStorageMigrations(remote) {
27355
27409
  for (const migration of PG_MIGRATIONS) {
27356
27410
  await remote.exec(migration);
27357
27411
  }
27358
27412
  }
27359
- function getCloudStatus(db2 = getDatabase()) {
27360
- const config2 = getCloudConfig();
27413
+ function getStorageStatus(db2 = getDatabase()) {
27414
+ const config2 = getStorageConfig();
27415
+ const activeEnv = getStorageDatabaseEnv();
27361
27416
  return {
27417
+ configured: Boolean(activeEnv),
27362
27418
  mode: config2.mode,
27363
- enabled: config2.mode === "hybrid" || config2.mode === "cloud",
27419
+ enabled: config2.mode === "hybrid" || config2.mode === "remote",
27420
+ env: STORAGE_DATABASE_ENV,
27421
+ activeEnv: activeEnv?.name ?? null,
27422
+ service: "sandboxes",
27364
27423
  db_path: getDbPath(),
27365
- tables: CLOUD_TABLES.map((table) => {
27424
+ tables: STORAGE_TABLES.map((table) => {
27366
27425
  try {
27367
27426
  const row = db2.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
27368
27427
  return { table, rows: row.count };
@@ -27372,12 +27431,12 @@ function getCloudStatus(db2 = getDatabase()) {
27372
27431
  })
27373
27432
  };
27374
27433
  }
27375
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
27434
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
27376
27435
  const db2 = getDatabase();
27377
- const remote = await getCloudPg();
27436
+ const remote = await getStoragePg();
27378
27437
  const results = [];
27379
27438
  try {
27380
- await runCloudMigrations(remote);
27439
+ await runStorageMigrations(remote);
27381
27440
  for (const table of tables) {
27382
27441
  const result = { table, direction: "push", rowsRead: 0, rowsWritten: 0, errors: [] };
27383
27442
  try {
@@ -27394,12 +27453,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
27394
27453
  }
27395
27454
  return results;
27396
27455
  }
27397
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
27456
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
27398
27457
  const db2 = getDatabase();
27399
- const remote = await getCloudPg();
27458
+ const remote = await getStoragePg();
27400
27459
  const results = [];
27401
27460
  try {
27402
- await runCloudMigrations(remote);
27461
+ await runStorageMigrations(remote);
27403
27462
  for (const table of tables) {
27404
27463
  const result = { table, direction: "pull", rowsRead: 0, rowsWritten: 0, errors: [] };
27405
27464
  try {
@@ -27416,20 +27475,26 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
27416
27475
  }
27417
27476
  return results;
27418
27477
  }
27419
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
27478
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
27420
27479
  return {
27421
- push: await pushCloudChanges(tables),
27422
- pull: await pullCloudChanges(tables)
27480
+ push: await pushStorageChanges(tables),
27481
+ pull: await pullStorageChanges(tables)
27423
27482
  };
27424
27483
  }
27425
- function parseCloudTables(raw) {
27484
+ function parseStorageTables(raw) {
27426
27485
  if (!raw)
27427
- return [...CLOUD_TABLES];
27486
+ return [...STORAGE_TABLES];
27428
27487
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
27429
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
27488
+ if (requested.length === 0)
27489
+ return [...STORAGE_TABLES];
27490
+ const allowed = new Set(STORAGE_TABLES);
27491
+ const invalid = requested.filter((table) => !allowed.has(table));
27492
+ if (invalid.length > 0)
27493
+ throw new Error(`Unknown sandboxes sync table(s): ${invalid.join(", ")}`);
27494
+ return requested;
27430
27495
  }
27431
27496
 
27432
- // src/mcp/cloud-tools.ts
27497
+ // src/mcp/storage-tools.ts
27433
27498
  function ok(data) {
27434
27499
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
27435
27500
  }
@@ -27439,42 +27504,42 @@ function err(error2) {
27439
27504
  isError: true
27440
27505
  };
27441
27506
  }
27442
- function registerSandboxesCloudTools(server) {
27443
- server.tool("sandboxes_cloud_status", "Show sandboxes local database and cloud sync status", {}, async () => {
27507
+ function registerSandboxesStorageTools(server) {
27508
+ server.tool("sandboxes_storage_status", "Show sandboxes local database and storage sync status", {}, async () => {
27444
27509
  try {
27445
- return ok(getCloudStatus());
27510
+ return ok(getStorageStatus());
27446
27511
  } catch (error2) {
27447
27512
  return err(error2);
27448
27513
  }
27449
27514
  });
27450
- server.tool("sandboxes_cloud_push", "Push local sandboxes data to PostgreSQL", {
27515
+ server.tool("sandboxes_storage_push", "Push local sandboxes data to PostgreSQL", {
27451
27516
  tables: exports_external.string().optional().describe("Comma-separated table names")
27452
27517
  }, async ({ tables }) => {
27453
27518
  try {
27454
- return ok(await pushCloudChanges(parseCloudTables(tables)));
27519
+ return ok(await pushStorageChanges(parseStorageTables(tables)));
27455
27520
  } catch (error2) {
27456
27521
  return err(error2);
27457
27522
  }
27458
27523
  });
27459
- server.tool("sandboxes_cloud_pull", "Pull PostgreSQL sandboxes data into the local database", {
27524
+ server.tool("sandboxes_storage_pull", "Pull PostgreSQL sandboxes data into the local database", {
27460
27525
  tables: exports_external.string().optional().describe("Comma-separated table names")
27461
27526
  }, async ({ tables }) => {
27462
27527
  try {
27463
- return ok(await pullCloudChanges(parseCloudTables(tables)));
27528
+ return ok(await pullStorageChanges(parseStorageTables(tables)));
27464
27529
  } catch (error2) {
27465
27530
  return err(error2);
27466
27531
  }
27467
27532
  });
27468
- server.tool("sandboxes_cloud_sync", "Push local changes, then pull remote changes", {
27533
+ server.tool("sandboxes_storage_sync", "Push local changes, then pull remote changes", {
27469
27534
  tables: exports_external.string().optional().describe("Comma-separated table names")
27470
27535
  }, async ({ tables }) => {
27471
27536
  try {
27472
- return ok(await syncCloudChanges(parseCloudTables(tables)));
27537
+ return ok(await syncStorageChanges(parseStorageTables(tables)));
27473
27538
  } catch (error2) {
27474
27539
  return err(error2);
27475
27540
  }
27476
27541
  });
27477
- server.tool("sandboxes_cloud_feedback", "Save feedback for sandboxes", {
27542
+ server.tool("sandboxes_feedback", "Save feedback for sandboxes", {
27478
27543
  message: exports_external.string(),
27479
27544
  email: exports_external.string().optional(),
27480
27545
  category: exports_external.enum(["bug", "feature", "general"]).optional()
@@ -28298,7 +28363,7 @@ function buildServer() {
28298
28363
  return err2(e);
28299
28364
  }
28300
28365
  });
28301
- registerSandboxesCloudTools(server);
28366
+ registerSandboxesStorageTools(server);
28302
28367
  return server;
28303
28368
  }
28304
28369
 
@@ -0,0 +1,6 @@
1
+ export { SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV, SANDBOXES_STORAGE_MODE_ENV, SANDBOXES_STORAGE_MODE_FALLBACK_ENV, STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, getConnectionString, getStorageConfig, getStorageConnectionString, getStorageDatabaseEnv, getStorageDatabaseEnvName, getStorageDatabaseUrl, } from "./db/storage-config.js";
2
+ export type { StorageConfig, StorageEnv, StorageMode } from "./db/storage-config.js";
3
+ export { SANDBOXES_STORAGE_TABLES, STORAGE_TABLES, getStoragePg, getStorageStatus, parseStorageTables, pullStorageChanges, pushStorageChanges, runStorageMigrations, syncStorageChanges, } from "./db/storage-sync.js";
4
+ export type { StorageStatus, SyncResult } from "./db/storage-sync.js";
5
+ export { PgAdapterAsync } from "./db/remote-storage.js";
6
+ export { PG_MIGRATIONS } from "./db/pg-migrations.js";