@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.
package/dist/mcp/index.js CHANGED
@@ -9941,6 +9941,17 @@ INSERT OR IGNORE INTO _migrations (id) VALUES (6);
9941
9941
  `
9942
9942
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
9943
9943
  INSERT OR IGNORE INTO _migrations (id) VALUES (7);
9944
+ `,
9945
+ `
9946
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC);
9947
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC);
9948
+ CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC);
9949
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC);
9950
+ CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC);
9951
+ CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC);
9952
+ CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC);
9953
+ CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC);
9954
+ INSERT OR IGNORE INTO _migrations (id) VALUES (8);
9944
9955
  `
9945
9956
  ];
9946
9957
  var db = null;
@@ -9971,16 +9982,26 @@ function uuid() {
9971
9982
  function now() {
9972
9983
  return new Date().toISOString().replace("T", " ").replace("Z", "");
9973
9984
  }
9985
+ var PARTIAL_ID_TABLES = new Set([
9986
+ "agents",
9987
+ "projects",
9988
+ "sandbox_sessions",
9989
+ "sandboxes",
9990
+ "snapshots",
9991
+ "templates",
9992
+ "webhooks"
9993
+ ]);
9974
9994
  function resolvePartialId(table, partialId) {
9995
+ if (!PARTIAL_ID_TABLES.has(table)) {
9996
+ throw new Error(`Invalid partial-id table: ${table}`);
9997
+ }
9975
9998
  const database = getDatabase();
9976
- const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? || '%'`).all(partialId);
9977
- if (rows.length === 1)
9978
- return rows[0].id;
9979
- if (rows.length === 0)
9980
- return null;
9981
- const exact = rows.find((r) => r.id === partialId);
9999
+ const exact = database.query(`SELECT id FROM ${table} WHERE id = ?`).get(partialId);
9982
10000
  if (exact)
9983
10001
  return exact.id;
10002
+ const rows = database.query(`SELECT id FROM ${table} WHERE id LIKE ? ORDER BY id LIMIT 2`).all(`${partialId}%`);
10003
+ if (rows.length === 1)
10004
+ return rows[0].id;
9984
10005
  return null;
9985
10006
  }
9986
10007
 
@@ -10189,7 +10210,7 @@ function listEvents(opts) {
10189
10210
  const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
10190
10211
  const limit = opts?.limit ?? 100;
10191
10212
  const offset = opts?.offset ?? 0;
10192
- const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
10213
+ const rows = db2.query(`SELECT * FROM sandbox_events${where} ORDER BY created_at ASC, rowid ASC LIMIT ? OFFSET ?`).all(...[...params, limit, offset]);
10193
10214
  return rows.map(rowToEvent);
10194
10215
  }
10195
10216
 
@@ -10861,55 +10882,80 @@ function getPackageVersion() {
10861
10882
  return cachedVersion;
10862
10883
  }
10863
10884
 
10864
- // src/db/cloud-config.ts
10885
+ // src/db/storage-config.ts
10865
10886
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
10866
10887
  import { homedir } from "os";
10867
10888
  import { join as join4 } from "path";
10868
- var CONFIG_PATH = join4(homedir(), ".hasna", "sandboxes", "cloud", "config.json");
10869
- function isMode(value) {
10870
- return value === "local" || value === "hybrid" || value === "cloud";
10889
+ var STORAGE_CONFIG_PATH = join4(homedir(), ".hasna", "sandboxes", "storage", "config.json");
10890
+ var SANDBOXES_STORAGE_ENV = "HASNA_SANDBOXES_DATABASE_URL";
10891
+ var SANDBOXES_STORAGE_FALLBACK_ENV = "SANDBOXES_DATABASE_URL";
10892
+ var SANDBOXES_STORAGE_MODE_ENV = "HASNA_SANDBOXES_STORAGE_MODE";
10893
+ var SANDBOXES_STORAGE_MODE_FALLBACK_ENV = "SANDBOXES_STORAGE_MODE";
10894
+ var STORAGE_DATABASE_ENV = [SANDBOXES_STORAGE_ENV, SANDBOXES_STORAGE_FALLBACK_ENV];
10895
+ function readEnv(name) {
10896
+ const value = process.env[name]?.trim();
10897
+ return value || undefined;
10898
+ }
10899
+ function normalizeStorageMode(value) {
10900
+ const normalized = value?.trim().toLowerCase();
10901
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
10902
+ return normalized;
10903
+ return;
10904
+ }
10905
+ function getStorageDatabaseEnvName() {
10906
+ for (const name of STORAGE_DATABASE_ENV) {
10907
+ if (readEnv(name))
10908
+ return name;
10909
+ }
10910
+ return null;
10911
+ }
10912
+ function getStorageDatabaseEnv() {
10913
+ const name = getStorageDatabaseEnvName();
10914
+ return name ? { name } : null;
10871
10915
  }
10872
- function envConnectionString() {
10873
- return process.env["HASNA_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["OPEN_SANDBOXES_CLOUD_DATABASE_URL"] ?? process.env["SANDBOXES_CLOUD_DATABASE_URL"];
10916
+ function getStorageDatabaseUrl() {
10917
+ const env = getStorageDatabaseEnv();
10918
+ return env ? readEnv(env.name) : undefined;
10874
10919
  }
10875
- function getCloudConfig() {
10920
+ function getStorageConfig() {
10876
10921
  const config = {
10877
10922
  mode: "local",
10878
10923
  rds: {
10879
10924
  host: "",
10880
10925
  port: 5432,
10881
10926
  username: "",
10882
- password_env: "SANDBOXES_CLOUD_DATABASE_PASSWORD",
10927
+ password_env: "SANDBOXES_DATABASE_PASSWORD",
10883
10928
  ssl: true
10884
10929
  }
10885
10930
  };
10886
- if (existsSync4(CONFIG_PATH)) {
10931
+ if (existsSync4(STORAGE_CONFIG_PATH)) {
10887
10932
  try {
10888
- const raw = JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
10889
- config.mode = raw.mode ?? config.mode;
10933
+ const raw = JSON.parse(readFileSync3(STORAGE_CONFIG_PATH, "utf-8"));
10934
+ config.mode = normalizeStorageMode(raw.mode) ?? config.mode;
10890
10935
  config.rds = { ...config.rds, ...raw.rds ?? {} };
10891
10936
  } catch {}
10892
10937
  }
10893
- const modeOverride = process.env["HASNA_SANDBOXES_CLOUD_MODE"] ?? process.env["OPEN_SANDBOXES_CLOUD_MODE"] ?? process.env["SANDBOXES_CLOUD_MODE"];
10894
- if (isMode(modeOverride)) {
10895
- config.mode = modeOverride;
10896
- } else if (envConnectionString() && config.mode === "local") {
10938
+ const modeOverride = readEnv(SANDBOXES_STORAGE_MODE_ENV) ?? readEnv(SANDBOXES_STORAGE_MODE_FALLBACK_ENV);
10939
+ const mode = normalizeStorageMode(modeOverride);
10940
+ if (mode) {
10941
+ config.mode = mode;
10942
+ } else if (getStorageDatabaseUrl() && config.mode === "local") {
10897
10943
  config.mode = "hybrid";
10898
10944
  }
10899
10945
  return config;
10900
10946
  }
10901
- function getConnectionString(dbName = "sandboxes") {
10902
- const direct = envConnectionString();
10947
+ function getStorageConnectionString(dbName = "sandboxes") {
10948
+ const direct = getStorageDatabaseUrl();
10903
10949
  if (direct)
10904
10950
  return direct;
10905
- const config = getCloudConfig();
10951
+ const config = getStorageConfig();
10906
10952
  const { host, port, username, password_env, ssl } = config.rds;
10907
10953
  if (!host || !username) {
10908
- throw new Error("Cloud database is not configured. Set HASNA_SANDBOXES_CLOUD_DATABASE_URL or configure ~/.hasna/sandboxes/cloud/config.json.");
10954
+ throw new Error("Storage database is not configured. Set HASNA_SANDBOXES_DATABASE_URL or configure ~/.hasna/sandboxes/storage/config.json.");
10909
10955
  }
10910
10956
  const password = process.env[password_env];
10911
10957
  if (!password) {
10912
- throw new Error(`Cloud database password is not set. Export ${password_env}.`);
10958
+ throw new Error(`Storage database password is not set. Export ${password_env}.`);
10913
10959
  }
10914
10960
  const sslParam = ssl ? "?sslmode=require" : "";
10915
10961
  return `postgres://${username}:${encodeURIComponent(password)}@${host}:${port}/${dbName}${sslParam}`;
@@ -11005,6 +11051,9 @@ var PG_MIGRATIONS = [
11005
11051
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_status ON sandboxes(status)`,
11006
11052
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider ON sandboxes(provider)`,
11007
11053
  `CREATE INDEX IF NOT EXISTS idx_sandboxes_project ON sandboxes(project_id)`,
11054
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_status_created ON sandboxes(status, created_at DESC)`,
11055
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_provider_created ON sandboxes(provider, created_at DESC)`,
11056
+ `CREATE INDEX IF NOT EXISTS idx_sandboxes_project_created ON sandboxes(project_id, created_at DESC)`,
11008
11057
  `CREATE TABLE IF NOT EXISTS sandbox_sessions (
11009
11058
  id TEXT PRIMARY KEY,
11010
11059
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -11018,6 +11067,9 @@ var PG_MIGRATIONS = [
11018
11067
  )`,
11019
11068
  `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox ON sandbox_sessions(sandbox_id)`,
11020
11069
  `CREATE INDEX IF NOT EXISTS idx_sessions_status ON sandbox_sessions(status)`,
11070
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_started ON sandbox_sessions(sandbox_id, started_at DESC)`,
11071
+ `CREATE INDEX IF NOT EXISTS idx_sessions_status_started ON sandbox_sessions(status, started_at DESC)`,
11072
+ `CREATE INDEX IF NOT EXISTS idx_sessions_sandbox_status_started ON sandbox_sessions(sandbox_id, status, started_at DESC)`,
11021
11073
  `CREATE TABLE IF NOT EXISTS sandbox_events (
11022
11074
  id TEXT PRIMARY KEY,
11023
11075
  sandbox_id TEXT NOT NULL REFERENCES sandboxes(id) ON DELETE CASCADE,
@@ -11029,6 +11081,8 @@ var PG_MIGRATIONS = [
11029
11081
  `CREATE INDEX IF NOT EXISTS idx_events_sandbox ON sandbox_events(sandbox_id)`,
11030
11082
  `CREATE INDEX IF NOT EXISTS idx_events_session ON sandbox_events(session_id)`,
11031
11083
  `CREATE INDEX IF NOT EXISTS idx_events_type ON sandbox_events(type)`,
11084
+ `CREATE INDEX IF NOT EXISTS idx_events_sandbox_created ON sandbox_events(sandbox_id, created_at ASC)`,
11085
+ `CREATE INDEX IF NOT EXISTS idx_events_session_created ON sandbox_events(session_id, created_at ASC)`,
11032
11086
  `CREATE TABLE IF NOT EXISTS webhooks (
11033
11087
  id TEXT PRIMARY KEY,
11034
11088
  url TEXT NOT NULL,
@@ -11079,8 +11133,8 @@ var PG_MIGRATIONS = [
11079
11133
  `ALTER TABLE agents ADD COLUMN IF NOT EXISTS active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
11080
11134
  ];
11081
11135
 
11082
- // src/db/cloud-sync.ts
11083
- var CLOUD_TABLES = [
11136
+ // src/db/storage-sync.ts
11137
+ var STORAGE_TABLES = [
11084
11138
  "projects",
11085
11139
  "agents",
11086
11140
  "sandboxes",
@@ -11174,21 +11228,26 @@ function upsertSqlite(db2, table, rows) {
11174
11228
  }
11175
11229
  return written;
11176
11230
  }
11177
- async function getCloudPg() {
11178
- return new PgAdapterAsync(getConnectionString("sandboxes"));
11231
+ async function getStoragePg() {
11232
+ return new PgAdapterAsync(getStorageConnectionString("sandboxes"));
11179
11233
  }
11180
- async function runCloudMigrations(remote) {
11234
+ async function runStorageMigrations(remote) {
11181
11235
  for (const migration of PG_MIGRATIONS) {
11182
11236
  await remote.exec(migration);
11183
11237
  }
11184
11238
  }
11185
- function getCloudStatus(db2 = getDatabase()) {
11186
- const config = getCloudConfig();
11239
+ function getStorageStatus(db2 = getDatabase()) {
11240
+ const config = getStorageConfig();
11241
+ const activeEnv = getStorageDatabaseEnv();
11187
11242
  return {
11243
+ configured: Boolean(activeEnv),
11188
11244
  mode: config.mode,
11189
- enabled: config.mode === "hybrid" || config.mode === "cloud",
11245
+ enabled: config.mode === "hybrid" || config.mode === "remote",
11246
+ env: STORAGE_DATABASE_ENV,
11247
+ activeEnv: activeEnv?.name ?? null,
11248
+ service: "sandboxes",
11190
11249
  db_path: getDbPath(),
11191
- tables: CLOUD_TABLES.map((table) => {
11250
+ tables: STORAGE_TABLES.map((table) => {
11192
11251
  try {
11193
11252
  const row = db2.query(`SELECT COUNT(*) as count FROM ${quoteId(table)}`).get();
11194
11253
  return { table, rows: row.count };
@@ -11198,12 +11257,12 @@ function getCloudStatus(db2 = getDatabase()) {
11198
11257
  })
11199
11258
  };
11200
11259
  }
11201
- async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
11260
+ async function pushStorageChanges(tables = [...STORAGE_TABLES]) {
11202
11261
  const db2 = getDatabase();
11203
- const remote = await getCloudPg();
11262
+ const remote = await getStoragePg();
11204
11263
  const results = [];
11205
11264
  try {
11206
- await runCloudMigrations(remote);
11265
+ await runStorageMigrations(remote);
11207
11266
  for (const table of tables) {
11208
11267
  const result = { table, direction: "push", rowsRead: 0, rowsWritten: 0, errors: [] };
11209
11268
  try {
@@ -11220,12 +11279,12 @@ async function pushCloudChanges(tables = [...CLOUD_TABLES]) {
11220
11279
  }
11221
11280
  return results;
11222
11281
  }
11223
- async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
11282
+ async function pullStorageChanges(tables = [...STORAGE_TABLES]) {
11224
11283
  const db2 = getDatabase();
11225
- const remote = await getCloudPg();
11284
+ const remote = await getStoragePg();
11226
11285
  const results = [];
11227
11286
  try {
11228
- await runCloudMigrations(remote);
11287
+ await runStorageMigrations(remote);
11229
11288
  for (const table of tables) {
11230
11289
  const result = { table, direction: "pull", rowsRead: 0, rowsWritten: 0, errors: [] };
11231
11290
  try {
@@ -11242,20 +11301,26 @@ async function pullCloudChanges(tables = [...CLOUD_TABLES]) {
11242
11301
  }
11243
11302
  return results;
11244
11303
  }
11245
- async function syncCloudChanges(tables = [...CLOUD_TABLES]) {
11304
+ async function syncStorageChanges(tables = [...STORAGE_TABLES]) {
11246
11305
  return {
11247
- push: await pushCloudChanges(tables),
11248
- pull: await pullCloudChanges(tables)
11306
+ push: await pushStorageChanges(tables),
11307
+ pull: await pullStorageChanges(tables)
11249
11308
  };
11250
11309
  }
11251
- function parseCloudTables(raw) {
11310
+ function parseStorageTables(raw) {
11252
11311
  if (!raw)
11253
- return [...CLOUD_TABLES];
11312
+ return [...STORAGE_TABLES];
11254
11313
  const requested = raw.split(",").map((table) => table.trim()).filter(Boolean);
11255
- return requested.length > 0 ? requested : [...CLOUD_TABLES];
11314
+ if (requested.length === 0)
11315
+ return [...STORAGE_TABLES];
11316
+ const allowed = new Set(STORAGE_TABLES);
11317
+ const invalid = requested.filter((table) => !allowed.has(table));
11318
+ if (invalid.length > 0)
11319
+ throw new Error(`Unknown sandboxes sync table(s): ${invalid.join(", ")}`);
11320
+ return requested;
11256
11321
  }
11257
11322
 
11258
- // src/mcp/cloud-tools.ts
11323
+ // src/mcp/storage-tools.ts
11259
11324
  function ok(data) {
11260
11325
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
11261
11326
  }
@@ -11265,42 +11330,42 @@ function err(error) {
11265
11330
  isError: true
11266
11331
  };
11267
11332
  }
11268
- function registerSandboxesCloudTools(server) {
11269
- server.tool("sandboxes_cloud_status", "Show sandboxes local database and cloud sync status", {}, async () => {
11333
+ function registerSandboxesStorageTools(server) {
11334
+ server.tool("sandboxes_storage_status", "Show sandboxes local database and storage sync status", {}, async () => {
11270
11335
  try {
11271
- return ok(getCloudStatus());
11336
+ return ok(getStorageStatus());
11272
11337
  } catch (error) {
11273
11338
  return err(error);
11274
11339
  }
11275
11340
  });
11276
- server.tool("sandboxes_cloud_push", "Push local sandboxes data to PostgreSQL", {
11341
+ server.tool("sandboxes_storage_push", "Push local sandboxes data to PostgreSQL", {
11277
11342
  tables: exports_external.string().optional().describe("Comma-separated table names")
11278
11343
  }, async ({ tables }) => {
11279
11344
  try {
11280
- return ok(await pushCloudChanges(parseCloudTables(tables)));
11345
+ return ok(await pushStorageChanges(parseStorageTables(tables)));
11281
11346
  } catch (error) {
11282
11347
  return err(error);
11283
11348
  }
11284
11349
  });
11285
- server.tool("sandboxes_cloud_pull", "Pull PostgreSQL sandboxes data into the local database", {
11350
+ server.tool("sandboxes_storage_pull", "Pull PostgreSQL sandboxes data into the local database", {
11286
11351
  tables: exports_external.string().optional().describe("Comma-separated table names")
11287
11352
  }, async ({ tables }) => {
11288
11353
  try {
11289
- return ok(await pullCloudChanges(parseCloudTables(tables)));
11354
+ return ok(await pullStorageChanges(parseStorageTables(tables)));
11290
11355
  } catch (error) {
11291
11356
  return err(error);
11292
11357
  }
11293
11358
  });
11294
- server.tool("sandboxes_cloud_sync", "Push local changes, then pull remote changes", {
11359
+ server.tool("sandboxes_storage_sync", "Push local changes, then pull remote changes", {
11295
11360
  tables: exports_external.string().optional().describe("Comma-separated table names")
11296
11361
  }, async ({ tables }) => {
11297
11362
  try {
11298
- return ok(await syncCloudChanges(parseCloudTables(tables)));
11363
+ return ok(await syncStorageChanges(parseStorageTables(tables)));
11299
11364
  } catch (error) {
11300
11365
  return err(error);
11301
11366
  }
11302
11367
  });
11303
- server.tool("sandboxes_cloud_feedback", "Save feedback for sandboxes", {
11368
+ server.tool("sandboxes_feedback", "Save feedback for sandboxes", {
11304
11369
  message: exports_external.string(),
11305
11370
  email: exports_external.string().optional(),
11306
11371
  category: exports_external.enum(["bug", "feature", "general"]).optional()
@@ -12124,7 +12189,7 @@ function buildServer() {
12124
12189
  return err2(e);
12125
12190
  }
12126
12191
  });
12127
- registerSandboxesCloudTools(server);
12192
+ registerSandboxesStorageTools(server);
12128
12193
  return server;
12129
12194
  }
12130
12195
 
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSandboxesStorageTools(server: McpServer): void;