@hasna/computer 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2211,10 +2211,15 @@ import { mkdir } from "fs/promises";
2211
2211
  import { tmpdir } from "os";
2212
2212
  import { join } from "path";
2213
2213
  import { readFile, unlink } from "fs/promises";
2214
- async function captureScreenshot() {
2214
+ async function captureScreenshot(displayNumber) {
2215
2215
  const timestamp = Date.now();
2216
2216
  const tmpPath = join(tmpdir(), `computer-screenshot-${timestamp}.png`);
2217
- const proc = Bun.spawn(["screencapture", "-x", "-C", "-t", "png", tmpPath], {
2217
+ const args = ["screencapture", "-x", "-C", "-t", "png"];
2218
+ if (displayNumber) {
2219
+ args.push(`-D${displayNumber}`);
2220
+ }
2221
+ args.push(tmpPath);
2222
+ const proc = Bun.spawn(args, {
2218
2223
  stdout: "pipe",
2219
2224
  stderr: "pipe"
2220
2225
  });
@@ -2437,19 +2442,23 @@ function getScrollHelperPath() {
2437
2442
 
2438
2443
  // src/drivers/mac/index.ts
2439
2444
  class MacDriver {
2445
+ displayNumber;
2446
+ constructor(opts) {
2447
+ this.displayNumber = opts?.displayNumber;
2448
+ }
2440
2449
  async getScreenSize() {
2441
2450
  return getScreenSize();
2442
2451
  }
2443
2452
  async screenshot() {
2444
- return captureScreenshot();
2453
+ return captureScreenshot(this.displayNumber);
2445
2454
  }
2446
2455
  async execute(action) {
2447
2456
  return executeAction(action);
2448
2457
  }
2449
2458
  async dispose() {}
2450
2459
  }
2451
- function createMacDriver() {
2452
- return new MacDriver;
2460
+ function createMacDriver(opts) {
2461
+ return new MacDriver(opts);
2453
2462
  }
2454
2463
 
2455
2464
  // node_modules/@anthropic-ai/sdk/version.mjs
@@ -21386,6 +21395,7 @@ function getDb() {
21386
21395
  total_tokens_in INTEGER NOT NULL DEFAULT 0,
21387
21396
  total_tokens_out INTEGER NOT NULL DEFAULT 0,
21388
21397
  total_duration_ms INTEGER NOT NULL DEFAULT 0,
21398
+ tags TEXT,
21389
21399
  error TEXT,
21390
21400
  created_at TEXT NOT NULL,
21391
21401
  completed_at TEXT
@@ -21441,9 +21451,9 @@ function getDb() {
21441
21451
  async function createSession(session) {
21442
21452
  const d = getDb();
21443
21453
  d.prepare(`
21444
- INSERT INTO sessions (id, task, provider, model, status, steps, total_tokens_in, total_tokens_out, total_duration_ms, error, created_at, completed_at)
21445
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21446
- `).run(session.id, session.task, session.provider, session.model, session.status, session.steps, session.total_tokens_in, session.total_tokens_out, session.total_duration_ms, session.error ?? null, session.created_at, session.completed_at ?? null);
21454
+ INSERT INTO sessions (id, task, provider, model, status, steps, total_tokens_in, total_tokens_out, total_duration_ms, tags, error, created_at, completed_at)
21455
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21456
+ `).run(session.id, session.task, session.provider, session.model, session.status, session.steps, session.total_tokens_in, session.total_tokens_out, session.total_duration_ms, session.tags?.length ? JSON.stringify(session.tags) : null, session.error ?? null, session.created_at, session.completed_at ?? null);
21447
21457
  }
21448
21458
  async function updateSession(session) {
21449
21459
  const d = getDb();
@@ -21460,14 +21470,29 @@ async function logAction(params) {
21460
21470
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21461
21471
  `).run(params.session_id, params.step, params.action.type, JSON.stringify(params.action), params.reasoning, params.screenshot_path ?? null, params.success ? 1 : 0, params.error ?? null, params.duration_ms, params.tokens_in ?? null, params.tokens_out ?? null);
21462
21472
  }
21473
+ function getSession(id) {
21474
+ const d = getDb();
21475
+ const row = d.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
21476
+ if (!row)
21477
+ return null;
21478
+ return rowToSession(row);
21479
+ }
21463
21480
  function listSessions(opts) {
21464
21481
  const d = getDb();
21465
21482
  let sql = "SELECT * FROM sessions";
21466
21483
  const params = [];
21484
+ const conditions = [];
21467
21485
  if (opts?.status) {
21468
- sql += " WHERE status = ?";
21486
+ conditions.push("status = ?");
21469
21487
  params.push(opts.status);
21470
21488
  }
21489
+ if (opts?.tag) {
21490
+ conditions.push("tags LIKE ?");
21491
+ params.push(`%"${opts.tag}"%`);
21492
+ }
21493
+ if (conditions.length > 0) {
21494
+ sql += " WHERE " + conditions.join(" AND ");
21495
+ }
21471
21496
  sql += " ORDER BY created_at DESC";
21472
21497
  if (opts?.limit) {
21473
21498
  sql += " LIMIT ?";
@@ -21544,12 +21569,104 @@ function rowToSession(row) {
21544
21569
  total_tokens_in: row.total_tokens_in,
21545
21570
  total_tokens_out: row.total_tokens_out,
21546
21571
  total_duration_ms: row.total_duration_ms,
21572
+ tags: row.tags ? JSON.parse(row.tags) : undefined,
21547
21573
  error: row.error,
21548
21574
  created_at: row.created_at,
21549
21575
  completed_at: row.completed_at
21550
21576
  };
21551
21577
  }
21552
21578
 
21579
+ // src/lib/integrations.ts
21580
+ async function saveToRecordings(session, logs) {
21581
+ try {
21582
+ const { saveRecording } = await import("@hasna/recordings");
21583
+ await saveRecording({
21584
+ title: `Computer Use: ${session.task.slice(0, 100)}`,
21585
+ type: "computer-use",
21586
+ source: "computer",
21587
+ duration_ms: session.total_duration_ms,
21588
+ metadata: {
21589
+ session_id: session.id,
21590
+ provider: session.provider,
21591
+ model: session.model,
21592
+ steps: session.steps,
21593
+ tokens_in: session.total_tokens_in,
21594
+ tokens_out: session.total_tokens_out,
21595
+ status: session.status,
21596
+ tags: session.tags
21597
+ },
21598
+ transcript: logs.map((l) => ({
21599
+ step: l.step,
21600
+ action: l.action.type,
21601
+ reasoning: l.reasoning?.slice(0, 200),
21602
+ success: l.success,
21603
+ timestamp: l.created_at
21604
+ }))
21605
+ });
21606
+ return true;
21607
+ } catch {
21608
+ return false;
21609
+ }
21610
+ }
21611
+ async function registerWithSessions(session) {
21612
+ try {
21613
+ const mod = await import("@hasna/sessions");
21614
+ const registerSession = mod.registerSession ?? mod.createSession ?? mod.saveSession;
21615
+ if (typeof registerSession !== "function")
21616
+ return false;
21617
+ await registerSession({
21618
+ id: session.id,
21619
+ type: "computer-use",
21620
+ source: "computer",
21621
+ status: session.status,
21622
+ metadata: {
21623
+ task: session.task,
21624
+ provider: session.provider,
21625
+ model: session.model,
21626
+ steps: session.steps
21627
+ },
21628
+ started_at: session.created_at,
21629
+ ended_at: session.completed_at
21630
+ });
21631
+ return true;
21632
+ } catch {
21633
+ return false;
21634
+ }
21635
+ }
21636
+ async function pushToLogs(session, logs) {
21637
+ try {
21638
+ const mod = await import("@hasna/logs");
21639
+ const pushBatch = mod.logPushBatch ?? mod.pushBatch;
21640
+ if (typeof pushBatch !== "function")
21641
+ return false;
21642
+ await pushBatch(logs.map((l) => ({
21643
+ level: l.success ? "info" : "error",
21644
+ source: "computer",
21645
+ message: `[${l.action.type}] ${l.reasoning?.slice(0, 100) ?? ""}`,
21646
+ metadata: {
21647
+ session_id: session.id,
21648
+ step: l.step,
21649
+ action_type: l.action.type,
21650
+ success: l.success,
21651
+ error: l.error,
21652
+ duration_ms: l.duration_ms
21653
+ },
21654
+ timestamp: l.created_at
21655
+ })));
21656
+ return true;
21657
+ } catch {
21658
+ return false;
21659
+ }
21660
+ }
21661
+ async function runPostSessionIntegrations(session, logs) {
21662
+ const [recordings, sessions, logsPushed] = await Promise.all([
21663
+ saveToRecordings(session, logs),
21664
+ registerWithSessions(session),
21665
+ pushToLogs(session, logs)
21666
+ ]);
21667
+ return { recordings, sessions, logs: logsPushed };
21668
+ }
21669
+
21553
21670
  // src/agent/loop.ts
21554
21671
  var DEFAULT_MAX_STEPS = 50;
21555
21672
  async function runTask(options) {
@@ -21563,10 +21680,12 @@ async function runTask(options) {
21563
21680
  systemPrompt,
21564
21681
  screenshotMaxWidth,
21565
21682
  dryRun = false,
21683
+ tags,
21684
+ displayNumber,
21566
21685
  onStep,
21567
21686
  onDone
21568
21687
  } = options;
21569
- const driver = createMacDriver();
21688
+ const driver = createMacDriver({ displayNumber });
21570
21689
  const provider = createProvider(providerName, { model });
21571
21690
  const config = loadConfig();
21572
21691
  const safetyConfig = config.safety;
@@ -21577,6 +21696,7 @@ async function runTask(options) {
21577
21696
  provider: providerName,
21578
21697
  model: model ?? (providerName === "anthropic" ? "claude-sonnet-4-5-20250514" : "computer-use-preview"),
21579
21698
  status: "running",
21699
+ tags,
21580
21700
  steps: 0,
21581
21701
  total_tokens_in: 0,
21582
21702
  total_tokens_out: 0,
@@ -21629,6 +21749,8 @@ async function runTask(options) {
21629
21749
  });
21630
21750
  await updateSession(session);
21631
21751
  onStep?.(step, response, { success: true, duration_ms: 0 });
21752
+ const logs = getActionLogs(sessionId);
21753
+ await runPostSessionIntegrations(session, logs).catch(() => {});
21632
21754
  onDone?.(session);
21633
21755
  await driver.dispose();
21634
21756
  return session;
@@ -21694,6 +21816,8 @@ async function runTask(options) {
21694
21816
  session.completed_at = new Date().toISOString();
21695
21817
  session.error = `Reached max steps (${maxSteps})`;
21696
21818
  await updateSession(session);
21819
+ const endLogs = getActionLogs(sessionId);
21820
+ await runPostSessionIntegrations(session, endLogs).catch(() => {});
21697
21821
  onDone?.(session);
21698
21822
  await driver.dispose();
21699
21823
  return session;
@@ -21703,6 +21827,8 @@ async function runTask(options) {
21703
21827
  session.total_duration_ms = Date.now() - startTime;
21704
21828
  session.completed_at = new Date().toISOString();
21705
21829
  await updateSession(session);
21830
+ const errLogs = getActionLogs(sessionId);
21831
+ await runPostSessionIntegrations(session, errLogs).catch(() => {});
21706
21832
  onDone?.(session);
21707
21833
  await driver.dispose();
21708
21834
  return session;
@@ -21775,10 +21901,65 @@ function findPricing(model) {
21775
21901
  return { input: 3, output: 12 };
21776
21902
  }
21777
21903
 
21904
+ // src/lib/terminal-image.ts
21905
+ function detectProtocol() {
21906
+ const term = process.env.TERM_PROGRAM ?? "";
21907
+ const termInfo = process.env.TERM ?? "";
21908
+ if (term === "iTerm.app" || term === "WezTerm") {
21909
+ return "iterm2";
21910
+ }
21911
+ if (termInfo.includes("kitty") || process.env.KITTY_PID) {
21912
+ return "kitty";
21913
+ }
21914
+ return "none";
21915
+ }
21916
+ function renderInlineImage(base64, opts) {
21917
+ const protocol = detectProtocol();
21918
+ const width = opts?.width ?? 40;
21919
+ const height = opts?.height ?? 15;
21920
+ switch (protocol) {
21921
+ case "iterm2":
21922
+ return renderIterm2(base64, width, height, opts?.preserveAspectRatio ?? true);
21923
+ case "kitty":
21924
+ return renderKitty(base64, width, height);
21925
+ case "none":
21926
+ return "";
21927
+ }
21928
+ }
21929
+ function supportsInlineImages() {
21930
+ return detectProtocol() !== "none";
21931
+ }
21932
+ function renderIterm2(base64, width, height, preserveAspectRatio) {
21933
+ const params = [
21934
+ `width=${width}`,
21935
+ `height=${height}`,
21936
+ `preserveAspectRatio=${preserveAspectRatio ? 1 : 0}`,
21937
+ "inline=1"
21938
+ ].join(";");
21939
+ return `\x1B]1337;File=${params}:${base64}\x07
21940
+ `;
21941
+ }
21942
+ function renderKitty(base64, width, height) {
21943
+ const chunkSize = 4096;
21944
+ const chunks = [];
21945
+ for (let i = 0;i < base64.length; i += chunkSize) {
21946
+ const chunk = base64.slice(i, i + chunkSize);
21947
+ const isLast = i + chunkSize >= base64.length;
21948
+ const more = isLast ? 0 : 1;
21949
+ if (i === 0) {
21950
+ chunks.push(`\x1B_Gf=100,a=T,m=${more},c=${width},r=${height};${chunk}\x1B\\`);
21951
+ } else {
21952
+ chunks.push(`\x1B_Gm=${more};${chunk}\x1B\\`);
21953
+ }
21954
+ }
21955
+ return chunks.join("") + `
21956
+ `;
21957
+ }
21958
+
21778
21959
  // src/cli/index.ts
21779
21960
  var program2 = new Command;
21780
21961
  program2.name("computer").description("Open-source computer use for AI agents \u2014 control your Mac with AI").version("0.1.0");
21781
- program2.command("run").description("Run a computer use task").argument("<task>", "Natural language description of the task").option("-p, --provider <provider>", "AI provider (anthropic|openai)", "anthropic").option("-m, --model <model>", "Model to use").option("-s, --max-steps <n>", "Maximum number of steps", "50").option("--save-screenshots", "Save screenshots to disk", false).option("--screenshots-dir <dir>", "Directory to save screenshots").option("--system-prompt <prompt>", "Custom system prompt").option("--max-width <px>", "Max screenshot width for AI model (default: 1280)", "1280").option("--dry-run", "Plan actions without executing them", false).action(async (task, opts) => {
21962
+ program2.command("run").description("Run a computer use task").argument("<task>", "Natural language description of the task").option("-p, --provider <provider>", "AI provider (anthropic|openai)", "anthropic").option("-m, --model <model>", "Model to use").option("-s, --max-steps <n>", "Maximum number of steps", "50").option("--save-screenshots", "Save screenshots to disk", false).option("--screenshots-dir <dir>", "Directory to save screenshots").option("--system-prompt <prompt>", "Custom system prompt").option("--max-width <px>", "Max screenshot width for AI model (default: 1280)", "1280").option("--dry-run", "Plan actions without executing them", false).option("--no-preview", "Disable inline screenshot preview in terminal").option("--tag <tags...>", "Tag this session (can specify multiple)").option("--display <n>", "Display number to capture (1=main, 2=secondary)").action(async (task, opts) => {
21782
21963
  const cfg = loadConfig();
21783
21964
  const provider = opts.provider ?? cfg.provider;
21784
21965
  const maxSteps = parseInt(opts.maxSteps) || cfg.maxSteps;
@@ -21797,6 +21978,8 @@ program2.command("run").description("Run a computer use task").argument("<task>"
21797
21978
  systemPrompt: opts.systemPrompt,
21798
21979
  screenshotMaxWidth: maxWidth,
21799
21980
  dryRun: opts.dryRun,
21981
+ tags: opts.tag,
21982
+ displayNumber: opts.display ? parseInt(opts.display) : undefined,
21800
21983
  onStep: (step, response, result) => {
21801
21984
  const status = result.success ? chalk.green("OK") : chalk.red("FAIL");
21802
21985
  const actionDesc = response.action ? `${response.action.type}${response.action.type === "click" ? ` (${response.action.point.x},${response.action.point.y})` : ""}` : "done";
@@ -21806,6 +21989,11 @@ program2.command("run").description("Run a computer use task").argument("<task>"
21806
21989
  const short = response.reasoning.slice(0, 120).replace(/\n/g, " ");
21807
21990
  console.log(chalk.dim(` ${short}${response.reasoning.length > 120 ? "..." : ""}`));
21808
21991
  }
21992
+ if (opts.preview !== false && result.screenshot && supportsInlineImages()) {
21993
+ const img = renderInlineImage(result.screenshot.base64, { width: 40, height: 12 });
21994
+ if (img)
21995
+ process.stdout.write(img);
21996
+ }
21809
21997
  },
21810
21998
  onDone: (session2) => {
21811
21999
  console.log();
@@ -21834,10 +22022,11 @@ program2.command("screenshot").description("Take a screenshot of the current scr
21834
22022
  console.log(chalk.dim(`Base64 length: ${ss.base64.length} chars`));
21835
22023
  }
21836
22024
  });
21837
- program2.command("sessions").description("List computer use sessions").option("-n, --limit <n>", "Number of sessions to show", "20").option("--status <status>", "Filter by status").action(async (opts) => {
22025
+ 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) => {
21838
22026
  const sessions = listSessions({
21839
22027
  limit: parseInt(opts.limit),
21840
- status: opts.status
22028
+ status: opts.status,
22029
+ tag: opts.tag
21841
22030
  });
21842
22031
  if (sessions.length === 0) {
21843
22032
  console.log(chalk.dim("No sessions found."));
@@ -21845,7 +22034,8 @@ program2.command("sessions").description("List computer use sessions").option("-
21845
22034
  }
21846
22035
  for (const s of sessions) {
21847
22036
  const statusColor = s.status === "completed" ? chalk.green : s.status === "failed" ? chalk.red : s.status === "running" ? chalk.yellow : chalk.dim;
21848
- console.log(`${chalk.dim(s.id.slice(0, 8))} ${statusColor(s.status.padEnd(10))} ${chalk.cyan(s.provider.padEnd(10))} ${s.steps} steps ${chalk.dim(s.created_at)}`);
22037
+ const tagStr = s.tags?.length ? chalk.magenta(` [${s.tags.join(", ")}]`) : "";
22038
+ console.log(`${chalk.dim(s.id.slice(0, 8))} ${statusColor(s.status.padEnd(10))} ${chalk.cyan(s.provider.padEnd(10))} ${s.steps} steps${tagStr} ${chalk.dim(s.created_at)}`);
21849
22039
  console.log(chalk.dim(` ${s.task.slice(0, 100)}`));
21850
22040
  }
21851
22041
  });
@@ -21899,6 +22089,80 @@ program2.command("stats").description("Show usage statistics").action(async () =
21899
22089
  console.log(` Steps: ${stats.total_steps}`);
21900
22090
  console.log(` Tokens: ${stats.total_tokens.toLocaleString()}`);
21901
22091
  });
22092
+ program2.command("watch").description("Live-stream what the agent sees (polls running session)").argument("[id]", "Session ID to watch (default: latest running)").option("-i, --interval <ms>", "Poll interval in milliseconds", "500").option("--no-preview", "Disable inline screenshot preview").action(async (id, opts) => {
22093
+ const interval = parseInt(opts.interval);
22094
+ let sessionId = id;
22095
+ if (!sessionId) {
22096
+ const running = listSessions({ status: "running", limit: 1 });
22097
+ if (running.length === 0) {
22098
+ console.log(chalk.yellow("No running sessions found. Start one with `computer run <task>`."));
22099
+ process.exit(0);
22100
+ }
22101
+ sessionId = running[0].id;
22102
+ } else {
22103
+ const all = listSessions({ limit: 100 });
22104
+ const match = all.find((s) => s.id.startsWith(sessionId));
22105
+ if (match)
22106
+ sessionId = match.id;
22107
+ }
22108
+ console.log(chalk.bold.cyan("computer watch") + ` \u2014 session ${chalk.dim(sessionId.slice(0, 8))}`);
22109
+ console.log(chalk.dim(`Polling every ${interval}ms. Press Ctrl+C to stop.
22110
+ `));
22111
+ let lastStep = -1;
22112
+ const poll = async () => {
22113
+ const session = getSession(sessionId);
22114
+ if (!session) {
22115
+ console.log(chalk.red("Session not found."));
22116
+ process.exit(1);
22117
+ }
22118
+ const logs = getActionLogs(sessionId);
22119
+ const newLogs = logs.filter((l) => l.step > lastStep);
22120
+ for (const log of newLogs) {
22121
+ const status = log.success ? chalk.green("OK") : chalk.red("FAIL");
22122
+ console.log(chalk.dim(`[${String(log.step + 1).padStart(3)}]`) + ` ${status} ${chalk.yellow(log.action.type)}` + chalk.dim(` (${log.duration_ms}ms)`));
22123
+ if (log.reasoning) {
22124
+ const short = log.reasoning.slice(0, 120).replace(/\n/g, " ");
22125
+ console.log(chalk.dim(` ${short}${log.reasoning.length > 120 ? "..." : ""}`));
22126
+ }
22127
+ if (log.error) {
22128
+ console.log(chalk.red(` Error: ${log.error}`));
22129
+ }
22130
+ if (opts.preview !== false && log.screenshot_path && supportsInlineImages()) {
22131
+ try {
22132
+ const { readFileSync: readFileSync3 } = await import("fs");
22133
+ const imgData = readFileSync3(log.screenshot_path);
22134
+ const b64 = imgData.toString("base64");
22135
+ const img = renderInlineImage(b64, { width: 40, height: 12 });
22136
+ if (img)
22137
+ process.stdout.write(img);
22138
+ } catch {}
22139
+ }
22140
+ lastStep = log.step;
22141
+ }
22142
+ if (session.status !== "running") {
22143
+ console.log();
22144
+ const totalCost = formatCost(calculateCost(session.model, session.total_tokens_in, session.total_tokens_out));
22145
+ if (session.status === "completed" && !session.error) {
22146
+ console.log(chalk.green.bold("Session completed."));
22147
+ } else if (session.status === "completed") {
22148
+ console.log(chalk.yellow.bold(`Session finished: ${session.error}`));
22149
+ } else {
22150
+ console.log(chalk.red.bold(`Session ${session.status}: ${session.error}`));
22151
+ }
22152
+ console.log(chalk.dim(`Steps: ${session.steps} | Tokens: ${(session.total_tokens_in + session.total_tokens_out).toLocaleString()} | Cost: ${totalCost} | Duration: ${(session.total_duration_ms / 1000).toFixed(1)}s`));
22153
+ process.exit(0);
22154
+ }
22155
+ };
22156
+ await poll();
22157
+ const timer = setInterval(poll, interval);
22158
+ process.on("SIGINT", () => {
22159
+ clearInterval(timer);
22160
+ console.log(chalk.dim(`
22161
+ Stopped watching.`));
22162
+ process.exit(0);
22163
+ });
22164
+ await new Promise(() => {});
22165
+ });
21902
22166
  program2.command("search").description("Search sessions by task text").argument("<query>", "Search query").option("-n, --limit <n>", "Max results", "20").action(async (query, opts) => {
21903
22167
  const sessions = searchSessions(query, parseInt(opts.limit));
21904
22168
  if (sessions.length === 0) {
@@ -21907,7 +22171,8 @@ program2.command("search").description("Search sessions by task text").argument(
21907
22171
  }
21908
22172
  for (const s of sessions) {
21909
22173
  const statusColor = s.status === "completed" ? chalk.green : s.status === "failed" ? chalk.red : chalk.dim;
21910
- console.log(`${chalk.dim(s.id.slice(0, 8))} ${statusColor(s.status.padEnd(10))} ${chalk.cyan(s.provider.padEnd(10))} ${s.steps} steps ${chalk.dim(s.created_at)}`);
22174
+ const tagStr = s.tags?.length ? chalk.magenta(` [${s.tags.join(", ")}]`) : "";
22175
+ console.log(`${chalk.dim(s.id.slice(0, 8))} ${statusColor(s.status.padEnd(10))} ${chalk.cyan(s.provider.padEnd(10))} ${s.steps} steps${tagStr} ${chalk.dim(s.created_at)}`);
21911
22176
  console.log(chalk.dim(` ${s.task.slice(0, 100)}`));
21912
22177
  }
21913
22178
  });
@@ -21949,5 +22214,167 @@ configCmd.command("reset").description("Reset config to defaults").action(async
21949
22214
  saveConfig2(DEFAULT_CONFIG2);
21950
22215
  console.log(chalk.green("Config reset to defaults."));
21951
22216
  });
22217
+ program2.command("replay").description("Replay a session \u2014 show actions and screenshots in sequence").argument("<id>", "Session ID (or prefix)").option("--speed <x>", "Replay speed multiplier (default: 2)", "2").option("--no-preview", "Disable inline screenshot preview").action(async (id, opts) => {
22218
+ const sessions = listSessions({ limit: 1000 });
22219
+ const session = sessions.find((s) => s.id.startsWith(id));
22220
+ if (!session) {
22221
+ console.log(chalk.red(`Session not found: ${id}`));
22222
+ process.exit(1);
22223
+ }
22224
+ const logs = getActionLogs(session.id);
22225
+ if (logs.length === 0) {
22226
+ console.log(chalk.dim("No action logs for this session."));
22227
+ process.exit(0);
22228
+ }
22229
+ const speed = parseFloat(opts.speed);
22230
+ const totalCost = formatCost(calculateCost(session.model, session.total_tokens_in, session.total_tokens_out));
22231
+ console.log(chalk.bold.cyan("computer replay") + ` \u2014 ${chalk.dim(session.id.slice(0, 8))}`);
22232
+ console.log(chalk.dim(`Task: ${session.task}`));
22233
+ console.log(chalk.dim(`Provider: ${session.provider} | ${logs.length} steps | Speed: ${speed}x
22234
+ `));
22235
+ for (let i = 0;i < logs.length; i++) {
22236
+ const log = logs[i];
22237
+ const status = log.success ? chalk.green("OK") : chalk.red("FAIL");
22238
+ console.log(chalk.dim(`[${String(log.step + 1).padStart(3)}]`) + ` ${status} ${chalk.yellow(log.action.type)}` + chalk.dim(` (${log.duration_ms}ms)`));
22239
+ if (log.reasoning) {
22240
+ const short = log.reasoning.slice(0, 120).replace(/\n/g, " ");
22241
+ console.log(chalk.dim(` ${short}${log.reasoning.length > 120 ? "..." : ""}`));
22242
+ }
22243
+ if (log.error) {
22244
+ console.log(chalk.red(` Error: ${log.error}`));
22245
+ }
22246
+ if (opts.preview !== false && log.screenshot_path && supportsInlineImages()) {
22247
+ try {
22248
+ const { readFileSync: readFileSync3 } = await import("fs");
22249
+ const imgData = readFileSync3(log.screenshot_path);
22250
+ const b64 = imgData.toString("base64");
22251
+ const img = renderInlineImage(b64, { width: 40, height: 12 });
22252
+ if (img)
22253
+ process.stdout.write(img);
22254
+ } catch {}
22255
+ }
22256
+ if (i < logs.length - 1) {
22257
+ const nextLog = logs[i + 1];
22258
+ const delay = Math.max(100, Math.round(log.duration_ms / speed));
22259
+ await new Promise((r) => setTimeout(r, delay));
22260
+ }
22261
+ }
22262
+ console.log();
22263
+ console.log(chalk.dim(`Replay complete. ${logs.length} steps | Cost: ${totalCost} | Duration: ${(session.total_duration_ms / 1000).toFixed(1)}s`));
22264
+ });
22265
+ program2.command("completions").description("Generate shell completions").argument("<shell>", "Shell type (zsh or bash)").action(async (shell) => {
22266
+ if (shell === "zsh") {
22267
+ console.log(generateZshCompletions());
22268
+ } else if (shell === "bash") {
22269
+ console.log(generateBashCompletions());
22270
+ } else {
22271
+ console.error(chalk.red(`Unknown shell: ${shell}. Use "zsh" or "bash".`));
22272
+ process.exit(1);
22273
+ }
22274
+ });
21952
22275
  registerCloudCommands(program2, "computer");
21953
22276
  program2.parse();
22277
+ function generateZshCompletions() {
22278
+ return `#compdef computer
22279
+ # Zsh completions for @hasna/computer
22280
+ # Install: computer completions zsh > ~/.zsh/completions/_computer
22281
+
22282
+ _computer() {
22283
+ local -a commands
22284
+ commands=(
22285
+ 'run:Run a computer use task'
22286
+ 'screenshot:Take a screenshot'
22287
+ 'sessions:List sessions'
22288
+ 'session:Show session details'
22289
+ 'delete:Delete a session'
22290
+ 'stats:Show usage statistics'
22291
+ 'watch:Live-stream agent activity'
22292
+ 'search:Search sessions'
22293
+ 'config:View or modify configuration'
22294
+ 'completions:Generate shell completions'
22295
+ 'cloud:Cloud sync and feedback'
22296
+ )
22297
+
22298
+ _arguments -C \\
22299
+ '1:command:->command' \\
22300
+ '*::arg:->args'
22301
+
22302
+ case "$state" in
22303
+ command)
22304
+ _describe 'command' commands
22305
+ ;;
22306
+ args)
22307
+ case "$words[1]" in
22308
+ run)
22309
+ _arguments \\
22310
+ '-p[AI provider]:provider:(anthropic openai)' \\
22311
+ '-m[Model to use]:model:' \\
22312
+ '-s[Max steps]:steps:' \\
22313
+ '--save-screenshots[Save screenshots]' \\
22314
+ '--dry-run[Plan without executing]' \\
22315
+ '--tag[Tag session]:tag:' \\
22316
+ '--max-width[Max screenshot width]:width:' \\
22317
+ '--no-preview[Disable inline preview]' \\
22318
+ '1:task:'
22319
+ ;;
22320
+ sessions)
22321
+ _arguments \\
22322
+ '-n[Limit]:limit:' \\
22323
+ '--status[Filter by status]:status:(running completed failed cancelled)' \\
22324
+ '--tag[Filter by tag]:tag:'
22325
+ ;;
22326
+ config)
22327
+ local -a config_cmds
22328
+ config_cmds=(show get set path edit reset)
22329
+ _describe 'config command' config_cmds
22330
+ ;;
22331
+ completions)
22332
+ _arguments '1:shell:(zsh bash)'
22333
+ ;;
22334
+ esac
22335
+ ;;
22336
+ esac
22337
+ }
22338
+
22339
+ _computer "$@"`;
22340
+ }
22341
+ function generateBashCompletions() {
22342
+ return `# Bash completions for @hasna/computer
22343
+ # Install: computer completions bash >> ~/.bashrc
22344
+
22345
+ _computer_completions() {
22346
+ local cur prev commands
22347
+ COMPREPLY=()
22348
+ cur="\${COMP_WORDS[COMP_CWORD]}"
22349
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
22350
+ commands="run screenshot sessions session delete stats watch search config completions cloud"
22351
+
22352
+ if [ "$COMP_CWORD" -eq 1 ]; then
22353
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
22354
+ return 0
22355
+ fi
22356
+
22357
+ case "\${COMP_WORDS[1]}" in
22358
+ run)
22359
+ COMPREPLY=( $(compgen -W "-p --provider -m --model -s --max-steps --save-screenshots --dry-run --tag --max-width --no-preview" -- "$cur") )
22360
+ ;;
22361
+ sessions)
22362
+ COMPREPLY=( $(compgen -W "-n --limit --status --tag" -- "$cur") )
22363
+ ;;
22364
+ config)
22365
+ COMPREPLY=( $(compgen -W "show get set path edit reset" -- "$cur") )
22366
+ ;;
22367
+ completions)
22368
+ COMPREPLY=( $(compgen -W "zsh bash" -- "$cur") )
22369
+ ;;
22370
+ --provider|-p)
22371
+ COMPREPLY=( $(compgen -W "anthropic openai" -- "$cur") )
22372
+ ;;
22373
+ --status)
22374
+ COMPREPLY=( $(compgen -W "running completed failed cancelled" -- "$cur") )
22375
+ ;;
22376
+ esac
22377
+ }
22378
+
22379
+ complete -F _computer_completions computer`;
22380
+ }
@@ -0,0 +1,26 @@
1
+ export interface Agent {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ capabilities?: string[];
6
+ focus?: string;
7
+ last_heartbeat: string;
8
+ created_at: string;
9
+ }
10
+ /** Ensure agents table exists */
11
+ export declare function ensureAgentsTable(): void;
12
+ /** Register or update an agent */
13
+ export declare function registerAgent(agent: {
14
+ name: string;
15
+ description?: string;
16
+ capabilities?: string[];
17
+ }): Agent;
18
+ /** Send a heartbeat for an agent */
19
+ export declare function heartbeat(agentId: string): boolean;
20
+ /** Set agent focus */
21
+ export declare function setFocus(agentId: string, focus: string): boolean;
22
+ /** Get an agent by ID */
23
+ export declare function getAgent(id: string): Agent | null;
24
+ /** List all agents */
25
+ export declare function listAgents(): Agent[];
26
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/db/agents.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,IAAI,IAAI,CAaxC;AAED,kCAAkC;AAClC,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,KAAK,CA0BR;AAED,oCAAoC;AACpC,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQlD;AAED,sBAAsB;AACtB,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED,yBAAyB;AACzB,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAMjD;AAED,sBAAsB;AACtB,wBAAgB,UAAU,IAAI,KAAK,EAAE,CAKpC"}
@@ -24,6 +24,7 @@ export declare function getSession(id: string): Session | null;
24
24
  /** List sessions */
25
25
  export declare function listSessions(opts?: {
26
26
  status?: SessionStatus;
27
+ tag?: string;
27
28
  limit?: number;
28
29
  offset?: number;
29
30
  }): Session[];
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE7D,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMzF,0DAA0D;AAC1D,wBAAgB,KAAK,IAAI,SAAS,CA4EjC;AAED,2BAA2B;AAC3B,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnE;AAED,uBAAuB;AACvB,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBnE;AAED,qCAAqC;AACrC,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAED,0BAA0B;AAC1B,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAKrD;AAED,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE;IAClC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,EAAE,CAsBZ;AAED,0CAA0C;AAC1C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,EAAE,CAU3E;AAED,kDAAkD;AAClD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,GACjB,SAAS,EAAE,CAuBb;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,CAoB5D;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED,wBAAwB;AACxB,wBAAgB,QAAQ,IAAI;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAmBA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAE7D,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAMzF,0DAA0D;AAC1D,wBAAgB,KAAK,IAAI,SAAS,CA6EjC;AAED,2BAA2B;AAC3B,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBnE;AAED,uBAAuB;AACvB,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBnE;AAED,qCAAqC;AACrC,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhB;AAED,0BAA0B;AAC1B,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAKrD;AAED,oBAAoB;AACpB,wBAAgB,YAAY,CAAC,IAAI,CAAC,EAAE;IAClC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,EAAE,CA8BZ;AAED,0CAA0C;AAC1C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG,OAAO,EAAE,CAU3E;AAED,kDAAkD;AAClD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,GACjB,SAAS,EAAE,CAuBb;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,CAoB5D;AAED,oCAAoC;AACpC,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAKjD;AAED,wBAAwB;AACxB,wBAAgB,QAAQ,IAAI;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAmBA"}