@rehpic/vcli 0.1.0-beta.61.1 → 0.1.0-beta.68.1

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/index.js CHANGED
@@ -1056,15 +1056,68 @@ function printOutput(data, json = false) {
1056
1056
  }
1057
1057
 
1058
1058
  // src/session.ts
1059
- import { mkdir, readFile, rm, writeFile } from "fs/promises";
1059
+ import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
1060
1060
  import { homedir } from "os";
1061
1061
  import path from "path";
1062
1062
  function getSessionRoot() {
1063
1063
  return process.env.VECTOR_HOME?.trim() || path.join(homedir(), ".vector");
1064
1064
  }
1065
+ function getProfileConfigPath() {
1066
+ return path.join(getSessionRoot(), "cli-config.json");
1067
+ }
1065
1068
  function getSessionPath(profile = "default") {
1066
1069
  return path.join(getSessionRoot(), `cli-${profile}.json`);
1067
1070
  }
1071
+ async function readDefaultProfile() {
1072
+ try {
1073
+ const raw = await readFile(getProfileConfigPath(), "utf8");
1074
+ const parsed = JSON.parse(raw);
1075
+ const profile = parsed.defaultProfile?.trim();
1076
+ return profile || "default";
1077
+ } catch {
1078
+ return "default";
1079
+ }
1080
+ }
1081
+ async function writeDefaultProfile(profile) {
1082
+ const normalized = profile.trim() || "default";
1083
+ await mkdir(getSessionRoot(), { recursive: true });
1084
+ const config = {
1085
+ version: 1,
1086
+ defaultProfile: normalized
1087
+ };
1088
+ await writeFile(
1089
+ getProfileConfigPath(),
1090
+ `${JSON.stringify(config, null, 2)}
1091
+ `,
1092
+ "utf8"
1093
+ );
1094
+ }
1095
+ async function listProfiles() {
1096
+ const root = getSessionRoot();
1097
+ const defaultProfile = await readDefaultProfile();
1098
+ try {
1099
+ const entries = await readdir(root, { withFileTypes: true });
1100
+ const names = entries.filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => /^cli-.+\.json$/.test(name)).map((name) => name.replace(/^cli-/, "").replace(/\.json$/, ""));
1101
+ const uniqueNames = Array.from(/* @__PURE__ */ new Set([...names, defaultProfile])).sort(
1102
+ (left, right) => left.localeCompare(right)
1103
+ );
1104
+ return Promise.all(
1105
+ uniqueNames.map(async (name) => ({
1106
+ name,
1107
+ isDefault: name === defaultProfile,
1108
+ hasSession: await readSession(name) !== null
1109
+ }))
1110
+ );
1111
+ } catch {
1112
+ return [
1113
+ {
1114
+ name: defaultProfile,
1115
+ isDefault: true,
1116
+ hasSession: await readSession(defaultProfile) !== null
1117
+ }
1118
+ ];
1119
+ }
1120
+ }
1068
1121
  async function readSession(profile = "default") {
1069
1122
  try {
1070
1123
  const raw = await readFile(getSessionPath(profile), "utf8");
@@ -1100,8 +1153,241 @@ function createEmptySession() {
1100
1153
  // src/bridge-service.ts
1101
1154
  import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
1102
1155
  import { execFileSync, execSync as execSync2 } from "child_process";
1156
+
1157
+ // src/terminal-peer.ts
1158
+ import { createServer } from "http";
1159
+ import { WebSocketServer, WebSocket } from "ws";
1160
+ import { ConvexClient } from "convex/browser";
1161
+ import * as pty from "node-pty";
1162
+ import { existsSync } from "fs";
1163
+ import { randomUUID } from "crypto";
1164
+ import localtunnel from "localtunnel";
1165
+ function findTmuxPath() {
1166
+ for (const p of [
1167
+ "/opt/homebrew/bin/tmux",
1168
+ "/usr/local/bin/tmux",
1169
+ "/usr/bin/tmux"
1170
+ ]) {
1171
+ if (existsSync(p)) return p;
1172
+ }
1173
+ return "tmux";
1174
+ }
1175
+ function ts() {
1176
+ return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
1177
+ }
1178
+ function findPort(start = 9100) {
1179
+ return new Promise((resolve, reject) => {
1180
+ const srv = createServer();
1181
+ srv.listen(0, "127.0.0.1", () => {
1182
+ const addr = srv.address();
1183
+ const port = typeof addr === "object" && addr ? addr.port : start;
1184
+ srv.close(() => resolve(port));
1185
+ });
1186
+ srv.on("error", reject);
1187
+ });
1188
+ }
1189
+ var TerminalPeerManager = class {
1190
+ constructor(config) {
1191
+ this.terminals = /* @__PURE__ */ new Map();
1192
+ this.failedSessions = /* @__PURE__ */ new Set();
1193
+ this.pendingStops = /* @__PURE__ */ new Map();
1194
+ this.unsubscribers = /* @__PURE__ */ new Map();
1195
+ this.config = config;
1196
+ this.client = new ConvexClient(config.convexUrl);
1197
+ }
1198
+ watchSession(workSessionId, tmuxSessionName) {
1199
+ if (this.unsubscribers.has(workSessionId)) return;
1200
+ const unsub = this.client.onUpdate(
1201
+ api.agentBridge.bridgePublic.getWorkSessionTerminalState,
1202
+ {
1203
+ deviceId: this.config.deviceId,
1204
+ deviceSecret: this.config.deviceSecret,
1205
+ workSessionId
1206
+ },
1207
+ (state) => {
1208
+ if (!state) return;
1209
+ const terminal = this.terminals.get(workSessionId);
1210
+ if (state.terminalViewerActive && !terminal && !this.failedSessions.has(workSessionId)) {
1211
+ const pendingStop = this.pendingStops.get(workSessionId);
1212
+ if (pendingStop) {
1213
+ clearTimeout(pendingStop);
1214
+ this.pendingStops.delete(workSessionId);
1215
+ }
1216
+ console.log(`[${ts()}] Viewer active for ${tmuxSessionName}`);
1217
+ void this.startTerminal(
1218
+ workSessionId,
1219
+ tmuxSessionName,
1220
+ state.terminalCols,
1221
+ state.terminalRows
1222
+ );
1223
+ } else if (!state.terminalViewerActive && terminal) {
1224
+ if (!this.pendingStops.has(workSessionId)) {
1225
+ this.pendingStops.set(
1226
+ workSessionId,
1227
+ setTimeout(() => {
1228
+ this.pendingStops.delete(workSessionId);
1229
+ console.log(`[${ts()}] Viewer inactive for ${tmuxSessionName}`);
1230
+ this.stopTerminal(workSessionId);
1231
+ this.failedSessions.delete(workSessionId);
1232
+ }, 2e3)
1233
+ );
1234
+ }
1235
+ }
1236
+ }
1237
+ );
1238
+ this.unsubscribers.set(workSessionId, unsub);
1239
+ }
1240
+ unwatchSession(workSessionId) {
1241
+ const unsub = this.unsubscribers.get(workSessionId);
1242
+ if (unsub) {
1243
+ unsub();
1244
+ this.unsubscribers.delete(workSessionId);
1245
+ }
1246
+ this.stopTerminal(workSessionId);
1247
+ }
1248
+ async startTerminal(workSessionId, tmuxSessionName, cols, rows) {
1249
+ if (this.terminals.has(workSessionId)) return;
1250
+ const tmuxBin = findTmuxPath();
1251
+ try {
1252
+ const port = await findPort();
1253
+ console.log(
1254
+ `[${ts()}] Spawning PTY: ${tmuxBin} attach-session -t ${tmuxSessionName}`
1255
+ );
1256
+ const ptyProcess = pty.spawn(
1257
+ tmuxBin,
1258
+ ["attach-session", "-t", tmuxSessionName],
1259
+ {
1260
+ name: "xterm-256color",
1261
+ cols: Math.max(cols, 10),
1262
+ rows: Math.max(rows, 4),
1263
+ cwd: process.env.HOME ?? "/",
1264
+ env: { ...process.env, TERM: "xterm-256color" }
1265
+ }
1266
+ );
1267
+ console.log(`[${ts()}] PTY started for ${tmuxSessionName}`);
1268
+ const token = randomUUID();
1269
+ const httpServer = createServer();
1270
+ const wss = new WebSocketServer({ server: httpServer });
1271
+ wss.on("connection", (ws, req) => {
1272
+ const url = new URL(req.url ?? "/", `http://localhost`);
1273
+ const clientToken = url.searchParams.get("token");
1274
+ if (clientToken !== token) {
1275
+ console.log(`[${ts()}] Rejected unauthorized WebSocket connection`);
1276
+ ws.close(4401, "Unauthorized");
1277
+ return;
1278
+ }
1279
+ console.log(
1280
+ `[${ts()}] WebSocket client connected (${tmuxSessionName})`
1281
+ );
1282
+ const dataHandler = ptyProcess.onData((data) => {
1283
+ if (ws.readyState === WebSocket.OPEN) {
1284
+ ws.send(data);
1285
+ }
1286
+ });
1287
+ ws.on("message", (msg) => {
1288
+ const str = msg.toString();
1289
+ if (str.startsWith("\0{")) {
1290
+ try {
1291
+ const parsed = JSON.parse(str.slice(1));
1292
+ if (parsed.type === "resize" && parsed.cols && parsed.rows) {
1293
+ ptyProcess.resize(
1294
+ Math.max(parsed.cols, 10),
1295
+ Math.max(parsed.rows, 4)
1296
+ );
1297
+ return;
1298
+ }
1299
+ } catch {
1300
+ }
1301
+ }
1302
+ ptyProcess.write(str);
1303
+ });
1304
+ ws.on("close", () => {
1305
+ console.log(
1306
+ `[${ts()}] WebSocket client disconnected (${tmuxSessionName})`
1307
+ );
1308
+ dataHandler.dispose();
1309
+ });
1310
+ });
1311
+ await new Promise((resolve) => {
1312
+ httpServer.listen(port, "127.0.0.1", resolve);
1313
+ });
1314
+ console.log(`[${ts()}] WS server on port ${port}`);
1315
+ console.log(`[${ts()}] Opening tunnel...`);
1316
+ const tunnel = await localtunnel({ port });
1317
+ const tunnelUrl = tunnel.url;
1318
+ console.log(`[${ts()}] Tunnel: ${tunnelUrl}`);
1319
+ const wsUrl = tunnelUrl.replace(/^https?:\/\//, "wss://");
1320
+ const terminal = {
1321
+ ptyProcess,
1322
+ httpServer,
1323
+ wss,
1324
+ tunnel,
1325
+ tunnelUrl: wsUrl,
1326
+ token,
1327
+ workSessionId,
1328
+ tmuxSessionName,
1329
+ port
1330
+ };
1331
+ this.terminals.set(workSessionId, terminal);
1332
+ await this.client.mutation(
1333
+ api.agentBridge.bridgePublic.updateWorkSessionTerminalUrl,
1334
+ {
1335
+ deviceId: this.config.deviceId,
1336
+ deviceSecret: this.config.deviceSecret,
1337
+ workSessionId,
1338
+ terminalUrl: wsUrl,
1339
+ terminalToken: token
1340
+ }
1341
+ );
1342
+ ptyProcess.onExit(() => {
1343
+ console.log(`[${ts()}] PTY exited for ${tmuxSessionName}`);
1344
+ this.stopTerminal(workSessionId);
1345
+ });
1346
+ } catch (err) {
1347
+ console.error(`[${ts()}] Failed to start terminal:`, err);
1348
+ this.failedSessions.add(workSessionId);
1349
+ }
1350
+ }
1351
+ stopTerminal(workSessionId) {
1352
+ const terminal = this.terminals.get(workSessionId);
1353
+ if (!terminal) return;
1354
+ try {
1355
+ terminal.ptyProcess.kill();
1356
+ } catch {
1357
+ }
1358
+ try {
1359
+ terminal.tunnel.close();
1360
+ } catch {
1361
+ }
1362
+ try {
1363
+ terminal.wss.close();
1364
+ } catch {
1365
+ }
1366
+ try {
1367
+ terminal.httpServer.close();
1368
+ } catch {
1369
+ }
1370
+ this.terminals.delete(workSessionId);
1371
+ console.log(`[${ts()}] Terminal stopped for ${terminal.tmuxSessionName}`);
1372
+ }
1373
+ stop() {
1374
+ for (const unsub of this.unsubscribers.values()) {
1375
+ try {
1376
+ unsub();
1377
+ } catch {
1378
+ }
1379
+ }
1380
+ this.unsubscribers.clear();
1381
+ for (const id of this.terminals.keys()) {
1382
+ this.stopTerminal(id);
1383
+ }
1384
+ void this.client.close();
1385
+ }
1386
+ };
1387
+
1388
+ // src/bridge-service.ts
1103
1389
  import {
1104
- existsSync as existsSync2,
1390
+ existsSync as existsSync3,
1105
1391
  mkdirSync,
1106
1392
  readFileSync as readFileSync2,
1107
1393
  writeFileSync,
@@ -1109,17 +1395,18 @@ import {
1109
1395
  } from "fs";
1110
1396
  import { homedir as homedir3, hostname, platform } from "os";
1111
1397
  import { join as join2 } from "path";
1112
- import { randomUUID } from "crypto";
1398
+ import { randomUUID as randomUUID2 } from "crypto";
1113
1399
 
1114
1400
  // src/agent-adapters.ts
1115
- import { execSync, spawn } from "child_process";
1116
- import { existsSync, readFileSync, readdirSync } from "fs";
1401
+ import { execSync, spawn as spawn2 } from "child_process";
1402
+ import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
1117
1403
  import { homedir as homedir2, userInfo } from "os";
1118
1404
  import { basename, join } from "path";
1119
1405
  var LSOF_PATHS = ["/usr/sbin/lsof", "/usr/bin/lsof"];
1120
1406
  var VECTOR_BRIDGE_CLIENT_VERSION = "0.1.0";
1121
1407
  function discoverAttachableSessions() {
1122
1408
  return dedupeSessions([
1409
+ ...discoverTmuxSessions(),
1123
1410
  ...discoverCodexSessions(),
1124
1411
  ...discoverClaudeSessions()
1125
1412
  ]);
@@ -1141,7 +1428,7 @@ async function resumeProviderSession(provider, sessionKey, cwd, prompt2) {
1141
1428
  });
1142
1429
  }
1143
1430
  async function runCodexAppServerTurn(args) {
1144
- const child = spawn("codex", ["app-server"], {
1431
+ const child = spawn2("codex", ["app-server"], {
1145
1432
  cwd: args.cwd,
1146
1433
  env: { ...process.env },
1147
1434
  stdio: ["pipe", "pipe", "pipe"]
@@ -1429,6 +1716,65 @@ function discoverClaudeSessions() {
1429
1716
  return parsed ? [parsed] : [];
1430
1717
  }).sort(compareObservedSessions);
1431
1718
  }
1719
+ function discoverTmuxSessions() {
1720
+ try {
1721
+ const output = execSync(
1722
+ "tmux list-panes -a -F '#{pane_id} #{pane_pid} #{session_name} #{window_name} #{pane_current_path} #{pane_current_command} #{pane_title}'",
1723
+ {
1724
+ encoding: "utf-8",
1725
+ timeout: 3e3
1726
+ }
1727
+ );
1728
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
1729
+ const [
1730
+ paneId,
1731
+ panePid,
1732
+ sessionName,
1733
+ windowName,
1734
+ cwd,
1735
+ currentCommand,
1736
+ paneTitle
1737
+ ] = line.split(" ");
1738
+ if (!paneId || !panePid || !sessionName || !windowName || !cwd) {
1739
+ return [];
1740
+ }
1741
+ const normalizedCommand = (currentCommand ?? "").trim().toLowerCase();
1742
+ if (normalizedCommand === "codex" || normalizedCommand === "claude") {
1743
+ return [];
1744
+ }
1745
+ const gitInfo = getGitInfo(cwd);
1746
+ const title = summarizeTitle(
1747
+ buildTmuxPaneTitle({
1748
+ paneTitle,
1749
+ sessionName,
1750
+ windowName,
1751
+ cwd,
1752
+ currentCommand
1753
+ }),
1754
+ cwd
1755
+ );
1756
+ return [
1757
+ {
1758
+ provider: "vector_cli",
1759
+ providerLabel: "Tmux",
1760
+ localProcessId: panePid,
1761
+ sessionKey: `tmux:${paneId}`,
1762
+ cwd,
1763
+ ...gitInfo,
1764
+ title,
1765
+ tmuxSessionName: sessionName,
1766
+ tmuxWindowName: windowName,
1767
+ tmuxPaneId: paneId,
1768
+ mode: "observed",
1769
+ status: "observed",
1770
+ supportsInboundMessages: true
1771
+ }
1772
+ ];
1773
+ }).sort(compareObservedSessions);
1774
+ } catch {
1775
+ return [];
1776
+ }
1777
+ }
1432
1778
  function getCodexHistoryFile() {
1433
1779
  return join(getRealHomeDir(), ".codex", "history.jsonl");
1434
1780
  }
@@ -1453,7 +1799,7 @@ function getRealHomeDir() {
1453
1799
  }
1454
1800
  function resolveExecutable(fallbackCommand, absoluteCandidates) {
1455
1801
  for (const candidate of absoluteCandidates) {
1456
- if (existsSync(candidate)) {
1802
+ if (existsSync2(candidate)) {
1457
1803
  return candidate;
1458
1804
  }
1459
1805
  }
@@ -1512,7 +1858,7 @@ function getCodexTranscriptPath(pid) {
1512
1858
  }
1513
1859
  function readClaudePidSession(pid) {
1514
1860
  const path3 = join(getClaudeSessionStateDir(), `${pid}.json`);
1515
- if (!existsSync(path3)) {
1861
+ if (!existsSync2(path3)) {
1516
1862
  return null;
1517
1863
  }
1518
1864
  try {
@@ -1534,7 +1880,7 @@ function findClaudeTranscriptPath(sessionId) {
1534
1880
  return findJsonlFileByStem(getClaudeProjectsDir(), sessionId);
1535
1881
  }
1536
1882
  function findJsonlFileByStem(root, stem) {
1537
- if (!existsSync(root)) {
1883
+ if (!existsSync2(root)) {
1538
1884
  return void 0;
1539
1885
  }
1540
1886
  for (const entry of readdirSync(root, { withFileTypes: true })) {
@@ -1694,6 +2040,17 @@ function summarizeTitle(message, cwd) {
1694
2040
  }
1695
2041
  return "Local session";
1696
2042
  }
2043
+ function buildTmuxPaneTitle(args) {
2044
+ const paneTitle = cleanSessionTitleCandidate(args.paneTitle ?? "");
2045
+ if (paneTitle) {
2046
+ return paneTitle;
2047
+ }
2048
+ const command = asString(args.currentCommand);
2049
+ if (command && !["zsh", "bash", "fish", "sh", "nu"].includes(command)) {
2050
+ return `${command} in ${basename(args.cwd)}`;
2051
+ }
2052
+ return `${basename(args.cwd)} (${args.sessionName}:${args.windowName})`;
2053
+ }
1697
2054
  function truncate(value, maxLength) {
1698
2055
  return value.length > maxLength ? `${value.slice(0, maxLength - 3).trimEnd()}...` : value;
1699
2056
  }
@@ -1912,7 +2269,7 @@ var COMMAND_POLL_INTERVAL_MS = 5e3;
1912
2269
  var LIVE_ACTIVITY_SYNC_INTERVAL_MS = 5e3;
1913
2270
  var PROCESS_DISCOVERY_INTERVAL_MS = 6e4;
1914
2271
  function loadBridgeConfig() {
1915
- if (!existsSync2(BRIDGE_CONFIG_FILE)) return null;
2272
+ if (!existsSync3(BRIDGE_CONFIG_FILE)) return null;
1916
2273
  try {
1917
2274
  return JSON.parse(readFileSync2(BRIDGE_CONFIG_FILE, "utf-8"));
1918
2275
  } catch {
@@ -1920,17 +2277,18 @@ function loadBridgeConfig() {
1920
2277
  }
1921
2278
  }
1922
2279
  function saveBridgeConfig(config) {
1923
- if (!existsSync2(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2280
+ if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
1924
2281
  writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(config, null, 2));
1925
2282
  persistDeviceKey(config.deviceKey);
1926
2283
  }
1927
2284
  function writeLiveActivitiesCache(activities) {
1928
- if (!existsSync2(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2285
+ if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
1929
2286
  writeFileSync(LIVE_ACTIVITIES_CACHE, JSON.stringify(activities, null, 2));
1930
2287
  }
1931
2288
  var BridgeService = class {
1932
2289
  constructor(config) {
1933
2290
  this.timers = [];
2291
+ this.terminalPeer = null;
1934
2292
  this.config = config;
1935
2293
  this.client = new ConvexHttpClient2(config.convexUrl);
1936
2294
  }
@@ -1949,7 +2307,7 @@ var BridgeService = class {
1949
2307
  }
1950
2308
  );
1951
2309
  if (commands.length > 0) {
1952
- console.log(`[${ts()}] ${commands.length} pending command(s)`);
2310
+ console.log(`[${ts2()}] ${commands.length} pending command(s)`);
1953
2311
  }
1954
2312
  for (const cmd of commands) {
1955
2313
  await this.handleCommand(cmd);
@@ -1978,6 +2336,10 @@ var BridgeService = class {
1978
2336
  await this.handleLaunchCommand(cmd);
1979
2337
  await this.completeCommand(cmd._id, "delivered");
1980
2338
  return;
2339
+ case "resize":
2340
+ await this.handleResizeCommand(cmd);
2341
+ await this.completeCommand(cmd._id, "delivered");
2342
+ return;
1981
2343
  default:
1982
2344
  throw new Error(`Unsupported bridge command: ${cmd.kind}`);
1983
2345
  }
@@ -2012,7 +2374,7 @@ var BridgeService = class {
2012
2374
  }
2013
2375
  if (processes.length > 0) {
2014
2376
  console.log(
2015
- `[${ts()}] Discovered ${processes.length} attachable session(s)`
2377
+ `[${ts2()}] Discovered ${processes.length} attachable session(s)`
2016
2378
  );
2017
2379
  }
2018
2380
  }
@@ -2027,6 +2389,16 @@ var BridgeService = class {
2027
2389
  );
2028
2390
  writeLiveActivitiesCache(activities);
2029
2391
  await this.syncWorkSessionTerminals(activities);
2392
+ if (this.terminalPeer) {
2393
+ for (const activity of activities) {
2394
+ if (activity.workSessionId && activity.tmuxSessionName) {
2395
+ this.terminalPeer.watchSession(
2396
+ activity.workSessionId,
2397
+ activity.tmuxSessionName
2398
+ );
2399
+ }
2400
+ }
2401
+ }
2030
2402
  } catch {
2031
2403
  }
2032
2404
  }
@@ -2115,45 +2487,59 @@ var BridgeService = class {
2115
2487
  console.log(` Convex: ${this.config.convexUrl}`);
2116
2488
  console.log(` PID: ${process.pid}`);
2117
2489
  console.log("");
2118
- if (!existsSync2(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2490
+ if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2119
2491
  writeFileSync(PID_FILE, String(process.pid));
2492
+ try {
2493
+ this.terminalPeer = new TerminalPeerManager({
2494
+ deviceId: this.config.deviceId,
2495
+ deviceSecret: this.config.deviceSecret,
2496
+ convexUrl: this.config.convexUrl
2497
+ });
2498
+ console.log(` WebRTC: ready`);
2499
+ } catch (e) {
2500
+ console.error(
2501
+ ` WebRTC: failed (${e instanceof Error ? e.message : "unknown"})`
2502
+ );
2503
+ }
2504
+ console.log("");
2120
2505
  await this.heartbeat();
2121
2506
  await this.reportProcesses();
2122
2507
  await this.refreshLiveActivities();
2123
- console.log(`[${ts()}] Service running. Ctrl+C to stop.
2508
+ console.log(`[${ts2()}] Service running. Ctrl+C to stop.
2124
2509
  `);
2125
2510
  this.timers.push(
2126
2511
  setInterval(() => {
2127
2512
  this.heartbeat().catch(
2128
- (e) => console.error(`[${ts()}] Heartbeat error:`, e.message)
2513
+ (e) => console.error(`[${ts2()}] Heartbeat error:`, e.message)
2129
2514
  );
2130
2515
  }, HEARTBEAT_INTERVAL_MS)
2131
2516
  );
2132
2517
  this.timers.push(
2133
2518
  setInterval(() => {
2134
2519
  this.pollCommands().catch(
2135
- (e) => console.error(`[${ts()}] Command poll error:`, e.message)
2520
+ (e) => console.error(`[${ts2()}] Command poll error:`, e.message)
2136
2521
  );
2137
2522
  }, COMMAND_POLL_INTERVAL_MS)
2138
2523
  );
2139
2524
  this.timers.push(
2140
2525
  setInterval(() => {
2141
2526
  this.refreshLiveActivities().catch(
2142
- (e) => console.error(`[${ts()}] Live activity sync error:`, e.message)
2527
+ (e) => console.error(`[${ts2()}] Live activity sync error:`, e.message)
2143
2528
  );
2144
2529
  }, LIVE_ACTIVITY_SYNC_INTERVAL_MS)
2145
2530
  );
2146
2531
  this.timers.push(
2147
2532
  setInterval(() => {
2148
2533
  this.reportProcesses().catch(
2149
- (e) => console.error(`[${ts()}] Discovery error:`, e.message)
2534
+ (e) => console.error(`[${ts2()}] Discovery error:`, e.message)
2150
2535
  );
2151
2536
  }, PROCESS_DISCOVERY_INTERVAL_MS)
2152
2537
  );
2153
2538
  const shutdown = () => {
2154
2539
  console.log(`
2155
- [${ts()}] Shutting down...`);
2540
+ [${ts2()}] Shutting down...`);
2156
2541
  for (const t of this.timers) clearInterval(t);
2542
+ this.terminalPeer?.stop();
2157
2543
  try {
2158
2544
  unlinkSync(PID_FILE);
2159
2545
  } catch {
@@ -2252,6 +2638,29 @@ var BridgeService = class {
2252
2638
  title: cmd.liveActivity?.title ?? process9.title
2253
2639
  });
2254
2640
  }
2641
+ async handleResizeCommand(cmd) {
2642
+ const payload = cmd.payload;
2643
+ const cols = payload?.cols;
2644
+ const rows = payload?.rows;
2645
+ const paneId = cmd.workSession?.tmuxPaneId;
2646
+ if (!paneId || !cols || !rows) {
2647
+ throw new Error("Resize command missing paneId, cols, or rows");
2648
+ }
2649
+ console.log(` Resize ${paneId} \u2192 ${cols}x${rows}`);
2650
+ resizeTmuxPane(paneId, cols, rows);
2651
+ if (cmd.workSession) {
2652
+ await this.refreshWorkSessionTerminal(cmd.workSession._id, {
2653
+ tmuxSessionName: cmd.workSession.tmuxSessionName,
2654
+ tmuxWindowName: cmd.workSession.tmuxWindowName,
2655
+ tmuxPaneId: paneId,
2656
+ cwd: cmd.workSession.cwd,
2657
+ repoRoot: cmd.workSession.repoRoot,
2658
+ branch: cmd.workSession.branch,
2659
+ agentProvider: cmd.workSession.agentProvider,
2660
+ agentSessionKey: cmd.workSession.agentSessionKey
2661
+ });
2662
+ }
2663
+ }
2255
2664
  async handleLaunchCommand(cmd) {
2256
2665
  if (!cmd.liveActivityId) {
2257
2666
  throw new Error("Launch command is missing liveActivityId");
@@ -2365,6 +2774,9 @@ var BridgeService = class {
2365
2774
  branch,
2366
2775
  title,
2367
2776
  model,
2777
+ tmuxSessionName,
2778
+ tmuxWindowName,
2779
+ tmuxPaneId,
2368
2780
  mode,
2369
2781
  status,
2370
2782
  supportsInboundMessages
@@ -2383,6 +2795,9 @@ var BridgeService = class {
2383
2795
  branch,
2384
2796
  title,
2385
2797
  model,
2798
+ tmuxSessionName,
2799
+ tmuxWindowName,
2800
+ tmuxPaneId,
2386
2801
  mode,
2387
2802
  status,
2388
2803
  supportsInboundMessages
@@ -2440,7 +2855,7 @@ var BridgeService = class {
2440
2855
  };
2441
2856
  function createTmuxWorkSession(args) {
2442
2857
  const slug = sanitizeTmuxName(args.issueKey.toLowerCase());
2443
- const sessionName = `vector-${slug}-${randomUUID().slice(0, 8)}`;
2858
+ const sessionName = `vector-${slug}-${randomUUID2().slice(0, 8)}`;
2444
2859
  const windowName = sanitizeTmuxName(
2445
2860
  args.provider === "codex" ? "codex" : args.provider === "claude_code" ? "claude" : "shell"
2446
2861
  );
@@ -2503,9 +2918,24 @@ function sendTextToTmuxPane(paneId, text2) {
2503
2918
  function captureTmuxPane(paneId) {
2504
2919
  return execFileSync(
2505
2920
  "tmux",
2506
- ["capture-pane", "-p", "-t", paneId, "-S", "-120"],
2921
+ ["capture-pane", "-p", "-e", "-t", paneId, "-S", "-120"],
2507
2922
  { encoding: "utf-8" }
2508
- ).replace(/\u001B\[[0-9;?]*[A-Za-z]/g, "").trimEnd();
2923
+ ).trimEnd();
2924
+ }
2925
+ function resizeTmuxPane(paneId, cols, rows) {
2926
+ try {
2927
+ execFileSync("tmux", [
2928
+ "resize-pane",
2929
+ "-t",
2930
+ paneId,
2931
+ "-x",
2932
+ String(cols),
2933
+ "-y",
2934
+ String(rows)
2935
+ ]);
2936
+ } catch (e) {
2937
+ console.error(`Failed to resize pane ${paneId}:`, e);
2938
+ }
2509
2939
  }
2510
2940
  function currentGitBranch(cwd) {
2511
2941
  try {
@@ -2564,18 +2994,18 @@ function getStableDeviceKey() {
2564
2994
  persistDeviceKey(existingKey);
2565
2995
  return existingKey;
2566
2996
  }
2567
- if (existsSync2(DEVICE_KEY_FILE)) {
2997
+ if (existsSync3(DEVICE_KEY_FILE)) {
2568
2998
  const savedKey = readFileSync2(DEVICE_KEY_FILE, "utf-8").trim();
2569
2999
  if (savedKey) {
2570
3000
  return savedKey;
2571
3001
  }
2572
3002
  }
2573
- const generatedKey = `${hostname()}-${randomUUID().slice(0, 8)}`;
3003
+ const generatedKey = `${hostname()}-${randomUUID2().slice(0, 8)}`;
2574
3004
  persistDeviceKey(generatedKey);
2575
3005
  return generatedKey;
2576
3006
  }
2577
3007
  function persistDeviceKey(deviceKey) {
2578
- if (!existsSync2(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
3008
+ if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
2579
3009
  writeFileSync(DEVICE_KEY_FILE, `${deviceKey}
2580
3010
  `);
2581
3011
  }
@@ -2672,7 +3102,13 @@ function isBridgeProvider(provider) {
2672
3102
  return provider === "codex" || provider === "claude_code";
2673
3103
  }
2674
3104
  function providerLabel(provider) {
2675
- return provider === "codex" ? "Codex" : "Claude";
3105
+ if (provider === "codex") {
3106
+ return "Codex";
3107
+ }
3108
+ if (provider === "claude_code") {
3109
+ return "Claude";
3110
+ }
3111
+ return "Vector CLI";
2676
3112
  }
2677
3113
  function installLaunchAgent(vcliPath) {
2678
3114
  if (platform() !== "darwin") {
@@ -2710,7 +3146,7 @@ ${environmentVariables}
2710
3146
  </dict>
2711
3147
  </dict>
2712
3148
  </plist>`;
2713
- if (!existsSync2(LAUNCHAGENT_DIR)) {
3149
+ if (!existsSync3(LAUNCHAGENT_DIR)) {
2714
3150
  mkdirSync(LAUNCHAGENT_DIR, { recursive: true });
2715
3151
  }
2716
3152
  removeLegacyMenuBarLaunchAgent();
@@ -2741,7 +3177,7 @@ function resolveCliInvocation(vcliPath) {
2741
3177
  ".bin",
2742
3178
  "tsx"
2743
3179
  );
2744
- if (existsSync2(tsxPath)) {
3180
+ if (existsSync3(tsxPath)) {
2745
3181
  return [tsxPath, vcliPath];
2746
3182
  }
2747
3183
  }
@@ -2790,7 +3226,7 @@ function uninstallLaunchAgent() {
2790
3226
  }
2791
3227
  var MENUBAR_PID_FILE = join2(CONFIG_DIR, "menubar.pid");
2792
3228
  function removeLegacyMenuBarLaunchAgent() {
2793
- if (platform() !== "darwin" || !existsSync2(LEGACY_MENUBAR_LAUNCHAGENT_PLIST)) {
3229
+ if (platform() !== "darwin" || !existsSync3(LEGACY_MENUBAR_LAUNCHAGENT_PLIST)) {
2794
3230
  return;
2795
3231
  }
2796
3232
  try {
@@ -2825,7 +3261,7 @@ function findCliEntrypoint() {
2825
3261
  join2(import.meta.dirname ?? "", "..", "dist", "index.js")
2826
3262
  ];
2827
3263
  for (const p of candidates) {
2828
- if (existsSync2(p)) return p;
3264
+ if (existsSync3(p)) return p;
2829
3265
  }
2830
3266
  return null;
2831
3267
  }
@@ -2857,7 +3293,7 @@ function findMenuBarExecutable() {
2857
3293
  )
2858
3294
  ];
2859
3295
  for (const p of candidates) {
2860
- if (existsSync2(p)) {
3296
+ if (existsSync3(p)) {
2861
3297
  return p;
2862
3298
  }
2863
3299
  }
@@ -2875,7 +3311,7 @@ function isKnownMenuBarProcess(pid) {
2875
3311
  }
2876
3312
  }
2877
3313
  function killExistingMenuBar() {
2878
- if (existsSync2(MENUBAR_PID_FILE)) {
3314
+ if (existsSync3(MENUBAR_PID_FILE)) {
2879
3315
  try {
2880
3316
  const pid = Number(readFileSync2(MENUBAR_PID_FILE, "utf-8").trim());
2881
3317
  if (Number.isFinite(pid) && pid > 0 && isKnownMenuBarProcess(pid)) {
@@ -2890,7 +3326,7 @@ function killExistingMenuBar() {
2890
3326
  }
2891
3327
  }
2892
3328
  function getRunningMenuBarPid() {
2893
- if (!existsSync2(MENUBAR_PID_FILE)) {
3329
+ if (!existsSync3(MENUBAR_PID_FILE)) {
2894
3330
  return null;
2895
3331
  }
2896
3332
  try {
@@ -2945,7 +3381,7 @@ function getBridgeStatus() {
2945
3381
  let running = false;
2946
3382
  let starting = false;
2947
3383
  let pid;
2948
- if (existsSync2(PID_FILE)) {
3384
+ if (existsSync3(PID_FILE)) {
2949
3385
  const pidStr = readFileSync2(PID_FILE, "utf-8").trim();
2950
3386
  pid = Number(pidStr);
2951
3387
  try {
@@ -2968,7 +3404,7 @@ function stopBridge(options) {
2968
3404
  writeLiveActivitiesCache([]);
2969
3405
  } catch {
2970
3406
  }
2971
- if (!existsSync2(PID_FILE)) return false;
3407
+ if (!existsSync3(PID_FILE)) return false;
2972
3408
  const pid = Number(readFileSync2(PID_FILE, "utf-8").trim());
2973
3409
  try {
2974
3410
  process.kill(pid, "SIGTERM");
@@ -2977,7 +3413,7 @@ function stopBridge(options) {
2977
3413
  return false;
2978
3414
  }
2979
3415
  }
2980
- function ts() {
3416
+ function ts2() {
2981
3417
  return (/* @__PURE__ */ new Date()).toLocaleTimeString();
2982
3418
  }
2983
3419
 
@@ -3204,7 +3640,7 @@ async function fetchConvexUrl(appUrl) {
3204
3640
  }
3205
3641
  async function getRuntime(command) {
3206
3642
  const options = command.optsWithGlobals();
3207
- const profile = options.profile ?? "default";
3643
+ const profile = options.profile ?? await readDefaultProfile();
3208
3644
  const session = await readSession(profile);
3209
3645
  const appUrlSource = options.appUrl ?? session?.appUrl ?? process.env.NEXT_PUBLIC_APP_URL;
3210
3646
  const appUrl = await resolveAppUrl(requiredString(appUrlSource, "app URL"));
@@ -3541,7 +3977,7 @@ function readPackageVersionSync() {
3541
3977
  program.name("vcli").description("Vector CLI").version(readPackageVersionSync(), "-v, --version").showHelpAfterError().option(
3542
3978
  "--app-url <url>",
3543
3979
  "Vector app URL. Required unless saved in the profile or NEXT_PUBLIC_APP_URL is set."
3544
- ).option("--convex-url <url>", "Convex deployment URL").option("--org <slug>", "Organization slug override").option("--profile <name>", "CLI profile name", "default").option("--json", "Output JSON");
3980
+ ).option("--convex-url <url>", "Convex deployment URL").option("--org <slug>", "Organization slug override").option("--profile <name>", "CLI profile name").option("--json", "Output JSON");
3545
3981
  var authCommand = program.command("auth").description("Authentication");
3546
3982
  authCommand.command("signup").option("--email <email>", "Email address").option("--username <username>", "Username").option("--password <password>", "Password").action(async (options, command) => {
3547
3983
  const runtime = await getRuntime(command);
@@ -3664,6 +4100,38 @@ authCommand.command("whoami").action(async (_options, command) => {
3664
4100
  runtime.json
3665
4101
  );
3666
4102
  });
4103
+ authCommand.command("profiles").action(async (_options, command) => {
4104
+ const options = command.optsWithGlobals();
4105
+ const explicitProfile = options.profile?.trim();
4106
+ const defaultProfile = await readDefaultProfile();
4107
+ const profiles = await listProfiles();
4108
+ const activeProfile = explicitProfile || defaultProfile;
4109
+ printOutput(
4110
+ {
4111
+ activeProfile,
4112
+ defaultProfile,
4113
+ profiles
4114
+ },
4115
+ Boolean(options.json)
4116
+ );
4117
+ });
4118
+ authCommand.command("use-profile <name>").action(async (name, _options, command) => {
4119
+ const options = command.optsWithGlobals();
4120
+ const profile = name.trim();
4121
+ if (!profile) {
4122
+ throw new Error("Profile name is required.");
4123
+ }
4124
+ await writeDefaultProfile(profile);
4125
+ const session = await readSession(profile);
4126
+ printOutput(
4127
+ {
4128
+ ok: true,
4129
+ defaultProfile: profile,
4130
+ hasSession: session !== null
4131
+ },
4132
+ Boolean(options.json)
4133
+ );
4134
+ });
3667
4135
  var orgCommand = program.command("org").description("Organizations");
3668
4136
  orgCommand.command("list").action(async (_options, command) => {
3669
4137
  const { client, runtime } = await getClient(command);
@@ -5145,8 +5613,11 @@ serviceCommand.command("status").description("Show bridge service status").actio
5145
5613
  serviceCommand.command("menu-state").description("Return JSON state for the macOS tray").action(async (_options, command) => {
5146
5614
  const status = getBridgeStatus();
5147
5615
  const globalOptions = command.optsWithGlobals();
5148
- const profile = globalOptions.profile ?? "default";
5616
+ const profile = globalOptions.profile ?? await readDefaultProfile();
5149
5617
  const session = await readSession(profile);
5618
+ const profiles = await listProfiles();
5619
+ const defaultProfile = await readDefaultProfile();
5620
+ let workspaces = [];
5150
5621
  let workSessions = [];
5151
5622
  let detectedSessions = [];
5152
5623
  try {
@@ -5157,6 +5628,13 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
5157
5628
  runtime.appUrl,
5158
5629
  runtime.convexUrl
5159
5630
  );
5631
+ workspaces = await runQuery(
5632
+ client,
5633
+ api.agentBridge.queries.listDeviceWorkspaces,
5634
+ {
5635
+ deviceId: status.config.deviceId
5636
+ }
5637
+ );
5160
5638
  workSessions = await runQuery(
5161
5639
  client,
5162
5640
  api.agentBridge.queries.listDeviceWorkSessions,
@@ -5175,6 +5653,7 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
5175
5653
  detectedSessions = currentDevice?.processes ?? [];
5176
5654
  }
5177
5655
  } catch {
5656
+ workspaces = [];
5178
5657
  workSessions = [];
5179
5658
  detectedSessions = [];
5180
5659
  }
@@ -5186,12 +5665,27 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
5186
5665
  pid: status.pid,
5187
5666
  config: status.config,
5188
5667
  sessionInfo: buildMenuSessionInfo(session),
5668
+ activeProfile: profile,
5669
+ defaultProfile,
5670
+ profiles,
5671
+ workspaces,
5189
5672
  workSessions,
5190
5673
  detectedSessions
5191
5674
  },
5192
5675
  Boolean(globalOptions.json)
5193
5676
  );
5194
5677
  });
5678
+ serviceCommand.command("set-default-workspace").description("Set the default workspace for this device").requiredOption("--workspace-id <id>").action(async (options, command) => {
5679
+ const { client, runtime } = await getClient(command);
5680
+ const workspaceId = await runMutation(
5681
+ client,
5682
+ api.agentBridge.mutations.setDefaultWorkspace,
5683
+ {
5684
+ workspaceId: options.workspaceId
5685
+ }
5686
+ );
5687
+ printOutput({ ok: true, workspaceId }, runtime.json);
5688
+ });
5195
5689
  serviceCommand.command("search-issues <query>").description("Search issues for tray attach actions").option("--limit <n>").action(async (query, options, command) => {
5196
5690
  const { client, runtime } = await getClient(command);
5197
5691
  const orgSlug = requireOrg(runtime);