@rehpic/vcli 0.1.0-beta.59.1 → 0.1.0-beta.61.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
@@ -742,6 +742,7 @@ import { dirname, extname, join as join3 } from "path";
742
742
  import { fileURLToPath as fileURLToPath2 } from "url";
743
743
  import { config as loadEnv } from "dotenv";
744
744
  import { Command } from "commander";
745
+ import { ConvexHttpClient as ConvexHttpClient3 } from "convex/browser";
745
746
  import { makeFunctionReference } from "convex/server";
746
747
 
747
748
  // ../../convex/_generated/api.js
@@ -1112,239 +1113,334 @@ import { randomUUID } from "crypto";
1112
1113
 
1113
1114
  // src/agent-adapters.ts
1114
1115
  import { execSync, spawn } from "child_process";
1115
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
1116
+ import { existsSync, readFileSync, readdirSync } from "fs";
1116
1117
  import { homedir as homedir2, userInfo } from "os";
1117
1118
  import { basename, join } from "path";
1118
- var MAX_DISCOVERED_FILES_PER_PROVIDER = 30;
1119
+ var LSOF_PATHS = ["/usr/sbin/lsof", "/usr/bin/lsof"];
1120
+ var VECTOR_BRIDGE_CLIENT_VERSION = "0.1.0";
1119
1121
  function discoverAttachableSessions() {
1120
1122
  return dedupeSessions([
1121
1123
  ...discoverCodexSessions(),
1122
1124
  ...discoverClaudeSessions()
1123
1125
  ]);
1124
1126
  }
1125
- async function launchProviderSession(provider, cwd, prompt2) {
1126
- if (provider === "codex") {
1127
- const stdout2 = await runCommand("codex", ["exec", "--json", prompt2], cwd);
1128
- return parseCodexRunResult(stdout2, cwd, "codex exec --json");
1129
- }
1130
- const stdout = await runCommand(
1131
- "claude",
1132
- ["-p", "--output-format", "json", prompt2],
1133
- cwd
1134
- );
1135
- return parseClaudeRunResult(stdout, cwd, "claude -p --output-format json");
1136
- }
1137
1127
  async function resumeProviderSession(provider, sessionKey, cwd, prompt2) {
1138
1128
  if (provider === "codex") {
1139
- const stdout2 = await runCommand(
1140
- "codex",
1141
- ["exec", "resume", "--json", sessionKey, prompt2],
1142
- cwd
1143
- );
1144
- return parseCodexRunResult(stdout2, cwd, "codex exec resume --json");
1129
+ return runCodexAppServerTurn({
1130
+ cwd,
1131
+ prompt: prompt2,
1132
+ sessionKey,
1133
+ launchCommand: "codex app-server (thread/resume)"
1134
+ });
1145
1135
  }
1146
- const stdout = await runCommand(
1147
- "claude",
1148
- ["-p", "--resume", sessionKey, "--output-format", "json", prompt2],
1149
- cwd
1150
- );
1151
- return parseClaudeRunResult(
1152
- stdout,
1136
+ return runClaudeSdkTurn({
1153
1137
  cwd,
1154
- "claude -p --resume --output-format json"
1155
- );
1138
+ prompt: prompt2,
1139
+ sessionKey,
1140
+ launchCommand: "@anthropic-ai/claude-agent-sdk query(resume)"
1141
+ });
1156
1142
  }
