@hasna/computer 0.1.8 → 0.1.9

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 (44) hide show
  1. package/LICENSE +4 -2
  2. package/README.md +73 -7
  3. package/dist/apps/ghostty/applescript.d.ts +36 -0
  4. package/dist/apps/ghostty/applescript.d.ts.map +1 -0
  5. package/dist/apps/ghostty/applescript.test.d.ts +2 -0
  6. package/dist/apps/ghostty/applescript.test.d.ts.map +1 -0
  7. package/dist/apps/ghostty/driver.d.ts +10 -0
  8. package/dist/apps/ghostty/driver.d.ts.map +1 -0
  9. package/dist/apps/registry.d.ts +5 -0
  10. package/dist/apps/registry.d.ts.map +1 -0
  11. package/dist/apps/registry.test.d.ts +2 -0
  12. package/dist/apps/registry.test.d.ts.map +1 -0
  13. package/dist/apps/types.d.ts +47 -0
  14. package/dist/apps/types.d.ts.map +1 -0
  15. package/dist/cli/index.js +1055 -91
  16. package/dist/cli/storage.d.ts +3 -0
  17. package/dist/cli/storage.d.ts.map +1 -0
  18. package/dist/cli/storage.test.d.ts +2 -0
  19. package/dist/cli/storage.test.d.ts.map +1 -0
  20. package/dist/db/storage-sync.d.ts +57 -0
  21. package/dist/db/storage-sync.d.ts.map +1 -0
  22. package/dist/db/storage-sync.test.d.ts +2 -0
  23. package/dist/db/storage-sync.test.d.ts.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +84 -26
  27. package/dist/mcp/http.d.ts +16 -0
  28. package/dist/mcp/http.d.ts.map +1 -0
  29. package/dist/mcp/http.test.d.ts +2 -0
  30. package/dist/mcp/http.test.d.ts.map +1 -0
  31. package/dist/mcp/index.d.ts +1 -1
  32. package/dist/mcp/index.d.ts.map +1 -1
  33. package/dist/mcp/index.js +609 -262
  34. package/dist/mcp/server.d.ts +4 -0
  35. package/dist/mcp/server.d.ts.map +1 -0
  36. package/dist/server/index.js +34565 -8747
  37. package/dist/storage.d.ts +5 -0
  38. package/dist/storage.d.ts.map +1 -0
  39. package/dist/storage.js +5519 -0
  40. package/package.json +7 -2
  41. package/dist/cli/cloud.d.ts +0 -3
  42. package/dist/cli/cloud.d.ts.map +0 -1
  43. package/dist/db/cloud-sync.d.ts +0 -33
  44. package/dist/db/cloud-sync.d.ts.map +0 -1
package/dist/cli/index.js CHANGED
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
993
993
  this._exitCallback = (err) => {
994
994
  if (err.code !== "commander.executeSubCommandAsync") {
995
995
  throw err;
996
- } else {}
996
+ }
997
997
  };
998
998
  }
999
999
  return this;
@@ -2091,11 +2091,11 @@ __export(exports_config, {
2091
2091
  getConfigPath: () => getConfigPath,
2092
2092
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
2093
2093
  });
