@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 +1052 -261
- package/dist/index.js.map +1 -1
- package/native/VectorMenuBar.app/Contents/MacOS/VectorMenuBar +0 -0
- package/package.json +2 -1
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
|
|
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
|
|
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
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1129
|
+
return runCodexAppServerTurn({
|
|
1130
|
+
cwd,
|
|
1131
|
+
prompt: prompt2,
|
|
1132
|
+
sessionKey,
|
|
1133
|
+
launchCommand: "codex app-server (thread/resume)"
|
|
1134
|
+
});
|
|
1145
1135
|
}
|
|
1146
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
1138
|
+
prompt: prompt2,
|
|
1139
|
+
sessionKey,
|
|
1140
|
+
launchCommand: "@anthropic-ai/claude-agent-sdk query(resume)"
|
|
1141
|
+
});
|
|
1156
1142
|
}
|
|
1157
|
-
async function
|
|
1158
|
-
const child = spawn(
|
|
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: ["
|
|
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
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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 (
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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("
|
|
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:
|
|
1332
|
-
launchCommand
|
|
1394
|
+
responseText: responseText.trim() || void 0,
|
|
1395
|
+
launchCommand: args.launchCommand
|
|
1333
1396
|
};
|
|
1334
1397
|
}
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
|
1343
|
-
return join(getRealHomeDir(), ".codex", "
|
|
1432
|
+
function getCodexHistoryFile() {
|
|
1433
|
+
return join(getRealHomeDir(), ".codex", "history.jsonl");
|
|
1344
1434
|
}
|
|
1345
|
-
function
|
|
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
|
|
1359
|
-
const
|
|
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
|
-
|
|
1543
|
+
const nested = findJsonlFileByStem(path3, stem);
|
|
1544
|
+
if (nested) {
|
|
1545
|
+
return nested;
|
|
1546
|
+
}
|
|
1364
1547
|
continue;
|
|
1365
1548
|
}
|
|
1366
|
-
if (entry.isFile() && entry.name.
|
|
1367
|
-
|
|
1549
|
+
if (entry.isFile() && entry.name === `${stem}.jsonl`) {
|
|
1550
|
+
return path3;
|
|
1368
1551
|
}
|
|
1369
1552
|
}
|
|
1370
|
-
return
|
|
1553
|
+
return void 0;
|
|
1371
1554
|
}
|
|
1372
1555
|
function readJsonLines(path3) {
|
|
1373
|
-
|
|
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
|
|
1394
|
-
|
|
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
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
return content;
|
|
1622
|
+
if (!sessionKey) {
|
|
1623
|
+
return null;
|
|
1409
1624
|
}
|
|
1410
|
-
|
|
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
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
const
|
|
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 ${
|
|
1719
|
-
delegatedRunId: payload
|
|
2274
|
+
latestSummary: `Launching ${launchLabel} in ${workspacePath}`,
|
|
2275
|
+
delegatedRunId: payload?.delegatedRunId,
|
|
1720
2276
|
launchStatus: "launching",
|
|
1721
|
-
title:
|
|
2277
|
+
title: workSessionTitle
|
|
1722
2278
|
});
|
|
1723
2279
|
await this.postAgentMessage(
|
|
1724
2280
|
cmd.liveActivityId,
|
|
1725
2281
|
"status",
|
|
1726
|
-
`Launching ${
|
|
2282
|
+
`Launching ${launchLabel} in ${workspacePath}`
|
|
1727
2283
|
);
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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:
|
|
1737
|
-
delegatedRunId: payload
|
|
2324
|
+
latestSummary: `Running in ${tmuxSession.sessionName}:${tmuxSession.windowName}`,
|
|
2325
|
+
delegatedRunId: payload?.delegatedRunId,
|
|
1738
2326
|
launchStatus: "running",
|
|
1739
|
-
|
|
2327
|
+
processId: attachedSession?.processId,
|
|
2328
|
+
title: workSessionTitle
|
|
1740
2329
|
});
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4365
|
-
|
|
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
|
-
|
|
5175
|
+
detectedSessions = currentDevice?.processes ?? [];
|
|
4386
5176
|
}
|
|
4387
5177
|
} catch {
|
|
4388
|
-
|
|
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
|
-
|
|
4399
|
-
|
|
5189
|
+
workSessions,
|
|
5190
|
+
detectedSessions
|
|
4400
5191
|
},
|
|
4401
5192
|
Boolean(globalOptions.json)
|
|
4402
5193
|
);
|