1157
- async function runCommand(command, args, cwd) {
1158
- const child = spawn(command, args, {
1159
- cwd,
1143
+ async function runCodexAppServerTurn(args) {
1144
+ const child = spawn("codex", ["app-server"], {
1145
+ cwd: args.cwd,
1160
1146
  env: { ...process.env },
1161
- stdio: ["ignore", "pipe", "pipe"]
1147
+ stdio: ["pipe", "pipe", "pipe"]
1162
1148
  });
1163
- let stdout = "";
1164
1149
  let stderr = "";
1150
+ let stdoutBuffer = "";
1151
+ let sessionKey = args.sessionKey;
1152
+ let finalAssistantText = "";
1153
+ let completed = false;
1154
+ let nextRequestId = 1;
1155
+ const pending = /* @__PURE__ */ new Map();
1156
+ let completeTurn;
1157
+ let failTurn;
1158
+ const turnCompleted = new Promise((resolve, reject) => {
1159
+ completeTurn = () => {
1160
+ completed = true;
1161
+ resolve();
1162
+ };
1163
+ failTurn = (error) => {
1164
+ completed = true;
1165
+ reject(error);
1166
+ };
1167
+ });
1165
1168
  child.stdout.on("data", (chunk) => {
1166
- stdout += chunk.toString();
1169
+ stdoutBuffer += chunk.toString();
1170
+ while (true) {
1171
+ const newlineIndex = stdoutBuffer.indexOf("\n");
1172
+ if (newlineIndex < 0) {
1173
+ break;
1174
+ }
1175
+ const line = stdoutBuffer.slice(0, newlineIndex).trim();
1176
+ stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
1177
+ if (!line) {
1178
+ continue;
1179
+ }
1180
+ const payload = tryParseJson(line);
1181
+ if (!payload || typeof payload !== "object") {
1182
+ continue;
1183
+ }
1184
+ const responseId = payload.id;
1185
+ if (typeof responseId === "number" && pending.has(responseId)) {
1186
+ const entry = pending.get(responseId);
1187
+ pending.delete(responseId);
1188
+ const errorRecord = asObject(payload.error);
1189
+ if (errorRecord) {
1190
+ entry.reject(
1191
+ new Error(
1192
+ `codex app-server error: ${asString(errorRecord.message) ?? "Unknown JSON-RPC error"}`
1193
+ )
1194
+ );
1195
+ continue;
1196
+ }
1197
+ entry.resolve(payload.result);
1198
+ continue;
1199
+ }
1200
+ const method = asString(payload.method);
1201
+ const params = asObject(payload.params);
1202
+ if (!method || !params) {
1203
+ continue;
1204
+ }
1205
+ if (method === "thread/started") {
1206
+ sessionKey = asString(asObject(params.thread)?.id) ?? asString(asObject(params.thread)?.threadId) ?? sessionKey;
1207
+ continue;
1208
+ }
1209
+ if (method === "item/agentMessage/delta") {
1210
+ finalAssistantText += asString(params.delta) ?? "";
1211
+ continue;
1212
+ }
1213
+ if (method === "item/completed") {
1214
+ const item = asObject(params.item);
1215
+ if (asString(item?.type) === "agentMessage") {
1216
+ finalAssistantText = asString(item?.text) ?? finalAssistantText;
1217
+ }
1218
+ continue;
1219
+ }
1220
+ if (method === "turn/completed") {
1221
+ const turn = asObject(params.turn);
1222
+ const status = asString(turn?.status);
1223
+ if (status === "failed") {
1224
+ const turnError = asObject(turn?.error);
1225
+ failTurn?.(
1226
+ new Error(
1227
+ asString(turnError?.message) ?? "Codex turn failed without an error message"
1228
+ )
1229
+ );
1230
+ } else if (status === "interrupted") {
1231
+ failTurn?.(new Error("Codex turn was interrupted"));
1232
+ } else {
1233
+ completeTurn?.();
1234
+ }
1235
+ }
1236
+ }
1167
1237
  });
1168
1238
  child.stderr.on("data", (chunk) => {
1169
1239
  stderr += chunk.toString();
1170
1240
  });
1171
- return await new Promise((resolve, reject) => {
1172
- child.on("error", reject);
1241
+ const request = (method, params) => new Promise((resolve, reject) => {
1242
+ const id = nextRequestId++;
1243
+ pending.set(id, { resolve, reject });
1244
+ child.stdin.write(`${JSON.stringify({ method, id, params })}
1245
+ `);
1246
+ });
1247
+ const notify = (method, params) => {
1248
+ child.stdin.write(`${JSON.stringify({ method, params })}
1249
+ `);
1250
+ };
1251
+ const waitForExit = new Promise((_, reject) => {
1252
+ child.on("error", (error) => reject(error));
1173
1253
  child.on("close", (code) => {
1174
- if (code === 0) {
1175
- resolve(stdout);
1176
- return;
1254
+ if (!completed) {
1255
+ const detail = stderr.trim() || `codex app-server exited with code ${code}`;
1256
+ reject(new Error(detail));
1177
1257
  }
1178
- const detail = stderr.trim() || stdout.trim() || `exit code ${code}`;
1179
- reject(new Error(`${command} failed: ${detail}`));
1180
1258
  });
1181
1259
  });
1182
- }
1183
- function discoverCodexSessions() {
1184
- return listRecentJsonlFiles(getCodexSessionsDir()).flatMap((file) => {
1185
- const entries = readJsonLines(file);
1186
- let sessionKey;
1187
- let cwd;
1188
- let title;
1189
- let lastUserMessage;
1190
- let lastAssistantMessage;
1191
- for (const entry of entries) {
1192
- if (entry.type === "session_meta") {
1193
- sessionKey = asString(entry.payload?.id) ?? sessionKey;
1194
- cwd = asString(entry.payload?.cwd) ?? cwd;
1195
- }
1196
- if (entry.type === "event_msg" && entry.payload?.type === "user_message") {
1197
- lastUserMessage = asString(entry.payload?.message) ?? lastUserMessage;
1198
- }
1199
- if (entry.type === "response_item" && entry.payload?.type === "message" && entry.payload?.role === "user") {
1200
- lastUserMessage = extractCodexResponseText(entry.payload?.content) ?? lastUserMessage;
1201
- }
1202
- if (entry.type === "event_msg" && entry.payload?.type === "agent_message") {
1203
- lastAssistantMessage = asString(entry.payload?.message) ?? lastAssistantMessage;
1204
- }
1205
- if (entry.type === "response_item" && entry.payload?.type === "message" && entry.payload?.role === "assistant") {
1206
- lastAssistantMessage = extractCodexResponseText(entry.payload?.content) ?? lastAssistantMessage;
1207
- }
1208
- }
1260
+ try {
1261
+ await Promise.race([
1262
+ request("initialize", {
1263
+ clientInfo: {
1264
+ name: "vector_bridge",
1265
+ title: "Vector Bridge",
1266
+ version: VECTOR_BRIDGE_CLIENT_VERSION
1267
+ }
1268
+ }),
1269
+ waitForExit
1270
+ ]);
1271
+ notify("initialized", {});
1272
+ const threadResult = await Promise.race([
1273
+ args.sessionKey ? request("thread/resume", {
1274
+ threadId: args.sessionKey,
1275
+ cwd: args.cwd,
1276
+ approvalPolicy: "never",
1277
+ personality: "pragmatic"
1278
+ }) : request("thread/start", {
1279
+ cwd: args.cwd,
1280
+ approvalPolicy: "never",
1281
+ personality: "pragmatic",
1282
+ serviceName: "vector_bridge"
1283
+ }),
1284
+ waitForExit
1285
+ ]);
1286
+ sessionKey = asString(asObject(threadResult.thread)?.id) ?? asString(asObject(threadResult.thread)?.threadId) ?? sessionKey;
1209
1287
  if (!sessionKey) {
1210
- return [];
1288
+ throw new Error("Codex app-server did not return a thread id");
1211
1289
  }
1212
- title = summarizeTitle(lastUserMessage ?? lastAssistantMessage, cwd);
1213
- const gitInfo = cwd ? getGitInfo(cwd) : {};
1214
- return [
1215
- {
1216
- provider: "codex",
1217
- providerLabel: "Codex",
1218
- sessionKey,
1219
- cwd,
1220
- ...gitInfo,
1221
- title,
1222
- mode: "observed",
1223
- status: "observed",
1224
- supportsInboundMessages: true
1290
+ await Promise.race([
1291
+ request("turn/start", {
1292
+ threadId: sessionKey,
1293
+ input: [{ type: "text", text: args.prompt }],
1294
+ cwd: args.cwd,
1295
+ approvalPolicy: "never",
1296
+ personality: "pragmatic"
1297
+ }),
1298
+ waitForExit
1299
+ ]);
1300
+ await Promise.race([turnCompleted, waitForExit]);
1301
+ const gitInfo = getGitInfo(args.cwd);
1302
+ return {
1303
+ provider: "codex",
1304
+ providerLabel: "Codex",
1305
+ sessionKey,
1306
+ cwd: args.cwd,
1307
+ ...gitInfo,
1308
+ title: summarizeTitle(void 0, args.cwd),
1309
+ mode: "managed",
1310
+ status: "waiting",
1311
+ supportsInboundMessages: true,
1312
+ responseText: finalAssistantText.trim() || void 0,
1313
+ launchCommand: args.launchCommand
1314
+ };
1315
+ } finally {
1316
+ for (const entry of pending.values()) {
1317
+ entry.reject(
1318
+ new Error("codex app-server closed before request resolved")
1319
+ );
1320
+ }
1321
+ pending.clear();
1322
+ child.kill();
1323
+ }
1324
+ }
1325
+ async function runClaudeSdkTurn(args) {
1326
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
1327
+ const stream = query({
1328
+ prompt: args.prompt,
1329
+ options: {
1330
+ cwd: args.cwd,
1331
+ resume: args.sessionKey,
1332
+ persistSession: true,
1333
+ permissionMode: "bypassPermissions",
1334
+ allowDangerouslySkipPermissions: true,
1335
+ env: {
1336
+ ...process.env,
1337
+ CLAUDE_AGENT_SDK_CLIENT_APP: `vector-bridge/${VECTOR_BRIDGE_CLIENT_VERSION}`
1225
1338
  }
1226
- ];
1339
+ }
1227
1340
  });
1228
- }
1229
- function discoverClaudeSessions() {
1230
- return listRecentJsonlFiles(getClaudeSessionsDir()).flatMap((file) => {
1231
- const entries = readJsonLines(file);
1232
- let sessionKey;
1233
- let cwd;
1234
- let branch;
1235
- let model;
1236
- let lastUserMessage;
1237
- let lastAssistantMessage;
1238
- for (const entry of entries) {
1239
- sessionKey = asString(entry.sessionId) ?? sessionKey;
1240
- cwd = asString(entry.cwd) ?? cwd;
1241
- branch = asString(entry.gitBranch) ?? branch;
1242
- if (entry.type === "user") {
1243
- lastUserMessage = extractClaudeUserText(entry.message) ?? lastUserMessage;
1341
+ let sessionKey = args.sessionKey;
1342
+ let responseText = "";
1343
+ let model;
1344
+ try {
1345
+ for await (const message of stream) {
1346
+ if (!message || typeof message !== "object") {
1347
+ continue;
1244
1348
  }
1245
- if (entry.type === "assistant") {
1246
- model = asString(entry.message?.model) ?? model;
1247
- lastAssistantMessage = extractClaudeAssistantText(entry.message) ?? lastAssistantMessage;
1349
+ sessionKey = asString(message.session_id) ?? sessionKey;
1350
+ if (message.type === "assistant") {
1351
+ const assistantText = extractClaudeMessageTexts(
1352
+ message.message
1353
+ ).join("\n\n").trim();
1354
+ if (assistantText) {
1355
+ responseText = assistantText;
1356
+ }
1357
+ continue;
1248
1358
  }
1249
- }
1250
- if (!sessionKey) {
1251
- return [];
1252
- }
1253
- const gitInfo = cwd ? getGitInfo(cwd) : {};
1254
- return [
1255
- {
1256
- provider: "claude_code",
1257
- providerLabel: "Claude",
1258
- sessionKey,
1259
- cwd,
1260
- repoRoot: gitInfo.repoRoot,
1261
- branch: branch ?? gitInfo.branch,
1262
- title: summarizeTitle(lastUserMessage ?? lastAssistantMessage, cwd),
1263
- model,
1264
- mode: "observed",
1265
- status: "observed",
1266
- supportsInboundMessages: true
1359
+ if (message.type !== "result") {
1360
+ continue;
1267
1361
  }
1268
- ];
1269
- });
1270
- }
1271
- function parseCodexRunResult(stdout, cwd, launchCommand) {
1272
- let sessionKey;
1273
- const responseParts = [];
1274
- for (const line of stdout.split("\n").map((part) => part.trim()).filter(Boolean)) {
1275
- const payload = tryParseJson(line);
1276
- if (!payload) continue;
1277
- if (payload.type === "thread.started") {
1278
- sessionKey = asString(payload.thread_id) ?? sessionKey;
1279
- }
1280
- if (payload.type === "item.completed" && payload.item?.type === "agent_message" && typeof payload.item.text === "string") {
1281
- responseParts.push(payload.item.text.trim());
1282
- }
1283
- if (payload.type === "response_item" && payload.payload?.type === "message" && payload.payload?.role === "assistant") {
1284
- const responseText2 = extractCodexResponseText(payload.payload?.content);
1285
- if (responseText2) {
1286
- responseParts.push(responseText2);
1362
+ if (message.subtype === "success") {
1363
+ const resultText = asString(message.result);
1364
+ if (resultText) {
1365
+ responseText = resultText;
1366
+ }
1367
+ model = firstObjectKey(
1368
+ message.modelUsage
1369
+ );
1370
+ continue;
1287
1371
  }
1372
+ const errors = message.errors;
1373
+ const detail = Array.isArray(errors) && errors.length > 0 ? errors.join("\n") : "Claude execution failed";
1374
+ throw new Error(detail);
1288
1375
  }
1376
+ } finally {
1377
+ stream.close();
1289
1378
  }
1290
1379
  if (!sessionKey) {
1291
- throw new Error("Codex did not return a thread id");
1380
+ throw new Error("Claude Agent SDK did not return a session id");
1292
1381
  }
1293
- const gitInfo = getGitInfo(cwd);
1294
- const responseText = responseParts.filter(Boolean).join("\n\n") || void 0;
1295
- return {
1296
- provider: "codex",
1297
- providerLabel: "Codex",
1298
- sessionKey,
1299
- cwd,
1300
- ...gitInfo,
1301
- title: summarizeTitle(void 0, cwd),
1302
- mode: "managed",
1303
- status: "waiting",
1304
- supportsInboundMessages: true,
1305
- responseText,
1306
- launchCommand
1307
- };
1308
- }
1309
- function parseClaudeRunResult(stdout, cwd, launchCommand) {
1310
- const payload = stdout.split("\n").map((line) => tryParseJson(line)).filter(Boolean).pop();
1311
- if (!payload) {
1312
- throw new Error("Claude did not return JSON output");
1313
- }
1314
- const sessionKey = asString(payload.session_id);
1315
- if (!sessionKey) {
1316
- throw new Error("Claude did not return a session id");
1317
- }
1318
- const gitInfo = getGitInfo(cwd);
1319
- const model = firstObjectKey(payload.modelUsage);
1382
+ const gitInfo = getGitInfo(args.cwd);
1320
1383
  return {
1321
1384
  provider: "claude_code",
1322
1385
  providerLabel: "Claude",
1323
1386
  sessionKey,
1324
- cwd,
1387
+ cwd: args.cwd,
1325
1388
  ...gitInfo,
1326
- title: summarizeTitle(void 0, cwd),
1389
+ title: summarizeTitle(void 0, args.cwd),
1327
1390
  model,
1328
1391
  mode: "managed",
1329
1392
  status: "waiting",
1330
1393
  supportsInboundMessages: true,
1331
- responseText: asString(payload.result) ?? void 0,
1332
- launchCommand
1394
+ responseText: responseText.trim() || void 0,
1395
+ launchCommand: args.launchCommand
1333
1396
  };
1334
1397
  }
1335
- function listRecentJsonlFiles(root) {
1336
- if (!existsSync(root)) {
1337
- return [];
1338
- }
1339
- const files = collectJsonlFiles(root);
1340
- return files.map((path3) => ({ path: path3, mtimeMs: statSync(path3).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_DISCOVERED_FILES_PER_PROVIDER).map((file) => file.path);
1398
+ function discoverCodexSessions() {
1399
+ const historyBySession = buildCodexHistoryIndex();
1400
+ return listLiveProcessIds("codex").flatMap((pid) => {
1401
+ const transcriptPath = getCodexTranscriptPath(pid);
1402
+ if (!transcriptPath) {
1403
+ return [];
1404
+ }
1405
+ const processCwd = getProcessCwd(pid);
1406
+ const parsed = parseObservedCodexSession(
1407
+ pid,
1408
+ transcriptPath,
1409
+ processCwd,
1410
+ historyBySession
1411
+ );
1412
+ return parsed ? [parsed] : [];
1413
+ }).sort(compareObservedSessions);
1414
+ }
1415
+ function discoverClaudeSessions() {
1416
+ const historyBySession = buildClaudeHistoryIndex();
1417
+ return listLiveProcessIds("claude").flatMap((pid) => {
1418
+ const sessionMeta = readClaudePidSession(pid);
1419
+ if (!sessionMeta?.sessionId) {
1420
+ return [];
1421
+ }
1422
+ const transcriptPath = findClaudeTranscriptPath(sessionMeta.sessionId);
1423
+ const parsed = parseObservedClaudeSession(
1424
+ pid,
1425
+ sessionMeta,
1426
+ transcriptPath,
1427
+ historyBySession
1428
+ );
1429
+ return parsed ? [parsed] : [];
1430
+ }).sort(compareObservedSessions);
1341
1431
  }
1342
- function getCodexSessionsDir() {
1343
- return join(getRealHomeDir(), ".codex", "sessions");
1432
+ function getCodexHistoryFile() {
1433
+ return join(getRealHomeDir(), ".codex", "history.jsonl");
1344
1434
  }
1345
- function getClaudeSessionsDir() {
1435
+ function getClaudeProjectsDir() {
1346
1436
  return join(getRealHomeDir(), ".claude", "projects");
1347
1437
  }
1438
+ function getClaudeSessionStateDir() {
1439
+ return join(getRealHomeDir(), ".claude", "sessions");
1440
+ }
1441
+ function getClaudeHistoryFile() {
1442
+ return join(getRealHomeDir(), ".claude", "history.jsonl");
1443
+ }
1348
1444
  function getRealHomeDir() {
1349
1445
  try {
1350
1446
  const realHome = userInfo().homedir?.trim();
@@ -1355,22 +1451,113 @@ function getRealHomeDir() {
1355
1451
  }
1356
1452
  return homedir2();
1357
1453
  }
1358
- function collectJsonlFiles(root) {
1359
- const files = [];
1454
+ function resolveExecutable(fallbackCommand, absoluteCandidates) {
1455
+ for (const candidate of absoluteCandidates) {
1456
+ if (existsSync(candidate)) {
1457
+ return candidate;
1458
+ }
1459
+ }
1460
+ try {
1461
+ const output = execSync(`command -v ${fallbackCommand}`, {
1462
+ encoding: "utf-8",
1463
+ timeout: 1e3
1464
+ }).trim();
1465
+ return output || void 0;
1466
+ } catch {
1467
+ return void 0;
1468
+ }
1469
+ }
1470
+ function listLiveProcessIds(commandName) {
1471
+ try {
1472
+ const output = execSync("ps -axo pid=,comm=", {
1473
+ encoding: "utf-8",
1474
+ timeout: 3e3
1475
+ });
1476
+ return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(/\s+/, 2)).filter(([, command]) => command === commandName).map(([pid]) => pid).filter(Boolean);
1477
+ } catch {
1478
+ return [];
1479
+ }
1480
+ }
1481
+ function getProcessCwd(pid) {
1482
+ const lsofCommand = resolveExecutable("lsof", LSOF_PATHS);
1483
+ if (!lsofCommand) {
1484
+ return void 0;
1485
+ }
1486
+ try {
1487
+ const output = execSync(`${lsofCommand} -a -p ${pid} -Fn -d cwd`, {
1488
+ encoding: "utf-8",
1489
+ timeout: 3e3
1490
+ });
1491
+ return output.split("\n").map((line) => line.trim()).find((line) => line.startsWith("n"))?.slice(1);
1492
+ } catch {
1493
+ return void 0;
1494
+ }
1495
+ }
1496
+ function getCodexTranscriptPath(pid) {
1497
+ const lsofCommand = resolveExecutable("lsof", LSOF_PATHS);
1498
+ if (!lsofCommand) {
1499
+ return void 0;
1500
+ }
1501
+ try {
1502
+ const output = execSync(`${lsofCommand} -p ${pid} -Fn`, {
1503
+ encoding: "utf-8",
1504
+ timeout: 3e3
1505
+ });
1506
+ return output.split("\n").map((line) => line.trim()).find(
1507
+ (line) => line.startsWith("n") && line.includes("/.codex/sessions/") && line.endsWith(".jsonl")
1508
+ )?.slice(1);
1509
+ } catch {
1510
+ return void 0;
1511
+ }
1512
+ }
1513
+ function readClaudePidSession(pid) {
1514
+ const path3 = join(getClaudeSessionStateDir(), `${pid}.json`);
1515
+ if (!existsSync(path3)) {
1516
+ return null;
1517
+ }
1518
+ try {
1519
+ const payload = JSON.parse(readFileSync(path3, "utf-8"));
1520
+ const sessionId = asString(payload.sessionId);
1521
+ if (!sessionId) {
1522
+ return null;
1523
+ }
1524
+ return {
1525
+ sessionId,
1526
+ cwd: asString(payload.cwd),
1527
+ startedAt: typeof payload.startedAt === "number" ? payload.startedAt : void 0
1528
+ };
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+ function findClaudeTranscriptPath(sessionId) {
1534
+ return findJsonlFileByStem(getClaudeProjectsDir(), sessionId);
1535
+ }
1536
+ function findJsonlFileByStem(root, stem) {
1537
+ if (!existsSync(root)) {
1538
+ return void 0;
1539
+ }
1360
1540
  for (const entry of readdirSync(root, { withFileTypes: true })) {
1361
1541
  const path3 = join(root, entry.name);
1362
1542
  if (entry.isDirectory()) {
1363
- files.push(...collectJsonlFiles(path3));
1543
+ const nested = findJsonlFileByStem(path3, stem);
1544
+ if (nested) {
1545
+ return nested;
1546
+ }
1364
1547
  continue;
1365
1548
  }
1366
- if (entry.isFile() && entry.name.endsWith(".jsonl")) {
1367
- files.push(path3);
1549
+ if (entry.isFile() && entry.name === `${stem}.jsonl`) {
1550
+ return path3;
1368
1551
  }
1369
1552
  }
1370
- return files;
1553
+ return void 0;
1371
1554
  }
1372
1555
  function readJsonLines(path3) {
1373
- return readFileSync(path3, "utf-8").split("\n").map((line) => line.trim()).filter(Boolean).map(tryParseJson).filter(Boolean);
1556
+ try {
1557
+ return readFileSync(path3, "utf-8").split("\n").map((line) => line.trim()).filter(Boolean).map(tryParseJson).filter(Boolean);
1558
+ } catch {
1559
+ return [];
1560
+ }
1374
1561
  }
1375
1562
  function tryParseJson(value) {
1376
1563
  try {
@@ -1382,7 +1569,7 @@ function tryParseJson(value) {
1382
1569
  function dedupeSessions(sessions) {
1383
1570
  const seen = /* @__PURE__ */ new Set();
1384
1571
  return sessions.filter((session) => {
1385
- const key = `${session.provider}:${session.sessionKey}`;
1572
+ const key = `${session.provider}:${session.localProcessId ?? session.sessionKey}`;
1386
1573
  if (seen.has(key)) {
1387
1574
  return false;
1388
1575
  }
@@ -1390,37 +1577,113 @@ function dedupeSessions(sessions) {
1390
1577
  return true;
1391
1578
  });
1392
1579
  }
1393
- function extractCodexResponseText(content) {
1394
- if (!Array.isArray(content)) {
1395
- return void 0;
1396
- }
1397
- const texts = content.map(
1398
- (item) => item && typeof item === "object" && "text" in item ? asString(item.text) : void 0
1399
- ).filter(Boolean);
1400
- return texts.length > 0 ? texts.join("\n\n") : void 0;
1580
+ function compareObservedSessions(a, b) {
1581
+ return Number(b.localProcessId ?? 0) - Number(a.localProcessId ?? 0);
1401
1582
  }
1402
- function extractClaudeUserText(message) {
1403
- if (!message || typeof message !== "object") {
1404
- return void 0;
1583
+ function parseObservedCodexSession(pid, transcriptPath, processCwd, historyBySession) {
1584
+ const entries = readJsonLines(transcriptPath);
1585
+ let sessionKey;
1586
+ let cwd = processCwd;
1587
+ const userMessages = [];
1588
+ const assistantMessages = [];
1589
+ for (const rawEntry of entries) {
1590
+ const entry = asObject(rawEntry);
1591
+ if (!entry) {
1592
+ continue;
1593
+ }
1594
+ if (entry.type === "session_meta") {
1595
+ const payload = asObject(entry.payload);
1596
+ sessionKey = asString(payload?.id) ?? sessionKey;
1597
+ cwd = asString(payload?.cwd) ?? cwd;
1598
+ }
1599
+ if (entry.type === "event_msg") {
1600
+ const payload = asObject(entry.payload);
1601
+ if (payload?.type === "user_message") {
1602
+ pushIfPresent(userMessages, payload.message);
1603
+ }
1604
+ }
1605
+ if (entry.type === "response_item" && asObject(entry.payload)?.type === "message" && asObject(entry.payload)?.role === "user") {
1606
+ userMessages.push(
1607
+ ...extractTextSegments(asObject(entry.payload)?.content)
1608
+ );
1609
+ }
1610
+ if (entry.type === "event_msg") {
1611
+ const payload = asObject(entry.payload);
1612
+ if (payload?.type === "agent_message") {
1613
+ pushIfPresent(assistantMessages, payload.message);
1614
+ }
1615
+ }
1616
+ if (entry.type === "response_item" && asObject(entry.payload)?.type === "message" && asObject(entry.payload)?.role === "assistant") {
1617
+ assistantMessages.push(
1618
+ ...extractTextSegments(asObject(entry.payload)?.content)
1619
+ );
1620
+ }
1405
1621
  }
1406
- const content = message.content;
1407
- if (typeof content === "string") {
1408
- return content;
1622
+ if (!sessionKey) {
1623
+ return null;
1409
1624
  }
1410
- return void 0;
1625
+ const gitInfo = cwd ? getGitInfo(cwd) : {};
1626
+ const historyTitle = sessionKey ? selectSessionTitle(historyBySession?.get(sessionKey) ?? []) : void 0;
1627
+ return {
1628
+ provider: "codex",
1629
+ providerLabel: "Codex",
1630
+ localProcessId: pid,
1631
+ sessionKey,
1632
+ cwd,
1633
+ ...gitInfo,
1634
+ title: summarizeTitle(
1635
+ historyTitle ?? selectSessionTitle(userMessages) ?? selectSessionTitle(assistantMessages),
1636
+ cwd
1637
+ ),
1638
+ mode: "observed",
1639
+ status: "observed",
1640
+ supportsInboundMessages: true
1641
+ };
1411
1642
  }
1412
- function extractClaudeAssistantText(message) {
1413
- if (!message || typeof message !== "object") {
1414
- return void 0;
1415
- }
1416
- const content = message.content;
1417
- if (!Array.isArray(content)) {
1418
- return void 0;
1643
+ function parseObservedClaudeSession(pid, sessionMeta, transcriptPath, historyBySession) {
1644
+ const entries = transcriptPath ? readJsonLines(transcriptPath) : [];
1645
+ let cwd = sessionMeta.cwd;
1646
+ let branch;
1647
+ let model;
1648
+ const userMessages = [];
1649
+ const assistantMessages = [];
1650
+ for (const rawEntry of entries) {
1651
+ const entry = asObject(rawEntry);
1652
+ if (!entry) {
1653
+ continue;
1654
+ }
1655
+ cwd = asString(entry.cwd) ?? cwd;
1656
+ branch = asString(entry.gitBranch) ?? branch;
1657
+ if (entry.type === "user") {
1658
+ userMessages.push(...extractClaudeMessageTexts(entry.message));
1659
+ }
1660
+ if (entry.type === "assistant") {
1661
+ const message = asObject(entry.message);
1662
+ model = asString(message?.model) ?? model;
1663
+ assistantMessages.push(...extractClaudeMessageTexts(entry.message));
1664
+ }
1419
1665
  }
1420
- const texts = content.map(
1421
- (item) => item && typeof item === "object" && "text" in item ? asString(item.text) : void 0
1422
- ).filter(Boolean);
1423
- return texts.length > 0 ? texts.join("\n\n") : void 0;
1666
+ const gitInfo = cwd ? getGitInfo(cwd) : {};
1667
+ const historyTitle = selectSessionTitle(
1668
+ historyBySession?.get(sessionMeta.sessionId) ?? []
1669
+ );
1670
+ return {
1671
+ provider: "claude_code",
1672
+ providerLabel: "Claude",
1673
+ localProcessId: pid,
1674
+ sessionKey: sessionMeta.sessionId,
1675
+ cwd,
1676
+ repoRoot: gitInfo.repoRoot,
1677
+ branch: branch ?? gitInfo.branch,
1678
+ title: summarizeTitle(
1679
+ historyTitle ?? selectSessionTitle(userMessages) ?? selectSessionTitle(assistantMessages),
1680
+ cwd
1681
+ ),
1682
+ model,
1683
+ mode: "observed",
1684
+ status: "observed",
1685
+ supportsInboundMessages: true
1686
+ };
1424
1687
  }
1425
1688
  function summarizeTitle(message, cwd) {
1426
1689
  if (message) {
@@ -1439,11 +1702,176 @@ function firstObjectKey(value) {
1439
1702
  return void 0;
1440
1703
  }
1441
1704
  const [firstKey] = Object.keys(value);
1442
- return firstKey;
1705
+ return firstKey ? normalizeModelKey(firstKey) : void 0;
1706
+ }
1707
+ function normalizeModelKey(value) {
1708
+ const normalized = stripAnsi(value).replace(/\[\d+(?:;\d+)*m$/g, "").trim();
1709
+ return normalized || void 0;
1710
+ }
1711
+ function asObject(value) {
1712
+ return value && typeof value === "object" ? value : void 0;
1443
1713
  }
1444
1714
  function asString(value) {
1445
1715
  return typeof value === "string" && value.trim() ? value : void 0;
1446
1716
  }
1717
+ function pushIfPresent(target, value) {
1718
+ const text2 = asString(value);
1719
+ if (text2) {
1720
+ target.push(text2);
1721
+ }
1722
+ }
1723
+ function extractClaudeMessageTexts(message) {
1724
+ if (!message || typeof message !== "object") {
1725
+ return [];
1726
+ }
1727
+ return extractTextSegments(message.content);
1728
+ }
1729
+ function extractTextSegments(value) {
1730
+ if (typeof value === "string") {
1731
+ return [value];
1732
+ }
1733
+ if (!Array.isArray(value)) {
1734
+ return [];
1735
+ }
1736
+ return value.flatMap(extractTextSegmentFromBlock).filter(Boolean);
1737
+ }
1738
+ function extractTextSegmentFromBlock(block) {
1739
+ if (!block || typeof block !== "object") {
1740
+ return [];
1741
+ }
1742
+ const typedBlock = block;
1743
+ const blockType = asString(typedBlock.type);
1744
+ if (blockType && isIgnoredContentBlockType(blockType)) {
1745
+ return [];
1746
+ }
1747
+ const directText = asString(typedBlock.text);
1748
+ if (directText) {
1749
+ return [directText];
1750
+ }
1751
+ if (typeof typedBlock.content === "string") {
1752
+ return [typedBlock.content];
1753
+ }
1754
+ return [];
1755
+ }
1756
+ function isIgnoredContentBlockType(blockType) {
1757
+ return [
1758
+ "tool_result",
1759
+ "tool_use",
1760
+ "image",
1761
+ "thinking",
1762
+ "reasoning",
1763
+ "contextCompaction"
1764
+ ].includes(blockType);
1765
+ }
1766
+ function buildCodexHistoryIndex() {
1767
+ const historyBySession = /* @__PURE__ */ new Map();
1768
+ for (const rawEntry of readJsonLines(getCodexHistoryFile())) {
1769
+ const entry = asObject(rawEntry);
1770
+ if (!entry) {
1771
+ continue;
1772
+ }
1773
+ const sessionId = asString(entry.session_id);
1774
+ const text2 = asString(entry.text);
1775
+ if (!sessionId || !text2) {
1776
+ continue;
1777
+ }
1778
+ appendHistoryEntry(historyBySession, sessionId, text2);
1779
+ }
1780
+ return historyBySession;
1781
+ }
1782
+ function buildClaudeHistoryIndex() {
1783
+ const historyBySession = /* @__PURE__ */ new Map();
1784
+ for (const rawEntry of readJsonLines(getClaudeHistoryFile())) {
1785
+ const entry = asObject(rawEntry);
1786
+ if (!entry) {
1787
+ continue;
1788
+ }
1789
+ const sessionId = asString(entry.sessionId);
1790
+ if (!sessionId) {
1791
+ continue;
1792
+ }
1793
+ const texts = extractClaudeHistoryTexts(entry);
1794
+ for (const text2 of texts) {
1795
+ appendHistoryEntry(historyBySession, sessionId, text2);
1796
+ }
1797
+ }
1798
+ return historyBySession;
1799
+ }
1800
+ function appendHistoryEntry(historyBySession, sessionId, text2) {
1801
+ const existing = historyBySession.get(sessionId);
1802
+ if (existing) {
1803
+ existing.push(text2);
1804
+ return;
1805
+ }
1806
+ historyBySession.set(sessionId, [text2]);
1807
+ }
1808
+ function extractClaudeHistoryTexts(entry) {
1809
+ if (!entry || typeof entry !== "object") {
1810
+ return [];
1811
+ }
1812
+ const record = entry;
1813
+ const pastedTexts = extractClaudePastedTexts(record.pastedContents);
1814
+ if (pastedTexts.length > 0) {
1815
+ return pastedTexts;
1816
+ }
1817
+ const display = asString(record.display);
1818
+ return display ? [display] : [];
1819
+ }
1820
+ function extractClaudePastedTexts(value) {
1821
+ if (!value || typeof value !== "object") {
1822
+ return [];
1823
+ }
1824
+ return Object.values(value).flatMap((item) => {
1825
+ if (!item || typeof item !== "object") {
1826
+ return [];
1827
+ }
1828
+ const record = item;
1829
+ return record.type === "text" && typeof record.content === "string" ? [record.content] : [];
1830
+ }).filter(Boolean);
1831
+ }
1832
+ function selectSessionTitle(messages) {
1833
+ for (const message of messages) {
1834
+ const cleaned = cleanSessionTitleCandidate(message);
1835
+ if (cleaned) {
1836
+ return cleaned;
1837
+ }
1838
+ }
1839
+ return void 0;
1840
+ }
1841
+ function cleanSessionTitleCandidate(message) {
1842
+ const normalized = stripAnsi(message).replace(/\s+/g, " ").trim();
1843
+ if (!normalized) {
1844
+ return void 0;
1845
+ }
1846
+ if (normalized.length < 4) {
1847
+ return void 0;
1848
+ }
1849
+ if (normalized.startsWith("/") || looksLikeGeneratedTagEnvelope(normalized) || looksLikeGeneratedImageSummary(normalized) || looksLikeStandaloneImagePath(normalized) || looksLikeInstructionScaffold(normalized)) {
1850
+ return void 0;
1851
+ }
1852
+ return normalized;
1853
+ }
1854
+ function looksLikeGeneratedTagEnvelope(value) {
1855
+ return /^<[\w:-]+>[\s\S]*<\/[\w:-]+>$/.test(value);
1856
+ }
1857
+ function looksLikeGeneratedImageSummary(value) {
1858
+ return /^\[image:/i.test(value) || /displayed at/i.test(value) && /coordinates/i.test(value);
1859
+ }
1860
+ function looksLikeStandaloneImagePath(value) {
1861
+ return /^\/\S+\.(png|jpe?g|gif|webp|heic|bmp)$/i.test(value) || /^file:\S+\.(png|jpe?g|gif|webp|heic|bmp)$/i.test(value);
1862
+ }
1863
+ function looksLikeInstructionScaffold(value) {
1864
+ if (value.length < 700) {
1865
+ return false;
1866
+ }
1867
+ const headingCount = value.match(/^#{1,3}\s/gm)?.length ?? 0;
1868
+ const tagCount = value.match(/<\/?[\w:-]+>/g)?.length ?? 0;
1869
+ const bulletCount = value.match(/^\s*[-*]\s/gm)?.length ?? 0;
1870
+ return headingCount + tagCount + bulletCount >= 6;
1871
+ }
1872
+ function stripAnsi(value) {
1873
+ return value.replace(/\u001B\[[0-9;]*m/g, "");
1874
+ }
1447
1875
  function getGitInfo(cwd) {
1448
1876
  try {
1449
1877
  const repoRoot = execSync("git rev-parse --show-toplevel", {
@@ -1481,6 +1909,7 @@ var LEGACY_MENUBAR_LAUNCHAGENT_PLIST = join2(
1481
1909
  );
1482
1910
  var HEARTBEAT_INTERVAL_MS = 3e4;
1483
1911
  var COMMAND_POLL_INTERVAL_MS = 5e3;
1912
+ var LIVE_ACTIVITY_SYNC_INTERVAL_MS = 5e3;
1484
1913
  var PROCESS_DISCOVERY_INTERVAL_MS = 6e4;
1485
1914
  function loadBridgeConfig() {
1486
1915
  if (!existsSync2(BRIDGE_CONFIG_FILE)) return null;
@@ -1561,12 +1990,26 @@ var BridgeService = class {
1561
1990
  }
1562
1991
  async reportProcesses() {
1563
1992
  const processes = discoverAttachableSessions();
1993
+ const activeSessionKeys = processes.map((proc) => proc.sessionKey).filter((value) => Boolean(value));
1994
+ const activeLocalProcessIds = processes.map((proc) => proc.localProcessId).filter((value) => Boolean(value));
1564
1995
  for (const proc of processes) {
1565
1996
  try {
1566
1997
  await this.reportProcess(proc);
1567
1998
  } catch {
1568
1999
  }
1569
2000
  }
2001
+ try {
2002
+ await this.client.mutation(
2003
+ api.agentBridge.bridgePublic.reconcileObservedProcesses,
2004
+ {
2005
+ deviceId: this.config.deviceId,
2006
+ deviceSecret: this.config.deviceSecret,
2007
+ activeSessionKeys,
2008
+ activeLocalProcessIds
2009
+ }
2010
+ );
2011
+ } catch {
2012
+ }
1570
2013
  if (processes.length > 0) {
1571
2014
  console.log(
1572
2015
  `[${ts()}] Discovered ${processes.length} attachable session(s)`
@@ -1583,9 +2026,87 @@ var BridgeService = class {
1583
2026
  }
1584
2027
  );
1585
2028
  writeLiveActivitiesCache(activities);
2029
+ await this.syncWorkSessionTerminals(activities);
1586
2030
  } catch {
1587
2031
  }
1588
2032
  }
2033
+ async syncWorkSessionTerminals(activities) {
2034
+ for (const activity of activities) {
2035
+ if (!activity.workSessionId || !activity.tmuxPaneId) {
2036
+ continue;
2037
+ }
2038
+ try {
2039
+ await this.refreshWorkSessionTerminal(activity.workSessionId, {
2040
+ tmuxPaneId: activity.tmuxPaneId,
2041
+ cwd: activity.cwd,
2042
+ repoRoot: activity.repoRoot,
2043
+ branch: activity.branch,
2044
+ agentProvider: activity.agentProvider,
2045
+ agentSessionKey: activity.agentSessionKey
2046
+ });
2047
+ await this.verifyManagedWorkSession(activity);
2048
+ } catch {
2049
+ }
2050
+ }
2051
+ }
2052
+ async verifyManagedWorkSession(activity) {
2053
+ if (!activity.workSessionId || !activity.tmuxPaneId || !activity.agentProvider || !isBridgeProvider(activity.agentProvider) || activity.agentProcessId) {
2054
+ return;
2055
+ }
2056
+ const workspacePath = activity.workspacePath ?? activity.cwd ?? activity.repoRoot;
2057
+ if (!workspacePath) {
2058
+ return;
2059
+ }
2060
+ const attachedSession = await this.attachObservedAgentSession(
2061
+ activity.agentProvider,
2062
+ workspacePath
2063
+ );
2064
+ if (!attachedSession) {
2065
+ return;
2066
+ }
2067
+ await this.refreshWorkSessionTerminal(activity.workSessionId, {
2068
+ tmuxPaneId: activity.tmuxPaneId,
2069
+ cwd: attachedSession.process.cwd ?? activity.cwd ?? workspacePath,
2070
+ repoRoot: attachedSession.process.repoRoot ?? activity.repoRoot ?? workspacePath,
2071
+ branch: attachedSession.process.branch ?? activity.branch,
2072
+ agentProvider: attachedSession.process.provider,
2073
+ agentSessionKey: attachedSession.process.sessionKey
2074
+ });
2075
+ await this.postAgentMessage(
2076
+ activity._id,
2077
+ "status",
2078
+ `Verified ${providerLabel(attachedSession.process.provider)} in ${activity.tmuxPaneId}`
2079
+ );
2080
+ await this.updateLiveActivity(activity._id, {
2081
+ status: "waiting_for_input",
2082
+ latestSummary: `Verified ${providerLabel(attachedSession.process.provider)} in ${activity.tmuxPaneId}`,
2083
+ processId: attachedSession.processId,
2084
+ title: activity.title
2085
+ });
2086
+ }
2087
+ async refreshWorkSessionTerminal(workSessionId, metadata) {
2088
+ if (!workSessionId || !metadata.tmuxPaneId) {
2089
+ return;
2090
+ }
2091
+ const terminalSnapshot = captureTmuxPane(metadata.tmuxPaneId);
2092
+ await this.client.mutation(
2093
+ api.agentBridge.bridgePublic.updateWorkSessionTerminal,
2094
+ {
2095
+ deviceId: this.config.deviceId,
2096
+ deviceSecret: this.config.deviceSecret,
2097
+ workSessionId,
2098
+ terminalSnapshot,
2099
+ tmuxSessionName: metadata.tmuxSessionName,
2100
+ tmuxWindowName: metadata.tmuxWindowName,
2101
+ tmuxPaneId: metadata.tmuxPaneId,
2102
+ cwd: metadata.cwd,
2103
+ repoRoot: metadata.repoRoot,
2104
+ branch: metadata.branch,
2105
+ agentProvider: metadata.agentProvider,
2106
+ agentSessionKey: metadata.agentSessionKey
2107
+ }
2108
+ );
2109
+ }
1589
2110
  async run() {
1590
2111
  console.log("Vector Bridge Service");
1591
2112
  console.log(
@@ -1606,8 +2127,6 @@ var BridgeService = class {
1606
2127
  this.heartbeat().catch(
1607
2128
  (e) => console.error(`[${ts()}] Heartbeat error:`, e.message)
1608
2129
  );
1609
- this.refreshLiveActivities().catch(() => {
1610
- });
1611
2130
  }, HEARTBEAT_INTERVAL_MS)
1612
2131
  );
1613
2132
  this.timers.push(
@@ -1617,6 +2136,13 @@ var BridgeService = class {
1617
2136
  );
1618
2137
  }, COMMAND_POLL_INTERVAL_MS)
1619
2138
  );
2139
+ this.timers.push(
2140
+ setInterval(() => {
2141
+ this.refreshLiveActivities().catch(
2142
+ (e) => console.error(`[${ts()}] Live activity sync error:`, e.message)
2143
+ );
2144
+ }, LIVE_ACTIVITY_SYNC_INTERVAL_MS)
2145
+ );
1620
2146
  this.timers.push(
1621
2147
  setInterval(() => {
1622
2148
  this.reportProcesses().catch(
@@ -1653,10 +2179,39 @@ var BridgeService = class {
1653
2179
  throw new Error("Message command is missing a body");
1654
2180
  }
1655
2181
  const process9 = cmd.process;
2182
+ console.log(` > "${truncateForLog(body)}"`);
2183
+ if (cmd.workSession?.tmuxPaneId) {
2184
+ sendTextToTmuxPane(cmd.workSession.tmuxPaneId, body);
2185
+ const attachedSession = cmd.workSession.agentProvider && isBridgeProvider(cmd.workSession.agentProvider) ? await this.attachObservedAgentSession(
2186
+ cmd.workSession.agentProvider,
2187
+ cmd.workSession.workspacePath ?? cmd.workSession.cwd ?? process9?.cwd
2188
+ ) : null;
2189
+ await this.postAgentMessage(
2190
+ cmd.liveActivityId,
2191
+ "status",
2192
+ "Sent input to work session terminal"
2193
+ );
2194
+ await this.refreshWorkSessionTerminal(cmd.workSession._id, {
2195
+ tmuxSessionName: cmd.workSession.tmuxSessionName,
2196
+ tmuxWindowName: cmd.workSession.tmuxWindowName,
2197
+ tmuxPaneId: cmd.workSession.tmuxPaneId,
2198
+ cwd: cmd.workSession.cwd,
2199
+ repoRoot: cmd.workSession.repoRoot,
2200
+ branch: cmd.workSession.branch,
2201
+ agentProvider: attachedSession?.process.provider ?? cmd.workSession.agentProvider,
2202
+ agentSessionKey: attachedSession?.process.sessionKey ?? cmd.workSession.agentSessionKey
2203
+ });
2204
+ await this.updateLiveActivity(cmd.liveActivityId, {
2205
+ status: "waiting_for_input",
2206
+ latestSummary: `Input sent to ${cmd.workSession.tmuxPaneId}`,
2207
+ title: cmd.liveActivity?.title,
2208
+ processId: attachedSession?.processId ?? process9?._id
2209
+ });
2210
+ return;
2211
+ }
1656
2212
  if (!process9 || !process9.supportsInboundMessages || !process9.sessionKey || !process9.cwd || !isBridgeProvider(process9.provider)) {
1657
2213
  throw new Error("No resumable local session is attached to this issue");
1658
2214
  }
1659
- console.log(` > "${truncateForLog(body)}"`);
1660
2215
  await this.reportProcess({
1661
2216
  provider: process9.provider,
1662
2217
  providerLabel: process9.providerLabel ?? providerLabel(process9.provider),
@@ -1706,46 +2261,98 @@ var BridgeService = class {
1706
2261
  if (!workspacePath) {
1707
2262
  throw new Error("Launch command is missing workspacePath");
1708
2263
  }
1709
- if (!payload?.provider || !isBridgeProvider(payload.provider)) {
1710
- throw new Error("Launch command is missing a supported provider");
1711
- }
1712
- const provider = payload.provider;
1713
- const issueKey = payload.issueKey ?? cmd.liveActivity?.issueKey ?? "ISSUE";
1714
- const issueTitle = payload.issueTitle ?? cmd.liveActivity?.issueTitle ?? "Untitled issue";
2264
+ const requestedProvider = payload?.provider;
2265
+ const provider = requestedProvider && isBridgeProvider(requestedProvider) ? requestedProvider : void 0;
2266
+ const issueKey = payload?.issueKey ?? cmd.liveActivity?.issueKey ?? "ISSUE";
2267
+ const issueTitle = payload?.issueTitle ?? cmd.liveActivity?.issueTitle ?? "Untitled issue";
1715
2268
  const prompt2 = buildLaunchPrompt(issueKey, issueTitle, workspacePath);
2269
+ const launchLabel = provider ? providerLabel(provider) : "shell session";
2270
+ const workSessionTitle = `${issueKey}: ${issueTitle}`;
2271
+ const sessionsBeforeLaunch = provider ? listObservedSessionsForWorkspace(provider, workspacePath) : [];
1716
2272
  await this.updateLiveActivity(cmd.liveActivityId, {
1717
2273
  status: "active",
1718
- latestSummary: `Launching ${providerLabel(provider)} in ${workspacePath}`,
1719
- delegatedRunId: payload.delegatedRunId,
2274
+ latestSummary: `Launching ${launchLabel} in ${workspacePath}`,
2275
+ delegatedRunId: payload?.delegatedRunId,
1720
2276
  launchStatus: "launching",
1721
- title: `${providerLabel(provider)} on ${this.config.displayName}`
2277
+ title: workSessionTitle
1722
2278
  });
1723
2279
  await this.postAgentMessage(
1724
2280
  cmd.liveActivityId,
1725
2281
  "status",
1726
- `Launching ${providerLabel(provider)} in ${workspacePath}`
2282
+ `Launching ${launchLabel} in ${workspacePath}`
1727
2283
  );
1728
- const result = await launchProviderSession(provider, workspacePath, prompt2);
1729
- const processId = await this.reportProcess({
1730
- ...result,
1731
- title: `${issueKey}: ${issueTitle}`
2284
+ const tmuxSession = createTmuxWorkSession({
2285
+ workspacePath,
2286
+ issueKey,
2287
+ issueTitle,
2288
+ provider,
2289
+ prompt: prompt2
1732
2290
  });
2291
+ const attachedSession = provider ? await this.attachObservedAgentSession(
2292
+ provider,
2293
+ workspacePath,
2294
+ sessionsBeforeLaunch,
2295
+ tmuxSession.paneProcessId
2296
+ ) : null;
2297
+ await this.refreshWorkSessionTerminal(cmd.workSession?._id, {
2298
+ tmuxSessionName: tmuxSession.sessionName,
2299
+ tmuxWindowName: tmuxSession.windowName,
2300
+ tmuxPaneId: tmuxSession.paneId,
2301
+ cwd: workspacePath,
2302
+ repoRoot: workspacePath,
2303
+ branch: currentGitBranch(workspacePath),
2304
+ agentProvider: provider,
2305
+ agentSessionKey: attachedSession?.process.sessionKey
2306
+ });
2307
+ if (provider && !attachedSession) {
2308
+ await this.postAgentMessage(
2309
+ cmd.liveActivityId,
2310
+ "status",
2311
+ `Started tmux session ${tmuxSession.sessionName}:${tmuxSession.windowName}. Waiting to verify ${providerLabel(provider)} in ${tmuxSession.paneId}.`
2312
+ );
2313
+ await this.updateLiveActivity(cmd.liveActivityId, {
2314
+ status: "waiting_for_input",
2315
+ latestSummary: `Running in ${tmuxSession.sessionName}:${tmuxSession.windowName}; waiting to verify ${providerLabel(provider)}`,
2316
+ delegatedRunId: payload?.delegatedRunId,
2317
+ launchStatus: "running",
2318
+ title: `${providerLabel(provider)} on ${this.config.displayName}`
2319
+ });
2320
+ return;
2321
+ }
1733
2322
  await this.updateLiveActivity(cmd.liveActivityId, {
1734
- processId,
1735
2323
  status: "waiting_for_input",
1736
- latestSummary: summarizeMessage(result.responseText),
1737
- delegatedRunId: payload.delegatedRunId,
2324
+ latestSummary: `Running in ${tmuxSession.sessionName}:${tmuxSession.windowName}`,
2325
+ delegatedRunId: payload?.delegatedRunId,
1738
2326
  launchStatus: "running",
1739
- title: `${providerLabel(provider)} on ${this.config.displayName}`
2327
+ processId: attachedSession?.processId,
2328
+ title: workSessionTitle
1740
2329
  });
1741
- if (result.responseText) {
1742
- await this.postAgentMessage(
1743
- cmd.liveActivityId,
1744
- "assistant",
1745
- result.responseText
2330
+ }
2331
+ async attachObservedAgentSession(provider, workspacePath, sessionsBeforeLaunch = [], paneProcessId) {
2332
+ if (!workspacePath) {
2333
+ return null;
2334
+ }
2335
+ const existingKeys = new Set(
2336
+ sessionsBeforeLaunch.map(sessionIdentityKey).filter(Boolean)
2337
+ );
2338
+ for (let attempt = 0; attempt < 10; attempt += 1) {
2339
+ const observedSessions = listObservedSessionsForWorkspace(
2340
+ provider,
2341
+ workspacePath
1746
2342
  );
1747
- console.log(` < "${truncateForLog(result.responseText)}"`);
2343
+ const candidate = (paneProcessId ? findObservedSessionInProcessTree(observedSessions, paneProcessId) : void 0) ?? observedSessions.find(
2344
+ (session) => !existingKeys.has(sessionIdentityKey(session))
2345
+ ) ?? (attempt === 9 ? observedSessions[0] : void 0);
2346
+ if (candidate) {
2347
+ const processId = await this.reportProcess(candidate);
2348
+ return {
2349
+ process: candidate,
2350
+ processId
2351
+ };
2352
+ }
2353
+ await sleep(750);
1748
2354
  }
2355
+ return null;
1749
2356
  }
1750
2357
  async reportProcess(process9) {
1751
2358
  const {
@@ -1831,6 +2438,98 @@ var BridgeService = class {
1831
2438
  }
1832
2439
  }
1833
2440
  };
2441
+ function createTmuxWorkSession(args) {
2442
+ const slug = sanitizeTmuxName(args.issueKey.toLowerCase());
2443
+ const sessionName = `vector-${slug}-${randomUUID().slice(0, 8)}`;
2444
+ const windowName = sanitizeTmuxName(
2445
+ args.provider === "codex" ? "codex" : args.provider === "claude_code" ? "claude" : "shell"
2446
+ );
2447
+ execFileSync("tmux", [
2448
+ "new-session",
2449
+ "-d",
2450
+ "-s",
2451
+ sessionName,
2452
+ "-n",
2453
+ windowName,
2454
+ "-c",
2455
+ args.workspacePath
2456
+ ]);
2457
+ const paneId = execFileSync(
2458
+ "tmux",
2459
+ [
2460
+ "display-message",
2461
+ "-p",
2462
+ "-t",
2463
+ `${sessionName}:${windowName}.0`,
2464
+ "#{pane_id}"
2465
+ ],
2466
+ { encoding: "utf-8" }
2467
+ ).trim();
2468
+ const paneProcessId = execFileSync(
2469
+ "tmux",
2470
+ ["display-message", "-p", "-t", paneId, "#{pane_pid}"],
2471
+ { encoding: "utf-8" }
2472
+ ).trim();
2473
+ if (args.provider) {
2474
+ execFileSync("tmux", [
2475
+ "send-keys",
2476
+ "-t",
2477
+ paneId,
2478
+ buildManagedLaunchCommand(args.provider, args.prompt),
2479
+ "Enter"
2480
+ ]);
2481
+ } else {
2482
+ execFileSync("tmux", [
2483
+ "send-keys",
2484
+ "-t",
2485
+ paneId,
2486
+ `printf '%s\\n\\n' ${shellQuote(args.prompt)}`,
2487
+ "Enter"
2488
+ ]);
2489
+ }
2490
+ return {
2491
+ sessionName,
2492
+ windowName,
2493
+ paneId,
2494
+ paneProcessId
2495
+ };
2496
+ }
2497
+ function sendTextToTmuxPane(paneId, text2) {
2498
+ execFileSync("tmux", ["set-buffer", "--", text2]);
2499
+ execFileSync("tmux", ["paste-buffer", "-t", paneId]);
2500
+ execFileSync("tmux", ["send-keys", "-t", paneId, "Enter"]);
2501
+ execFileSync("tmux", ["delete-buffer"]);
2502
+ }
2503
+ function captureTmuxPane(paneId) {
2504
+ return execFileSync(
2505
+ "tmux",
2506
+ ["capture-pane", "-p", "-t", paneId, "-S", "-120"],
2507
+ { encoding: "utf-8" }
2508
+ ).replace(/\u001B\[[0-9;?]*[A-Za-z]/g, "").trimEnd();
2509
+ }
2510
+ function currentGitBranch(cwd) {
2511
+ try {
2512
+ return execSync2("git rev-parse --abbrev-ref HEAD", {
2513
+ encoding: "utf-8",
2514
+ cwd,
2515
+ timeout: 3e3
2516
+ }).trim();
2517
+ } catch {
2518
+ return void 0;
2519
+ }
2520
+ }
2521
+ function buildManagedLaunchCommand(provider, prompt2) {
2522
+ if (provider === "codex") {
2523
+ return `codex --no-alt-screen -a never ${shellQuote(prompt2)}`;
2524
+ }
2525
+ return `claude --permission-mode bypassPermissions --dangerously-skip-permissions ${shellQuote(prompt2)}`;
2526
+ }
2527
+ function sanitizeTmuxName(value) {
2528
+ return value.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "work";
2529
+ }
2530
+ function shellQuote(value) {
2531
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
2532
+ }
1834
2533
  async function setupBridgeDevice(client, convexUrl) {
1835
2534
  const deviceKey = getStableDeviceKey();
1836
2535
  const displayName = `${process.env.USER ?? "user"}'s ${platform() === "darwin" ? "Mac" : "machine"}`;
@@ -1897,6 +2596,78 @@ function summarizeMessage(message) {
1897
2596
  function truncateForLog(message) {
1898
2597
  return message.length > 80 ? `${message.slice(0, 77).trimEnd()}...` : message;
1899
2598
  }
2599
+ function listObservedSessionsForWorkspace(provider, workspacePath) {
2600
+ return discoverAttachableSessions().filter(
2601
+ (session) => session.provider === provider && matchesWorkspacePath(session, workspacePath)
2602
+ ).sort(compareLocalSessionRecency);
2603
+ }
2604
+ function findObservedSessionInProcessTree(sessions, paneProcessId) {
2605
+ const descendantIds = listDescendantProcessIds(paneProcessId);
2606
+ if (descendantIds.size === 0) {
2607
+ return void 0;
2608
+ }
2609
+ return sessions.find(
2610
+ (session) => session.localProcessId ? descendantIds.has(session.localProcessId) : false
2611
+ );
2612
+ }
2613
+ function listDescendantProcessIds(rootPid) {
2614
+ const descendants = /* @__PURE__ */ new Set([rootPid]);
2615
+ try {
2616
+ const output = execSync2("ps -axo pid=,ppid=", {
2617
+ encoding: "utf-8",
2618
+ timeout: 3e3
2619
+ });
2620
+ const parentToChildren = /* @__PURE__ */ new Map();
2621
+ for (const line of output.split("\n").map((value) => value.trim()).filter(Boolean)) {
2622
+ const [pid, ppid] = line.split(/\s+/, 2);
2623
+ if (!pid || !ppid) {
2624
+ continue;
2625
+ }
2626
+ const children = parentToChildren.get(ppid) ?? [];
2627
+ children.push(pid);
2628
+ parentToChildren.set(ppid, children);
2629
+ }
2630
+ const queue = [rootPid];
2631
+ while (queue.length > 0) {
2632
+ const currentPid = queue.shift();
2633
+ if (!currentPid) {
2634
+ continue;
2635
+ }
2636
+ for (const childPid of parentToChildren.get(currentPid) ?? []) {
2637
+ if (descendants.has(childPid)) {
2638
+ continue;
2639
+ }
2640
+ descendants.add(childPid);
2641
+ queue.push(childPid);
2642
+ }
2643
+ }
2644
+ } catch {
2645
+ return descendants;
2646
+ }
2647
+ return descendants;
2648
+ }
2649
+ function matchesWorkspacePath(session, workspacePath) {
2650
+ const normalizedWorkspace = normalizePath(workspacePath);
2651
+ const candidatePaths = [session.cwd, session.repoRoot].filter((value) => Boolean(value)).map(normalizePath);
2652
+ return candidatePaths.some((path3) => path3 === normalizedWorkspace);
2653
+ }
2654
+ function normalizePath(value) {
2655
+ return value.replace(/\/+$/, "");
2656
+ }
2657
+ function sessionIdentityKey(session) {
2658
+ return [
2659
+ session.provider,
2660
+ session.sessionKey,
2661
+ session.localProcessId,
2662
+ session.cwd
2663
+ ].filter(Boolean).join("::");
2664
+ }
2665
+ function compareLocalSessionRecency(a, b) {
2666
+ return Number(b.localProcessId ?? 0) - Number(a.localProcessId ?? 0);
2667
+ }
2668
+ function sleep(ms) {
2669
+ return new Promise((resolve) => setTimeout(resolve, ms));
2670
+ }
1900
2671
  function isBridgeProvider(provider) {
1901
2672
  return provider === "codex" || provider === "claude_code";
1902
2673
  }
@@ -1911,7 +2682,7 @@ function installLaunchAgent(vcliPath) {
1911
2682
  const programArguments = getLaunchAgentProgramArguments(vcliPath);
1912
2683
  const environmentVariables = [
1913
2684
  " <key>PATH</key>",
1914
- " <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>",
2685
+ " <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>",
1915
2686
  ...process.env.VECTOR_HOME?.trim() ? [
1916
2687
  " <key>VECTOR_HOME</key>",
1917
2688
  ` <string>${process.env.VECTOR_HOME.trim()}</string>`
@@ -2466,13 +3237,6 @@ function requireOrg(runtime, explicit) {
2466
3237
  }
2467
3238
  return orgSlug;
2468
3239
  }
2469
- function readJsonFile(path3, fallback) {
2470
- try {
2471
- return JSON.parse(readFileSync3(path3, "utf8"));
2472
- } catch {
2473
- return fallback;
2474
- }
2475
- }
2476
3240
  function hostForAppUrl(appUrl) {
2477
3241
  if (!appUrl) {
2478
3242
  return void 0;
@@ -2525,6 +3289,28 @@ function parseAgentProvider(value) {
2525
3289
  }
2526
3290
  throw new Error("provider must be one of: codex, claude_code, vector_cli");
2527
3291
  }
3292
+ function isBridgeDeviceAuthError(error) {
3293
+ if (!error || typeof error !== "object") {
3294
+ return false;
3295
+ }
3296
+ const maybeData = error.data;
3297
+ return maybeData === "INVALID_DEVICE_SECRET" || maybeData === "DEVICE_NOT_FOUND";
3298
+ }
3299
+ async function validateStoredBridgeConfig(config) {
3300
+ const client = new ConvexHttpClient3(config.convexUrl);
3301
+ try {
3302
+ await client.mutation(api.agentBridge.bridgePublic.heartbeat, {
3303
+ deviceId: config.deviceId,
3304
+ deviceSecret: config.deviceSecret
3305
+ });
3306
+ return true;
3307
+ } catch (error) {
3308
+ if (isBridgeDeviceAuthError(error)) {
3309
+ return false;
3310
+ }
3311
+ throw error;
3312
+ }
3313
+ }
2528
3314
  async function getClient(command) {
2529
3315
  const runtime = await getRuntime(command);
2530
3316
  const session = requireSession(runtime);
@@ -2552,7 +3338,7 @@ async function ensureBridgeConfig(command) {
2552
3338
  const backendDevice = config ? await runQuery(client, api.agentBridge.queries.getDevice, {
2553
3339
  deviceId: config.deviceId
2554
3340
  }) : null;
2555
- const needsRegistration = !config || config.userId !== user._id || config.convexUrl !== runtime.convexUrl || !backendDevice;
3341
+ const needsRegistration = !config || config.userId !== user._id || config.convexUrl !== runtime.convexUrl || !backendDevice || !await validateStoredBridgeConfig(config);
2556
3342
  if (needsRegistration) {
2557
3343
  config = await setupBridgeDevice(client, runtime.convexUrl);
2558
3344
  }
@@ -4361,11 +5147,8 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
4361
5147
  const globalOptions = command.optsWithGlobals();
4362
5148
  const profile = globalOptions.profile ?? "default";
4363
5149
  const session = await readSession(profile);
4364
- const liveActivities = readJsonFile(
4365
- join3(VECTOR_HOME, "live-activities.json"),
4366
- []
4367
- );
4368
- let processes = [];
5150
+ let workSessions = [];
5151
+ let detectedSessions = [];
4369
5152
  try {
4370
5153
  const runtime = await getRuntime(command);
4371
5154
  if (runtime.session && status.config?.deviceId) {
@@ -4374,6 +5157,13 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
4374
5157
  runtime.appUrl,
4375
5158
  runtime.convexUrl
4376
5159
  );
5160
+ workSessions = await runQuery(
5161
+ client,
5162
+ api.agentBridge.queries.listDeviceWorkSessions,
5163
+ {
5164
+ deviceId: status.config.deviceId
5165
+ }
5166
+ );
4377
5167
  const devices = await runQuery(
4378
5168
  client,
4379
5169
  api.agentBridge.queries.listProcessesForAttach,
@@ -4382,10 +5172,11 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
4382
5172
  const currentDevice = devices.find(
4383
5173
  (entry) => entry.device._id === status.config?.deviceId
4384
5174
  );
4385
- processes = currentDevice?.processes ?? [];
5175
+ detectedSessions = currentDevice?.processes ?? [];
4386
5176
  }
4387
5177
  } catch {
4388
- processes = [];
5178
+ workSessions = [];
5179
+ detectedSessions = [];
4389
5180
  }
4390
5181
  printOutput(
4391
5182
  {
@@ -4395,8 +5186,8 @@ serviceCommand.command("menu-state").description("Return JSON state for the macO
4395
5186
  pid: status.pid,
4396
5187
  config: status.config,
4397
5188
  sessionInfo: buildMenuSessionInfo(session),
4398
- liveActivities,
4399
- processes
5189
+ workSessions,
5190
+ detectedSessions
4400
5191
  },
4401
5192
  Boolean(globalOptions.json)
4402
5193
  );