2094
- import { join as join4 } from "path";
2095
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "fs";
2094
+ import { join as join5 } from "path";
2095
+ import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync } from "fs";
2096
2096
  function loadConfig() {
2097
2097
  try {
2098
- if (existsSync2(CONFIG_PATH)) {
2098
+ if (existsSync3(CONFIG_PATH)) {
2099
2099
  const raw = readFileSync(CONFIG_PATH, "utf-8");
2100
2100
  const user = JSON.parse(raw);
2101
2101
  return mergeConfig(DEFAULT_CONFIG, user);
@@ -2162,8 +2162,8 @@ function getConfigPath() {
2162
2162
  }
2163
2163
  var CONFIG_DIR, CONFIG_PATH, DEFAULT_CONFIG;
2164
2164
  var init_config = __esm(() => {
2165
- CONFIG_DIR = join4(process.env.HOME ?? "~", ".hasna", "computer");
2166
- CONFIG_PATH = join4(CONFIG_DIR, "config.json");
2165
+ CONFIG_DIR = join5(process.env.HOME ?? "~", ".hasna", "computer");
2166
+ CONFIG_PATH = join5(CONFIG_DIR, "config.json");
2167
2167
  DEFAULT_CONFIG = {
2168
2168
  provider: "anthropic",
2169
2169
  maxSteps: 50,
@@ -7133,20 +7133,698 @@ var {
7133
7133
  Help
7134
7134
  } = import__.default;
7135
7135
 
7136
+ // node_modules/@hasna/events/dist/commander.js
7137
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
7138
+ import { existsSync } from "fs";
7139
+ import { homedir } from "os";
7140
+ import { join } from "path";
7141
+ import { createHmac, timingSafeEqual } from "crypto";
7142
+ import { randomUUID } from "crypto";
7143
+ import { spawn } from "child_process";
7144
+ import { randomUUID as randomUUID2 } from "crypto";
7145
+ function getPathValue(input, path) {
7146
+ return path.split(".").reduce((value, part) => {
7147
+ if (value && typeof value === "object" && part in value) {
7148
+ return value[part];
7149
+ }
7150
+ return;
7151
+ }, input);
7152
+ }
7153
+ function wildcardToRegExp(pattern) {
7154
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
7155
+ return new RegExp(`^${escaped}$`);
7156
+ }
7157
+ function matchString(value, matcher) {
7158
+ if (matcher === undefined)
7159
+ return true;
7160
+ if (value === undefined)
7161
+ return false;
7162
+ const matchers = Array.isArray(matcher) ? matcher : [matcher];
7163
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
7164
+ }
7165
+ function matchRecord(input, matcher) {
7166
+ if (!matcher)
7167
+ return true;
7168
+ return Object.entries(matcher).every(([path, expected]) => {
7169
+ const actual = getPathValue(input, path);
7170
+ if (typeof expected === "string" || Array.isArray(expected)) {
7171
+ return matchString(actual === undefined ? undefined : String(actual), expected);
7172
+ }
7173
+ return actual === expected;
7174
+ });
7175
+ }
7176
+ function eventMatchesFilter(event, filter) {
7177
+ return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
7178
+ }
7179
+ function channelMatchesEvent(channel, event) {
7180
+ if (!channel.enabled)
7181
+ return false;
7182
+ if (!channel.filters || channel.filters.length === 0)
7183
+ return true;
7184
+ return channel.filters.some((filter) => eventMatchesFilter(event, filter));
7185
+ }
7186
+ var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
7187
+ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
7188
+ function getEventsDataDir(override) {
7189
+ return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
7190
+ }
7191
+
7192
+ class JsonEventsStore {
7193
+ dataDir;
7194
+ channelsPath;
7195
+ eventsPath;
7196
+ deliveriesPath;
7197
+ constructor(dataDir = getEventsDataDir()) {
7198
+ this.dataDir = dataDir;
7199
+ this.channelsPath = join(dataDir, "channels.json");
7200
+ this.eventsPath = join(dataDir, "events.json");
7201
+ this.deliveriesPath = join(dataDir, "deliveries.json");
7202
+ }
7203
+ async init() {
7204
+ await mkdir(this.dataDir, { recursive: true, mode: 448 });
7205
+ await chmod(this.dataDir, 448).catch(() => {
7206
+ return;
7207
+ });
7208
+ await this.ensureArrayFile(this.channelsPath);
7209
+ await this.ensureArrayFile(this.eventsPath);
7210
+ await this.ensureArrayFile(this.deliveriesPath);
7211
+ }
7212
+ async addChannel(channel) {
7213
+ await this.init();
7214
+ const channels = await this.readJson(this.channelsPath, []);
7215
+ const index = channels.findIndex((item) => item.id === channel.id);
7216
+ if (index >= 0) {
7217
+ channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
7218
+ } else {
7219
+ channels.push(channel);
7220
+ }
7221
+ await this.writeJson(this.channelsPath, channels);
7222
+ return index >= 0 ? channels[index] : channel;
7223
+ }
7224
+ async listChannels() {
7225
+ await this.init();
7226
+ return this.readJson(this.channelsPath, []);
7227
+ }
7228
+ async getChannel(id) {
7229
+ const channels = await this.listChannels();
7230
+ return channels.find((channel) => channel.id === id);
7231
+ }
7232
+ async removeChannel(id) {
7233
+ await this.init();
7234
+ const channels = await this.readJson(this.channelsPath, []);
7235
+ const next = channels.filter((channel) => channel.id !== id);
7236
+ await this.writeJson(this.channelsPath, next);
7237
+ return next.length !== channels.length;
7238
+ }
7239
+ async appendEvent(event) {
7240
+ await this.init();
7241
+ const events = await this.readJson(this.eventsPath, []);
7242
+ events.push(event);
7243
+ await this.writeJson(this.eventsPath, events);
7244
+ return event;
7245
+ }
7246
+ async listEvents() {
7247
+ await this.init();
7248
+ return this.readJson(this.eventsPath, []);
7249
+ }
7250
+ async findEventByIdentity(identity) {
7251
+ const events = await this.listEvents();
7252
+ return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
7253
+ }
7254
+ async appendDelivery(result) {
7255
+ await this.init();
7256
+ const deliveries = await this.readJson(this.deliveriesPath, []);
7257
+ deliveries.push(result);
7258
+ await this.writeJson(this.deliveriesPath, deliveries);
7259
+ return result;
7260
+ }
7261
+ async listDeliveries() {
7262
+ await this.init();
7263
+ return this.readJson(this.deliveriesPath, []);
7264
+ }
7265
+ async exportData() {
7266
+ return {
7267
+ channels: await this.listChannels(),
7268
+ events: await this.listEvents(),
7269
+ deliveries: await this.listDeliveries()
7270
+ };
7271
+ }
7272
+ async ensureArrayFile(path) {
7273
+ if (!existsSync(path)) {
7274
+ await writeFile(path, `[]
7275
+ `, { encoding: "utf-8", mode: 384 });
7276
+ }
7277
+ await chmod(path, 384).catch(() => {
7278
+ return;
7279
+ });
7280
+ }
7281
+ async readJson(path, fallback) {
7282
+ try {
7283
+ const raw = await readFile(path, "utf-8");
7284
+ if (!raw.trim())
7285
+ return fallback;
7286
+ return JSON.parse(raw);
7287
+ } catch (error) {
7288
+ if (error.code === "ENOENT")
7289
+ return fallback;
7290
+ throw error;
7291
+ }
7292
+ }
7293
+ async writeJson(path, value) {
7294
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
7295
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
7296
+ `, { encoding: "utf-8", mode: 384 });
7297
+ await rename(tempPath, path);
7298
+ await chmod(path, 384).catch(() => {
7299
+ return;
7300
+ });
7301
+ }
7302
+ }
7303
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
7304
+ function buildSignatureBase(timestamp, body) {
7305
+ return `${timestamp}.${body}`;
7306
+ }
7307
+ function signPayload(secret, timestamp, body) {
7308
+ const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
7309
+ return `sha256=${digest}`;
7310
+ }
7311
+ function now() {
7312
+ return new Date().toISOString();
7313
+ }
7314
+ function truncate(value, max = 4096) {
7315
+ return value.length > max ? `${value.slice(0, max)}...` : value;
7316
+ }
7317
+ function buildWebhookRequest(event, channel) {
7318
+ if (!channel.webhook)
7319
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7320
+ const body = JSON.stringify(event);
7321
+ const timestamp = event.time;
7322
+ const headers = {
7323
+ "Content-Type": "application/json",
7324
+ "User-Agent": "@hasna/events",
7325
+ "X-Hasna-Event-Id": event.id,
7326
+ "X-Hasna-Event-Type": event.type,
7327
+ "X-Hasna-Timestamp": timestamp,
7328
+ ...channel.webhook.headers
7329
+ };
7330
+ if (channel.webhook.secret) {
7331
+ headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
7332
+ }
7333
+ return { body, headers };
7334
+ }
7335
+ async function dispatchWebhook(event, channel, options = {}) {
7336
+ if (!channel.webhook)
7337
+ throw new Error(`Channel ${channel.id} has no webhook config`);
7338
+ const startedAt = now();
7339
+ const { body, headers } = buildWebhookRequest(event, channel);
7340
+ const controller = new AbortController;
7341
+ const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
7342
+ try {
7343
+ const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
7344
+ method: "POST",
7345
+ headers,
7346
+ body,
7347
+ signal: controller.signal
7348
+ });
7349
+ const responseBody = truncate(await response.text());
7350
+ return {
7351
+ attempt: 1,
7352
+ status: response.ok ? "success" : "failed",
7353
+ startedAt,
7354
+ completedAt: now(),
7355
+ responseStatus: response.status,
7356
+ responseBody,
7357
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
7358
+ };
7359
+ } catch (error) {
7360
+ return {
7361
+ attempt: 1,
7362
+ status: "failed",
7363
+ startedAt,
7364
+ completedAt: now(),
7365
+ error: error instanceof Error ? error.message : String(error)
7366
+ };
7367
+ } finally {
7368
+ clearTimeout(timeout);
7369
+ }
7370
+ }
7371
+ async function dispatchCommand(event, channel) {
7372
+ if (!channel.command)
7373
+ throw new Error(`Channel ${channel.id} has no command config`);
7374
+ const startedAt = now();
7375
+ const eventJson = JSON.stringify(event);
7376
+ const env = {
7377
+ ...process.env,
7378
+ ...channel.command.env,
7379
+ HASNA_CHANNEL_ID: channel.id,
7380
+ HASNA_EVENT_ID: event.id,
7381
+ HASNA_EVENT_TYPE: event.type,
7382
+ HASNA_EVENT_SOURCE: event.source,
7383
+ HASNA_EVENT_SUBJECT: event.subject ?? "",
7384
+ HASNA_EVENT_SEVERITY: event.severity,
7385
+ HASNA_EVENT_TIME: event.time,
7386
+ HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
7387
+ HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
7388
+ HASNA_EVENT_JSON: eventJson
7389
+ };
7390
+ return new Promise((resolve) => {
7391
+ const child = spawn(channel.command.command, channel.command.args ?? [], {
7392
+ cwd: channel.command.cwd,
7393
+ env,
7394
+ stdio: ["pipe", "pipe", "pipe"]
7395
+ });
7396
+ let stdout = "";
7397
+ let stderr = "";
7398
+ const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
7399
+ child.stdin.end(eventJson);
7400
+ child.stdout.on("data", (chunk) => {
7401
+ stdout += chunk.toString();
7402
+ });
7403
+ child.stderr.on("data", (chunk) => {
7404
+ stderr += chunk.toString();
7405
+ });
7406
+ child.on("error", (error) => {
7407
+ clearTimeout(timeout);
7408
+ resolve({
7409
+ attempt: 1,
7410
+ status: "failed",
7411
+ startedAt,
7412
+ completedAt: now(),
7413
+ stdout: truncate(stdout),
7414
+ stderr: truncate(stderr),
7415
+ error: error.message
7416
+ });
7417
+ });
7418
+ child.on("close", (code, signal) => {
7419
+ clearTimeout(timeout);
7420
+ const success = code === 0;
7421
+ resolve({
7422
+ attempt: 1,
7423
+ status: success ? "success" : "failed",
7424
+ startedAt,
7425
+ completedAt: now(),
7426
+ stdout: truncate(stdout),
7427
+ stderr: truncate(stderr),
7428
+ error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
7429
+ });
7430
+ });
7431
+ });
7432
+ }
7433
+ async function dispatchChannel(event, channel, options = {}) {
7434
+ if (channel.transport === "webhook")
7435
+ return dispatchWebhook(event, channel, options);
7436
+ if (channel.transport === "command")
7437
+ return dispatchCommand(event, channel);
7438
+ return {
7439
+ attempt: 1,
7440
+ status: "skipped",
7441
+ startedAt: now(),
7442
+ completedAt: now(),
7443
+ error: `Unsupported transport: ${channel.transport}`
7444
+ };
7445
+ }
7446
+ function createDeliveryResult(event, channel, attempts) {
7447
+ const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
7448
+ return {
7449
+ id: randomUUID(),
7450
+ eventId: event.id,
7451
+ channelId: channel.id,
7452
+ transport: channel.transport,
7453
+ status,
7454
+ attempts,
7455
+ createdAt: attempts[0]?.startedAt ?? now(),
7456
+ completedAt: attempts.at(-1)?.completedAt ?? now()
7457
+ };
7458
+ }
7459
+ function createEvent(input) {
7460
+ return {
7461
+ id: input.id ?? randomUUID2(),
7462
+ source: input.source,
7463
+ type: input.type,
7464
+ time: normalizeTime(input.time),
7465
+ subject: input.subject,
7466
+ severity: input.severity ?? "info",
7467
+ data: input.data ?? {},
7468
+ message: input.message,
7469
+ dedupeKey: input.dedupeKey,
7470
+ schemaVersion: input.schemaVersion ?? "1.0",
7471
+ metadata: input.metadata ?? {}
7472
+ };
7473
+ }
7474
+
7475
+ class EventsClient {
7476
+ store;
7477
+ redactors;
7478
+ transportOptions;
7479
+ constructor(options = {}) {
7480
+ this.store = options.store ?? new JsonEventsStore(options.dataDir);
7481
+ this.redactors = options.redactors ?? [];
7482
+ this.transportOptions = { fetchImpl: options.fetchImpl };
7483
+ }
7484
+ async addChannel(input) {
7485
+ const timestamp = new Date().toISOString();
7486
+ return this.store.addChannel({
7487
+ ...input,
7488
+ createdAt: input.createdAt ?? timestamp,
7489
+ updatedAt: input.updatedAt ?? timestamp
7490
+ });
7491
+ }
7492
+ async listChannels() {
7493
+ return this.store.listChannels();
7494
+ }
7495
+ async removeChannel(id) {
7496
+ return this.store.removeChannel(id);
7497
+ }
7498
+ async emit(input, options = {}) {
7499
+ const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
7500
+ if (options.dedupe !== false) {
7501
+ const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
7502
+ if (existing) {
7503
+ return { event: existing, deliveries: [], deduped: true };
7504
+ }
7505
+ }
7506
+ await this.store.appendEvent(event);
7507
+ const deliveries = options.deliver === false ? [] : await this.deliver(event);
7508
+ return { event, deliveries, deduped: false };
7509
+ }
7510
+ async listEvents() {
7511
+ return this.store.listEvents();
7512
+ }
7513
+ async listDeliveries() {
7514
+ return this.store.listDeliveries();
7515
+ }
7516
+ async deliver(event) {
7517
+ const channels = await this.store.listChannels();
7518
+ const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
7519
+ const deliveries = [];
7520
+ for (const channel of selected) {
7521
+ const eventForChannel = await this.applyRedaction(event, channel);
7522
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7523
+ await this.store.appendDelivery(result);
7524
+ deliveries.push(result);
7525
+ }
7526
+ return deliveries;
7527
+ }
7528
+ async testChannel(id, input = {}) {
7529
+ const channel = await this.store.getChannel(id);
7530
+ if (!channel)
7531
+ throw new Error(`Channel not found: ${id}`);
7532
+ const event = createEvent({
7533
+ source: input.source ?? "hasna.events",
7534
+ type: input.type ?? "events.test",
7535
+ subject: input.subject ?? id,
7536
+ severity: input.severity ?? "info",
7537
+ data: input.data ?? { test: true },
7538
+ message: input.message ?? "Hasna events test delivery",
7539
+ dedupeKey: input.dedupeKey,
7540
+ schemaVersion: input.schemaVersion,
7541
+ metadata: input.metadata,
7542
+ time: input.time,
7543
+ id: input.id
7544
+ });
7545
+ const eventForChannel = await this.applyRedaction(event, channel);
7546
+ const result = await this.deliverWithRetry(eventForChannel, channel);
7547
+ await this.store.appendDelivery(result);
7548
+ return result;
7549
+ }
7550
+ async replay(options = {}) {
7551
+ const events = (await this.store.listEvents()).filter((event) => {
7552
+ if (options.eventId && event.id !== options.eventId)
7553
+ return false;
7554
+ if (options.source && event.source !== options.source)
7555
+ return false;
7556
+ if (options.type && event.type !== options.type)
7557
+ return false;
7558
+ return true;
7559
+ });
7560
+ if (options.dryRun)
7561
+ return { events, deliveries: [] };
7562
+ const deliveries = [];
7563
+ for (const event of events) {
7564
+ deliveries.push(...await this.deliver(event));
7565
+ }
7566
+ return { events, deliveries };
7567
+ }
7568
+ async applyRedaction(event, channel) {
7569
+ let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
7570
+ for (const redactor of this.redactors) {
7571
+ next = await redactor(next, channel);
7572
+ }
7573
+ return next;
7574
+ }
7575
+ async deliverWithRetry(event, channel) {
7576
+ const policy = normalizeRetryPolicy(channel.retry);
7577
+ const attempts = [];
7578
+ for (let index = 0;index < policy.maxAttempts; index += 1) {
7579
+ const attempt = await dispatchChannel(event, channel, this.transportOptions);
7580
+ attempt.attempt = index + 1;
7581
+ if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
7582
+ attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
7583
+ }
7584
+ attempts.push(attempt);
7585
+ if (attempt.status !== "failed")
7586
+ break;
7587
+ if (attempt.nextBackoffMs)
7588
+ await Bun.sleep(attempt.nextBackoffMs);
7589
+ }
7590
+ return createDeliveryResult(event, channel, attempts);
7591
+ }
7592
+ }
7593
+ function redactPaths(event, paths, replacement = "[REDACTED]") {
7594
+ if (paths.length === 0)
7595
+ return event;
7596
+ const copy = structuredClone(event);
7597
+ for (const path of paths) {
7598
+ setPath(copy, path, replacement);
7599
+ }
7600
+ return copy;
7601
+ }
7602
+ function sanitizeChannelForOutput(channel) {
7603
+ const copy = structuredClone(channel);
7604
+ if (copy.webhook?.secret)
7605
+ copy.webhook.secret = "[REDACTED]";
7606
+ if (copy.command?.env) {
7607
+ copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
7608
+ }
7609
+ return copy;
7610
+ }
7611
+ function sanitizeChannelsForOutput(channels) {
7612
+ return channels.map(sanitizeChannelForOutput);
7613
+ }
7614
+ function redactSensitiveKeys(event, replacement = "[REDACTED]") {
7615
+ return redactValue(event, replacement);
7616
+ }
7617
+ function shouldRedactKey(key) {
7618
+ return /secret|token|password|api[_-]?key|authorization/i.test(key);
7619
+ }
7620
+ function redactValue(value, replacement) {
7621
+ if (Array.isArray(value))
7622
+ return value.map((item) => redactValue(item, replacement));
7623
+ if (!value || typeof value !== "object")
7624
+ return value;
7625
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
7626
+ key,
7627
+ shouldRedactKey(key) ? replacement : redactValue(item, replacement)
7628
+ ]));
7629
+ }
7630
+ function setPath(input, path, replacement) {
7631
+ const parts = path.split(".");
7632
+ let cursor = input;
7633
+ for (const part of parts.slice(0, -1)) {
7634
+ const next = cursor[part];
7635
+ if (!next || typeof next !== "object")
7636
+ return;
7637
+ cursor = next;
7638
+ }
7639
+ const last = parts.at(-1);
7640
+ if (last && last in cursor)
7641
+ cursor[last] = replacement;
7642
+ }
7643
+ function normalizeTime(value) {
7644
+ if (!value)
7645
+ return new Date().toISOString();
7646
+ return value instanceof Date ? value.toISOString() : value;
7647
+ }
7648
+ function normalizeRetryPolicy(policy) {
7649
+ return {
7650
+ maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
7651
+ backoffMs: Math.max(0, policy?.backoffMs ?? 250),
7652
+ multiplier: Math.max(1, policy?.multiplier ?? 2)
7653
+ };
7654
+ }
7655
+ function parseJsonObject(value, fallback) {
7656
+ if (!value)
7657
+ return fallback;
7658
+ const parsed = JSON.parse(value);
7659
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7660
+ throw new Error("Expected a JSON object");
7661
+ }
7662
+ return parsed;
7663
+ }
7664
+ function parseHeaders(values) {
7665
+ if (!values?.length)
7666
+ return;
7667
+ const headers = {};
7668
+ for (const value of values) {
7669
+ const separator = value.indexOf("=");
7670
+ if (separator === -1)
7671
+ throw new Error(`Invalid header, expected name=value: ${value}`);
7672
+ headers[value.slice(0, separator)] = value.slice(separator + 1);
7673
+ }
7674
+ return headers;
7675
+ }
7676
+ function parseFilter(options) {
7677
+ const filter2 = {};
7678
+ if (options.source)
7679
+ filter2.source = options.source;
7680
+ if (options.type)
7681
+ filter2.type = options.type;
7682
+ if (options.subject)
7683
+ filter2.subject = options.subject;
7684
+ if (options.severity)
7685
+ filter2.severity = options.severity;
7686
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
7687
+ }
7688
+ function createClient(options) {
7689
+ if (options.createClient)
7690
+ return options.createClient();
7691
+ return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
7692
+ }
7693
+ function print(value, json, text) {
7694
+ if (json)
7695
+ console.log(JSON.stringify(value, null, 2));
7696
+ else
7697
+ console.log(text);
7698
+ }
7699
+ function registerWebhookCommands(program2, options) {
7700
+ const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
7701
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions) => {
7702
+ const timestamp = new Date().toISOString();
7703
+ const channel = {
7704
+ id: actionOptions.id,
7705
+ name: actionOptions.name,
7706
+ enabled: !actionOptions.disabled,
7707
+ transport: actionOptions.transport,
7708
+ filters: parseFilter(actionOptions),
7709
+ retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
7710
+ redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
7711
+ createdAt: timestamp,
7712
+ updatedAt: timestamp
7713
+ };
7714
+ if (actionOptions.transport === "webhook") {
7715
+ channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
7716
+ } else if (actionOptions.transport === "command") {
7717
+ channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
7718
+ } else {
7719
+ throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
7720
+ }
7721
+ const saved = await createClient(options).addChannel(channel);
7722
+ print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
7723
+ });
7724
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
7725
+ const channels = await createClient(options).listChannels();
7726
+ if (actionOptions.json) {
7727
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
7728
+ return;
7729
+ }
7730
+ if (!channels.length) {
7731
+ console.log("No channels configured.");
7732
+ return;
7733
+ }
7734
+ for (const channel of channels) {
7735
+ console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
7736
+ }
7737
+ });
7738
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
7739
+ const removed = await createClient(options).removeChannel(id);
7740
+ print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
7741
+ });
7742
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
7743
+ const result = await createClient(options).testChannel(id, {
7744
+ source: options.source,
7745
+ type: actionOptions.type,
7746
+ subject: actionOptions.subject ?? id,
7747
+ message: actionOptions.message,
7748
+ data: parseJsonObject(actionOptions.data, { test: true })
7749
+ });
7750
+ print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
7751
+ });
7752
+ return webhooks;
7753
+ }
7754
+ function registerEventCommands(program2, options) {
7755
+ const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
7756
+ events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
7757
+ const result = await createClient(options).emit({
7758
+ source: actionOptions.source ?? options.source,
7759
+ type,
7760
+ subject: actionOptions.subject,
7761
+ severity: actionOptions.severity,
7762
+ message: actionOptions.message,
7763
+ dedupeKey: actionOptions.dedupeKey,
7764
+ data: parseJsonObject(actionOptions.data, {}),
7765
+ metadata: parseJsonObject(actionOptions.metadata, {})
7766
+ }, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
7767
+ print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
7768
+ });
7769
+ events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
7770
+ let rows = await createClient(options).listEvents();
7771
+ if (actionOptions.source)
7772
+ rows = rows.filter((event) => event.source === actionOptions.source);
7773
+ if (actionOptions.type)
7774
+ rows = rows.filter((event) => event.type === actionOptions.type);
7775
+ if (actionOptions.limit)
7776
+ rows = rows.slice(-actionOptions.limit);
7777
+ if (actionOptions.json) {
7778
+ console.log(JSON.stringify(rows, null, 2));
7779
+ return;
7780
+ }
7781
+ if (!rows.length) {
7782
+ console.log("No events recorded.");
7783
+ return;
7784
+ }
7785
+ for (const event of rows)
7786
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
7787
+ });
7788
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
7789
+ const result = await createClient(options).replay({
7790
+ eventId: actionOptions.id,
7791
+ source: actionOptions.source,
7792
+ type: actionOptions.type,
7793
+ dryRun: actionOptions.dryRun
7794
+ });
7795
+ print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
7796
+ });
7797
+ return events;
7798
+ }
7799
+ function registerEventsCommands(program2, options) {
7800
+ registerWebhookCommands(program2, options);
7801
+ registerEventCommands(program2, options);
7802
+ }
7803
+ function parseNumber(value) {
7804
+ const parsed = Number(value);
7805
+ if (!Number.isFinite(parsed))
7806
+ throw new Error(`Expected a number, got ${value}`);
7807
+ return parsed;
7808
+ }
7809
+ function collectValues(value, previous) {
7810
+ previous.push(value);
7811
+ return previous;
7812
+ }
7813
+
7136
7814
  // src/cli/index.ts
7137
7815
  import chalk from "chalk";
7138
7816
 
7139
7817
  // src/agent/loop.ts
7140
- import { randomUUID } from "crypto";
7141
- import { mkdir } from "fs/promises";
7818
+ import { randomUUID as randomUUID3 } from "crypto";
7819
+ import { mkdir as mkdir2 } from "fs/promises";
7142
7820
 
7143
7821
  // src/drivers/mac/screenshot.ts
7144
7822
  import { tmpdir } from "os";
7145
- import { join } from "path";
7146
- import { readFile, unlink } from "fs/promises";
7823
+ import { join as join2 } from "path";
7824
+ import { readFile as readFile2, unlink } from "fs/promises";
7147
7825
  async function captureScreenshot(displayNumber) {
7148
7826
  const timestamp = Date.now();
7149
- const tmpPath = join(tmpdir(), `computer-screenshot-${timestamp}.png`);
7827
+ const tmpPath = join2(tmpdir(), `computer-screenshot-${timestamp}.png`);
7150
7828
  const args = ["screencapture", "-x", "-C", "-t", "png"];
7151
7829
  if (displayNumber) {
7152
7830
  args.push(`-D${displayNumber}`);
@@ -7161,7 +7839,7 @@ async function captureScreenshot(displayNumber) {
7161
7839
  const stderr = await new Response(proc.stderr).text();
7162
7840
  throw new Error(`screencapture failed: ${stderr}`);
7163
7841
  }
7164
- const data = await readFile(tmpPath);
7842
+ const data = await readFile2(tmpPath);
7165
7843
  const base64 = data.toString("base64");
7166
7844
  await unlink(tmpPath).catch(() => {});
7167
7845
  const size = await getScreenSize();
@@ -7185,15 +7863,15 @@ async function getScreenSize() {
7185
7863
  return { width: 1920, height: 1080 };
7186
7864
  }
7187
7865
  async function saveScreenshotToFile(screenshot, dir, filename) {
7188
- const path = join(dir, filename);
7866
+ const path = join2(dir, filename);
7189
7867
  const buffer = Buffer.from(screenshot.base64, "base64");
7190
7868
  await Bun.write(path, buffer);
7191
7869
  return path;
7192
7870
  }
7193
7871
 
7194
7872
  // src/drivers/mac/input.ts
7195
- import { join as join2, dirname } from "path";
7196
- import { existsSync } from "fs";
7873
+ import { join as join3, dirname } from "path";
7874
+ import { existsSync as existsSync2 } from "fs";
7197
7875
  import { fileURLToPath } from "url";
7198
7876
  async function executeAction(action) {
7199
7877
  const start = Date.now();
@@ -7360,12 +8038,12 @@ function getScrollHelperPath() {
7360
8038
  if (_scrollHelperPath)
7361
8039
  return _scrollHelperPath;
7362
8040
  const candidates = [
7363
- join2(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "helpers", "scroll"),
7364
- join2(dirname(fileURLToPath(import.meta.url)), "..", "helpers", "scroll"),
7365
- join2(process.env.HOME ?? "~", ".hasna", "computer", "helpers", "scroll")
8041
+ join3(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "helpers", "scroll"),
8042
+ join3(dirname(fileURLToPath(import.meta.url)), "..", "helpers", "scroll"),
8043
+ join3(process.env.HOME ?? "~", ".hasna", "computer", "helpers", "scroll")
7366
8044
  ];
7367
8045
  for (const candidate of candidates) {
7368
- if (existsSync(candidate)) {
8046
+ if (existsSync2(candidate)) {
7369
8047
  _scrollHelperPath = candidate;
7370
8048
  return candidate;
7371
8049
  }
@@ -16376,8 +17054,8 @@ function createProvider(provider, opts) {
16376
17054
 
16377
17055
  // src/lib/scale.ts
16378
17056
  import { tmpdir as tmpdir2 } from "os";
16379
- import { join as join3 } from "path";
16380
- import { readFile as readFile2, unlink as unlink2, writeFile } from "fs/promises";
17057
+ import { join as join4 } from "path";
17058
+ import { readFile as readFile3, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
16381
17059
  var RECOMMENDED_WIDTHS = {
16382
17060
  xga: 1024,
16383
17061
  wxga: 1280,
@@ -16392,11 +17070,11 @@ async function scaleScreenshot(screenshot, maxWidth = DEFAULT_MAX_WIDTH) {
16392
17070
  const newWidth = maxWidth;
16393
17071
  const newHeight = Math.round(screenshot.size.height * ratio);
16394
17072
  const timestamp = Date.now();
16395
- const tmpInput = join3(tmpdir2(), `computer-scale-in-${timestamp}.png`);
16396
- const tmpOutput = join3(tmpdir2(), `computer-scale-out-${timestamp}.png`);
17073
+ const tmpInput = join4(tmpdir2(), `computer-scale-in-${timestamp}.png`);
17074
+ const tmpOutput = join4(tmpdir2(), `computer-scale-out-${timestamp}.png`);
16397
17075
  try {
16398
17076
  const buffer = Buffer.from(screenshot.base64, "base64");
16399
- await writeFile(tmpInput, buffer);
17077
+ await writeFile2(tmpInput, buffer);
16400
17078
  const proc = Bun.spawn([
16401
17079
  "sips",
16402
17080
  "--resampleWidth",
@@ -16411,7 +17089,7 @@ async function scaleScreenshot(screenshot, maxWidth = DEFAULT_MAX_WIDTH) {
16411
17089
  console.warn(`sips resize failed (${stderr}), using original screenshot`);
16412
17090
  return screenshot;
16413
17091
  }
16414
- const resized = await readFile2(tmpOutput);
17092
+ const resized = await readFile3(tmpOutput);
16415
17093
  const base64 = resized.toString("base64");
16416
17094
  return {
16417
17095
  base64,
@@ -16520,8 +17198,8 @@ function checkAction(action, config) {
16520
17198
  }
16521
17199
  }
16522
17200
  function checkRateLimit(maxPerMinute) {
16523
- const now = Date.now();
16524
- const oneMinuteAgo = now - 60000;
17201
+ const now2 = Date.now();
17202
+ const oneMinuteAgo = now2 - 60000;
16525
17203
  actionTimestamps = actionTimestamps.filter((t) => t > oneMinuteAgo);
16526
17204
  if (actionTimestamps.length >= maxPerMinute) {
16527
17205
  return {
@@ -16529,7 +17207,7 @@ function checkRateLimit(maxPerMinute) {
16529
17207
  reason: `Rate limit exceeded: ${actionTimestamps.length}/${maxPerMinute} actions per minute`
16530
17208
  };
16531
17209
  }
16532
- actionTimestamps.push(now);
17210
+ actionTimestamps.push(now2);
16533
17211
  return { allowed: true };
16534
17212
  }
16535
17213
  function looksLikePassword(text) {
@@ -16575,26 +17253,26 @@ function screenshotsMatch(prev, curr, threshold = 0.98) {
16575
17253
 
16576
17254
  // src/db/index.ts
16577
17255
  import { Database } from "bun:sqlite";
16578
- import { join as join5 } from "path";
17256
+ import { join as join6 } from "path";
16579
17257
  import { mkdirSync as mkdirSync2 } from "fs";
16580
- import { homedir } from "os";
17258
+ import { homedir as homedir2 } from "os";
16581
17259
  var SERVICE_NAME = "computer";
16582
17260
  function getDataDir(service = SERVICE_NAME) {
16583
17261
  if (service === SERVICE_NAME && process.env["COMPUTER_DATA_DIR"]) {
16584
17262
  mkdirSync2(process.env["COMPUTER_DATA_DIR"], { recursive: true });
16585
17263
  return process.env["COMPUTER_DATA_DIR"];
16586
17264
  }
16587
- const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
16588
- const dir = join5(home, ".hasna", service);
17265
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
17266
+ const dir = join6(home, ".hasna", service);
16589
17267
  mkdirSync2(dir, { recursive: true });
16590
17268
  return dir;
16591
17269
  }
16592
17270
  function getDbPath(service = SERVICE_NAME) {
16593
17271
  if (service === SERVICE_NAME && process.env["COMPUTER_DB_PATH"]) {
16594
- mkdirSync2(join5(process.env["COMPUTER_DB_PATH"], ".."), { recursive: true });
17272
+ mkdirSync2(join6(process.env["COMPUTER_DB_PATH"], ".."), { recursive: true });
16595
17273
  return process.env["COMPUTER_DB_PATH"];
16596
17274
  }
16597
- return join5(getDataDir(service), `${service}.db`);
17275
+ return join6(getDataDir(service), `${service}.db`);
16598
17276
  }
16599
17277
  var db = null;
16600
17278
  function getDb() {
@@ -16910,7 +17588,7 @@ async function runTask(options) {
16910
17588
  const provider = createProvider(providerName, { model });
16911
17589
  const config = loadConfig();
16912
17590
  const safetyConfig = config.safety;
16913
- const sessionId = randomUUID();
17591
+ const sessionId = randomUUID3();
16914
17592
  const session = {
16915
17593
  id: sessionId,
16916
17594
  task,
@@ -16927,7 +17605,7 @@ async function runTask(options) {
16927
17605
  await createSession(session);
16928
17606
  const ssDir = screenshotsDir ?? `${process.env.HOME}/.hasna/computer/screenshots/${sessionId}`;
16929
17607
  if (saveScreenshots) {
16930
- await mkdir(ssDir, { recursive: true });
17608
+ await mkdir2(ssDir, { recursive: true });
16931
17609
  }
16932
17610
  const history = [];
16933
17611
  const startTime = Date.now();
@@ -17231,32 +17909,65 @@ class PgAdapterAsync {
17231
17909
  }
17232
17910
  }
17233
17911
 
17234
- // src/db/cloud-sync.ts
17235
- var CLOUD_TABLES = ["sessions", "action_logs", "feedback"];
17912
+ // src/db/storage-sync.ts
17913
+ var STORAGE_TABLES = ["sessions", "action_logs", "feedback"];
17914
+ var COMPUTER_STORAGE_ENV = "HASNA_COMPUTER_DATABASE_URL";
17915
+ var COMPUTER_STORAGE_FALLBACK_ENV = "COMPUTER_DATABASE_URL";
17916
+ var COMPUTER_STORAGE_MODE_ENV = "HASNA_COMPUTER_STORAGE_MODE";
17917
+ var COMPUTER_STORAGE_MODE_FALLBACK_ENV = "COMPUTER_STORAGE_MODE";
17918
+ var STORAGE_DATABASE_ENV = [COMPUTER_STORAGE_ENV, COMPUTER_STORAGE_FALLBACK_ENV];
17236
17919
  var PRIMARY_KEYS = {
17237
17920
  sessions: ["id"],
17238
17921
  action_logs: ["id"],
17239
17922
  feedback: ["id"]
17240
17923
  };
17241
- function getCloudDatabaseUrl() {
17242
- return process.env["HASNA_COMPUTER_CLOUD_DATABASE_URL"] ?? process.env["OPEN_COMPUTER_CLOUD_DATABASE_URL"] ?? process.env["COMPUTER_CLOUD_DATABASE_URL"] ?? null;
17924
+ function readEnv3(name) {
17925
+ const value = process.env[name]?.trim();
17926
+ return value || undefined;
17927
+ }
17928
+ function normalizeStorageMode(value) {
17929
+ const normalized = value?.trim().toLowerCase();
17930
+ if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
17931
+ return normalized;
17932
+ return;
17933
+ }
17934
+ function getStorageDatabaseEnvName() {
17935
+ for (const name of STORAGE_DATABASE_ENV) {
17936
+ if (readEnv3(name))
17937
+ return name;
17938
+ }
17939
+ return null;
17940
+ }
17941
+ function getStorageDatabaseEnv() {
17942
+ const name = getStorageDatabaseEnvName();
17943
+ return name ? { name } : null;
17944
+ }
17945
+ function getStorageDatabaseUrl() {
17946
+ const env = getStorageDatabaseEnv();
17947
+ return env ? readEnv3(env.name) ?? null : null;
17243
17948
  }
17244
- async function getCloudPg() {
17245
- const url = getCloudDatabaseUrl();
17949
+ function getStorageMode() {
17950
+ const mode = normalizeStorageMode(readEnv3(COMPUTER_STORAGE_MODE_ENV) ?? readEnv3(COMPUTER_STORAGE_MODE_FALLBACK_ENV));
17951
+ if (mode)
17952
+ return mode;
17953
+ return getStorageDatabaseUrl() ? "hybrid" : "local";
17954
+ }
17955
+ async function getStoragePg() {
17956
+ const url = getStorageDatabaseUrl();
17246
17957
  if (!url) {
17247
- throw new Error("Missing HASNA_COMPUTER_CLOUD_DATABASE_URL, OPEN_COMPUTER_CLOUD_DATABASE_URL, or COMPUTER_CLOUD_DATABASE_URL");
17958
+ throw new Error("Missing HASNA_COMPUTER_DATABASE_URL");
17248
17959
  }
17249
17960
  return new PgAdapterAsync(url);
17250
17961
  }
17251
- async function runCloudMigrations(remote) {
17962
+ async function runStorageMigrations(remote) {
17252
17963
  for (const sql of PG_MIGRATIONS)
17253
17964
  await remote.run(sql);
17254
17965
  }
17255
- async function cloudPush(options) {
17256
- const remote = await getCloudPg();
17966
+ async function storagePush(options) {
17967
+ const remote = await getStoragePg();
17257
17968
  const db2 = getDb();
17258
17969
  try {
17259
- await runCloudMigrations(remote);
17970
+ await runStorageMigrations(remote);
17260
17971
  const results = [];
17261
17972
  for (const table of resolveTables(options?.tables))
17262
17973
  results.push(await pushTable(db2, remote, table));
@@ -17266,11 +17977,11 @@ async function cloudPush(options) {
17266
17977
  await remote.close();
17267
17978
  }
17268
17979
  }
17269
- async function cloudPull(options) {
17270
- const remote = await getCloudPg();
17980
+ async function storagePull(options) {
17981
+ const remote = await getStoragePg();
17271
17982
  const db2 = getDb();
17272
17983
  try {
17273
- await runCloudMigrations(remote);
17984
+ await runStorageMigrations(remote);
17274
17985
  const results = [];
17275
17986
  for (const table of resolveTables(options?.tables))
17276
17987
  results.push(await pullTable(remote, db2, table));
@@ -17280,9 +17991,9 @@ async function cloudPull(options) {
17280
17991
  await remote.close();
17281
17992
  }
17282
17993
  }
17283
- async function cloudSync(options) {
17284
- const pull = await cloudPull(options);
17285
- const push2 = await cloudPush(options);
17994
+ async function storageSync(options) {
17995
+ const pull = await storagePull(options);
17996
+ const push2 = await storagePush(options);
17286
17997
  return { pull, push: push2 };
17287
17998
  }
17288
17999
  function getSyncMetaAll() {
@@ -17290,10 +18001,22 @@ function getSyncMetaAll() {
17290
18001
  ensureSyncMetaTable(db2);
17291
18002
  return db2.prepare("SELECT table_name, last_synced_at, direction FROM _computer_sync_meta ORDER BY table_name, direction").all();
17292
18003
  }
18004
+ function getStorageStatus() {
18005
+ const activeEnv = getStorageDatabaseEnv();
18006
+ return {
18007
+ configured: Boolean(activeEnv),
18008
+ mode: getStorageMode(),
18009
+ env: STORAGE_DATABASE_ENV,
18010
+ activeEnv: activeEnv?.name ?? null,
18011
+ service: "computer",
18012
+ tables: STORAGE_TABLES,
18013
+ sync: getSyncMetaAll()
18014
+ };
18015
+ }
17293
18016
  function resolveTables(tables) {
17294
18017
  if (!tables || tables.length === 0)
17295
- return [...CLOUD_TABLES];
17296
- const allowed = new Set(CLOUD_TABLES);
18018
+ return [...STORAGE_TABLES];
18019
+ const allowed = new Set(STORAGE_TABLES);
17297
18020
  const requested = tables.map((table) => table.trim()).filter(Boolean);
17298
18021
  const invalid = requested.filter((table) => !allowed.has(table));
17299
18022
  if (invalid.length > 0)
@@ -17381,7 +18104,7 @@ function upsertSqlite(db2, table, columns, rows) {
17381
18104
  }
17382
18105
  function recordSyncMeta(db2, direction, results) {
17383
18106
  ensureSyncMetaTable(db2);
17384
- const now = new Date().toISOString();
18107
+ const now2 = new Date().toISOString();
17385
18108
  for (const result of results) {
17386
18109
  if (result.errors.length > 0)
17387
18110
  continue;
@@ -17389,7 +18112,7 @@ function recordSyncMeta(db2, direction, results) {
17389
18112
  INSERT INTO _computer_sync_meta (table_name, last_synced_at, direction)
17390
18113
  VALUES (?, ?, ?)
17391
18114
  ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at
17392
- `).run(result.table, now, direction);
18115
+ `).run(result.table, now2, direction);
17393
18116
  }
17394
18117
  }
17395
18118
  function ensureSyncMetaTable(db2) {
@@ -17419,12 +18142,7 @@ function coerceForSqlite(value) {
17419
18142
  return String(value);
17420
18143
  }
17421
18144
 
17422
- // src/cli/cloud.ts
17423
- var CLOUD_ENV = [
17424
- "HASNA_COMPUTER_CLOUD_DATABASE_URL",
17425
- "OPEN_COMPUTER_CLOUD_DATABASE_URL",
17426
- "COMPUTER_CLOUD_DATABASE_URL"
17427
- ];
18145
+ // src/cli/storage.ts
17428
18146
  function parseTables(value) {
17429
18147
  if (!value)
17430
18148
  return;
@@ -17441,21 +18159,15 @@ function printResults(results, label) {
17441
18159
  }
17442
18160
  console.log(`Done. ${total} rows ${label}.`);
17443
18161
  }
17444
- function registerCloudCommands(program2) {
17445
- const cloudCmd = program2.command("cloud").description("Cloud sync commands");
17446
- cloudCmd.command("status").description("Show cloud config and local sync state").option("--json", "Output as JSON").action((opts) => {
17447
- const info = {
17448
- configured: Boolean(getCloudDatabaseUrl()),
17449
- env: CLOUD_ENV,
17450
- service: "computer",
17451
- tables: CLOUD_TABLES,
17452
- sync: getSyncMetaAll()
17453
- };
18162
+ function registerStorageCommands(program2) {
18163
+ const storageCmd = program2.command("storage").description("Storage sync commands");
18164
+ storageCmd.command("status").description("Show storage config and local sync state").option("--json", "Output as JSON").action((opts) => {
18165
+ const info = getStorageStatus();
17454
18166
  if (opts.json) {
17455
18167
  printJson(info);
17456
18168
  return;
17457
18169
  }
17458
- console.log(`Cloud configured: ${info.configured ? "yes" : "no"}`);
18170
+ console.log(`Storage configured: ${info.configured ? "yes" : "no"}`);
17459
18171
  console.log(`Tables: ${info.tables.join(", ")}`);
17460
18172
  if (info.sync.length === 0)
17461
18173
  console.log("Sync: no local sync history");
@@ -17463,9 +18175,9 @@ function registerCloudCommands(program2) {
17463
18175
  console.log(` ${entry.table_name} ${entry.direction}: ${entry.last_synced_at ?? "never"}`);
17464
18176
  }
17465
18177
  });
17466
- cloudCmd.command("push").description("Push local computer data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
18178
+ storageCmd.command("push").description("Push local computer data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
17467
18179
  try {
17468
- const results = await cloudPush({ tables: parseTables(opts.tables) });
18180
+ const results = await storagePush({ tables: parseTables(opts.tables) });
17469
18181
  if (opts.json) {
17470
18182
  printJson(results);
17471
18183
  return;
@@ -17476,9 +18188,9 @@ function registerCloudCommands(program2) {
17476
18188
  process.exit(1);
17477
18189
  }
17478
18190
  });
17479
- cloudCmd.command("pull").description("Pull computer data from cloud PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
18191
+ storageCmd.command("pull").description("Pull computer data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
17480
18192
  try {
17481
- const results = await cloudPull({ tables: parseTables(opts.tables) });
18193
+ const results = await storagePull({ tables: parseTables(opts.tables) });
17482
18194
  if (opts.json) {
17483
18195
  printJson(results);
17484
18196
  return;
@@ -17489,9 +18201,9 @@ function registerCloudCommands(program2) {
17489
18201
  process.exit(1);
17490
18202
  }
17491
18203
  });
17492
- cloudCmd.command("sync").description("Bidirectional sync: pull then push").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
18204
+ storageCmd.command("sync").description("Bidirectional sync: pull then push").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
17493
18205
  try {
17494
- const result = await cloudSync({ tables: parseTables(opts.tables) });
18206
+ const result = await storageSync({ tables: parseTables(opts.tables) });
17495
18207
  if (opts.json) {
17496
18208
  printJson(result);
17497
18209
  return;
@@ -17560,6 +18272,188 @@ function renderKitty(base64, width, height) {
17560
18272
  `;
17561
18273
  }
17562
18274
 
18275
+ // src/apps/ghostty/driver.ts
18276
+ import { existsSync as existsSync4 } from "fs";
18277
+
18278
+ // src/apps/ghostty/applescript.ts
18279
+ function parseGrid(spec) {
18280
+ const m = /^(\d+)x(\d+)$/.exec(spec.trim());
18281
+ if (!m)
18282
+ throw new Error(`Invalid grid spec "${spec}" \u2014 expected RxC (e.g. 2x2, 3x1)`);
18283
+ const rows = parseInt(m[1], 10);
18284
+ const cols = parseInt(m[2], 10);
18285
+ if (rows < 1 || cols < 1) {
18286
+ throw new Error(`Invalid grid spec "${spec}" \u2014 rows and cols must be >= 1`);
18287
+ }
18288
+ return { rows, cols };
18289
+ }
18290
+ function parseTabsSpec(spec) {
18291
+ const parts = spec.split(",");
18292
+ if (parts.length === 0 || spec.trim() === "") {
18293
+ throw new Error(`Invalid tabs spec "${spec}" \u2014 expected comma-separated grids (e.g. "2x2,1x2")`);
18294
+ }
18295
+ return parts.map((p) => parseGrid(p));
18296
+ }
18297
+ function escapeAppleScript(s) {
18298
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
18299
+ }
18300
+ function shellQuote(s) {
18301
+ return `'${s.replace(/'/g, `'\\''`)}'`;
18302
+ }
18303
+ function assignPaneCommands(totalPanes, run, all) {
18304
+ if (all) {
18305
+ if (run.length !== 1) {
18306
+ throw new Error(`--all requires exactly one --run command (got ${run.length})`);
18307
+ }
18308
+ return Array.from({ length: totalPanes }, () => run[0]);
18309
+ }
18310
+ if (run.length > totalPanes) {
18311
+ throw new Error(`Got ${run.length} commands for ${totalPanes} pane${totalPanes === 1 ? "" : "s"} \u2014 add panes or drop commands`);
18312
+ }
18313
+ return Array.from({ length: totalPanes }, (_, i) => run[i]);
18314
+ }
18315
+ function buildGhosttyScript(opts) {
18316
+ const { tabs, commands = [], dir, max = false } = opts;
18317
+ if (tabs.length === 0)
18318
+ throw new Error("At least one tab grid is required");
18319
+ const lines = [];
18320
+ lines.push('tell application "Ghostty"');
18321
+ lines.push(" activate");
18322
+ lines.push(" set w to new window");
18323
+ const term = (t, r, c) => `t${t}_${r}_${c}`;
18324
+ let paneIndex = 0;
18325
+ for (let t = 0;t < tabs.length; t++) {
18326
+ const { rows, cols } = tabs[t];
18327
+ if (t === 0) {
18328
+ lines.push(` set ${term(0, 0, 0)} to focused terminal of selected tab of w`);
18329
+ if (max) {
18330
+ lines.push(` perform action "toggle_maximize" on ${term(0, 0, 0)}`);
18331
+ }
18332
+ } else {
18333
+ lines.push(` set tb${t} to new tab in w`);
18334
+ lines.push(` set ${term(t, 0, 0)} to focused terminal of tb${t}`);
18335
+ }
18336
+ for (let r = 1;r < rows; r++) {
18337
+ lines.push(` set ${term(t, r, 0)} to split ${term(t, r - 1, 0)} direction down`);
18338
+ }
18339
+ for (let r = 0;r < rows; r++) {
18340
+ for (let c = 1;c < cols; c++) {
18341
+ lines.push(` set ${term(t, r, c)} to split ${term(t, r, c - 1)} direction right`);
18342
+ }
18343
+ }
18344
+ if (rows * cols > 1) {
18345
+ lines.push(` perform action "equalize_splits" on ${term(t, 0, 0)}`);
18346
+ }
18347
+ for (let r = 0;r < rows; r++) {
18348
+ for (let c = 0;c < cols; c++) {
18349
+ const raw = commands[paneIndex];
18350
+ paneIndex++;
18351
+ let cmd;
18352
+ if (dir && raw)
18353
+ cmd = `cd ${shellQuote(dir)} && ${raw}`;
18354
+ else if (dir)
18355
+ cmd = `cd ${shellQuote(dir)}`;
18356
+ else
18357
+ cmd = raw;
18358
+ if (cmd) {
18359
+ lines.push(` input text "${escapeAppleScript(cmd)}" to ${term(t, r, c)}`);
18360
+ lines.push(` send key "enter" to ${term(t, r, c)}`);
18361
+ }
18362
+ }
18363
+ }
18364
+ }
18365
+ lines.push(` focus ${term(0, 0, 0)}`);
18366
+ lines.push("end tell");
18367
+ return lines.join(`
18368
+ `) + `
18369
+ `;
18370
+ }
18371
+
18372
+ // src/apps/ghostty/driver.ts
18373
+ var APP_BUNDLE = "/Applications/Ghostty.app";
18374
+ function ghosttyAvailability(env = {}) {
18375
+ const platform = env.platform ?? process.platform;
18376
+ if (platform !== "darwin") {
18377
+ return { available: false, reason: "Ghostty orchestration requires macOS (AppleScript)" };
18378
+ }
18379
+ const hasAppBundle = env.hasAppBundle ?? existsSync4(APP_BUNDLE);
18380
+ const hasBinary = env.hasBinary ?? Bun.which("ghostty") !== null;
18381
+ if (!hasAppBundle && !hasBinary) {
18382
+ return { available: false, reason: `Ghostty not found (${APP_BUNDLE} missing and no \`ghostty\` on PATH)` };
18383
+ }
18384
+ return { available: true };
18385
+ }
18386
+ async function runOsascript(script) {
18387
+ const proc = Bun.spawn(["osascript", "-e", script], {
18388
+ stdout: "pipe",
18389
+ stderr: "pipe"
18390
+ });
18391
+ const code = await proc.exited;
18392
+ const stderr = await new Response(proc.stderr).text();
18393
+ return { ok: code === 0, stderr: stderr.trim() };
18394
+ }
18395
+ function resolveTabs(spec) {
18396
+ if (spec.tabs && spec.tabs.length > 0)
18397
+ return spec.tabs;
18398
+ return [spec.grid ?? { rows: 1, cols: 1 }];
18399
+ }
18400
+ var ghosttyDriver = {
18401
+ name: "ghostty",
18402
+ description: "Ghostty terminal \u2014 windows with pane grids, tabs, and a command per pane",
18403
+ available() {
18404
+ return ghosttyAvailability();
18405
+ },
18406
+ async open(spec) {
18407
+ const availability = ghosttyAvailability();
18408
+ if (!availability.available) {
18409
+ return { ok: false, message: availability.reason ?? "Ghostty is not available" };
18410
+ }
18411
+ const tabs = resolveTabs(spec);
18412
+ const totalPanes = tabs.reduce((sum, g) => sum + g.rows * g.cols, 0);
18413
+ let commands;
18414
+ try {
18415
+ commands = assignPaneCommands(totalPanes, spec.run ?? [], spec.all ?? false);
18416
+ } catch (err) {
18417
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
18418
+ }
18419
+ const script = buildGhosttyScript({
18420
+ tabs,
18421
+ commands,
18422
+ dir: spec.dir,
18423
+ max: spec.max
18424
+ });
18425
+ const result = await runOsascript(script);
18426
+ if (!result.ok) {
18427
+ return {
18428
+ ok: false,
18429
+ message: `osascript failed: ${result.stderr || "unknown error"}`,
18430
+ panes: totalPanes,
18431
+ tabs: tabs.length
18432
+ };
18433
+ }
18434
+ const tabDesc = tabs.map((g) => `${g.rows}x${g.cols}`).join(",");
18435
+ return {
18436
+ ok: true,
18437
+ message: `Opened Ghostty: ${tabs.length} tab${tabs.length === 1 ? "" : "s"} (${tabDesc}), ${totalPanes} pane${totalPanes === 1 ? "" : "s"}`,
18438
+ panes: totalPanes,
18439
+ tabs: tabs.length
18440
+ };
18441
+ }
18442
+ };
18443
+
18444
+ // src/apps/registry.ts
18445
+ var drivers = new Map;
18446
+ function registerAppDriver(driver) {
18447
+ drivers.set(driver.name.toLowerCase(), driver);
18448
+ }
18449
+ function getAppDriver(name) {
18450
+ return drivers.get(name.toLowerCase());
18451
+ }
18452
+ function listAppDrivers() {
18453
+ return [...drivers.values()].sort((a, b) => a.name.localeCompare(b.name));
18454
+ }
18455
+ registerAppDriver(ghosttyDriver);
18456
+
17563
18457
  // src/cli/index.ts
17564
18458
  var program2 = new Command;
17565
18459
  program2.name("computer").description("Open-source computer use for AI agents \u2014 control your Mac with AI").version("0.1.0");
@@ -17626,6 +18520,60 @@ program2.command("screenshot").description("Take a screenshot of the current scr
17626
18520
  console.log(chalk.dim(`Base64 length: ${ss.base64.length} chars`));
17627
18521
  }
17628
18522
  });
18523
+ var collectRun = (value, previous) => [...previous, value];
18524
+ program2.command("open").description("Open an app deterministically via its driver (no AI; see `computer apps`)").argument("<app>", "App to open (registered driver name, e.g. ghostty)").option("--grid <RxC>", 'Split the window into R rows x C cols (e.g. "2x2")').option("--tabs <specs>", 'Multiple tabs in one window, one grid spec each (e.g. "2x2,1x2,1x2")').option("--run <command>", "Command for the next pane in order (repeatable)", collectRun, []).option("--all", "Run the single --run command in every pane", false).option("--dir <path>", "Working directory \u2014 every pane cds here first").option("--max", "Maximize the new window (not native fullscreen)", false).action(async (app, opts) => {
18525
+ const driver = getAppDriver(app);
18526
+ if (!driver) {
18527
+ console.error(chalk.red(`No app driver registered for "${app}".`));
18528
+ console.error(chalk.dim("List available drivers with `computer apps`."));
18529
+ process.exit(1);
18530
+ }
18531
+ const availability = driver.available();
18532
+ if (!availability.available) {
18533
+ console.error(chalk.red(`${driver.name} is not available on this machine.`));
18534
+ if (availability.reason)
18535
+ console.error(chalk.dim(`Reason: ${availability.reason}`));
18536
+ process.exit(1);
18537
+ }
18538
+ let spec;
18539
+ try {
18540
+ spec = {
18541
+ grid: opts.grid ? parseGrid(opts.grid) : undefined,
18542
+ tabs: opts.tabs ? parseTabsSpec(opts.tabs) : undefined,
18543
+ run: opts.run,
18544
+ all: opts.all,
18545
+ dir: opts.dir,
18546
+ max: opts.max
18547
+ };
18548
+ } catch (err) {
18549
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
18550
+ process.exit(1);
18551
+ }
18552
+ const result = await driver.open(spec);
18553
+ if (result.ok) {
18554
+ console.log(chalk.green(result.message));
18555
+ } else {
18556
+ console.error(chalk.red(result.message));
18557
+ process.exit(1);
18558
+ }
18559
+ });
18560
+ program2.command("apps").description("List registered app drivers and their availability on this machine").action(async () => {
18561
+ const drivers2 = listAppDrivers();
18562
+ if (drivers2.length === 0) {
18563
+ console.log(chalk.dim("No app drivers registered."));
18564
+ return;
18565
+ }
18566
+ console.log(chalk.bold(`App drivers
18567
+ `));
18568
+ for (const driver of drivers2) {
18569
+ const availability = driver.available();
18570
+ const status = availability.available ? chalk.green("available") : chalk.red("unavailable");
18571
+ console.log(` ${chalk.cyan(driver.name.padEnd(12))} ${status} ${chalk.dim(driver.description)}`);
18572
+ if (!availability.available && availability.reason) {
18573
+ console.log(chalk.dim(` ${" ".repeat(12)} ${availability.reason}`));
18574
+ }
18575
+ }
18576
+ });
17629
18577
  program2.command("sessions").description("List computer use sessions").option("-n, --limit <n>", "Number of sessions to show", "20").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").action(async (opts) => {
17630
18578
  const sessions = listSessions({
17631
18579
  limit: parseInt(opts.limit),
@@ -17878,16 +18826,16 @@ program2.command("headless").description("Check headless mode status and availab
17878
18826
  console.log(status.recommendation);
17879
18827
  });
17880
18828
  program2.command("record").description("Record mouse/keyboard events as a replayable macro").option("-d, --duration <seconds>", "Max recording duration in seconds", "60").option("-o, --output <file>", "Save recording to JSON file").action(async (opts) => {
17881
- const { join: join6, dirname: dirname2 } = await import("path");
18829
+ const { join: join7, dirname: dirname2 } = await import("path");
17882
18830
  const { fileURLToPath: fileURLToPath2 } = await import("url");
17883
- const { existsSync: existsSync3, writeFileSync: writeFileSync2 } = await import("fs");
18831
+ const { existsSync: existsSync5, writeFileSync: writeFileSync2 } = await import("fs");
17884
18832
  const __dirname2 = dirname2(fileURLToPath2(import.meta.url));
17885
18833
  const candidates = [
17886
- join6(__dirname2, "..", "..", "helpers", "record"),
17887
- join6(__dirname2, "..", "helpers", "record"),
17888
- join6(process.env.HOME ?? "~", ".hasna", "computer", "helpers", "record")
18834
+ join7(__dirname2, "..", "..", "helpers", "record"),
18835
+ join7(__dirname2, "..", "helpers", "record"),
18836
+ join7(process.env.HOME ?? "~", ".hasna", "computer", "helpers", "record")
17889
18837
  ];
17890
- const helperPath = candidates.find((c) => existsSync3(c));
18838
+ const helperPath = candidates.find((c) => existsSync5(c));
17891
18839
  if (!helperPath) {
17892
18840
  console.log(chalk.red("Record helper not found. Run: swiftc helpers/record.swift -o helpers/record -framework CoreGraphics"));
17893
18841
  process.exit(1);
@@ -17908,9 +18856,9 @@ program2.command("record").description("Record mouse/keyboard events as a replay
17908
18856
  } else {
17909
18857
  const dir = getDataDir("computer");
17910
18858
  const filename = `recording-${Date.now()}.json`;
17911
- const path = join6(dir, "recordings", filename);
18859
+ const path = join7(dir, "recordings", filename);
17912
18860
  const { mkdirSync: mkdirSync3 } = await import("fs");
17913
- mkdirSync3(join6(dir, "recordings"), { recursive: true });
18861
+ mkdirSync3(join7(dir, "recordings"), { recursive: true });
17914
18862
  writeFileSync2(path, stdout);
17915
18863
  console.log(chalk.green(`Recording saved: ${path}`));
17916
18864
  }
@@ -17926,7 +18874,8 @@ program2.command("completions").description("Generate shell completions").argume
17926
18874
  process.exit(1);
17927
18875
  }
17928
18876
  });
17929
- registerCloudCommands(program2);
18877
+ registerStorageCommands(program2);
18878
+ registerEventsCommands(program2, { source: "computer" });
17930
18879
  program2.parse();
17931
18880
  function generateZshCompletions() {
17932
18881
  return `#compdef computer
@@ -17937,6 +18886,8 @@ _computer() {
17937
18886
  local -a commands
17938
18887
  commands=(
17939
18888
  'run:Run a computer use task'
18889
+ 'open:Open an app via its driver'
18890
+ 'apps:List app drivers'
17940
18891
  'screenshot:Take a screenshot'
17941
18892
  'sessions:List sessions'
17942
18893
  'session:Show session details'
@@ -17946,7 +18897,7 @@ _computer() {
17946
18897
  'search:Search sessions'
17947
18898
  'config:View or modify configuration'
17948
18899
  'completions:Generate shell completions'
17949
- 'cloud:Cloud sync and feedback'
18900
+ 'storage:Storage sync and feedback'
17950
18901
  )
17951
18902
 
17952
18903
  _arguments -C \\
@@ -17971,6 +18922,16 @@ _computer() {
17971
18922
  '--no-preview[Disable inline preview]' \\
17972
18923
  '1:task:'
17973
18924
  ;;
18925
+ open)
18926
+ _arguments \\
18927
+ '--grid[Pane grid RxC]:grid:' \\
18928
+ '--tabs[Tab grid specs]:tabs:' \\
18929
+ '*--run[Command per pane]:command:' \\
18930
+ '--all[Same command in every pane]' \\
18931
+ '--dir[Working directory]:dir:_files -/' \\
18932
+ '--max[Maximize window]' \\
18933
+ '1:app:(ghostty)'
18934
+ ;;
17974
18935
  sessions)
17975
18936
  _arguments \\
17976
18937
  '-n[Limit]:limit:' \\
@@ -18001,7 +18962,7 @@ _computer_completions() {
18001
18962
  COMPREPLY=()
18002
18963
  cur="\${COMP_WORDS[COMP_CWORD]}"
18003
18964
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
18004
- commands="run screenshot sessions session delete stats watch search config completions cloud"
18965
+ commands="run open apps screenshot sessions session delete stats watch search config completions storage"
18005
18966
 
18006
18967
  if [ "$COMP_CWORD" -eq 1 ]; then
18007
18968
  COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
@@ -18012,6 +18973,9 @@ _computer_completions() {
18012
18973
  run)
18013
18974
  COMPREPLY=( $(compgen -W "-p --provider -m --model -s --max-steps --save-screenshots --dry-run --tag --max-width --no-preview" -- "$cur") )
18014
18975
  ;;
18976
+ open)
18977
+ COMPREPLY=( $(compgen -W "ghostty --grid --tabs --run --all --dir --max" -- "$cur") )
18978
+ ;;
18015
18979
  sessions)
18016
18980
  COMPREPLY=( $(compgen -W "-n --limit --status --tag" -- "$cur") )
18017
18981
  ;;