@rehpic/vcli 0.1.0-beta.10.1 → 0.1.0-beta.101.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/README.md +25 -5
- package/dist/index.js +4634 -53
- package/dist/index.js.map +1 -1
- package/native/VectorMenuBar.app/Contents/Info.plist +26 -0
- package/native/VectorMenuBar.app/Contents/MacOS/VectorMenuBar +0 -0
- package/native/VectorMenuBar.app/Contents/Resources/vector-menubar.png +0 -0
- package/native/VectorMenuBar.app/Contents/Resources/vector-menubar@2x.png +0 -0
- package/native/VectorMenuBar.app/Contents/_CodeSignature/CodeResources +139 -0
- package/package.json +16 -3
- package/scripts/build-menubar-app.js +141 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,748 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../../node_modules/.pnpm/is-docker@3.0.0/node_modules/is-docker/index.js
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
function hasDockerEnv() {
|
|
15
|
+
try {
|
|
16
|
+
fs.statSync("/.dockerenv");
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function hasDockerCGroup() {
|
|
23
|
+
try {
|
|
24
|
+
return fs.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isDocker() {
|
|
30
|
+
if (isDockerCached === void 0) {
|
|
31
|
+
isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
32
|
+
}
|
|
33
|
+
return isDockerCached;
|
|
34
|
+
}
|
|
35
|
+
var isDockerCached;
|
|
36
|
+
var init_is_docker = __esm({
|
|
37
|
+
"../../node_modules/.pnpm/is-docker@3.0.0/node_modules/is-docker/index.js"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ../../node_modules/.pnpm/is-inside-container@1.0.0/node_modules/is-inside-container/index.js
|
|
43
|
+
import fs2 from "fs";
|
|
44
|
+
function isInsideContainer() {
|
|
45
|
+
if (cachedResult === void 0) {
|
|
46
|
+
cachedResult = hasContainerEnv() || isDocker();
|
|
47
|
+
}
|
|
48
|
+
return cachedResult;
|
|
49
|
+
}
|
|
50
|
+
var cachedResult, hasContainerEnv;
|
|
51
|
+
var init_is_inside_container = __esm({
|
|
52
|
+
"../../node_modules/.pnpm/is-inside-container@1.0.0/node_modules/is-inside-container/index.js"() {
|
|
53
|
+
"use strict";
|
|
54
|
+
init_is_docker();
|
|
55
|
+
hasContainerEnv = () => {
|
|
56
|
+
try {
|
|
57
|
+
fs2.statSync("/run/.containerenv");
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ../../node_modules/.pnpm/is-wsl@3.1.1/node_modules/is-wsl/index.js
|
|
67
|
+
import process2 from "process";
|
|
68
|
+
import os from "os";
|
|
69
|
+
import fs3 from "fs";
|
|
70
|
+
var isWsl, is_wsl_default;
|
|
71
|
+
var init_is_wsl = __esm({
|
|
72
|
+
"../../node_modules/.pnpm/is-wsl@3.1.1/node_modules/is-wsl/index.js"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
init_is_inside_container();
|
|
75
|
+
isWsl = () => {
|
|
76
|
+
if (process2.platform !== "linux") {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (os.release().toLowerCase().includes("microsoft")) {
|
|
80
|
+
if (isInsideContainer()) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (fs3.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
87
|
+
return !isInsideContainer();
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
if (fs3.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs3.existsSync("/run/WSL")) {
|
|
92
|
+
return !isInsideContainer();
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ../../node_modules/.pnpm/powershell-utils@0.1.0/node_modules/powershell-utils/index.js
|
|
101
|
+
import process3 from "process";
|
|
102
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
103
|
+
import { promisify } from "util";
|
|
104
|
+
import childProcess from "child_process";
|
|
105
|
+
import fs4, { constants as fsConstants } from "fs/promises";
|
|
106
|
+
var execFile, powerShellPath, executePowerShell;
|
|
107
|
+
var init_powershell_utils = __esm({
|
|
108
|
+
"../../node_modules/.pnpm/powershell-utils@0.1.0/node_modules/powershell-utils/index.js"() {
|
|
109
|
+
"use strict";
|
|
110
|
+
execFile = promisify(childProcess.execFile);
|
|
111
|
+
powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
112
|
+
executePowerShell = async (command, options = {}) => {
|
|
113
|
+
const {
|
|
114
|
+
powerShellPath: psPath,
|
|
115
|
+
...execFileOptions
|
|
116
|
+
} = options;
|
|
117
|
+
const encodedCommand = executePowerShell.encodeCommand(command);
|
|
118
|
+
return execFile(
|
|
119
|
+
psPath ?? powerShellPath(),
|
|
120
|
+
[
|
|
121
|
+
...executePowerShell.argumentsPrefix,
|
|
122
|
+
encodedCommand
|
|
123
|
+
],
|
|
124
|
+
{
|
|
125
|
+
encoding: "utf8",
|
|
126
|
+
...execFileOptions
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
executePowerShell.argumentsPrefix = [
|
|
131
|
+
"-NoProfile",
|
|
132
|
+
"-NonInteractive",
|
|
133
|
+
"-ExecutionPolicy",
|
|
134
|
+
"Bypass",
|
|
135
|
+
"-EncodedCommand"
|
|
136
|
+
];
|
|
137
|
+
executePowerShell.encodeCommand = (command) => Buffer2.from(command, "utf16le").toString("base64");
|
|
138
|
+
executePowerShell.escapeArgument = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ../../node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/utilities.js
|
|
143
|
+
function parseMountPointFromConfig(content) {
|
|
144
|
+
for (const line of content.split("\n")) {
|
|
145
|
+
if (/^\s*#/.test(line)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const match = /^\s*root\s*=\s*(?<mountPoint>"[^"]*"|'[^']*'|[^#]*)/.exec(line);
|
|
149
|
+
if (!match) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
return match.groups.mountPoint.trim().replaceAll(/^["']|["']$/g, "");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var init_utilities = __esm({
|
|
156
|
+
"../../node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/utilities.js"() {
|
|
157
|
+
"use strict";
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ../../node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/index.js
|
|
162
|
+
import { promisify as promisify2 } from "util";
|
|
163
|
+
import childProcess2 from "child_process";
|
|
164
|
+
import fs5, { constants as fsConstants2 } from "fs/promises";
|
|
165
|
+
var execFile2, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath2, canAccessPowerShellPromise, canAccessPowerShell, wslDefaultBrowser, convertWslPathToWindows;
|
|
166
|
+
var init_wsl_utils = __esm({
|
|
167
|
+
"../../node_modules/.pnpm/wsl-utils@0.3.1/node_modules/wsl-utils/index.js"() {
|
|
168
|
+
"use strict";
|
|
169
|
+
init_is_wsl();
|
|
170
|
+
init_powershell_utils();
|
|
171
|
+
init_utilities();
|
|
172
|
+
init_is_wsl();
|
|
173
|
+
execFile2 = promisify2(childProcess2.execFile);
|
|
174
|
+
wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
175
|
+
const defaultMountPoint = "/mnt/";
|
|
176
|
+
let mountPoint;
|
|
177
|
+
return async function() {
|
|
178
|
+
if (mountPoint) {
|
|
179
|
+
return mountPoint;
|
|
180
|
+
}
|
|
181
|
+
const configFilePath = "/etc/wsl.conf";
|
|
182
|
+
let isConfigFileExists = false;
|
|
183
|
+
try {
|
|
184
|
+
await fs5.access(configFilePath, fsConstants2.F_OK);
|
|
185
|
+
isConfigFileExists = true;
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
if (!isConfigFileExists) {
|
|
189
|
+
return defaultMountPoint;
|
|
190
|
+
}
|
|
191
|
+
const configContent = await fs5.readFile(configFilePath, { encoding: "utf8" });
|
|
192
|
+
const parsedMountPoint = parseMountPointFromConfig(configContent);
|
|
193
|
+
if (parsedMountPoint === void 0) {
|
|
194
|
+
return defaultMountPoint;
|
|
195
|
+
}
|
|
196
|
+
mountPoint = parsedMountPoint;
|
|
197
|
+
mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
|
|
198
|
+
return mountPoint;
|
|
199
|
+
};
|
|
200
|
+
})();
|
|
201
|
+
powerShellPathFromWsl = async () => {
|
|
202
|
+
const mountPoint = await wslDrivesMountPoint();
|
|
203
|
+
return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
204
|
+
};
|
|
205
|
+
powerShellPath2 = is_wsl_default ? powerShellPathFromWsl : powerShellPath;
|
|
206
|
+
canAccessPowerShell = async () => {
|
|
207
|
+
canAccessPowerShellPromise ??= (async () => {
|
|
208
|
+
try {
|
|
209
|
+
const psPath = await powerShellPath2();
|
|
210
|
+
await fs5.access(psPath, fsConstants2.X_OK);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
return canAccessPowerShellPromise;
|
|
217
|
+
};
|
|
218
|
+
wslDefaultBrowser = async () => {
|
|
219
|
+
const psPath = await powerShellPath2();
|
|
220
|
+
const command = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
|
|
221
|
+
const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
|
|
222
|
+
return stdout.trim();
|
|
223
|
+
};
|
|
224
|
+
convertWslPathToWindows = async (path3) => {
|
|
225
|
+
if (/^[a-z]+:\/\//i.test(path3)) {
|
|
226
|
+
return path3;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execFile2("wslpath", ["-aw", path3], { encoding: "utf8" });
|
|
230
|
+
return stdout.trim();
|
|
231
|
+
} catch {
|
|
232
|
+
return path3;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ../../node_modules/.pnpm/define-lazy-prop@3.0.0/node_modules/define-lazy-prop/index.js
|
|
239
|
+
function defineLazyProperty(object, propertyName, valueGetter) {
|
|
240
|
+
const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
|
|
241
|
+
Object.defineProperty(object, propertyName, {
|
|
242
|
+
configurable: true,
|
|
243
|
+
enumerable: true,
|
|
244
|
+
get() {
|
|
245
|
+
const result = valueGetter();
|
|
246
|
+
define(result);
|
|
247
|
+
return result;
|
|
248
|
+
},
|
|
249
|
+
set(value) {
|
|
250
|
+
define(value);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return object;
|
|
254
|
+
}
|
|
255
|
+
var init_define_lazy_prop = __esm({
|
|
256
|
+
"../../node_modules/.pnpm/define-lazy-prop@3.0.0/node_modules/define-lazy-prop/index.js"() {
|
|
257
|
+
"use strict";
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ../../node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js
|
|
262
|
+
import { promisify as promisify3 } from "util";
|
|
263
|
+
import process4 from "process";
|
|
264
|
+
import { execFile as execFile3 } from "child_process";
|
|
265
|
+
async function defaultBrowserId() {
|
|
266
|
+
if (process4.platform !== "darwin") {
|
|
267
|
+
throw new Error("macOS only");
|
|
268
|
+
}
|
|
269
|
+
const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
|
|
270
|
+
const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
|
|
271
|
+
const browserId = match?.groups.id ?? "com.apple.Safari";
|
|
272
|
+
if (browserId === "com.apple.safari") {
|
|
273
|
+
return "com.apple.Safari";
|
|
274
|
+
}
|
|
275
|
+
return browserId;
|
|
276
|
+
}
|
|
277
|
+
var execFileAsync;
|
|
278
|
+
var init_default_browser_id = __esm({
|
|
279
|
+
"../../node_modules/.pnpm/default-browser-id@5.0.1/node_modules/default-browser-id/index.js"() {
|
|
280
|
+
"use strict";
|
|
281
|
+
execFileAsync = promisify3(execFile3);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ../../node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js
|
|
286
|
+
import process5 from "process";
|
|
287
|
+
import { promisify as promisify4 } from "util";
|
|
288
|
+
import { execFile as execFile4, execFileSync as execFileSync3 } from "child_process";
|
|
289
|
+
async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
|
|
290
|
+
if (process5.platform !== "darwin") {
|
|
291
|
+
throw new Error("macOS only");
|
|
292
|
+
}
|
|
293
|
+
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
294
|
+
const execOptions = {};
|
|
295
|
+
if (signal) {
|
|
296
|
+
execOptions.signal = signal;
|
|
297
|
+
}
|
|
298
|
+
const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
|
|
299
|
+
return stdout.trim();
|
|
300
|
+
}
|
|
301
|
+
var execFileAsync2;
|
|
302
|
+
var init_run_applescript = __esm({
|
|
303
|
+
"../../node_modules/.pnpm/run-applescript@7.1.0/node_modules/run-applescript/index.js"() {
|
|
304
|
+
"use strict";
|
|
305
|
+
execFileAsync2 = promisify4(execFile4);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ../../node_modules/.pnpm/bundle-name@4.1.0/node_modules/bundle-name/index.js
|
|
310
|
+
async function bundleName(bundleId) {
|
|
311
|
+
return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
|
|
312
|
+
tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
|
|
313
|
+
}
|
|
314
|
+
var init_bundle_name = __esm({
|
|
315
|
+
"../../node_modules/.pnpm/bundle-name@4.1.0/node_modules/bundle-name/index.js"() {
|
|
316
|
+
"use strict";
|
|
317
|
+
init_run_applescript();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// ../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/windows.js
|
|
322
|
+
import { promisify as promisify5 } from "util";
|
|
323
|
+
import { execFile as execFile5 } from "child_process";
|
|
324
|
+
async function defaultBrowser(_execFileAsync = execFileAsync3) {
|
|
325
|
+
const { stdout } = await _execFileAsync("reg", [
|
|
326
|
+
"QUERY",
|
|
327
|
+
" HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
328
|
+
"/v",
|
|
329
|
+
"ProgId"
|
|
330
|
+
]);
|
|
331
|
+
const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
|
|
332
|
+
if (!match) {
|
|
333
|
+
throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
|
|
334
|
+
}
|
|
335
|
+
const { id } = match.groups;
|
|
336
|
+
const dotIndex = id.lastIndexOf(".");
|
|
337
|
+
const hyphenIndex = id.lastIndexOf("-");
|
|
338
|
+
const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
|
|
339
|
+
const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
|
|
340
|
+
return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
|
|
341
|
+
}
|
|
342
|
+
var execFileAsync3, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
|
|
343
|
+
var init_windows = __esm({
|
|
344
|
+
"../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/windows.js"() {
|
|
345
|
+
"use strict";
|
|
346
|
+
execFileAsync3 = promisify5(execFile5);
|
|
347
|
+
windowsBrowserProgIds = {
|
|
348
|
+
MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
|
|
349
|
+
// The missing `L` is correct.
|
|
350
|
+
MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
|
|
351
|
+
MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
|
|
352
|
+
AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
|
|
353
|
+
ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
|
|
354
|
+
ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
|
|
355
|
+
ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
|
|
356
|
+
ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
|
|
357
|
+
BraveHTML: { name: "Brave", id: "com.brave.Browser" },
|
|
358
|
+
BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
|
|
359
|
+
BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
|
|
360
|
+
BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
|
|
361
|
+
FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
|
|
362
|
+
OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
|
|
363
|
+
VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
|
|
364
|
+
"IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
|
|
365
|
+
};
|
|
366
|
+
_windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
|
|
367
|
+
UnknownBrowserError = class extends Error {
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/index.js
|
|
373
|
+
import { promisify as promisify6 } from "util";
|
|
374
|
+
import process6 from "process";
|
|
375
|
+
import { execFile as execFile6 } from "child_process";
|
|
376
|
+
async function defaultBrowser2() {
|
|
377
|
+
if (process6.platform === "darwin") {
|
|
378
|
+
const id = await defaultBrowserId();
|
|
379
|
+
const name = await bundleName(id);
|
|
380
|
+
return { name, id };
|
|
381
|
+
}
|
|
382
|
+
if (process6.platform === "linux") {
|
|
383
|
+
const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
384
|
+
const id = stdout.trim();
|
|
385
|
+
const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
|
|
386
|
+
return { name, id };
|
|
387
|
+
}
|
|
388
|
+
if (process6.platform === "win32") {
|
|
389
|
+
return defaultBrowser();
|
|
390
|
+
}
|
|
391
|
+
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
392
|
+
}
|
|
393
|
+
var execFileAsync4, titleize;
|
|
394
|
+
var init_default_browser = __esm({
|
|
395
|
+
"../../node_modules/.pnpm/default-browser@5.5.0/node_modules/default-browser/index.js"() {
|
|
396
|
+
"use strict";
|
|
397
|
+
init_default_browser_id();
|
|
398
|
+
init_bundle_name();
|
|
399
|
+
init_windows();
|
|
400
|
+
init_windows();
|
|
401
|
+
execFileAsync4 = promisify6(execFile6);
|
|
402
|
+
titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ../../node_modules/.pnpm/is-in-ssh@1.0.0/node_modules/is-in-ssh/index.js
|
|
407
|
+
import process7 from "process";
|
|
408
|
+
var isInSsh, is_in_ssh_default;
|
|
409
|
+
var init_is_in_ssh = __esm({
|
|
410
|
+
"../../node_modules/.pnpm/is-in-ssh@1.0.0/node_modules/is-in-ssh/index.js"() {
|
|
411
|
+
"use strict";
|
|
412
|
+
isInSsh = Boolean(process7.env.SSH_CONNECTION || process7.env.SSH_CLIENT || process7.env.SSH_TTY);
|
|
413
|
+
is_in_ssh_default = isInSsh;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// ../../node_modules/.pnpm/open@11.0.0/node_modules/open/index.js
|
|
418
|
+
var open_exports = {};
|
|
419
|
+
__export(open_exports, {
|
|
420
|
+
apps: () => apps,
|
|
421
|
+
default: () => open_default,
|
|
422
|
+
openApp: () => openApp
|
|
423
|
+
});
|
|
424
|
+
import process8 from "process";
|
|
425
|
+
import path2 from "path";
|
|
426
|
+
import { fileURLToPath } from "url";
|
|
427
|
+
import childProcess3 from "child_process";
|
|
428
|
+
import fs6, { constants as fsConstants3 } from "fs/promises";
|
|
429
|
+
function detectArchBinary(binary) {
|
|
430
|
+
if (typeof binary === "string" || Array.isArray(binary)) {
|
|
431
|
+
return binary;
|
|
432
|
+
}
|
|
433
|
+
const { [arch]: archBinary } = binary;
|
|
434
|
+
if (!archBinary) {
|
|
435
|
+
throw new Error(`${arch} is not supported`);
|
|
436
|
+
}
|
|
437
|
+
return archBinary;
|
|
438
|
+
}
|
|
439
|
+
function detectPlatformBinary({ [platform2]: platformBinary }, { wsl } = {}) {
|
|
440
|
+
if (wsl && is_wsl_default) {
|
|
441
|
+
return detectArchBinary(wsl);
|
|
442
|
+
}
|
|
443
|
+
if (!platformBinary) {
|
|
444
|
+
throw new Error(`${platform2} is not supported`);
|
|
445
|
+
}
|
|
446
|
+
return detectArchBinary(platformBinary);
|
|
447
|
+
}
|
|
448
|
+
var fallbackAttemptSymbol, __dirname, localXdgOpenPath, platform2, arch, tryEachApp, baseOpen, open, openApp, apps, open_default;
|
|
449
|
+
var init_open = __esm({
|
|
450
|
+
"../../node_modules/.pnpm/open@11.0.0/node_modules/open/index.js"() {
|
|
451
|
+
"use strict";
|
|
452
|
+
init_wsl_utils();
|
|
453
|
+
init_powershell_utils();
|
|
454
|
+
init_define_lazy_prop();
|
|
455
|
+
init_default_browser();
|
|
456
|
+
init_is_inside_container();
|
|
457
|
+
init_is_in_ssh();
|
|
458
|
+
fallbackAttemptSymbol = Symbol("fallbackAttempt");
|
|
459
|
+
__dirname = import.meta.url ? path2.dirname(fileURLToPath(import.meta.url)) : "";
|
|
460
|
+
localXdgOpenPath = path2.join(__dirname, "xdg-open");
|
|
461
|
+
({ platform: platform2, arch } = process8);
|
|
462
|
+
tryEachApp = async (apps2, opener) => {
|
|
463
|
+
if (apps2.length === 0) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const errors = [];
|
|
467
|
+
for (const app of apps2) {
|
|
468
|
+
try {
|
|
469
|
+
return await opener(app);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
errors.push(error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
throw new AggregateError(errors, "Failed to open in all supported apps");
|
|
475
|
+
};
|
|
476
|
+
baseOpen = async (options) => {
|
|
477
|
+
options = {
|
|
478
|
+
wait: false,
|
|
479
|
+
background: false,
|
|
480
|
+
newInstance: false,
|
|
481
|
+
allowNonzeroExitCode: false,
|
|
482
|
+
...options
|
|
483
|
+
};
|
|
484
|
+
const isFallbackAttempt = options[fallbackAttemptSymbol] === true;
|
|
485
|
+
delete options[fallbackAttemptSymbol];
|
|
486
|
+
if (Array.isArray(options.app)) {
|
|
487
|
+
return tryEachApp(options.app, (singleApp) => baseOpen({
|
|
488
|
+
...options,
|
|
489
|
+
app: singleApp,
|
|
490
|
+
[fallbackAttemptSymbol]: true
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
let { name: app, arguments: appArguments = [] } = options.app ?? {};
|
|
494
|
+
appArguments = [...appArguments];
|
|
495
|
+
if (Array.isArray(app)) {
|
|
496
|
+
return tryEachApp(app, (appName) => baseOpen({
|
|
497
|
+
...options,
|
|
498
|
+
app: {
|
|
499
|
+
name: appName,
|
|
500
|
+
arguments: appArguments
|
|
501
|
+
},
|
|
502
|
+
[fallbackAttemptSymbol]: true
|
|
503
|
+
}));
|
|
504
|
+
}
|
|
505
|
+
if (app === "browser" || app === "browserPrivate") {
|
|
506
|
+
const ids = {
|
|
507
|
+
"com.google.chrome": "chrome",
|
|
508
|
+
"google-chrome.desktop": "chrome",
|
|
509
|
+
"com.brave.browser": "brave",
|
|
510
|
+
"org.mozilla.firefox": "firefox",
|
|
511
|
+
"firefox.desktop": "firefox",
|
|
512
|
+
"com.microsoft.msedge": "edge",
|
|
513
|
+
"com.microsoft.edge": "edge",
|
|
514
|
+
"com.microsoft.edgemac": "edge",
|
|
515
|
+
"microsoft-edge.desktop": "edge",
|
|
516
|
+
"com.apple.safari": "safari"
|
|
517
|
+
};
|
|
518
|
+
const flags = {
|
|
519
|
+
chrome: "--incognito",
|
|
520
|
+
brave: "--incognito",
|
|
521
|
+
firefox: "--private-window",
|
|
522
|
+
edge: "--inPrivate"
|
|
523
|
+
// Safari doesn't support private mode via command line
|
|
524
|
+
};
|
|
525
|
+
let browser;
|
|
526
|
+
if (is_wsl_default) {
|
|
527
|
+
const progId = await wslDefaultBrowser();
|
|
528
|
+
const browserInfo = _windowsBrowserProgIdMap.get(progId);
|
|
529
|
+
browser = browserInfo ?? {};
|
|
530
|
+
} else {
|
|
531
|
+
browser = await defaultBrowser2();
|
|
532
|
+
}
|
|
533
|
+
if (browser.id in ids) {
|
|
534
|
+
const browserName = ids[browser.id.toLowerCase()];
|
|
535
|
+
if (app === "browserPrivate") {
|
|
536
|
+
if (browserName === "safari") {
|
|
537
|
+
throw new Error("Safari doesn't support opening in private mode via command line");
|
|
538
|
+
}
|
|
539
|
+
appArguments.push(flags[browserName]);
|
|
540
|
+
}
|
|
541
|
+
return baseOpen({
|
|
542
|
+
...options,
|
|
543
|
+
app: {
|
|
544
|
+
name: apps[browserName],
|
|
545
|
+
arguments: appArguments
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
throw new Error(`${browser.name} is not supported as a default browser`);
|
|
550
|
+
}
|
|
551
|
+
let command;
|
|
552
|
+
const cliArguments = [];
|
|
553
|
+
const childProcessOptions = {};
|
|
554
|
+
let shouldUseWindowsInWsl = false;
|
|
555
|
+
if (is_wsl_default && !isInsideContainer() && !is_in_ssh_default && !app) {
|
|
556
|
+
shouldUseWindowsInWsl = await canAccessPowerShell();
|
|
557
|
+
}
|
|
558
|
+
if (platform2 === "darwin") {
|
|
559
|
+
command = "open";
|
|
560
|
+
if (options.wait) {
|
|
561
|
+
cliArguments.push("--wait-apps");
|
|
562
|
+
}
|
|
563
|
+
if (options.background) {
|
|
564
|
+
cliArguments.push("--background");
|
|
565
|
+
}
|
|
566
|
+
if (options.newInstance) {
|
|
567
|
+
cliArguments.push("--new");
|
|
568
|
+
}
|
|
569
|
+
if (app) {
|
|
570
|
+
cliArguments.push("-a", app);
|
|
571
|
+
}
|
|
572
|
+
} else if (platform2 === "win32" || shouldUseWindowsInWsl) {
|
|
573
|
+
command = await powerShellPath2();
|
|
574
|
+
cliArguments.push(...executePowerShell.argumentsPrefix);
|
|
575
|
+
if (!is_wsl_default) {
|
|
576
|
+
childProcessOptions.windowsVerbatimArguments = true;
|
|
577
|
+
}
|
|
578
|
+
if (is_wsl_default && options.target) {
|
|
579
|
+
options.target = await convertWslPathToWindows(options.target);
|
|
580
|
+
}
|
|
581
|
+
const encodedArguments = ["$ProgressPreference = 'SilentlyContinue';", "Start"];
|
|
582
|
+
if (options.wait) {
|
|
583
|
+
encodedArguments.push("-Wait");
|
|
584
|
+
}
|
|
585
|
+
if (app) {
|
|
586
|
+
encodedArguments.push(executePowerShell.escapeArgument(app));
|
|
587
|
+
if (options.target) {
|
|
588
|
+
appArguments.push(options.target);
|
|
589
|
+
}
|
|
590
|
+
} else if (options.target) {
|
|
591
|
+
encodedArguments.push(executePowerShell.escapeArgument(options.target));
|
|
592
|
+
}
|
|
593
|
+
if (appArguments.length > 0) {
|
|
594
|
+
appArguments = appArguments.map((argument) => executePowerShell.escapeArgument(argument));
|
|
595
|
+
encodedArguments.push("-ArgumentList", appArguments.join(","));
|
|
596
|
+
}
|
|
597
|
+
options.target = executePowerShell.encodeCommand(encodedArguments.join(" "));
|
|
598
|
+
if (!options.wait) {
|
|
599
|
+
childProcessOptions.stdio = "ignore";
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
if (app) {
|
|
603
|
+
command = app;
|
|
604
|
+
} else {
|
|
605
|
+
const isBundled = !__dirname || __dirname === "/";
|
|
606
|
+
let exeLocalXdgOpen = false;
|
|
607
|
+
try {
|
|
608
|
+
await fs6.access(localXdgOpenPath, fsConstants3.X_OK);
|
|
609
|
+
exeLocalXdgOpen = true;
|
|
610
|
+
} catch {
|
|
611
|
+
}
|
|
612
|
+
const useSystemXdgOpen = process8.versions.electron ?? (platform2 === "android" || isBundled || !exeLocalXdgOpen);
|
|
613
|
+
command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
|
|
614
|
+
}
|
|
615
|
+
if (appArguments.length > 0) {
|
|
616
|
+
cliArguments.push(...appArguments);
|
|
617
|
+
}
|
|
618
|
+
if (!options.wait) {
|
|
619
|
+
childProcessOptions.stdio = "ignore";
|
|
620
|
+
childProcessOptions.detached = true;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (platform2 === "darwin" && appArguments.length > 0) {
|
|
624
|
+
cliArguments.push("--args", ...appArguments);
|
|
625
|
+
}
|
|
626
|
+
if (options.target) {
|
|
627
|
+
cliArguments.push(options.target);
|
|
628
|
+
}
|
|
629
|
+
const subprocess = childProcess3.spawn(command, cliArguments, childProcessOptions);
|
|
630
|
+
if (options.wait) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
subprocess.once("error", reject);
|
|
633
|
+
subprocess.once("close", (exitCode) => {
|
|
634
|
+
if (!options.allowNonzeroExitCode && exitCode !== 0) {
|
|
635
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
resolve(subprocess);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (isFallbackAttempt) {
|
|
643
|
+
return new Promise((resolve, reject) => {
|
|
644
|
+
subprocess.once("error", reject);
|
|
645
|
+
subprocess.once("spawn", () => {
|
|
646
|
+
subprocess.once("close", (exitCode) => {
|
|
647
|
+
subprocess.off("error", reject);
|
|
648
|
+
if (exitCode !== 0) {
|
|
649
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
subprocess.unref();
|
|
653
|
+
resolve(subprocess);
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
subprocess.unref();
|
|
659
|
+
return new Promise((resolve, reject) => {
|
|
660
|
+
subprocess.once("error", reject);
|
|
661
|
+
subprocess.once("spawn", () => {
|
|
662
|
+
subprocess.off("error", reject);
|
|
663
|
+
resolve(subprocess);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
};
|
|
667
|
+
open = (target, options) => {
|
|
668
|
+
if (typeof target !== "string") {
|
|
669
|
+
throw new TypeError("Expected a `target`");
|
|
670
|
+
}
|
|
671
|
+
return baseOpen({
|
|
672
|
+
...options,
|
|
673
|
+
target
|
|
674
|
+
});
|
|
675
|
+
};
|
|
676
|
+
openApp = (name, options) => {
|
|
677
|
+
if (typeof name !== "string" && !Array.isArray(name)) {
|
|
678
|
+
throw new TypeError("Expected a valid `name`");
|
|
679
|
+
}
|
|
680
|
+
const { arguments: appArguments = [] } = options ?? {};
|
|
681
|
+
if (appArguments !== void 0 && appArguments !== null && !Array.isArray(appArguments)) {
|
|
682
|
+
throw new TypeError("Expected `appArguments` as Array type");
|
|
683
|
+
}
|
|
684
|
+
return baseOpen({
|
|
685
|
+
...options,
|
|
686
|
+
app: {
|
|
687
|
+
name,
|
|
688
|
+
arguments: appArguments
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
apps = {
|
|
693
|
+
browser: "browser",
|
|
694
|
+
browserPrivate: "browserPrivate"
|
|
695
|
+
};
|
|
696
|
+
defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
|
|
697
|
+
darwin: "google chrome",
|
|
698
|
+
win32: "chrome",
|
|
699
|
+
// `chromium-browser` is the older deb package name used by Ubuntu/Debian before snap.
|
|
700
|
+
linux: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]
|
|
701
|
+
}, {
|
|
702
|
+
wsl: {
|
|
703
|
+
ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
704
|
+
x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
|
|
705
|
+
}
|
|
706
|
+
}));
|
|
707
|
+
defineLazyProperty(apps, "brave", () => detectPlatformBinary({
|
|
708
|
+
darwin: "brave browser",
|
|
709
|
+
win32: "brave",
|
|
710
|
+
linux: ["brave-browser", "brave"]
|
|
711
|
+
}, {
|
|
712
|
+
wsl: {
|
|
713
|
+
ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
714
|
+
x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
|
|
715
|
+
}
|
|
716
|
+
}));
|
|
717
|
+
defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
|
|
718
|
+
darwin: "firefox",
|
|
719
|
+
win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
|
|
720
|
+
linux: "firefox"
|
|
721
|
+
}, {
|
|
722
|
+
wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
|
|
723
|
+
}));
|
|
724
|
+
defineLazyProperty(apps, "edge", () => detectPlatformBinary({
|
|
725
|
+
darwin: "microsoft edge",
|
|
726
|
+
win32: "msedge",
|
|
727
|
+
linux: ["microsoft-edge", "microsoft-edge-dev"]
|
|
728
|
+
}, {
|
|
729
|
+
wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
730
|
+
}));
|
|
731
|
+
defineLazyProperty(apps, "safari", () => detectPlatformBinary({
|
|
732
|
+
darwin: "Safari"
|
|
733
|
+
}));
|
|
734
|
+
open_default = open;
|
|
735
|
+
}
|
|
736
|
+
});
|
|
2
737
|
|
|
3
|
-
//
|
|
738
|
+
// src/index.ts
|
|
739
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
4
740
|
import { readFile as readFile2 } from "fs/promises";
|
|
5
|
-
import { extname } from "path";
|
|
741
|
+
import { dirname as dirname2, extname, join as join3 } from "path";
|
|
742
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
743
|
import { config as loadEnv } from "dotenv";
|
|
7
744
|
import { Command } from "commander";
|
|
745
|
+
import { ConvexHttpClient as ConvexHttpClient3 } from "convex/browser";
|
|
8
746
|
import { makeFunctionReference } from "convex/server";
|
|
9
747
|
|
|
10
748
|
// ../../convex/_generated/api.js
|
|
@@ -12,7 +750,7 @@ import { anyApi, componentsGeneric } from "convex/server";
|
|
|
12
750
|
var api = anyApi;
|
|
13
751
|
var components = componentsGeneric();
|
|
14
752
|
|
|
15
|
-
//
|
|
753
|
+
// src/auth.ts
|
|
16
754
|
import { isCancel, password as passwordPrompt, text } from "@clack/prompts";
|
|
17
755
|
function buildUrl(appUrl, pathname) {
|
|
18
756
|
return new URL(pathname, appUrl).toString();
|
|
@@ -52,6 +790,9 @@ function applySetCookieHeaders(session, response) {
|
|
|
52
790
|
async function authRequest(session, appUrl, pathname, init = {}) {
|
|
53
791
|
const headers = new Headers(init.headers);
|
|
54
792
|
const origin = new URL(appUrl).origin;
|
|
793
|
+
if (session.bearerToken && !headers.has("authorization")) {
|
|
794
|
+
headers.set("authorization", `Bearer ${session.bearerToken}`);
|
|
795
|
+
}
|
|
55
796
|
if (Object.keys(session.cookies).length > 0) {
|
|
56
797
|
headers.set("cookie", cookieHeader(session.cookies));
|
|
57
798
|
}
|
|
@@ -146,7 +887,7 @@ async function fetchAuthSession(session, appUrl) {
|
|
|
146
887
|
const data = await response.json();
|
|
147
888
|
return {
|
|
148
889
|
session: nextSession,
|
|
149
|
-
user: data
|
|
890
|
+
user: data?.user ?? null
|
|
150
891
|
};
|
|
151
892
|
}
|
|
152
893
|
async function fetchConvexToken(session, appUrl) {
|
|
@@ -170,6 +911,65 @@ async function fetchConvexToken(session, appUrl) {
|
|
|
170
911
|
token: data.token
|
|
171
912
|
};
|
|
172
913
|
}
|
|
914
|
+
async function requestDeviceCode(appUrl, clientId) {
|
|
915
|
+
const response = await fetch(buildUrl(appUrl, "/api/auth/device/code"), {
|
|
916
|
+
method: "POST",
|
|
917
|
+
headers: { "content-type": "application/json" },
|
|
918
|
+
body: JSON.stringify({ client_id: clientId })
|
|
919
|
+
});
|
|
920
|
+
if (!response.ok) {
|
|
921
|
+
throw new Error(`Failed to request device code: HTTP ${response.status}`);
|
|
922
|
+
}
|
|
923
|
+
return await response.json();
|
|
924
|
+
}
|
|
925
|
+
async function pollDeviceToken(session, appUrl, deviceCode, clientId, interval, expiresIn) {
|
|
926
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
927
|
+
let pollInterval = interval * 1e3;
|
|
928
|
+
while (Date.now() < deadline) {
|
|
929
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
930
|
+
const { response, session: nextSession } = await authRequest(
|
|
931
|
+
session,
|
|
932
|
+
appUrl,
|
|
933
|
+
"/api/auth/device/token",
|
|
934
|
+
{
|
|
935
|
+
method: "POST",
|
|
936
|
+
body: JSON.stringify({
|
|
937
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
938
|
+
device_code: deviceCode,
|
|
939
|
+
client_id: clientId
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
session = nextSession;
|
|
944
|
+
if (response.ok) {
|
|
945
|
+
const data = await response.json();
|
|
946
|
+
if (data.access_token) {
|
|
947
|
+
session.bearerToken = data.access_token;
|
|
948
|
+
return session;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
let errorData;
|
|
952
|
+
try {
|
|
953
|
+
errorData = await response.json();
|
|
954
|
+
} catch {
|
|
955
|
+
errorData = { error: `HTTP ${response.status}` };
|
|
956
|
+
}
|
|
957
|
+
switch (errorData.error) {
|
|
958
|
+
case "authorization_pending":
|
|
959
|
+
break;
|
|
960
|
+
case "slow_down":
|
|
961
|
+
pollInterval += 5e3;
|
|
962
|
+
break;
|
|
963
|
+
case "access_denied":
|
|
964
|
+
throw new Error("Authorization denied by user.");
|
|
965
|
+
case "expired_token":
|
|
966
|
+
throw new Error("Device code expired. Please try again.");
|
|
967
|
+
default:
|
|
968
|
+
throw new Error(`Device auth error: ${errorData.error}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
throw new Error("Device code expired. Please try again.");
|
|
972
|
+
}
|
|
173
973
|
async function prompt(question) {
|
|
174
974
|
const value = await text({
|
|
175
975
|
message: question.replace(/:\s*$/, "")
|
|
@@ -190,7 +990,7 @@ async function promptSecret(question) {
|
|
|
190
990
|
return String(value);
|
|
191
991
|
}
|
|
192
992
|
|
|
193
|
-
//
|
|
993
|
+
// src/convex.ts
|
|
194
994
|
import { ConvexHttpClient } from "convex/browser";
|
|
195
995
|
async function createConvexClient(session, appUrl, convexUrl) {
|
|
196
996
|
const { token } = await fetchConvexToken(session, appUrl);
|
|
@@ -208,7 +1008,7 @@ async function runAction(client, ref, ...args) {
|
|
|
208
1008
|
return await client.action(ref, ...args);
|
|
209
1009
|
}
|
|
210
1010
|
|
|
211
|
-
//
|
|
1011
|
+
// src/output.ts
|
|
212
1012
|
function simplify(value) {
|
|
213
1013
|
if (value === null || value === void 0) {
|
|
214
1014
|
return value;
|
|
@@ -255,13 +1055,68 @@ function printOutput(data, json = false) {
|
|
|
255
1055
|
console.log(String(data));
|
|
256
1056
|
}
|
|
257
1057
|
|
|
258
|
-
//
|
|
259
|
-
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
1058
|
+
// src/session.ts
|
|
1059
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
|
|
260
1060
|
import { homedir } from "os";
|
|
261
1061
|
import path from "path";
|
|
262
|
-
|
|
1062
|
+
function getSessionRoot() {
|
|
1063
|
+
return process.env.VECTOR_HOME?.trim() || path.join(homedir(), ".vector");
|
|
1064
|
+
}
|
|
1065
|
+
function getProfileConfigPath() {
|
|
1066
|
+
return path.join(getSessionRoot(), "cli-config.json");
|
|
1067
|
+
}
|
|
263
1068
|
function getSessionPath(profile = "default") {
|
|
264
|
-
return path.join(
|
|
1069
|
+
return path.join(getSessionRoot(), `cli-${profile}.json`);
|
|
1070
|
+
}
|
|
1071
|
+
async function readDefaultProfile() {
|
|
1072
|
+
try {
|
|
1073
|
+
const raw = await readFile(getProfileConfigPath(), "utf8");
|
|
1074
|
+
const parsed = JSON.parse(raw);
|
|
1075
|
+
const profile = parsed.defaultProfile?.trim();
|
|
1076
|
+
return profile || "default";
|
|
1077
|
+
} catch {
|
|
1078
|
+
return "default";
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
async function writeDefaultProfile(profile) {
|
|
1082
|
+
const normalized = profile.trim() || "default";
|
|
1083
|
+
await mkdir(getSessionRoot(), { recursive: true });
|
|
1084
|
+
const config = {
|
|
1085
|
+
version: 1,
|
|
1086
|
+
defaultProfile: normalized
|
|
1087
|
+
};
|
|
1088
|
+
await writeFile(
|
|
1089
|
+
getProfileConfigPath(),
|
|
1090
|
+
`${JSON.stringify(config, null, 2)}
|
|
1091
|
+
`,
|
|
1092
|
+
"utf8"
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
async function listProfiles() {
|
|
1096
|
+
const root = getSessionRoot();
|
|
1097
|
+
const defaultProfile = await readDefaultProfile();
|
|
1098
|
+
try {
|
|
1099
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
1100
|
+
const names = entries.filter((entry) => entry.isFile()).map((entry) => entry.name).filter((name) => /^cli-.+\.json$/.test(name)).map((name) => name.replace(/^cli-/, "").replace(/\.json$/, ""));
|
|
1101
|
+
const uniqueNames = Array.from(/* @__PURE__ */ new Set([...names, defaultProfile])).sort(
|
|
1102
|
+
(left, right) => left.localeCompare(right)
|
|
1103
|
+
);
|
|
1104
|
+
return Promise.all(
|
|
1105
|
+
uniqueNames.map(async (name) => ({
|
|
1106
|
+
name,
|
|
1107
|
+
isDefault: name === defaultProfile,
|
|
1108
|
+
hasSession: await readSession(name) !== null
|
|
1109
|
+
}))
|
|
1110
|
+
);
|
|
1111
|
+
} catch {
|
|
1112
|
+
return [
|
|
1113
|
+
{
|
|
1114
|
+
name: defaultProfile,
|
|
1115
|
+
isDefault: true,
|
|
1116
|
+
hasSession: await readSession(defaultProfile) !== null
|
|
1117
|
+
}
|
|
1118
|
+
];
|
|
1119
|
+
}
|
|
265
1120
|
}
|
|
266
1121
|
async function readSession(profile = "default") {
|
|
267
1122
|
try {
|
|
@@ -277,7 +1132,7 @@ async function readSession(profile = "default") {
|
|
|
277
1132
|
}
|
|
278
1133
|
}
|
|
279
1134
|
async function writeSession(session, profile = "default") {
|
|
280
|
-
await mkdir(
|
|
1135
|
+
await mkdir(getSessionRoot(), { recursive: true });
|
|
281
1136
|
await writeFile(
|
|
282
1137
|
getSessionPath(profile),
|
|
283
1138
|
`${JSON.stringify(session, null, 2)}
|
|
@@ -288,14 +1143,2925 @@ async function writeSession(session, profile = "default") {
|
|
|
288
1143
|
async function clearSession(profile = "default") {
|
|
289
1144
|
await rm(getSessionPath(profile), { force: true });
|
|
290
1145
|
}
|
|
291
|
-
function createEmptySession() {
|
|
292
|
-
return {
|
|
293
|
-
version: 1,
|
|
294
|
-
cookies: {}
|
|
295
|
-
};
|
|
1146
|
+
function createEmptySession() {
|
|
1147
|
+
return {
|
|
1148
|
+
version: 1,
|
|
1149
|
+
cookies: {}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// src/bridge-service.ts
|
|
1154
|
+
import { ConvexHttpClient as ConvexHttpClient2 } from "convex/browser";
|
|
1155
|
+
import { execFileSync as execFileSync2, execSync as execSync2 } from "child_process";
|
|
1156
|
+
|
|
1157
|
+
// src/terminal-peer.ts
|
|
1158
|
+
import { createServer } from "http";
|
|
1159
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
1160
|
+
import { ConvexClient } from "convex/browser";
|
|
1161
|
+
import * as pty from "node-pty";
|
|
1162
|
+
import { existsSync } from "fs";
|
|
1163
|
+
import { randomUUID } from "crypto";
|
|
1164
|
+
import { execFileSync } from "child_process";
|
|
1165
|
+
import localtunnel from "localtunnel";
|
|
1166
|
+
function findTmuxPath() {
|
|
1167
|
+
for (const p of [
|
|
1168
|
+
"/opt/homebrew/bin/tmux",
|
|
1169
|
+
"/usr/local/bin/tmux",
|
|
1170
|
+
"/usr/bin/tmux"
|
|
1171
|
+
]) {
|
|
1172
|
+
if (existsSync(p)) return p;
|
|
1173
|
+
}
|
|
1174
|
+
return "tmux";
|
|
1175
|
+
}
|
|
1176
|
+
var TMUX = findTmuxPath();
|
|
1177
|
+
function ts() {
|
|
1178
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
1179
|
+
}
|
|
1180
|
+
function findPort() {
|
|
1181
|
+
return new Promise((resolve, reject) => {
|
|
1182
|
+
const srv = createServer();
|
|
1183
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
1184
|
+
const addr = srv.address();
|
|
1185
|
+
const port = typeof addr === "object" && addr ? addr.port : 9100;
|
|
1186
|
+
srv.close(() => resolve(port));
|
|
1187
|
+
});
|
|
1188
|
+
srv.on("error", reject);
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
function createViewerSession(targetSession, paneId) {
|
|
1192
|
+
const viewerName = `viewer-${randomUUID().slice(0, 8)}`;
|
|
1193
|
+
try {
|
|
1194
|
+
execFileSync(TMUX, [
|
|
1195
|
+
"new-session",
|
|
1196
|
+
"-d",
|
|
1197
|
+
"-s",
|
|
1198
|
+
viewerName,
|
|
1199
|
+
"-t",
|
|
1200
|
+
targetSession
|
|
1201
|
+
]);
|
|
1202
|
+
execFileSync(TMUX, ["set-option", "-t", viewerName, "status", "off"]);
|
|
1203
|
+
if (paneId) {
|
|
1204
|
+
try {
|
|
1205
|
+
execFileSync(TMUX, ["select-pane", "-t", paneId]);
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return viewerName;
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
console.error(`[${ts()}] Failed to create viewer session:`, err);
|
|
1212
|
+
return targetSession;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function killViewerSession(sessionName) {
|
|
1216
|
+
try {
|
|
1217
|
+
execFileSync(TMUX, ["kill-session", "-t", sessionName]);
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
var TerminalPeerManager = class {
|
|
1222
|
+
constructor(config) {
|
|
1223
|
+
this.terminals = /* @__PURE__ */ new Map();
|
|
1224
|
+
this.failedSessions = /* @__PURE__ */ new Set();
|
|
1225
|
+
this.pendingStops = /* @__PURE__ */ new Map();
|
|
1226
|
+
this.unsubscribers = /* @__PURE__ */ new Map();
|
|
1227
|
+
this.config = config;
|
|
1228
|
+
this.client = new ConvexClient(config.convexUrl);
|
|
1229
|
+
}
|
|
1230
|
+
watchSession(workSessionId, tmuxSessionName, tmuxPaneId) {
|
|
1231
|
+
if (this.unsubscribers.has(workSessionId)) return;
|
|
1232
|
+
const unsub = this.client.onUpdate(
|
|
1233
|
+
api.agentBridge.bridgePublic.getWorkSessionTerminalState,
|
|
1234
|
+
{
|
|
1235
|
+
deviceId: this.config.deviceId,
|
|
1236
|
+
deviceSecret: this.config.deviceSecret,
|
|
1237
|
+
workSessionId
|
|
1238
|
+
},
|
|
1239
|
+
(state) => {
|
|
1240
|
+
if (!state) return;
|
|
1241
|
+
const terminal = this.terminals.get(workSessionId);
|
|
1242
|
+
if (state.terminalViewerActive && !terminal && !this.failedSessions.has(workSessionId)) {
|
|
1243
|
+
const pendingStop = this.pendingStops.get(workSessionId);
|
|
1244
|
+
if (pendingStop) {
|
|
1245
|
+
clearTimeout(pendingStop);
|
|
1246
|
+
this.pendingStops.delete(workSessionId);
|
|
1247
|
+
}
|
|
1248
|
+
console.log(`[${ts()}] Viewer active for ${tmuxSessionName}`);
|
|
1249
|
+
void this.startTerminal(
|
|
1250
|
+
workSessionId,
|
|
1251
|
+
tmuxSessionName,
|
|
1252
|
+
tmuxPaneId,
|
|
1253
|
+
state.terminalCols,
|
|
1254
|
+
state.terminalRows
|
|
1255
|
+
);
|
|
1256
|
+
} else if (!state.terminalViewerActive && terminal) {
|
|
1257
|
+
if (!this.pendingStops.has(workSessionId)) {
|
|
1258
|
+
this.pendingStops.set(
|
|
1259
|
+
workSessionId,
|
|
1260
|
+
setTimeout(() => {
|
|
1261
|
+
this.pendingStops.delete(workSessionId);
|
|
1262
|
+
console.log(`[${ts()}] Viewer inactive for ${tmuxSessionName}`);
|
|
1263
|
+
this.stopTerminal(workSessionId);
|
|
1264
|
+
this.failedSessions.delete(workSessionId);
|
|
1265
|
+
}, 2e3)
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
);
|
|
1271
|
+
this.unsubscribers.set(workSessionId, unsub);
|
|
1272
|
+
}
|
|
1273
|
+
unwatchSession(workSessionId) {
|
|
1274
|
+
const unsub = this.unsubscribers.get(workSessionId);
|
|
1275
|
+
if (unsub) {
|
|
1276
|
+
unsub();
|
|
1277
|
+
this.unsubscribers.delete(workSessionId);
|
|
1278
|
+
}
|
|
1279
|
+
this.stopTerminal(workSessionId);
|
|
1280
|
+
}
|
|
1281
|
+
async startTerminal(workSessionId, tmuxSessionName, tmuxPaneId, cols, rows) {
|
|
1282
|
+
if (this.terminals.has(workSessionId)) return;
|
|
1283
|
+
try {
|
|
1284
|
+
const port = await findPort();
|
|
1285
|
+
const viewerSession = createViewerSession(tmuxSessionName, tmuxPaneId);
|
|
1286
|
+
const isLinked = viewerSession !== tmuxSessionName;
|
|
1287
|
+
console.log(
|
|
1288
|
+
`[${ts()}] Viewer session: ${viewerSession}${isLinked ? " (linked)" : ""}`
|
|
1289
|
+
);
|
|
1290
|
+
console.log(
|
|
1291
|
+
`[${ts()}] Spawning PTY: ${TMUX} attach-session -t ${viewerSession}`
|
|
1292
|
+
);
|
|
1293
|
+
const ptyProcess = pty.spawn(
|
|
1294
|
+
TMUX,
|
|
1295
|
+
["attach-session", "-t", viewerSession],
|
|
1296
|
+
{
|
|
1297
|
+
name: "xterm-256color",
|
|
1298
|
+
cols: Math.max(cols, 10),
|
|
1299
|
+
rows: Math.max(rows, 4),
|
|
1300
|
+
cwd: process.env.HOME ?? "/",
|
|
1301
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
console.log(`[${ts()}] PTY started`);
|
|
1305
|
+
const token = randomUUID();
|
|
1306
|
+
const httpServer = createServer();
|
|
1307
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
1308
|
+
wss.on("connection", (ws, req) => {
|
|
1309
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
1310
|
+
const clientToken = url.searchParams.get("token");
|
|
1311
|
+
if (clientToken !== token) {
|
|
1312
|
+
console.log(`[${ts()}] Rejected unauthorized connection`);
|
|
1313
|
+
ws.close(4401, "Unauthorized");
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
console.log(`[${ts()}] Client connected (${tmuxSessionName})`);
|
|
1317
|
+
try {
|
|
1318
|
+
execFileSync(TMUX, ["refresh-client", "-t", viewerSession]);
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
const dataHandler = ptyProcess.onData((data) => {
|
|
1322
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1323
|
+
ws.send(data);
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
ws.on("message", (msg) => {
|
|
1327
|
+
const str = msg.toString();
|
|
1328
|
+
if (str.startsWith("\0{")) {
|
|
1329
|
+
try {
|
|
1330
|
+
const parsed = JSON.parse(str.slice(1));
|
|
1331
|
+
if (parsed.type === "resize" && parsed.cols && parsed.rows) {
|
|
1332
|
+
ptyProcess.resize(
|
|
1333
|
+
Math.max(parsed.cols, 10),
|
|
1334
|
+
Math.max(parsed.rows, 4)
|
|
1335
|
+
);
|
|
1336
|
+
try {
|
|
1337
|
+
execFileSync(TMUX, ["refresh-client", "-t", viewerSession]);
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
ptyProcess.write(str);
|
|
1346
|
+
});
|
|
1347
|
+
ws.on("close", () => {
|
|
1348
|
+
console.log(`[${ts()}] Client disconnected (${tmuxSessionName})`);
|
|
1349
|
+
dataHandler.dispose();
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
await new Promise((resolve) => {
|
|
1353
|
+
httpServer.listen(port, "0.0.0.0", resolve);
|
|
1354
|
+
});
|
|
1355
|
+
console.log(`[${ts()}] WS server on port ${port}`);
|
|
1356
|
+
const tunnelOpts = { port };
|
|
1357
|
+
if (this.config.tunnelHost) {
|
|
1358
|
+
tunnelOpts.host = this.config.tunnelHost;
|
|
1359
|
+
}
|
|
1360
|
+
console.log(
|
|
1361
|
+
`[${ts()}] Opening tunnel...${this.config.tunnelHost ? ` (host: ${this.config.tunnelHost})` : ""}`
|
|
1362
|
+
);
|
|
1363
|
+
const tunnel = await localtunnel(tunnelOpts);
|
|
1364
|
+
const tunnelUrl = tunnel.url;
|
|
1365
|
+
console.log(`[${ts()}] Tunnel: ${tunnelUrl}`);
|
|
1366
|
+
const wsUrl = tunnelUrl.replace(/^https?:\/\//, "wss://");
|
|
1367
|
+
const terminal = {
|
|
1368
|
+
ptyProcess,
|
|
1369
|
+
httpServer,
|
|
1370
|
+
wss,
|
|
1371
|
+
tunnel,
|
|
1372
|
+
viewerSessionName: isLinked ? viewerSession : null,
|
|
1373
|
+
token,
|
|
1374
|
+
workSessionId,
|
|
1375
|
+
tmuxSessionName,
|
|
1376
|
+
port
|
|
1377
|
+
};
|
|
1378
|
+
this.terminals.set(workSessionId, terminal);
|
|
1379
|
+
await this.client.mutation(
|
|
1380
|
+
api.agentBridge.bridgePublic.updateWorkSessionTerminalUrl,
|
|
1381
|
+
{
|
|
1382
|
+
deviceId: this.config.deviceId,
|
|
1383
|
+
deviceSecret: this.config.deviceSecret,
|
|
1384
|
+
workSessionId,
|
|
1385
|
+
terminalUrl: wsUrl,
|
|
1386
|
+
terminalToken: token,
|
|
1387
|
+
terminalLocalPort: port
|
|
1388
|
+
}
|
|
1389
|
+
);
|
|
1390
|
+
ptyProcess.onExit(() => {
|
|
1391
|
+
console.log(`[${ts()}] PTY exited for ${tmuxSessionName}`);
|
|
1392
|
+
this.stopTerminal(workSessionId);
|
|
1393
|
+
});
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
console.error(`[${ts()}] Failed to start terminal:`, err);
|
|
1396
|
+
this.failedSessions.add(workSessionId);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
stopTerminal(workSessionId) {
|
|
1400
|
+
const terminal = this.terminals.get(workSessionId);
|
|
1401
|
+
if (!terminal) return;
|
|
1402
|
+
try {
|
|
1403
|
+
terminal.ptyProcess.kill();
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
try {
|
|
1407
|
+
terminal.tunnel.close();
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
terminal.wss.close();
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1414
|
+
try {
|
|
1415
|
+
terminal.httpServer.close();
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
if (terminal.viewerSessionName) {
|
|
1419
|
+
killViewerSession(terminal.viewerSessionName);
|
|
1420
|
+
}
|
|
1421
|
+
this.terminals.delete(workSessionId);
|
|
1422
|
+
console.log(`[${ts()}] Terminal stopped for ${terminal.tmuxSessionName}`);
|
|
1423
|
+
}
|
|
1424
|
+
stop() {
|
|
1425
|
+
for (const unsub of this.unsubscribers.values()) {
|
|
1426
|
+
try {
|
|
1427
|
+
unsub();
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
this.unsubscribers.clear();
|
|
1432
|
+
for (const id of this.terminals.keys()) {
|
|
1433
|
+
this.stopTerminal(id);
|
|
1434
|
+
}
|
|
1435
|
+
void this.client.close();
|
|
1436
|
+
}
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
// src/bridge-service.ts
|
|
1440
|
+
import {
|
|
1441
|
+
existsSync as existsSync3,
|
|
1442
|
+
mkdirSync,
|
|
1443
|
+
readFileSync as readFileSync2,
|
|
1444
|
+
realpathSync,
|
|
1445
|
+
writeFileSync,
|
|
1446
|
+
unlinkSync
|
|
1447
|
+
} from "fs";
|
|
1448
|
+
import { homedir as homedir3, hostname, platform } from "os";
|
|
1449
|
+
import { dirname, join as join2 } from "path";
|
|
1450
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1451
|
+
|
|
1452
|
+
// src/agent-adapters.ts
|
|
1453
|
+
import { execSync, spawn as spawn2 } from "child_process";
|
|
1454
|
+
import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
|
|
1455
|
+
import { homedir as homedir2, userInfo } from "os";
|
|
1456
|
+
import { basename, join } from "path";
|
|
1457
|
+
var LSOF_PATHS = ["/usr/sbin/lsof", "/usr/bin/lsof"];
|
|
1458
|
+
var VECTOR_BRIDGE_CLIENT_VERSION = "0.1.0";
|
|
1459
|
+
function discoverAttachableSessions() {
|
|
1460
|
+
return dedupeSessions([
|
|
1461
|
+
...discoverTmuxSessions(),
|
|
1462
|
+
...discoverCodexSessions(),
|
|
1463
|
+
...discoverClaudeSessions()
|
|
1464
|
+
]);
|
|
1465
|
+
}
|
|
1466
|
+
async function launchProviderSession(provider, cwd, prompt2, onEvent) {
|
|
1467
|
+
if (provider === "codex") {
|
|
1468
|
+
return runCodexAppServerTurn({
|
|
1469
|
+
cwd,
|
|
1470
|
+
prompt: prompt2,
|
|
1471
|
+
launchCommand: "codex app-server",
|
|
1472
|
+
onEvent
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (provider === "claude_code") {
|
|
1476
|
+
return runClaudeSdkTurn({
|
|
1477
|
+
cwd,
|
|
1478
|
+
prompt: prompt2,
|
|
1479
|
+
launchCommand: "@anthropic-ai/claude-agent-sdk query()",
|
|
1480
|
+
onEvent
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
return runGenericCliAgentTurn({
|
|
1484
|
+
provider,
|
|
1485
|
+
cwd,
|
|
1486
|
+
prompt: prompt2,
|
|
1487
|
+
launchCommand: genericProviderLaunchCommand(provider),
|
|
1488
|
+
onEvent
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
async function resumeProviderSession(provider, sessionKey, cwd, prompt2, onEvent) {
|
|
1492
|
+
if (provider === "codex") {
|
|
1493
|
+
return runCodexAppServerTurn({
|
|
1494
|
+
cwd,
|
|
1495
|
+
prompt: prompt2,
|
|
1496
|
+
sessionKey,
|
|
1497
|
+
launchCommand: "codex app-server (thread/resume)",
|
|
1498
|
+
onEvent
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
if (provider === "claude_code") {
|
|
1502
|
+
return runClaudeSdkTurn({
|
|
1503
|
+
cwd,
|
|
1504
|
+
prompt: prompt2,
|
|
1505
|
+
sessionKey,
|
|
1506
|
+
launchCommand: "@anthropic-ai/claude-agent-sdk query(resume)",
|
|
1507
|
+
onEvent
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
return runGenericCliAgentTurn({
|
|
1511
|
+
provider,
|
|
1512
|
+
cwd,
|
|
1513
|
+
prompt: prompt2,
|
|
1514
|
+
sessionKey,
|
|
1515
|
+
launchCommand: genericProviderLaunchCommand(provider),
|
|
1516
|
+
onEvent
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
async function runGenericCliAgentTurn(args) {
|
|
1520
|
+
const command = genericProviderCommand(args.provider);
|
|
1521
|
+
const child = spawn2(command.bin, [...command.args, args.prompt], {
|
|
1522
|
+
cwd: args.cwd,
|
|
1523
|
+
env: { ...process.env },
|
|
1524
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1525
|
+
});
|
|
1526
|
+
let stdout = "";
|
|
1527
|
+
let stderr = "";
|
|
1528
|
+
await Promise.resolve(
|
|
1529
|
+
args.onEvent?.({
|
|
1530
|
+
provider: args.provider,
|
|
1531
|
+
role: "status",
|
|
1532
|
+
text: `Starting ${providerLabel(args.provider)} CLI session`,
|
|
1533
|
+
status: "in_progress"
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
child.stdout.on("data", (chunk) => {
|
|
1537
|
+
stdout += chunk.toString();
|
|
1538
|
+
});
|
|
1539
|
+
child.stderr.on("data", (chunk) => {
|
|
1540
|
+
stderr += chunk.toString();
|
|
1541
|
+
});
|
|
1542
|
+
const exit = await new Promise((resolve, reject) => {
|
|
1543
|
+
child.on("error", reject);
|
|
1544
|
+
child.on("close", (code, signal) => resolve({ code, signal }));
|
|
1545
|
+
});
|
|
1546
|
+
if (exit.code && exit.code !== 0) {
|
|
1547
|
+
const message = stderr.trim() || `${providerLabel(args.provider)} exited with code ${exit.code}`;
|
|
1548
|
+
await Promise.resolve(
|
|
1549
|
+
args.onEvent?.({
|
|
1550
|
+
provider: args.provider,
|
|
1551
|
+
role: "error",
|
|
1552
|
+
text: message,
|
|
1553
|
+
title: `${providerLabel(args.provider)} failed`,
|
|
1554
|
+
status: "failed"
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
throw new Error(message);
|
|
1558
|
+
}
|
|
1559
|
+
const responseText = stdout.trim();
|
|
1560
|
+
if (responseText) {
|
|
1561
|
+
await Promise.resolve(
|
|
1562
|
+
args.onEvent?.({
|
|
1563
|
+
provider: args.provider,
|
|
1564
|
+
role: "assistant",
|
|
1565
|
+
text: responseText,
|
|
1566
|
+
status: "completed"
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
return {
|
|
1571
|
+
provider: args.provider,
|
|
1572
|
+
providerLabel: providerLabel(args.provider),
|
|
1573
|
+
sessionKey: args.sessionKey ?? `${args.provider}:${Date.now()}`,
|
|
1574
|
+
cwd: args.cwd,
|
|
1575
|
+
...getGitInfo(args.cwd),
|
|
1576
|
+
title: summarizeTitle(responseText, args.cwd) ?? providerLabel(args.provider),
|
|
1577
|
+
mode: "managed",
|
|
1578
|
+
status: "waiting",
|
|
1579
|
+
supportsInboundMessages: true,
|
|
1580
|
+
responseText,
|
|
1581
|
+
launchCommand: args.launchCommand
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function genericProviderCommand(provider) {
|
|
1585
|
+
if (provider === "cursor") return { bin: "cursor-agent", args: ["--print"] };
|
|
1586
|
+
if (provider === "copilot") return { bin: "copilot", args: [] };
|
|
1587
|
+
if (provider === "opencode") return { bin: "opencode", args: ["run"] };
|
|
1588
|
+
return { bin: "pi", args: [] };
|
|
1589
|
+
}
|
|
1590
|
+
function genericProviderLaunchCommand(provider) {
|
|
1591
|
+
const command = genericProviderCommand(provider);
|
|
1592
|
+
return [command.bin, ...command.args].join(" ");
|
|
1593
|
+
}
|
|
1594
|
+
function providerLabel(provider) {
|
|
1595
|
+
if (provider === "codex") return "Codex";
|
|
1596
|
+
if (provider === "claude_code") return "Claude";
|
|
1597
|
+
if (provider === "cursor") return "Cursor";
|
|
1598
|
+
if (provider === "copilot") return "GitHub Copilot";
|
|
1599
|
+
if (provider === "opencode") return "OpenCode";
|
|
1600
|
+
return "Pi";
|
|
1601
|
+
}
|
|
1602
|
+
async function runCodexAppServerTurn(args) {
|
|
1603
|
+
const child = spawn2("codex", ["app-server"], {
|
|
1604
|
+
cwd: args.cwd,
|
|
1605
|
+
env: { ...process.env },
|
|
1606
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1607
|
+
});
|
|
1608
|
+
let stderr = "";
|
|
1609
|
+
let stdoutBuffer = "";
|
|
1610
|
+
let sessionKey = args.sessionKey;
|
|
1611
|
+
let finalAssistantText = "";
|
|
1612
|
+
let activeAssistantText = "";
|
|
1613
|
+
const eventWrites = [];
|
|
1614
|
+
let completed = false;
|
|
1615
|
+
let nextRequestId = 1;
|
|
1616
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1617
|
+
let completeTurn;
|
|
1618
|
+
let failTurn;
|
|
1619
|
+
const turnCompleted = new Promise((resolve, reject) => {
|
|
1620
|
+
completeTurn = () => {
|
|
1621
|
+
completed = true;
|
|
1622
|
+
resolve();
|
|
1623
|
+
};
|
|
1624
|
+
failTurn = (error) => {
|
|
1625
|
+
completed = true;
|
|
1626
|
+
reject(error);
|
|
1627
|
+
};
|
|
1628
|
+
});
|
|
1629
|
+
const emitEvent = (event) => {
|
|
1630
|
+
eventWrites.push(Promise.resolve(args.onEvent?.(event)));
|
|
1631
|
+
};
|
|
1632
|
+
child.stdout.on("data", (chunk) => {
|
|
1633
|
+
stdoutBuffer += chunk.toString();
|
|
1634
|
+
while (true) {
|
|
1635
|
+
const newlineIndex = stdoutBuffer.indexOf("\n");
|
|
1636
|
+
if (newlineIndex < 0) {
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1639
|
+
const line = stdoutBuffer.slice(0, newlineIndex).trim();
|
|
1640
|
+
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
|
1641
|
+
if (!line) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const payload = tryParseJson(line);
|
|
1645
|
+
if (!payload || typeof payload !== "object") {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
const responseId = readProperty(payload, "id");
|
|
1649
|
+
if (typeof responseId === "number" && pending.has(responseId)) {
|
|
1650
|
+
const entry = pending.get(responseId);
|
|
1651
|
+
pending.delete(responseId);
|
|
1652
|
+
const errorRecord = asObject(readProperty(payload, "error"));
|
|
1653
|
+
if (errorRecord) {
|
|
1654
|
+
entry.reject(
|
|
1655
|
+
new Error(
|
|
1656
|
+
`codex app-server error: ${asString(errorRecord.message) ?? "Unknown JSON-RPC error"}`
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
entry.resolve(readProperty(payload, "result"));
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const method = asString(readProperty(payload, "method"));
|
|
1665
|
+
const params = asObject(readProperty(payload, "params"));
|
|
1666
|
+
if (!method || !params) {
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
if (method === "thread/started") {
|
|
1670
|
+
sessionKey = asString(asObject(params.thread)?.id) ?? asString(asObject(params.thread)?.threadId) ?? sessionKey;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
if (method === "item/agentMessage/delta") {
|
|
1674
|
+
const delta = asString(params.delta) ?? "";
|
|
1675
|
+
finalAssistantText += delta;
|
|
1676
|
+
activeAssistantText += delta;
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
if (method === "item/completed") {
|
|
1680
|
+
const item = asObject(params.item);
|
|
1681
|
+
const itemType = asString(item?.type);
|
|
1682
|
+
if (itemType === "agentMessage") {
|
|
1683
|
+
finalAssistantText = asString(item?.text) ?? finalAssistantText;
|
|
1684
|
+
const text2 = finalAssistantText || activeAssistantText;
|
|
1685
|
+
if (text2.trim()) {
|
|
1686
|
+
emitEvent({
|
|
1687
|
+
provider: "codex",
|
|
1688
|
+
role: "assistant",
|
|
1689
|
+
text: text2.trim(),
|
|
1690
|
+
status: "completed"
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
activeAssistantText = "";
|
|
1694
|
+
} else if (itemType === "reasoning") {
|
|
1695
|
+
const text2 = asString(item?.text) ?? asString(item?.summary);
|
|
1696
|
+
if (text2?.trim()) {
|
|
1697
|
+
emitEvent({
|
|
1698
|
+
provider: "codex",
|
|
1699
|
+
role: "reasoning",
|
|
1700
|
+
text: text2.trim(),
|
|
1701
|
+
status: "completed"
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
} else if (itemType === "toolCall") {
|
|
1705
|
+
const title = asString(item?.title) ?? asString(item?.name) ?? asString(item?.command) ?? "Tool";
|
|
1706
|
+
const text2 = asString(item?.text) ?? asString(item?.output) ?? asString(item?.command) ?? title;
|
|
1707
|
+
emitEvent({
|
|
1708
|
+
provider: "codex",
|
|
1709
|
+
role: "tool",
|
|
1710
|
+
title,
|
|
1711
|
+
text: text2,
|
|
1712
|
+
status: asString(item?.status) === "failed" ? "failed" : "completed"
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (method === "turn/completed") {
|
|
1718
|
+
const turn = asObject(params.turn);
|
|
1719
|
+
const status = asString(turn?.status);
|
|
1720
|
+
if (status === "failed") {
|
|
1721
|
+
const turnError = asObject(turn?.error);
|
|
1722
|
+
emitEvent({
|
|
1723
|
+
provider: "codex",
|
|
1724
|
+
role: "error",
|
|
1725
|
+
title: "Codex turn failed",
|
|
1726
|
+
text: asString(turnError?.message) ?? "Codex turn failed without an error message",
|
|
1727
|
+
status: "failed"
|
|
1728
|
+
});
|
|
1729
|
+
failTurn?.(
|
|
1730
|
+
new Error(
|
|
1731
|
+
asString(turnError?.message) ?? "Codex turn failed without an error message"
|
|
1732
|
+
)
|
|
1733
|
+
);
|
|
1734
|
+
} else if (status === "interrupted") {
|
|
1735
|
+
emitEvent({
|
|
1736
|
+
provider: "codex",
|
|
1737
|
+
role: "status",
|
|
1738
|
+
text: "Codex turn was interrupted",
|
|
1739
|
+
status: "failed"
|
|
1740
|
+
});
|
|
1741
|
+
failTurn?.(new Error("Codex turn was interrupted"));
|
|
1742
|
+
} else {
|
|
1743
|
+
completeTurn?.();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
child.stderr.on("data", (chunk) => {
|
|
1749
|
+
stderr += chunk.toString();
|
|
1750
|
+
});
|
|
1751
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
1752
|
+
const id = nextRequestId++;
|
|
1753
|
+
pending.set(id, { resolve, reject });
|
|
1754
|
+
child.stdin.write(`${JSON.stringify({ method, id, params })}
|
|
1755
|
+
`);
|
|
1756
|
+
});
|
|
1757
|
+
const notify = (method, params) => {
|
|
1758
|
+
child.stdin.write(`${JSON.stringify({ method, params })}
|
|
1759
|
+
`);
|
|
1760
|
+
};
|
|
1761
|
+
const waitForExit = new Promise((_, reject) => {
|
|
1762
|
+
child.on("error", (error) => reject(error));
|
|
1763
|
+
child.on("close", (code) => {
|
|
1764
|
+
if (!completed) {
|
|
1765
|
+
const detail = stderr.trim() || `codex app-server exited with code ${code}`;
|
|
1766
|
+
reject(new Error(detail));
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
});
|
|
1770
|
+
try {
|
|
1771
|
+
await Promise.race([
|
|
1772
|
+
request("initialize", {
|
|
1773
|
+
clientInfo: {
|
|
1774
|
+
name: "vector_bridge",
|
|
1775
|
+
title: "Vector Bridge",
|
|
1776
|
+
version: VECTOR_BRIDGE_CLIENT_VERSION
|
|
1777
|
+
}
|
|
1778
|
+
}),
|
|
1779
|
+
waitForExit
|
|
1780
|
+
]);
|
|
1781
|
+
notify("initialized", {});
|
|
1782
|
+
const threadResult = asObject(
|
|
1783
|
+
await Promise.race([
|
|
1784
|
+
args.sessionKey ? request("thread/resume", {
|
|
1785
|
+
threadId: args.sessionKey,
|
|
1786
|
+
cwd: args.cwd,
|
|
1787
|
+
approvalPolicy: "never",
|
|
1788
|
+
personality: "pragmatic"
|
|
1789
|
+
}) : request("thread/start", {
|
|
1790
|
+
cwd: args.cwd,
|
|
1791
|
+
approvalPolicy: "never",
|
|
1792
|
+
personality: "pragmatic",
|
|
1793
|
+
serviceName: "vector_bridge"
|
|
1794
|
+
}),
|
|
1795
|
+
waitForExit
|
|
1796
|
+
])
|
|
1797
|
+
);
|
|
1798
|
+
sessionKey = asString(asObject(readProperty(threadResult, "thread"))?.id) ?? asString(asObject(readProperty(threadResult, "thread"))?.threadId) ?? sessionKey;
|
|
1799
|
+
if (!sessionKey) {
|
|
1800
|
+
throw new Error("Codex app-server did not return a thread id");
|
|
1801
|
+
}
|
|
1802
|
+
await Promise.race([
|
|
1803
|
+
request("turn/start", {
|
|
1804
|
+
threadId: sessionKey,
|
|
1805
|
+
input: [{ type: "text", text: args.prompt }],
|
|
1806
|
+
cwd: args.cwd,
|
|
1807
|
+
approvalPolicy: "never",
|
|
1808
|
+
personality: "pragmatic"
|
|
1809
|
+
}),
|
|
1810
|
+
waitForExit
|
|
1811
|
+
]);
|
|
1812
|
+
await Promise.race([turnCompleted, waitForExit]);
|
|
1813
|
+
await Promise.allSettled(eventWrites);
|
|
1814
|
+
const gitInfo = getGitInfo(args.cwd);
|
|
1815
|
+
return {
|
|
1816
|
+
provider: "codex",
|
|
1817
|
+
providerLabel: "Codex",
|
|
1818
|
+
sessionKey,
|
|
1819
|
+
cwd: args.cwd,
|
|
1820
|
+
...gitInfo,
|
|
1821
|
+
title: summarizeTitle(void 0, args.cwd),
|
|
1822
|
+
mode: "managed",
|
|
1823
|
+
status: "waiting",
|
|
1824
|
+
supportsInboundMessages: true,
|
|
1825
|
+
responseText: finalAssistantText.trim() || void 0,
|
|
1826
|
+
launchCommand: args.launchCommand
|
|
1827
|
+
};
|
|
1828
|
+
} finally {
|
|
1829
|
+
for (const entry of pending.values()) {
|
|
1830
|
+
entry.reject(
|
|
1831
|
+
new Error("codex app-server closed before request resolved")
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
pending.clear();
|
|
1835
|
+
child.kill();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
async function runClaudeSdkTurn(args) {
|
|
1839
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1840
|
+
const stream = query({
|
|
1841
|
+
prompt: args.prompt,
|
|
1842
|
+
options: {
|
|
1843
|
+
cwd: args.cwd,
|
|
1844
|
+
resume: args.sessionKey,
|
|
1845
|
+
persistSession: true,
|
|
1846
|
+
permissionMode: "bypassPermissions",
|
|
1847
|
+
allowDangerouslySkipPermissions: true,
|
|
1848
|
+
env: {
|
|
1849
|
+
...process.env,
|
|
1850
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: `vector-bridge/${VECTOR_BRIDGE_CLIENT_VERSION}`
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
let sessionKey = args.sessionKey;
|
|
1855
|
+
let responseText = "";
|
|
1856
|
+
let model;
|
|
1857
|
+
try {
|
|
1858
|
+
for await (const message of stream) {
|
|
1859
|
+
if (!message || typeof message !== "object") {
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
sessionKey = asString(readProperty(message, "session_id")) ?? sessionKey;
|
|
1863
|
+
if (readProperty(message, "type") === "assistant") {
|
|
1864
|
+
const assistantText = extractClaudeMessageTexts(
|
|
1865
|
+
readProperty(message, "message")
|
|
1866
|
+
).join("\n\n").trim();
|
|
1867
|
+
if (assistantText) {
|
|
1868
|
+
responseText = assistantText;
|
|
1869
|
+
await args.onEvent?.({
|
|
1870
|
+
provider: "claude_code",
|
|
1871
|
+
role: "assistant",
|
|
1872
|
+
text: assistantText,
|
|
1873
|
+
status: "completed"
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (readProperty(message, "type") !== "result") {
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (readProperty(message, "subtype") === "success") {
|
|
1882
|
+
const resultText = asString(readProperty(message, "result"));
|
|
1883
|
+
if (resultText) {
|
|
1884
|
+
responseText = resultText;
|
|
1885
|
+
await args.onEvent?.({
|
|
1886
|
+
provider: "claude_code",
|
|
1887
|
+
role: "assistant",
|
|
1888
|
+
text: resultText,
|
|
1889
|
+
status: "completed"
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
model = firstObjectKey(readProperty(message, "modelUsage"));
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
const errors = readProperty(message, "errors");
|
|
1896
|
+
const detail = Array.isArray(errors) && errors.length > 0 ? errors.join("\n") : "Claude execution failed";
|
|
1897
|
+
throw new Error(detail);
|
|
1898
|
+
}
|
|
1899
|
+
} finally {
|
|
1900
|
+
stream.close();
|
|
1901
|
+
}
|
|
1902
|
+
if (!sessionKey) {
|
|
1903
|
+
throw new Error("Claude Agent SDK did not return a session id");
|
|
1904
|
+
}
|
|
1905
|
+
const gitInfo = getGitInfo(args.cwd);
|
|
1906
|
+
return {
|
|
1907
|
+
provider: "claude_code",
|
|
1908
|
+
providerLabel: "Claude",
|
|
1909
|
+
sessionKey,
|
|
1910
|
+
cwd: args.cwd,
|
|
1911
|
+
...gitInfo,
|
|
1912
|
+
title: summarizeTitle(void 0, args.cwd),
|
|
1913
|
+
model,
|
|
1914
|
+
mode: "managed",
|
|
1915
|
+
status: "waiting",
|
|
1916
|
+
supportsInboundMessages: true,
|
|
1917
|
+
responseText: responseText.trim() || void 0,
|
|
1918
|
+
launchCommand: args.launchCommand
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function discoverCodexSessions() {
|
|
1922
|
+
const historyBySession = buildCodexHistoryIndex();
|
|
1923
|
+
return listLiveProcessIds("codex").flatMap((pid) => {
|
|
1924
|
+
const transcriptPath = getCodexTranscriptPath(pid);
|
|
1925
|
+
if (!transcriptPath) {
|
|
1926
|
+
return [];
|
|
1927
|
+
}
|
|
1928
|
+
const processCwd = getProcessCwd(pid);
|
|
1929
|
+
const parsed = parseObservedCodexSession(
|
|
1930
|
+
pid,
|
|
1931
|
+
transcriptPath,
|
|
1932
|
+
processCwd,
|
|
1933
|
+
historyBySession
|
|
1934
|
+
);
|
|
1935
|
+
return parsed ? [parsed] : [];
|
|
1936
|
+
}).sort(compareObservedSessions);
|
|
1937
|
+
}
|
|
1938
|
+
function discoverClaudeSessions() {
|
|
1939
|
+
const historyBySession = buildClaudeHistoryIndex();
|
|
1940
|
+
return listLiveProcessIds("claude").flatMap((pid) => {
|
|
1941
|
+
const sessionMeta = readClaudePidSession(pid);
|
|
1942
|
+
if (!sessionMeta?.sessionId) {
|
|
1943
|
+
return [];
|
|
1944
|
+
}
|
|
1945
|
+
const transcriptPath = findClaudeTranscriptPath(sessionMeta.sessionId);
|
|
1946
|
+
const parsed = parseObservedClaudeSession(
|
|
1947
|
+
pid,
|
|
1948
|
+
sessionMeta,
|
|
1949
|
+
transcriptPath,
|
|
1950
|
+
historyBySession
|
|
1951
|
+
);
|
|
1952
|
+
return parsed ? [parsed] : [];
|
|
1953
|
+
}).sort(compareObservedSessions);
|
|
1954
|
+
}
|
|
1955
|
+
function discoverTmuxSessions() {
|
|
1956
|
+
try {
|
|
1957
|
+
const output = execSync(
|
|
1958
|
+
"tmux list-panes -a -F '#{pane_id} #{pane_pid} #{session_name} #{window_name} #{pane_current_path} #{pane_current_command} #{pane_title}'",
|
|
1959
|
+
{
|
|
1960
|
+
encoding: "utf-8",
|
|
1961
|
+
timeout: 3e3
|
|
1962
|
+
}
|
|
1963
|
+
);
|
|
1964
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1965
|
+
const [
|
|
1966
|
+
paneId,
|
|
1967
|
+
panePid,
|
|
1968
|
+
sessionName,
|
|
1969
|
+
windowName,
|
|
1970
|
+
cwd,
|
|
1971
|
+
currentCommand,
|
|
1972
|
+
paneTitle
|
|
1973
|
+
] = line.split(" ");
|
|
1974
|
+
if (!paneId || !panePid || !sessionName || !windowName || !cwd) {
|
|
1975
|
+
return [];
|
|
1976
|
+
}
|
|
1977
|
+
const normalizedCommand = (currentCommand ?? "").trim().toLowerCase();
|
|
1978
|
+
if (normalizedCommand === "codex" || normalizedCommand === "claude") {
|
|
1979
|
+
return [];
|
|
1980
|
+
}
|
|
1981
|
+
const gitInfo = getGitInfo(cwd);
|
|
1982
|
+
const title = summarizeTitle(
|
|
1983
|
+
buildTmuxPaneTitle({
|
|
1984
|
+
paneTitle,
|
|
1985
|
+
sessionName,
|
|
1986
|
+
windowName,
|
|
1987
|
+
cwd,
|
|
1988
|
+
currentCommand
|
|
1989
|
+
}),
|
|
1990
|
+
cwd
|
|
1991
|
+
);
|
|
1992
|
+
return [
|
|
1993
|
+
{
|
|
1994
|
+
provider: "vector_cli",
|
|
1995
|
+
providerLabel: "Tmux",
|
|
1996
|
+
localProcessId: panePid,
|
|
1997
|
+
sessionKey: `tmux:${paneId}`,
|
|
1998
|
+
cwd,
|
|
1999
|
+
...gitInfo,
|
|
2000
|
+
title,
|
|
2001
|
+
tmuxSessionName: sessionName,
|
|
2002
|
+
tmuxWindowName: windowName,
|
|
2003
|
+
tmuxPaneId: paneId,
|
|
2004
|
+
mode: "observed",
|
|
2005
|
+
status: "observed",
|
|
2006
|
+
supportsInboundMessages: true
|
|
2007
|
+
}
|
|
2008
|
+
];
|
|
2009
|
+
}).sort(compareObservedSessions);
|
|
2010
|
+
} catch {
|
|
2011
|
+
return [];
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function getCodexHistoryFile() {
|
|
2015
|
+
return join(getRealHomeDir(), ".codex", "history.jsonl");
|
|
2016
|
+
}
|
|
2017
|
+
function getClaudeProjectsDir() {
|
|
2018
|
+
return join(getRealHomeDir(), ".claude", "projects");
|
|
2019
|
+
}
|
|
2020
|
+
function getClaudeSessionStateDir() {
|
|
2021
|
+
return join(getRealHomeDir(), ".claude", "sessions");
|
|
2022
|
+
}
|
|
2023
|
+
function getClaudeHistoryFile() {
|
|
2024
|
+
return join(getRealHomeDir(), ".claude", "history.jsonl");
|
|
2025
|
+
}
|
|
2026
|
+
function getRealHomeDir() {
|
|
2027
|
+
try {
|
|
2028
|
+
const realHome = userInfo().homedir?.trim();
|
|
2029
|
+
if (realHome) {
|
|
2030
|
+
return realHome;
|
|
2031
|
+
}
|
|
2032
|
+
} catch {
|
|
2033
|
+
}
|
|
2034
|
+
return homedir2();
|
|
2035
|
+
}
|
|
2036
|
+
function resolveExecutable(fallbackCommand, absoluteCandidates) {
|
|
2037
|
+
for (const candidate of absoluteCandidates) {
|
|
2038
|
+
if (existsSync2(candidate)) {
|
|
2039
|
+
return candidate;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
const output = execSync(`command -v ${fallbackCommand}`, {
|
|
2044
|
+
encoding: "utf-8",
|
|
2045
|
+
timeout: 1e3
|
|
2046
|
+
}).trim();
|
|
2047
|
+
return output || void 0;
|
|
2048
|
+
} catch {
|
|
2049
|
+
return void 0;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function listLiveProcessIds(commandName) {
|
|
2053
|
+
try {
|
|
2054
|
+
const output = execSync("ps -axo pid=,comm=", {
|
|
2055
|
+
encoding: "utf-8",
|
|
2056
|
+
timeout: 3e3
|
|
2057
|
+
});
|
|
2058
|
+
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);
|
|
2059
|
+
} catch {
|
|
2060
|
+
return [];
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
function getProcessCwd(pid) {
|
|
2064
|
+
const lsofCommand = resolveExecutable("lsof", LSOF_PATHS);
|
|
2065
|
+
if (!lsofCommand) {
|
|
2066
|
+
return void 0;
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
const output = execSync(`${lsofCommand} -a -p ${pid} -Fn -d cwd`, {
|
|
2070
|
+
encoding: "utf-8",
|
|
2071
|
+
timeout: 3e3
|
|
2072
|
+
});
|
|
2073
|
+
return output.split("\n").map((line) => line.trim()).find((line) => line.startsWith("n"))?.slice(1);
|
|
2074
|
+
} catch {
|
|
2075
|
+
return void 0;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
function getCodexTranscriptPath(pid) {
|
|
2079
|
+
const lsofCommand = resolveExecutable("lsof", LSOF_PATHS);
|
|
2080
|
+
if (!lsofCommand) {
|
|
2081
|
+
return void 0;
|
|
2082
|
+
}
|
|
2083
|
+
try {
|
|
2084
|
+
const output = execSync(`${lsofCommand} -p ${pid} -Fn`, {
|
|
2085
|
+
encoding: "utf-8",
|
|
2086
|
+
timeout: 3e3
|
|
2087
|
+
});
|
|
2088
|
+
return output.split("\n").map((line) => line.trim()).find(
|
|
2089
|
+
(line) => line.startsWith("n") && line.includes("/.codex/sessions/") && line.endsWith(".jsonl")
|
|
2090
|
+
)?.slice(1);
|
|
2091
|
+
} catch {
|
|
2092
|
+
return void 0;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function readClaudePidSession(pid) {
|
|
2096
|
+
const path3 = join(getClaudeSessionStateDir(), `${pid}.json`);
|
|
2097
|
+
if (!existsSync2(path3)) {
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
const payload = JSON.parse(readFileSync(path3, "utf-8"));
|
|
2102
|
+
const sessionId = asString(payload.sessionId);
|
|
2103
|
+
if (!sessionId) {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
return {
|
|
2107
|
+
sessionId,
|
|
2108
|
+
cwd: asString(payload.cwd),
|
|
2109
|
+
startedAt: typeof payload.startedAt === "number" ? payload.startedAt : void 0
|
|
2110
|
+
};
|
|
2111
|
+
} catch {
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function findClaudeTranscriptPath(sessionId) {
|
|
2116
|
+
return findJsonlFileByStem(getClaudeProjectsDir(), sessionId);
|
|
2117
|
+
}
|
|
2118
|
+
function findJsonlFileByStem(root, stem) {
|
|
2119
|
+
if (!existsSync2(root)) {
|
|
2120
|
+
return void 0;
|
|
2121
|
+
}
|
|
2122
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
2123
|
+
const path3 = join(root, entry.name);
|
|
2124
|
+
if (entry.isDirectory()) {
|
|
2125
|
+
const nested = findJsonlFileByStem(path3, stem);
|
|
2126
|
+
if (nested) {
|
|
2127
|
+
return nested;
|
|
2128
|
+
}
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
if (entry.isFile() && entry.name === `${stem}.jsonl`) {
|
|
2132
|
+
return path3;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
return void 0;
|
|
2136
|
+
}
|
|
2137
|
+
function readJsonLines(path3) {
|
|
2138
|
+
try {
|
|
2139
|
+
return readFileSync(path3, "utf-8").split("\n").map((line) => line.trim()).filter(Boolean).map(tryParseJson).filter(Boolean);
|
|
2140
|
+
} catch {
|
|
2141
|
+
return [];
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
function tryParseJson(value) {
|
|
2145
|
+
try {
|
|
2146
|
+
return JSON.parse(value);
|
|
2147
|
+
} catch {
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function dedupeSessions(sessions) {
|
|
2152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2153
|
+
return sessions.filter((session) => {
|
|
2154
|
+
const key = `${session.provider}:${session.localProcessId ?? session.sessionKey}`;
|
|
2155
|
+
if (seen.has(key)) {
|
|
2156
|
+
return false;
|
|
2157
|
+
}
|
|
2158
|
+
seen.add(key);
|
|
2159
|
+
return true;
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
function compareObservedSessions(a, b) {
|
|
2163
|
+
return Number(b.localProcessId ?? 0) - Number(a.localProcessId ?? 0);
|
|
2164
|
+
}
|
|
2165
|
+
function parseObservedCodexSession(pid, transcriptPath, processCwd, historyBySession) {
|
|
2166
|
+
const entries = readJsonLines(transcriptPath);
|
|
2167
|
+
let sessionKey;
|
|
2168
|
+
let cwd = processCwd;
|
|
2169
|
+
const userMessages = [];
|
|
2170
|
+
const assistantMessages = [];
|
|
2171
|
+
for (const rawEntry of entries) {
|
|
2172
|
+
const entry = asObject(rawEntry);
|
|
2173
|
+
if (!entry) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
if (entry.type === "session_meta") {
|
|
2177
|
+
const payload = asObject(entry.payload);
|
|
2178
|
+
sessionKey = asString(payload?.id) ?? sessionKey;
|
|
2179
|
+
cwd = asString(payload?.cwd) ?? cwd;
|
|
2180
|
+
}
|
|
2181
|
+
if (entry.type === "event_msg") {
|
|
2182
|
+
const payload = asObject(entry.payload);
|
|
2183
|
+
if (payload?.type === "user_message") {
|
|
2184
|
+
pushIfPresent(userMessages, payload.message);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (entry.type === "response_item" && asObject(entry.payload)?.type === "message" && asObject(entry.payload)?.role === "user") {
|
|
2188
|
+
userMessages.push(
|
|
2189
|
+
...extractTextSegments(asObject(entry.payload)?.content)
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
if (entry.type === "event_msg") {
|
|
2193
|
+
const payload = asObject(entry.payload);
|
|
2194
|
+
if (payload?.type === "agent_message") {
|
|
2195
|
+
pushIfPresent(assistantMessages, payload.message);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (entry.type === "response_item" && asObject(entry.payload)?.type === "message" && asObject(entry.payload)?.role === "assistant") {
|
|
2199
|
+
assistantMessages.push(
|
|
2200
|
+
...extractTextSegments(asObject(entry.payload)?.content)
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (!sessionKey) {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
const gitInfo = cwd ? getGitInfo(cwd) : {};
|
|
2208
|
+
const historyTitle = sessionKey ? selectSessionTitle(historyBySession?.get(sessionKey) ?? []) : void 0;
|
|
2209
|
+
return {
|
|
2210
|
+
provider: "codex",
|
|
2211
|
+
providerLabel: "Codex",
|
|
2212
|
+
localProcessId: pid,
|
|
2213
|
+
sessionKey,
|
|
2214
|
+
cwd,
|
|
2215
|
+
...gitInfo,
|
|
2216
|
+
title: summarizeTitle(
|
|
2217
|
+
historyTitle ?? selectSessionTitle(userMessages) ?? selectSessionTitle(assistantMessages),
|
|
2218
|
+
cwd
|
|
2219
|
+
),
|
|
2220
|
+
mode: "observed",
|
|
2221
|
+
status: "observed",
|
|
2222
|
+
supportsInboundMessages: true
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function parseObservedClaudeSession(pid, sessionMeta, transcriptPath, historyBySession) {
|
|
2226
|
+
const entries = transcriptPath ? readJsonLines(transcriptPath) : [];
|
|
2227
|
+
let cwd = sessionMeta.cwd;
|
|
2228
|
+
let branch;
|
|
2229
|
+
let model;
|
|
2230
|
+
const userMessages = [];
|
|
2231
|
+
const assistantMessages = [];
|
|
2232
|
+
for (const rawEntry of entries) {
|
|
2233
|
+
const entry = asObject(rawEntry);
|
|
2234
|
+
if (!entry) {
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
cwd = asString(entry.cwd) ?? cwd;
|
|
2238
|
+
branch = asString(entry.gitBranch) ?? branch;
|
|
2239
|
+
if (entry.type === "user") {
|
|
2240
|
+
userMessages.push(...extractClaudeMessageTexts(entry.message));
|
|
2241
|
+
}
|
|
2242
|
+
if (entry.type === "assistant") {
|
|
2243
|
+
const message = asObject(entry.message);
|
|
2244
|
+
model = asString(message?.model) ?? model;
|
|
2245
|
+
assistantMessages.push(...extractClaudeMessageTexts(entry.message));
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const gitInfo = cwd ? getGitInfo(cwd) : {};
|
|
2249
|
+
const historyTitle = selectSessionTitle(
|
|
2250
|
+
historyBySession?.get(sessionMeta.sessionId) ?? []
|
|
2251
|
+
);
|
|
2252
|
+
return {
|
|
2253
|
+
provider: "claude_code",
|
|
2254
|
+
providerLabel: "Claude",
|
|
2255
|
+
localProcessId: pid,
|
|
2256
|
+
sessionKey: sessionMeta.sessionId,
|
|
2257
|
+
cwd,
|
|
2258
|
+
repoRoot: gitInfo.repoRoot,
|
|
2259
|
+
branch: branch ?? gitInfo.branch,
|
|
2260
|
+
title: summarizeTitle(
|
|
2261
|
+
historyTitle ?? selectSessionTitle(userMessages) ?? selectSessionTitle(assistantMessages),
|
|
2262
|
+
cwd
|
|
2263
|
+
),
|
|
2264
|
+
model,
|
|
2265
|
+
mode: "observed",
|
|
2266
|
+
status: "observed",
|
|
2267
|
+
supportsInboundMessages: true
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
function summarizeTitle(message, cwd) {
|
|
2271
|
+
if (message) {
|
|
2272
|
+
return truncate(message.replace(/\s+/g, " ").trim(), 96);
|
|
2273
|
+
}
|
|
2274
|
+
if (cwd) {
|
|
2275
|
+
return basename(cwd);
|
|
2276
|
+
}
|
|
2277
|
+
return "Local session";
|
|
2278
|
+
}
|
|
2279
|
+
function buildTmuxPaneTitle(args) {
|
|
2280
|
+
const paneTitle = cleanSessionTitleCandidate(args.paneTitle ?? "");
|
|
2281
|
+
if (paneTitle) {
|
|
2282
|
+
return paneTitle;
|
|
2283
|
+
}
|
|
2284
|
+
const command = asString(args.currentCommand);
|
|
2285
|
+
if (command && !["zsh", "bash", "fish", "sh", "nu"].includes(command)) {
|
|
2286
|
+
return `${command} in ${basename(args.cwd)}`;
|
|
2287
|
+
}
|
|
2288
|
+
return `${basename(args.cwd)} (${args.sessionName}:${args.windowName})`;
|
|
2289
|
+
}
|
|
2290
|
+
function truncate(value, maxLength) {
|
|
2291
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 3).trimEnd()}...` : value;
|
|
2292
|
+
}
|
|
2293
|
+
function firstObjectKey(value) {
|
|
2294
|
+
if (!value || typeof value !== "object") {
|
|
2295
|
+
return void 0;
|
|
2296
|
+
}
|
|
2297
|
+
const [firstKey] = Object.keys(value);
|
|
2298
|
+
return firstKey ? normalizeModelKey(firstKey) : void 0;
|
|
2299
|
+
}
|
|
2300
|
+
function normalizeModelKey(value) {
|
|
2301
|
+
const normalized = stripAnsi(value).replace(/\[\d+(?:;\d+)*m$/g, "").trim();
|
|
2302
|
+
return normalized || void 0;
|
|
2303
|
+
}
|
|
2304
|
+
function asObject(value) {
|
|
2305
|
+
return isRecord(value) ? value : void 0;
|
|
2306
|
+
}
|
|
2307
|
+
function readProperty(value, key) {
|
|
2308
|
+
return isRecord(value) ? Reflect.get(value, key) : void 0;
|
|
2309
|
+
}
|
|
2310
|
+
function asString(value) {
|
|
2311
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
2312
|
+
}
|
|
2313
|
+
function isRecord(value) {
|
|
2314
|
+
return value !== null && typeof value === "object";
|
|
2315
|
+
}
|
|
2316
|
+
function pushIfPresent(target, value) {
|
|
2317
|
+
const text2 = asString(value);
|
|
2318
|
+
if (text2) {
|
|
2319
|
+
target.push(text2);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function extractClaudeMessageTexts(message) {
|
|
2323
|
+
if (!message || typeof message !== "object") {
|
|
2324
|
+
return [];
|
|
2325
|
+
}
|
|
2326
|
+
return extractTextSegments(message.content);
|
|
2327
|
+
}
|
|
2328
|
+
function extractTextSegments(value) {
|
|
2329
|
+
if (typeof value === "string") {
|
|
2330
|
+
return [value];
|
|
2331
|
+
}
|
|
2332
|
+
if (!Array.isArray(value)) {
|
|
2333
|
+
return [];
|
|
2334
|
+
}
|
|
2335
|
+
return value.flatMap(extractTextSegmentFromBlock).filter(Boolean);
|
|
2336
|
+
}
|
|
2337
|
+
function extractTextSegmentFromBlock(block) {
|
|
2338
|
+
if (!block || typeof block !== "object") {
|
|
2339
|
+
return [];
|
|
2340
|
+
}
|
|
2341
|
+
const typedBlock = block;
|
|
2342
|
+
const blockType = asString(typedBlock.type);
|
|
2343
|
+
if (blockType && isIgnoredContentBlockType(blockType)) {
|
|
2344
|
+
return [];
|
|
2345
|
+
}
|
|
2346
|
+
const directText = asString(typedBlock.text);
|
|
2347
|
+
if (directText) {
|
|
2348
|
+
return [directText];
|
|
2349
|
+
}
|
|
2350
|
+
if (typeof typedBlock.content === "string") {
|
|
2351
|
+
return [typedBlock.content];
|
|
2352
|
+
}
|
|
2353
|
+
return [];
|
|
2354
|
+
}
|
|
2355
|
+
function isIgnoredContentBlockType(blockType) {
|
|
2356
|
+
return [
|
|
2357
|
+
"tool_result",
|
|
2358
|
+
"tool_use",
|
|
2359
|
+
"image",
|
|
2360
|
+
"thinking",
|
|
2361
|
+
"reasoning",
|
|
2362
|
+
"contextCompaction"
|
|
2363
|
+
].includes(blockType);
|
|
2364
|
+
}
|
|
2365
|
+
function buildCodexHistoryIndex() {
|
|
2366
|
+
const historyBySession = /* @__PURE__ */ new Map();
|
|
2367
|
+
for (const rawEntry of readJsonLines(getCodexHistoryFile())) {
|
|
2368
|
+
const entry = asObject(rawEntry);
|
|
2369
|
+
if (!entry) {
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
const sessionId = asString(entry.session_id);
|
|
2373
|
+
const text2 = asString(entry.text);
|
|
2374
|
+
if (!sessionId || !text2) {
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
appendHistoryEntry(historyBySession, sessionId, text2);
|
|
2378
|
+
}
|
|
2379
|
+
return historyBySession;
|
|
2380
|
+
}
|
|
2381
|
+
function buildClaudeHistoryIndex() {
|
|
2382
|
+
const historyBySession = /* @__PURE__ */ new Map();
|
|
2383
|
+
for (const rawEntry of readJsonLines(getClaudeHistoryFile())) {
|
|
2384
|
+
const entry = asObject(rawEntry);
|
|
2385
|
+
if (!entry) {
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
const sessionId = asString(entry.sessionId);
|
|
2389
|
+
if (!sessionId) {
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const texts = extractClaudeHistoryTexts(entry);
|
|
2393
|
+
for (const text2 of texts) {
|
|
2394
|
+
appendHistoryEntry(historyBySession, sessionId, text2);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return historyBySession;
|
|
2398
|
+
}
|
|
2399
|
+
function appendHistoryEntry(historyBySession, sessionId, text2) {
|
|
2400
|
+
const existing = historyBySession.get(sessionId);
|
|
2401
|
+
if (existing) {
|
|
2402
|
+
existing.push(text2);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
historyBySession.set(sessionId, [text2]);
|
|
2406
|
+
}
|
|
2407
|
+
function extractClaudeHistoryTexts(entry) {
|
|
2408
|
+
if (!entry || typeof entry !== "object") {
|
|
2409
|
+
return [];
|
|
2410
|
+
}
|
|
2411
|
+
const record = entry;
|
|
2412
|
+
const pastedTexts = extractClaudePastedTexts(record.pastedContents);
|
|
2413
|
+
if (pastedTexts.length > 0) {
|
|
2414
|
+
return pastedTexts;
|
|
2415
|
+
}
|
|
2416
|
+
const display = asString(record.display);
|
|
2417
|
+
return display ? [display] : [];
|
|
2418
|
+
}
|
|
2419
|
+
function extractClaudePastedTexts(value) {
|
|
2420
|
+
if (!value || typeof value !== "object") {
|
|
2421
|
+
return [];
|
|
2422
|
+
}
|
|
2423
|
+
return Object.values(value).flatMap((item) => {
|
|
2424
|
+
if (!item || typeof item !== "object") {
|
|
2425
|
+
return [];
|
|
2426
|
+
}
|
|
2427
|
+
const record = item;
|
|
2428
|
+
return record.type === "text" && typeof record.content === "string" ? [record.content] : [];
|
|
2429
|
+
}).filter(Boolean);
|
|
2430
|
+
}
|
|
2431
|
+
function selectSessionTitle(messages) {
|
|
2432
|
+
for (const message of messages) {
|
|
2433
|
+
const cleaned = cleanSessionTitleCandidate(message);
|
|
2434
|
+
if (cleaned) {
|
|
2435
|
+
return cleaned;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return void 0;
|
|
2439
|
+
}
|
|
2440
|
+
function cleanSessionTitleCandidate(message) {
|
|
2441
|
+
const normalized = stripAnsi(message).replace(/\s+/g, " ").trim();
|
|
2442
|
+
if (!normalized) {
|
|
2443
|
+
return void 0;
|
|
2444
|
+
}
|
|
2445
|
+
if (normalized.length < 4) {
|
|
2446
|
+
return void 0;
|
|
2447
|
+
}
|
|
2448
|
+
if (normalized.startsWith("/") || looksLikeGeneratedTagEnvelope(normalized) || looksLikeGeneratedImageSummary(normalized) || looksLikeStandaloneImagePath(normalized) || looksLikeInstructionScaffold(normalized)) {
|
|
2449
|
+
return void 0;
|
|
2450
|
+
}
|
|
2451
|
+
return normalized;
|
|
2452
|
+
}
|
|
2453
|
+
function looksLikeGeneratedTagEnvelope(value) {
|
|
2454
|
+
return /^<[\w:-]+>[\s\S]*<\/[\w:-]+>$/.test(value);
|
|
2455
|
+
}
|
|
2456
|
+
function looksLikeGeneratedImageSummary(value) {
|
|
2457
|
+
return /^\[image:/i.test(value) || /displayed at/i.test(value) && /coordinates/i.test(value);
|
|
2458
|
+
}
|
|
2459
|
+
function looksLikeStandaloneImagePath(value) {
|
|
2460
|
+
return /^\/\S+\.(png|jpe?g|gif|webp|heic|bmp)$/i.test(value) || /^file:\S+\.(png|jpe?g|gif|webp|heic|bmp)$/i.test(value);
|
|
2461
|
+
}
|
|
2462
|
+
function looksLikeInstructionScaffold(value) {
|
|
2463
|
+
if (value.length < 700) {
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
const headingCount = value.match(/^#{1,3}\s/gm)?.length ?? 0;
|
|
2467
|
+
const tagCount = value.match(/<\/?[\w:-]+>/g)?.length ?? 0;
|
|
2468
|
+
const bulletCount = value.match(/^\s*[-*]\s/gm)?.length ?? 0;
|
|
2469
|
+
return headingCount + tagCount + bulletCount >= 6;
|
|
2470
|
+
}
|
|
2471
|
+
function stripAnsi(value) {
|
|
2472
|
+
return value.replace(/\u001B\[[0-9;]*m/g, "");
|
|
2473
|
+
}
|
|
2474
|
+
function getGitInfo(cwd) {
|
|
2475
|
+
try {
|
|
2476
|
+
const repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
2477
|
+
encoding: "utf-8",
|
|
2478
|
+
cwd,
|
|
2479
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2480
|
+
timeout: 3e3
|
|
2481
|
+
}).trim();
|
|
2482
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
2483
|
+
encoding: "utf-8",
|
|
2484
|
+
cwd,
|
|
2485
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2486
|
+
timeout: 3e3
|
|
2487
|
+
}).trim();
|
|
2488
|
+
return {
|
|
2489
|
+
repoRoot: repoRoot || void 0,
|
|
2490
|
+
branch: branch || void 0
|
|
2491
|
+
};
|
|
2492
|
+
} catch {
|
|
2493
|
+
return {};
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// src/bridge-service.ts
|
|
2498
|
+
var CONFIG_DIR = process.env.VECTOR_HOME?.trim() || join2(homedir3(), ".vector");
|
|
2499
|
+
var BRIDGE_CONFIG_FILE = join2(CONFIG_DIR, "bridge.json");
|
|
2500
|
+
var DEVICE_KEY_FILE = join2(CONFIG_DIR, "device-key");
|
|
2501
|
+
var PID_FILE = join2(CONFIG_DIR, "bridge.pid");
|
|
2502
|
+
var LIVE_ACTIVITIES_CACHE = join2(CONFIG_DIR, "live-activities.json");
|
|
2503
|
+
var LAUNCHAGENT_DIR = join2(homedir3(), "Library", "LaunchAgents");
|
|
2504
|
+
var LAUNCHAGENT_PLIST = join2(LAUNCHAGENT_DIR, "com.vector.bridge.plist");
|
|
2505
|
+
var LAUNCHAGENT_LABEL = "com.vector.bridge";
|
|
2506
|
+
var LEGACY_MENUBAR_LAUNCHAGENT_LABEL = "com.vector.menubar";
|
|
2507
|
+
var LEGACY_MENUBAR_LAUNCHAGENT_PLIST = join2(
|
|
2508
|
+
LAUNCHAGENT_DIR,
|
|
2509
|
+
`${LEGACY_MENUBAR_LAUNCHAGENT_LABEL}.plist`
|
|
2510
|
+
);
|
|
2511
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
2512
|
+
var COMMAND_POLL_INTERVAL_MS = 5e3;
|
|
2513
|
+
var LIVE_ACTIVITY_SYNC_INTERVAL_MS = 5e3;
|
|
2514
|
+
var PROCESS_DISCOVERY_INTERVAL_MS = 6e4;
|
|
2515
|
+
var TERMINAL_SNAPSHOT_REFRESH_INTERVAL_MS = 18e4;
|
|
2516
|
+
function loadBridgeConfig() {
|
|
2517
|
+
if (!existsSync3(BRIDGE_CONFIG_FILE)) return null;
|
|
2518
|
+
try {
|
|
2519
|
+
return JSON.parse(readFileSync2(BRIDGE_CONFIG_FILE, "utf-8"));
|
|
2520
|
+
} catch {
|
|
2521
|
+
return null;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
function saveBridgeConfig(config) {
|
|
2525
|
+
if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2526
|
+
writeFileSync(BRIDGE_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
2527
|
+
persistDeviceKey(config.deviceKey);
|
|
2528
|
+
}
|
|
2529
|
+
function writeLiveActivitiesCache(activities) {
|
|
2530
|
+
if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2531
|
+
writeFileSync(LIVE_ACTIVITIES_CACHE, JSON.stringify(activities, null, 2));
|
|
2532
|
+
}
|
|
2533
|
+
var BridgeService = class {
|
|
2534
|
+
constructor(config) {
|
|
2535
|
+
this.timers = [];
|
|
2536
|
+
this.terminalPeer = null;
|
|
2537
|
+
this.stopping = false;
|
|
2538
|
+
this.runningLoops = /* @__PURE__ */ new Set();
|
|
2539
|
+
this.deviceLiveActivities = [];
|
|
2540
|
+
this.config = config;
|
|
2541
|
+
this.client = new ConvexHttpClient2(config.convexUrl);
|
|
2542
|
+
}
|
|
2543
|
+
async heartbeat() {
|
|
2544
|
+
await this.client.mutation(api.agentBridge.bridgePublic.heartbeat, {
|
|
2545
|
+
deviceId: this.config.deviceId,
|
|
2546
|
+
deviceSecret: this.config.deviceSecret
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
async pollCommands() {
|
|
2550
|
+
const commands = await this.client.query(
|
|
2551
|
+
api.agentBridge.bridgePublic.getPendingCommands,
|
|
2552
|
+
{
|
|
2553
|
+
deviceId: this.config.deviceId,
|
|
2554
|
+
deviceSecret: this.config.deviceSecret
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
if (commands.length > 0) {
|
|
2558
|
+
console.log(`[${ts2()}] ${commands.length} pending command(s)`);
|
|
2559
|
+
}
|
|
2560
|
+
for (const cmd of commands) {
|
|
2561
|
+
await this.handleCommand(cmd);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
scheduleLoop(name, intervalMs, run) {
|
|
2565
|
+
this.timers.push(
|
|
2566
|
+
setInterval(() => {
|
|
2567
|
+
if (this.stopping || this.runningLoops.has(name)) {
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
this.runningLoops.add(name);
|
|
2571
|
+
run().catch((error) => {
|
|
2572
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2573
|
+
console.error(`[${ts2()}] ${name} error:`, message);
|
|
2574
|
+
}).finally(() => {
|
|
2575
|
+
this.runningLoops.delete(name);
|
|
2576
|
+
});
|
|
2577
|
+
}, intervalMs)
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
async runStartupStep(label, step) {
|
|
2581
|
+
try {
|
|
2582
|
+
await step();
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2585
|
+
console.error(`[${ts2()}] Startup ${label} failed: ${message}`);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
async handleCommand(cmd) {
|
|
2589
|
+
const claimed = await this.client.mutation(
|
|
2590
|
+
api.agentBridge.bridgePublic.claimCommand,
|
|
2591
|
+
{
|
|
2592
|
+
deviceId: this.config.deviceId,
|
|
2593
|
+
deviceSecret: this.config.deviceSecret,
|
|
2594
|
+
commandId: cmd._id
|
|
2595
|
+
}
|
|
2596
|
+
);
|
|
2597
|
+
if (!claimed) {
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
if (cmd.kind === "settings_update" || cmd.kind === "queue_update" || cmd.kind === "approval_response" || cmd.kind === "plan_response" || cmd.kind === "question_response" || cmd.kind === "stop" || cmd.kind === "resume") {
|
|
2601
|
+
try {
|
|
2602
|
+
await this.handleAgentControlCommand(cmd);
|
|
2603
|
+
await this.completeCommand(cmd._id, "delivered");
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
const message = error instanceof Error ? error.message : "Unknown bridge error";
|
|
2606
|
+
console.error(` ! ${message}`);
|
|
2607
|
+
await this.postCommandError(cmd, message);
|
|
2608
|
+
await this.completeCommand(cmd._id, "failed");
|
|
2609
|
+
}
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
console.log(` ${cmd.kind}: ${cmd._id}`);
|
|
2613
|
+
try {
|
|
2614
|
+
switch (cmd.kind) {
|
|
2615
|
+
case "message":
|
|
2616
|
+
await this.handleMessageCommand(cmd);
|
|
2617
|
+
await this.completeCommand(cmd._id, "delivered");
|
|
2618
|
+
return;
|
|
2619
|
+
case "launch":
|
|
2620
|
+
await this.handleLaunchCommand(cmd);
|
|
2621
|
+
await this.completeCommand(cmd._id, "delivered");
|
|
2622
|
+
return;
|
|
2623
|
+
case "resize":
|
|
2624
|
+
await this.handleResizeCommand(cmd);
|
|
2625
|
+
await this.completeCommand(cmd._id, "delivered");
|
|
2626
|
+
return;
|
|
2627
|
+
default:
|
|
2628
|
+
throw new Error(`Unsupported bridge command: ${cmd.kind}`);
|
|
2629
|
+
}
|
|
2630
|
+
} catch (error) {
|
|
2631
|
+
const message = error instanceof Error ? error.message : "Unknown bridge error";
|
|
2632
|
+
console.error(` ! ${message}`);
|
|
2633
|
+
await this.postCommandError(cmd, message);
|
|
2634
|
+
await this.completeCommand(cmd._id, "failed");
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async reportProcesses() {
|
|
2638
|
+
const processes = discoverAttachableSessions();
|
|
2639
|
+
const activeSessionKeys = processes.map((proc) => proc.sessionKey).filter((value) => Boolean(value));
|
|
2640
|
+
const activeLocalProcessIds = processes.map((proc) => proc.localProcessId).filter((value) => Boolean(value));
|
|
2641
|
+
for (const proc of processes) {
|
|
2642
|
+
try {
|
|
2643
|
+
await this.reportProcess(proc);
|
|
2644
|
+
} catch {
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
try {
|
|
2648
|
+
await this.client.mutation(
|
|
2649
|
+
api.agentBridge.bridgePublic.reconcileObservedProcesses,
|
|
2650
|
+
{
|
|
2651
|
+
deviceId: this.config.deviceId,
|
|
2652
|
+
deviceSecret: this.config.deviceSecret,
|
|
2653
|
+
activeSessionKeys,
|
|
2654
|
+
activeLocalProcessIds
|
|
2655
|
+
}
|
|
2656
|
+
);
|
|
2657
|
+
} catch {
|
|
2658
|
+
}
|
|
2659
|
+
if (processes.length > 0) {
|
|
2660
|
+
console.log(
|
|
2661
|
+
`[${ts2()}] Discovered ${processes.length} attachable session(s)`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
async refreshLiveActivities() {
|
|
2666
|
+
try {
|
|
2667
|
+
const activities = await this.client.query(
|
|
2668
|
+
api.agentBridge.bridgePublic.getDeviceLiveActivities,
|
|
2669
|
+
{
|
|
2670
|
+
deviceId: this.config.deviceId,
|
|
2671
|
+
deviceSecret: this.config.deviceSecret
|
|
2672
|
+
}
|
|
2673
|
+
);
|
|
2674
|
+
this.deviceLiveActivities = activities;
|
|
2675
|
+
writeLiveActivitiesCache(activities);
|
|
2676
|
+
for (const activity of activities) {
|
|
2677
|
+
if (activity.workSessionId && activity.tmuxSessionName) {
|
|
2678
|
+
this.terminalPeer?.watchSession(
|
|
2679
|
+
activity.workSessionId,
|
|
2680
|
+
activity.tmuxSessionName,
|
|
2681
|
+
activity.tmuxPaneId
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
if (activity.workSessionId && activity.tmuxPaneId) {
|
|
2685
|
+
try {
|
|
2686
|
+
const paneTitle = execFileSync2(
|
|
2687
|
+
"tmux",
|
|
2688
|
+
[
|
|
2689
|
+
"display-message",
|
|
2690
|
+
"-p",
|
|
2691
|
+
"-t",
|
|
2692
|
+
activity.tmuxPaneId,
|
|
2693
|
+
"#{pane_title}"
|
|
2694
|
+
],
|
|
2695
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
2696
|
+
).trim();
|
|
2697
|
+
if (paneTitle && paneTitle !== activity.workSessionTitle && !activity.titleLockedByUser) {
|
|
2698
|
+
void this.client.mutation(
|
|
2699
|
+
api.agentBridge.bridgePublic.updateWorkSessionAutoTitle,
|
|
2700
|
+
{
|
|
2701
|
+
deviceId: this.config.deviceId,
|
|
2702
|
+
deviceSecret: this.config.deviceSecret,
|
|
2703
|
+
workSessionId: activity.workSessionId,
|
|
2704
|
+
title: paneTitle
|
|
2705
|
+
}
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
} catch {
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
async syncWorkSessionTerminals(activities) {
|
|
2716
|
+
for (const activity of activities) {
|
|
2717
|
+
if (!activity.workSessionId || !activity.tmuxPaneId) {
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
try {
|
|
2721
|
+
await this.refreshWorkSessionTerminal(activity.workSessionId, {
|
|
2722
|
+
tmuxPaneId: activity.tmuxPaneId,
|
|
2723
|
+
cwd: activity.cwd,
|
|
2724
|
+
repoRoot: activity.repoRoot,
|
|
2725
|
+
branch: activity.branch,
|
|
2726
|
+
agentProvider: activity.agentProvider,
|
|
2727
|
+
agentSessionKey: activity.agentSessionKey
|
|
2728
|
+
});
|
|
2729
|
+
await this.verifyManagedWorkSession(activity);
|
|
2730
|
+
} catch {
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
async verifyManagedWorkSession(activity) {
|
|
2735
|
+
if (!activity.workSessionId || !activity.tmuxPaneId || !activity.agentProvider || !isBridgeProvider(activity.agentProvider) || activity.agentProcessId) {
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
const workspacePath = activity.workspacePath ?? activity.cwd ?? activity.repoRoot;
|
|
2739
|
+
if (!workspacePath) {
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
const attachedSession = await this.attachObservedAgentSession(
|
|
2743
|
+
activity.agentProvider,
|
|
2744
|
+
workspacePath
|
|
2745
|
+
);
|
|
2746
|
+
if (!attachedSession) {
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
await this.refreshWorkSessionTerminal(activity.workSessionId, {
|
|
2750
|
+
tmuxPaneId: activity.tmuxPaneId,
|
|
2751
|
+
cwd: attachedSession.process.cwd ?? activity.cwd ?? workspacePath,
|
|
2752
|
+
repoRoot: attachedSession.process.repoRoot ?? activity.repoRoot ?? workspacePath,
|
|
2753
|
+
branch: attachedSession.process.branch ?? activity.branch,
|
|
2754
|
+
agentProvider: attachedSession.process.provider,
|
|
2755
|
+
agentSessionKey: attachedSession.process.sessionKey
|
|
2756
|
+
});
|
|
2757
|
+
await this.postAgentMessage(
|
|
2758
|
+
activity._id,
|
|
2759
|
+
"status",
|
|
2760
|
+
`Verified ${providerLabel2(attachedSession.process.provider)} in ${activity.tmuxPaneId}`
|
|
2761
|
+
);
|
|
2762
|
+
await this.updateLiveActivity(activity._id, {
|
|
2763
|
+
status: "active",
|
|
2764
|
+
latestSummary: `Verified ${providerLabel2(attachedSession.process.provider)} in ${activity.tmuxPaneId}`,
|
|
2765
|
+
processId: attachedSession.processId,
|
|
2766
|
+
title: activity.title
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2769
|
+
async refreshWorkSessionTerminal(workSessionId, metadata) {
|
|
2770
|
+
if (!workSessionId) {
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
const terminalSnapshot = metadata.tmuxPaneId ? captureTmuxPane(metadata.tmuxPaneId) : void 0;
|
|
2774
|
+
await this.client.mutation(
|
|
2775
|
+
api.agentBridge.bridgePublic.updateWorkSessionTerminal,
|
|
2776
|
+
{
|
|
2777
|
+
deviceId: this.config.deviceId,
|
|
2778
|
+
deviceSecret: this.config.deviceSecret,
|
|
2779
|
+
workSessionId,
|
|
2780
|
+
terminalSnapshot,
|
|
2781
|
+
tmuxSessionName: metadata.tmuxSessionName,
|
|
2782
|
+
tmuxWindowName: metadata.tmuxWindowName,
|
|
2783
|
+
tmuxPaneId: metadata.tmuxPaneId,
|
|
2784
|
+
cwd: metadata.cwd,
|
|
2785
|
+
repoRoot: metadata.repoRoot,
|
|
2786
|
+
branch: metadata.branch,
|
|
2787
|
+
agentProvider: metadata.agentProvider,
|
|
2788
|
+
agentSessionKey: metadata.agentSessionKey,
|
|
2789
|
+
agentProcessId: metadata.agentProcessId,
|
|
2790
|
+
model: metadata.model,
|
|
2791
|
+
permissionMode: metadata.permissionMode,
|
|
2792
|
+
thinkingLevel: metadata.thinkingLevel,
|
|
2793
|
+
fastMode: metadata.fastMode,
|
|
2794
|
+
contextLength: metadata.contextLength
|
|
2795
|
+
}
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
async run() {
|
|
2799
|
+
console.log("Vector Bridge Service");
|
|
2800
|
+
console.log(
|
|
2801
|
+
` Device: ${this.config.displayName} (${this.config.deviceId})`
|
|
2802
|
+
);
|
|
2803
|
+
console.log(` Convex: ${this.config.convexUrl}`);
|
|
2804
|
+
console.log(` PID: ${process.pid}`);
|
|
2805
|
+
console.log("");
|
|
2806
|
+
if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
2807
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
2808
|
+
try {
|
|
2809
|
+
this.terminalPeer = new TerminalPeerManager({
|
|
2810
|
+
deviceId: this.config.deviceId,
|
|
2811
|
+
deviceSecret: this.config.deviceSecret,
|
|
2812
|
+
convexUrl: this.config.convexUrl,
|
|
2813
|
+
tunnelHost: this.config.tunnelHost
|
|
2814
|
+
});
|
|
2815
|
+
console.log(
|
|
2816
|
+
` Terminal: ready${this.config.tunnelHost ? ` (tunnel: ${this.config.tunnelHost})` : ""}`
|
|
2817
|
+
);
|
|
2818
|
+
} catch (e) {
|
|
2819
|
+
console.error(
|
|
2820
|
+
` WebRTC: failed (${e instanceof Error ? e.message : "unknown"})`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
console.log("");
|
|
2824
|
+
process.on("uncaughtException", (error) => {
|
|
2825
|
+
console.error(`[${ts2()}] Uncaught error:`, error.message);
|
|
2826
|
+
});
|
|
2827
|
+
process.on("unhandledRejection", (reason) => {
|
|
2828
|
+
console.error(
|
|
2829
|
+
`[${ts2()}] Unhandled rejection:`,
|
|
2830
|
+
reason instanceof Error ? reason.message : String(reason)
|
|
2831
|
+
);
|
|
2832
|
+
});
|
|
2833
|
+
await this.runStartupStep("heartbeat", () => this.heartbeat());
|
|
2834
|
+
await this.runStartupStep(
|
|
2835
|
+
"process discovery",
|
|
2836
|
+
() => this.reportProcesses()
|
|
2837
|
+
);
|
|
2838
|
+
await this.runStartupStep(
|
|
2839
|
+
"live activity sync",
|
|
2840
|
+
() => this.refreshLiveActivities()
|
|
2841
|
+
);
|
|
2842
|
+
await this.runStartupStep(
|
|
2843
|
+
"terminal snapshot sync",
|
|
2844
|
+
() => this.syncWorkSessionTerminals(this.deviceLiveActivities)
|
|
2845
|
+
);
|
|
2846
|
+
console.log(`[${ts2()}] Service running. Ctrl+C to stop.
|
|
2847
|
+
`);
|
|
2848
|
+
this.scheduleLoop(
|
|
2849
|
+
"Heartbeat",
|
|
2850
|
+
HEARTBEAT_INTERVAL_MS,
|
|
2851
|
+
() => this.heartbeat()
|
|
2852
|
+
);
|
|
2853
|
+
this.scheduleLoop(
|
|
2854
|
+
"Command poll",
|
|
2855
|
+
COMMAND_POLL_INTERVAL_MS,
|
|
2856
|
+
() => this.pollCommands()
|
|
2857
|
+
);
|
|
2858
|
+
this.scheduleLoop(
|
|
2859
|
+
"Live activity sync",
|
|
2860
|
+
LIVE_ACTIVITY_SYNC_INTERVAL_MS,
|
|
2861
|
+
() => this.refreshLiveActivities()
|
|
2862
|
+
);
|
|
2863
|
+
this.scheduleLoop(
|
|
2864
|
+
"Discovery",
|
|
2865
|
+
PROCESS_DISCOVERY_INTERVAL_MS,
|
|
2866
|
+
() => this.reportProcesses()
|
|
2867
|
+
);
|
|
2868
|
+
this.scheduleLoop(
|
|
2869
|
+
"Terminal snapshot refresh",
|
|
2870
|
+
TERMINAL_SNAPSHOT_REFRESH_INTERVAL_MS,
|
|
2871
|
+
() => this.syncWorkSessionTerminals(this.deviceLiveActivities)
|
|
2872
|
+
);
|
|
2873
|
+
const shutdown = () => {
|
|
2874
|
+
if (this.stopping) {
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
this.stopping = true;
|
|
2878
|
+
console.log(`
|
|
2879
|
+
[${ts2()}] Shutting down...`);
|
|
2880
|
+
for (const t of this.timers) clearInterval(t);
|
|
2881
|
+
this.terminalPeer?.stop();
|
|
2882
|
+
try {
|
|
2883
|
+
unlinkSync(PID_FILE);
|
|
2884
|
+
} catch {
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
writeLiveActivitiesCache([]);
|
|
2888
|
+
} catch {
|
|
2889
|
+
}
|
|
2890
|
+
process.exit(0);
|
|
2891
|
+
};
|
|
2892
|
+
process.on("SIGINT", shutdown);
|
|
2893
|
+
process.on("SIGTERM", shutdown);
|
|
2894
|
+
await new Promise(() => {
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
async handleMessageCommand(cmd) {
|
|
2898
|
+
if (!cmd.liveActivityId) {
|
|
2899
|
+
throw new Error("Message command is missing liveActivityId");
|
|
2900
|
+
}
|
|
2901
|
+
const body = readPayloadString(cmd.payload, "body")?.trim();
|
|
2902
|
+
if (!body) {
|
|
2903
|
+
throw new Error("Message command is missing a body");
|
|
2904
|
+
}
|
|
2905
|
+
const issueContext = readPayloadValue(cmd.payload, "issueContext");
|
|
2906
|
+
const process9 = cmd.process;
|
|
2907
|
+
console.log(` > "${truncateForLog(body)}"`);
|
|
2908
|
+
if (cmd.workSession?.tmuxPaneId) {
|
|
2909
|
+
const terminalInput = cmd.workSession.agentProvider && isBridgeProvider(cmd.workSession.agentProvider) ? buildFollowUpPrompt(body, issueContext) : body;
|
|
2910
|
+
sendTextToTmuxPane(cmd.workSession.tmuxPaneId, terminalInput);
|
|
2911
|
+
const attachedSession = cmd.workSession.agentProvider && isBridgeProvider(cmd.workSession.agentProvider) ? await this.attachObservedAgentSession(
|
|
2912
|
+
cmd.workSession.agentProvider,
|
|
2913
|
+
cmd.workSession.workspacePath ?? cmd.workSession.cwd ?? process9?.cwd
|
|
2914
|
+
) : null;
|
|
2915
|
+
await this.postAgentMessage(
|
|
2916
|
+
cmd.liveActivityId,
|
|
2917
|
+
"status",
|
|
2918
|
+
"Sent input to work session terminal"
|
|
2919
|
+
);
|
|
2920
|
+
await this.refreshWorkSessionTerminal(cmd.workSession._id, {
|
|
2921
|
+
tmuxSessionName: cmd.workSession.tmuxSessionName,
|
|
2922
|
+
tmuxWindowName: cmd.workSession.tmuxWindowName,
|
|
2923
|
+
tmuxPaneId: cmd.workSession.tmuxPaneId,
|
|
2924
|
+
cwd: cmd.workSession.cwd,
|
|
2925
|
+
repoRoot: cmd.workSession.repoRoot,
|
|
2926
|
+
branch: cmd.workSession.branch,
|
|
2927
|
+
agentProvider: attachedSession?.process.provider ?? cmd.workSession.agentProvider,
|
|
2928
|
+
agentSessionKey: attachedSession?.process.sessionKey ?? cmd.workSession.agentSessionKey
|
|
2929
|
+
});
|
|
2930
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
2931
|
+
status: "waiting_for_input",
|
|
2932
|
+
latestSummary: `Input sent to ${cmd.workSession.tmuxPaneId}`,
|
|
2933
|
+
title: cmd.liveActivity?.title,
|
|
2934
|
+
processId: attachedSession?.processId ?? process9?._id
|
|
2935
|
+
});
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
if (!process9 || !process9.supportsInboundMessages || !process9.sessionKey || !process9.cwd || !isBridgeProvider(process9.provider)) {
|
|
2939
|
+
throw new Error("No resumable local session is attached to this issue");
|
|
2940
|
+
}
|
|
2941
|
+
await this.reportProcess({
|
|
2942
|
+
provider: process9.provider,
|
|
2943
|
+
providerLabel: process9.providerLabel ?? providerLabel2(process9.provider),
|
|
2944
|
+
sessionKey: process9.sessionKey,
|
|
2945
|
+
cwd: process9.cwd,
|
|
2946
|
+
repoRoot: process9.repoRoot,
|
|
2947
|
+
branch: process9.branch,
|
|
2948
|
+
title: process9.title,
|
|
2949
|
+
model: process9.model,
|
|
2950
|
+
permissionMode: process9.permissionMode,
|
|
2951
|
+
thinkingLevel: process9.thinkingLevel,
|
|
2952
|
+
fastMode: process9.fastMode,
|
|
2953
|
+
contextLength: process9.contextLength,
|
|
2954
|
+
mode: "managed",
|
|
2955
|
+
status: "waiting",
|
|
2956
|
+
supportsInboundMessages: true
|
|
2957
|
+
});
|
|
2958
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
2959
|
+
status: "active",
|
|
2960
|
+
processId: process9._id,
|
|
2961
|
+
title: cmd.liveActivity?.title ?? process9.title
|
|
2962
|
+
});
|
|
2963
|
+
const liveActivityId = cmd.liveActivityId;
|
|
2964
|
+
let emittedAssistantEvent = false;
|
|
2965
|
+
const result = await resumeProviderSession(
|
|
2966
|
+
process9.provider,
|
|
2967
|
+
process9.sessionKey,
|
|
2968
|
+
process9.cwd,
|
|
2969
|
+
buildFollowUpPrompt(body, issueContext),
|
|
2970
|
+
(event) => {
|
|
2971
|
+
if (event.role === "assistant") emittedAssistantEvent = true;
|
|
2972
|
+
return this.postAgentSessionEvent(liveActivityId, event);
|
|
2973
|
+
}
|
|
2974
|
+
);
|
|
2975
|
+
const processId = await this.reportProcess(result);
|
|
2976
|
+
if (result.responseText && !emittedAssistantEvent) {
|
|
2977
|
+
await this.postAgentMessage(
|
|
2978
|
+
cmd.liveActivityId,
|
|
2979
|
+
"assistant",
|
|
2980
|
+
result.responseText
|
|
2981
|
+
);
|
|
2982
|
+
console.log(` < "${truncateForLog(result.responseText)}"`);
|
|
2983
|
+
}
|
|
2984
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
2985
|
+
processId,
|
|
2986
|
+
status: "waiting_for_input",
|
|
2987
|
+
latestSummary: summarizeMessage(result.responseText),
|
|
2988
|
+
title: cmd.liveActivity?.title ?? process9.title
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
async handleResizeCommand(cmd) {
|
|
2992
|
+
const cols = readPayloadNumber(cmd.payload, "cols");
|
|
2993
|
+
const rows = readPayloadNumber(cmd.payload, "rows");
|
|
2994
|
+
const paneId = cmd.workSession?.tmuxPaneId;
|
|
2995
|
+
if (!paneId || !cols || !rows) {
|
|
2996
|
+
throw new Error("Resize command missing paneId, cols, or rows");
|
|
2997
|
+
}
|
|
2998
|
+
console.log(` Resize ${paneId} \u2192 ${cols}x${rows}`);
|
|
2999
|
+
resizeTmuxPane(paneId, cols, rows);
|
|
3000
|
+
if (cmd.workSession) {
|
|
3001
|
+
await this.refreshWorkSessionTerminal(cmd.workSession._id, {
|
|
3002
|
+
tmuxSessionName: cmd.workSession.tmuxSessionName,
|
|
3003
|
+
tmuxWindowName: cmd.workSession.tmuxWindowName,
|
|
3004
|
+
tmuxPaneId: paneId,
|
|
3005
|
+
cwd: cmd.workSession.cwd,
|
|
3006
|
+
repoRoot: cmd.workSession.repoRoot,
|
|
3007
|
+
branch: cmd.workSession.branch,
|
|
3008
|
+
agentProvider: cmd.workSession.agentProvider,
|
|
3009
|
+
agentSessionKey: cmd.workSession.agentSessionKey
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
async handleAgentControlCommand(cmd) {
|
|
3014
|
+
if (!cmd.liveActivityId) {
|
|
3015
|
+
throw new Error(`${cmd.kind} command is missing liveActivityId`);
|
|
3016
|
+
}
|
|
3017
|
+
if (cmd.kind === "settings_update") {
|
|
3018
|
+
await this.postAgentMessage(
|
|
3019
|
+
cmd.liveActivityId,
|
|
3020
|
+
"status",
|
|
3021
|
+
"Updated agent settings"
|
|
3022
|
+
);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
if (cmd.kind === "queue_update") {
|
|
3026
|
+
await this.postAgentMessage(
|
|
3027
|
+
cmd.liveActivityId,
|
|
3028
|
+
"status",
|
|
3029
|
+
"Updated queued agent messages"
|
|
3030
|
+
);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (cmd.kind === "stop") {
|
|
3034
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3035
|
+
status: "paused",
|
|
3036
|
+
latestSummary: "Agent turn stop requested"
|
|
3037
|
+
});
|
|
3038
|
+
await this.postAgentMessage(
|
|
3039
|
+
cmd.liveActivityId,
|
|
3040
|
+
"status",
|
|
3041
|
+
"Stop requested for the local agent session"
|
|
3042
|
+
);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
if (cmd.kind === "resume") {
|
|
3046
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3047
|
+
status: "active",
|
|
3048
|
+
latestSummary: "Agent session resumed"
|
|
3049
|
+
});
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
const label = cmd.kind === "approval_response" ? "approval" : cmd.kind === "plan_response" ? "plan approval" : "question response";
|
|
3053
|
+
await this.postAgentMessage(
|
|
3054
|
+
cmd.liveActivityId,
|
|
3055
|
+
"status",
|
|
3056
|
+
`Received ${label}; the provider runtime will continue when supported`
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
async handleLaunchCommand(cmd) {
|
|
3060
|
+
if (!cmd.liveActivityId) {
|
|
3061
|
+
throw new Error("Launch command is missing liveActivityId");
|
|
3062
|
+
}
|
|
3063
|
+
const workspacePath = readPayloadString(
|
|
3064
|
+
cmd.payload,
|
|
3065
|
+
"workspacePath"
|
|
3066
|
+
)?.trim();
|
|
3067
|
+
if (!workspacePath) {
|
|
3068
|
+
throw new Error("Launch command is missing workspacePath");
|
|
3069
|
+
}
|
|
3070
|
+
const requestedProvider = readPayloadAgentProvider(cmd.payload, "provider");
|
|
3071
|
+
const provider = requestedProvider && isBridgeProvider(requestedProvider) ? requestedProvider : void 0;
|
|
3072
|
+
const issueKey = readPayloadString(cmd.payload, "issueKey") ?? cmd.liveActivity?.issueKey ?? "ISSUE";
|
|
3073
|
+
const issueTitle = readPayloadString(cmd.payload, "issueTitle") ?? cmd.liveActivity?.issueTitle ?? "Untitled issue";
|
|
3074
|
+
const issueDescription = readPayloadString(cmd.payload, "issueDescription");
|
|
3075
|
+
const issueContext = readPayloadValue(cmd.payload, "issueContext");
|
|
3076
|
+
const model = readPayloadString(cmd.payload, "model");
|
|
3077
|
+
const permissionMode = readPayloadPermissionMode(
|
|
3078
|
+
cmd.payload,
|
|
3079
|
+
"permissionMode"
|
|
3080
|
+
);
|
|
3081
|
+
const thinkingLevel = readPayloadThinkingLevel(
|
|
3082
|
+
cmd.payload,
|
|
3083
|
+
"thinkingLevel"
|
|
3084
|
+
);
|
|
3085
|
+
const fastMode = readPayloadBoolean(cmd.payload, "fastMode");
|
|
3086
|
+
const contextLength = readPayloadContextLength(
|
|
3087
|
+
cmd.payload,
|
|
3088
|
+
"contextLength"
|
|
3089
|
+
);
|
|
3090
|
+
const initialPrompt = readPayloadString(cmd.payload, "initialPrompt");
|
|
3091
|
+
const delegatedRunId = readPayloadId(
|
|
3092
|
+
cmd.payload,
|
|
3093
|
+
"delegatedRunId"
|
|
3094
|
+
);
|
|
3095
|
+
const prompt2 = buildLaunchPrompt(
|
|
3096
|
+
issueKey,
|
|
3097
|
+
issueTitle,
|
|
3098
|
+
workspacePath,
|
|
3099
|
+
issueDescription,
|
|
3100
|
+
issueContext,
|
|
3101
|
+
initialPrompt
|
|
3102
|
+
);
|
|
3103
|
+
const launchLabel = provider ? providerLabel2(provider) : "shell session";
|
|
3104
|
+
const workSessionTitle = `${issueKey}: ${issueTitle}`;
|
|
3105
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3106
|
+
status: "active",
|
|
3107
|
+
latestSummary: `Launching ${launchLabel} in ${workspacePath}`,
|
|
3108
|
+
delegatedRunId,
|
|
3109
|
+
launchStatus: "launching",
|
|
3110
|
+
title: workSessionTitle
|
|
3111
|
+
});
|
|
3112
|
+
if (provider) {
|
|
3113
|
+
await this.postAgentMessage(
|
|
3114
|
+
cmd.liveActivityId,
|
|
3115
|
+
"status",
|
|
3116
|
+
`Starting ${launchLabel} session in ${workspacePath}`
|
|
3117
|
+
);
|
|
3118
|
+
const liveActivityId = cmd.liveActivityId;
|
|
3119
|
+
const result = await launchProviderSession(
|
|
3120
|
+
provider,
|
|
3121
|
+
workspacePath,
|
|
3122
|
+
prompt2,
|
|
3123
|
+
(event) => this.postAgentSessionEvent(liveActivityId, event)
|
|
3124
|
+
);
|
|
3125
|
+
const processId = await this.reportProcess(result);
|
|
3126
|
+
await this.refreshWorkSessionTerminal(cmd.workSession?._id, {
|
|
3127
|
+
cwd: workspacePath,
|
|
3128
|
+
repoRoot: result.repoRoot ?? workspacePath,
|
|
3129
|
+
branch: result.branch ?? currentGitBranch(workspacePath),
|
|
3130
|
+
agentProvider: result.provider,
|
|
3131
|
+
agentSessionKey: result.sessionKey,
|
|
3132
|
+
agentProcessId: processId,
|
|
3133
|
+
model,
|
|
3134
|
+
permissionMode,
|
|
3135
|
+
thinkingLevel,
|
|
3136
|
+
fastMode,
|
|
3137
|
+
contextLength
|
|
3138
|
+
});
|
|
3139
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3140
|
+
status: "waiting_for_input",
|
|
3141
|
+
latestSummary: summarizeMessage(result.responseText),
|
|
3142
|
+
delegatedRunId,
|
|
3143
|
+
launchStatus: "running",
|
|
3144
|
+
title: workSessionTitle,
|
|
3145
|
+
processId
|
|
3146
|
+
});
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
const tmuxSession = createTmuxWorkSession({
|
|
3150
|
+
workspacePath,
|
|
3151
|
+
issueKey,
|
|
3152
|
+
issueTitle,
|
|
3153
|
+
provider,
|
|
3154
|
+
prompt: prompt2
|
|
3155
|
+
});
|
|
3156
|
+
await this.refreshWorkSessionTerminal(cmd.workSession?._id, {
|
|
3157
|
+
tmuxSessionName: tmuxSession.sessionName,
|
|
3158
|
+
tmuxWindowName: tmuxSession.windowName,
|
|
3159
|
+
tmuxPaneId: tmuxSession.paneId,
|
|
3160
|
+
cwd: workspacePath,
|
|
3161
|
+
repoRoot: workspacePath,
|
|
3162
|
+
branch: currentGitBranch(workspacePath),
|
|
3163
|
+
agentProvider: provider,
|
|
3164
|
+
model,
|
|
3165
|
+
permissionMode,
|
|
3166
|
+
thinkingLevel,
|
|
3167
|
+
fastMode,
|
|
3168
|
+
contextLength
|
|
3169
|
+
});
|
|
3170
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3171
|
+
status: "active",
|
|
3172
|
+
latestSummary: `Running ${launchLabel} in ${tmuxSession.sessionName}`,
|
|
3173
|
+
delegatedRunId,
|
|
3174
|
+
launchStatus: "running",
|
|
3175
|
+
title: workSessionTitle
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
async attachObservedAgentSession(provider, workspacePath, sessionsBeforeLaunch = [], paneProcessId) {
|
|
3179
|
+
if (!workspacePath) {
|
|
3180
|
+
return null;
|
|
3181
|
+
}
|
|
3182
|
+
const existingKeys = new Set(
|
|
3183
|
+
sessionsBeforeLaunch.map(sessionIdentityKey).filter(Boolean)
|
|
3184
|
+
);
|
|
3185
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
3186
|
+
const observedSessions = listObservedSessionsForWorkspace(
|
|
3187
|
+
provider,
|
|
3188
|
+
workspacePath
|
|
3189
|
+
);
|
|
3190
|
+
const candidate = (paneProcessId ? findObservedSessionInProcessTree(observedSessions, paneProcessId) : void 0) ?? observedSessions.find(
|
|
3191
|
+
(session) => !existingKeys.has(sessionIdentityKey(session))
|
|
3192
|
+
) ?? (attempt === 9 ? observedSessions[0] : void 0);
|
|
3193
|
+
if (candidate) {
|
|
3194
|
+
const processId = await this.reportProcess(candidate);
|
|
3195
|
+
return {
|
|
3196
|
+
process: candidate,
|
|
3197
|
+
processId
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
await sleep(750);
|
|
3201
|
+
}
|
|
3202
|
+
return null;
|
|
3203
|
+
}
|
|
3204
|
+
async reportProcess(process9) {
|
|
3205
|
+
const {
|
|
3206
|
+
provider,
|
|
3207
|
+
providerLabel: providerLabel3,
|
|
3208
|
+
localProcessId,
|
|
3209
|
+
sessionKey,
|
|
3210
|
+
cwd,
|
|
3211
|
+
repoRoot,
|
|
3212
|
+
branch,
|
|
3213
|
+
title,
|
|
3214
|
+
model,
|
|
3215
|
+
permissionMode,
|
|
3216
|
+
thinkingLevel,
|
|
3217
|
+
fastMode,
|
|
3218
|
+
contextLength,
|
|
3219
|
+
tmuxSessionName,
|
|
3220
|
+
tmuxWindowName,
|
|
3221
|
+
tmuxPaneId,
|
|
3222
|
+
mode,
|
|
3223
|
+
status,
|
|
3224
|
+
supportsInboundMessages
|
|
3225
|
+
} = process9;
|
|
3226
|
+
return await this.client.mutation(
|
|
3227
|
+
api.agentBridge.bridgePublic.reportProcess,
|
|
3228
|
+
{
|
|
3229
|
+
deviceId: this.config.deviceId,
|
|
3230
|
+
deviceSecret: this.config.deviceSecret,
|
|
3231
|
+
provider,
|
|
3232
|
+
providerLabel: providerLabel3,
|
|
3233
|
+
localProcessId,
|
|
3234
|
+
sessionKey,
|
|
3235
|
+
cwd,
|
|
3236
|
+
repoRoot,
|
|
3237
|
+
branch,
|
|
3238
|
+
title,
|
|
3239
|
+
model,
|
|
3240
|
+
permissionMode,
|
|
3241
|
+
thinkingLevel,
|
|
3242
|
+
fastMode,
|
|
3243
|
+
contextLength,
|
|
3244
|
+
tmuxSessionName,
|
|
3245
|
+
tmuxWindowName,
|
|
3246
|
+
tmuxPaneId,
|
|
3247
|
+
mode,
|
|
3248
|
+
status,
|
|
3249
|
+
supportsInboundMessages
|
|
3250
|
+
}
|
|
3251
|
+
);
|
|
3252
|
+
}
|
|
3253
|
+
async updateLiveActivity(liveActivityId, args) {
|
|
3254
|
+
await this.client.mutation(
|
|
3255
|
+
api.agentBridge.bridgePublic.updateLiveActivityState,
|
|
3256
|
+
{
|
|
3257
|
+
deviceId: this.config.deviceId,
|
|
3258
|
+
deviceSecret: this.config.deviceSecret,
|
|
3259
|
+
liveActivityId,
|
|
3260
|
+
...args
|
|
3261
|
+
}
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
async postAgentMessage(liveActivityId, role, body, structuredPayload) {
|
|
3265
|
+
await this.client.mutation(api.agentBridge.bridgePublic.postAgentMessage, {
|
|
3266
|
+
deviceId: this.config.deviceId,
|
|
3267
|
+
deviceSecret: this.config.deviceSecret,
|
|
3268
|
+
liveActivityId,
|
|
3269
|
+
role,
|
|
3270
|
+
body,
|
|
3271
|
+
structuredPayload
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
async postAgentSessionEvent(liveActivityId, event) {
|
|
3275
|
+
const body = event.text.trim();
|
|
3276
|
+
if (!body) return;
|
|
3277
|
+
await this.postAgentMessage(liveActivityId, event.role, body, {
|
|
3278
|
+
source: "cells_agent_event",
|
|
3279
|
+
provider: event.provider,
|
|
3280
|
+
title: event.title,
|
|
3281
|
+
status: event.status
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
async completeCommand(commandId, status) {
|
|
3285
|
+
await this.client.mutation(api.agentBridge.bridgePublic.completeCommand, {
|
|
3286
|
+
deviceId: this.config.deviceId,
|
|
3287
|
+
deviceSecret: this.config.deviceSecret,
|
|
3288
|
+
commandId,
|
|
3289
|
+
status
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
async postCommandError(cmd, errorMessage) {
|
|
3293
|
+
if (cmd.kind === "launch" && cmd.liveActivityId) {
|
|
3294
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3295
|
+
status: "failed",
|
|
3296
|
+
latestSummary: errorMessage,
|
|
3297
|
+
delegatedRunId: readPayloadId(
|
|
3298
|
+
cmd.payload,
|
|
3299
|
+
"delegatedRunId"
|
|
3300
|
+
),
|
|
3301
|
+
launchStatus: "failed"
|
|
3302
|
+
});
|
|
3303
|
+
await this.postAgentMessage(cmd.liveActivityId, "status", errorMessage);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
if (cmd.kind === "message" && cmd.liveActivityId) {
|
|
3307
|
+
await this.postAgentMessage(cmd.liveActivityId, "status", errorMessage);
|
|
3308
|
+
await this.updateLiveActivity(cmd.liveActivityId, {
|
|
3309
|
+
status: "waiting_for_input",
|
|
3310
|
+
latestSummary: errorMessage
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
};
|
|
3315
|
+
function createTmuxWorkSession(args) {
|
|
3316
|
+
const slug = sanitizeTmuxName(args.issueKey.toLowerCase());
|
|
3317
|
+
const sessionName = `vector-${slug}-${randomUUID2().slice(0, 8)}`;
|
|
3318
|
+
const windowName = sanitizeTmuxName(
|
|
3319
|
+
args.provider === "codex" ? "codex" : args.provider === "claude_code" ? "claude" : "shell"
|
|
3320
|
+
);
|
|
3321
|
+
execFileSync2("tmux", [
|
|
3322
|
+
"new-session",
|
|
3323
|
+
"-d",
|
|
3324
|
+
"-s",
|
|
3325
|
+
sessionName,
|
|
3326
|
+
"-n",
|
|
3327
|
+
windowName,
|
|
3328
|
+
"-c",
|
|
3329
|
+
args.workspacePath
|
|
3330
|
+
]);
|
|
3331
|
+
const paneId = execFileSync2(
|
|
3332
|
+
"tmux",
|
|
3333
|
+
[
|
|
3334
|
+
"display-message",
|
|
3335
|
+
"-p",
|
|
3336
|
+
"-t",
|
|
3337
|
+
`${sessionName}:${windowName}.0`,
|
|
3338
|
+
"#{pane_id}"
|
|
3339
|
+
],
|
|
3340
|
+
{ encoding: "utf-8" }
|
|
3341
|
+
).trim();
|
|
3342
|
+
const paneProcessId = execFileSync2(
|
|
3343
|
+
"tmux",
|
|
3344
|
+
["display-message", "-p", "-t", paneId, "#{pane_pid}"],
|
|
3345
|
+
{ encoding: "utf-8" }
|
|
3346
|
+
).trim();
|
|
3347
|
+
if (args.provider) {
|
|
3348
|
+
execFileSync2("tmux", [
|
|
3349
|
+
"send-keys",
|
|
3350
|
+
"-t",
|
|
3351
|
+
paneId,
|
|
3352
|
+
buildManagedLaunchCommand(args.provider, args.prompt),
|
|
3353
|
+
"Enter"
|
|
3354
|
+
]);
|
|
3355
|
+
} else {
|
|
3356
|
+
execFileSync2("tmux", [
|
|
3357
|
+
"send-keys",
|
|
3358
|
+
"-t",
|
|
3359
|
+
paneId,
|
|
3360
|
+
`printf '%s\\n\\n' ${shellQuote(args.prompt)}`,
|
|
3361
|
+
"Enter"
|
|
3362
|
+
]);
|
|
3363
|
+
}
|
|
3364
|
+
return {
|
|
3365
|
+
sessionName,
|
|
3366
|
+
windowName,
|
|
3367
|
+
paneId,
|
|
3368
|
+
paneProcessId
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
function sendTextToTmuxPane(paneId, text2) {
|
|
3372
|
+
execFileSync2("tmux", ["set-buffer", "--", text2]);
|
|
3373
|
+
execFileSync2("tmux", ["paste-buffer", "-t", paneId]);
|
|
3374
|
+
execFileSync2("tmux", ["send-keys", "-t", paneId, "Enter"]);
|
|
3375
|
+
execFileSync2("tmux", ["delete-buffer"]);
|
|
3376
|
+
}
|
|
3377
|
+
function captureTmuxPane(paneId) {
|
|
3378
|
+
return execFileSync2(
|
|
3379
|
+
"tmux",
|
|
3380
|
+
["capture-pane", "-p", "-e", "-t", paneId, "-S", "-120"],
|
|
3381
|
+
{ encoding: "utf-8" }
|
|
3382
|
+
).trimEnd();
|
|
3383
|
+
}
|
|
3384
|
+
function resizeTmuxPane(paneId, cols, rows) {
|
|
3385
|
+
try {
|
|
3386
|
+
execFileSync2("tmux", [
|
|
3387
|
+
"resize-pane",
|
|
3388
|
+
"-t",
|
|
3389
|
+
paneId,
|
|
3390
|
+
"-x",
|
|
3391
|
+
String(cols),
|
|
3392
|
+
"-y",
|
|
3393
|
+
String(rows)
|
|
3394
|
+
]);
|
|
3395
|
+
} catch (e) {
|
|
3396
|
+
console.error(`Failed to resize pane ${paneId}:`, e);
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
function currentGitBranch(cwd) {
|
|
3400
|
+
try {
|
|
3401
|
+
return execSync2("git rev-parse --abbrev-ref HEAD", {
|
|
3402
|
+
encoding: "utf-8",
|
|
3403
|
+
cwd,
|
|
3404
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3405
|
+
timeout: 3e3
|
|
3406
|
+
}).trim();
|
|
3407
|
+
} catch {
|
|
3408
|
+
return void 0;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
function buildManagedLaunchCommand(provider, prompt2) {
|
|
3412
|
+
if (provider === "codex") {
|
|
3413
|
+
return `codex --no-alt-screen -a never ${shellQuote(prompt2)}`;
|
|
3414
|
+
}
|
|
3415
|
+
if (provider === "claude_code") {
|
|
3416
|
+
return `claude --permission-mode bypassPermissions --dangerously-skip-permissions ${shellQuote(prompt2)}`;
|
|
3417
|
+
}
|
|
3418
|
+
if (provider === "cursor")
|
|
3419
|
+
return `cursor-agent --print ${shellQuote(prompt2)}`;
|
|
3420
|
+
if (provider === "copilot") return `copilot ${shellQuote(prompt2)}`;
|
|
3421
|
+
if (provider === "opencode") return `opencode run ${shellQuote(prompt2)}`;
|
|
3422
|
+
return `pi ${shellQuote(prompt2)}`;
|
|
3423
|
+
}
|
|
3424
|
+
function sanitizeTmuxName(value) {
|
|
3425
|
+
return value.replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "work";
|
|
3426
|
+
}
|
|
3427
|
+
function shellQuote(value) {
|
|
3428
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
3429
|
+
}
|
|
3430
|
+
async function setupBridgeDevice(client, convexUrl) {
|
|
3431
|
+
const deviceKey = getStableDeviceKey();
|
|
3432
|
+
const displayName = `${process.env.USER ?? "user"}'s ${platform() === "darwin" ? "Mac" : "machine"}`;
|
|
3433
|
+
const result = await client.mutation(
|
|
3434
|
+
api.agentBridge.mutations.registerBridgeDevice,
|
|
3435
|
+
{
|
|
3436
|
+
deviceKey,
|
|
3437
|
+
displayName,
|
|
3438
|
+
hostname: hostname(),
|
|
3439
|
+
platform: platform(),
|
|
3440
|
+
serviceType: "foreground",
|
|
3441
|
+
cliVersion: "0.1.0",
|
|
3442
|
+
capabilities: ["codex", "claude_code"]
|
|
3443
|
+
}
|
|
3444
|
+
);
|
|
3445
|
+
const config = {
|
|
3446
|
+
deviceId: result.deviceId,
|
|
3447
|
+
deviceKey,
|
|
3448
|
+
deviceSecret: result.deviceSecret,
|
|
3449
|
+
userId: result.userId,
|
|
3450
|
+
displayName,
|
|
3451
|
+
convexUrl,
|
|
3452
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3453
|
+
};
|
|
3454
|
+
saveBridgeConfig(config);
|
|
3455
|
+
return config;
|
|
3456
|
+
}
|
|
3457
|
+
function getStableDeviceKey() {
|
|
3458
|
+
const existingConfig = loadBridgeConfig();
|
|
3459
|
+
const existingKey = existingConfig?.deviceKey?.trim();
|
|
3460
|
+
if (existingKey) {
|
|
3461
|
+
persistDeviceKey(existingKey);
|
|
3462
|
+
return existingKey;
|
|
3463
|
+
}
|
|
3464
|
+
if (existsSync3(DEVICE_KEY_FILE)) {
|
|
3465
|
+
const savedKey = readFileSync2(DEVICE_KEY_FILE, "utf-8").trim();
|
|
3466
|
+
if (savedKey) {
|
|
3467
|
+
return savedKey;
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
const generatedKey = `${hostname()}-${randomUUID2().slice(0, 8)}`;
|
|
3471
|
+
persistDeviceKey(generatedKey);
|
|
3472
|
+
return generatedKey;
|
|
3473
|
+
}
|
|
3474
|
+
function persistDeviceKey(deviceKey) {
|
|
3475
|
+
if (!existsSync3(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true });
|
|
3476
|
+
writeFileSync(DEVICE_KEY_FILE, `${deviceKey}
|
|
3477
|
+
`);
|
|
3478
|
+
}
|
|
3479
|
+
function buildLaunchPrompt(issueKey, issueTitle, workspacePath, issueDescription, issueContext, initialPrompt) {
|
|
3480
|
+
const lines = [`You are working on issue ${issueKey}: ${issueTitle}`];
|
|
3481
|
+
if (issueDescription?.trim()) {
|
|
3482
|
+
lines.push("", "Issue description:", issueDescription.trim());
|
|
3483
|
+
}
|
|
3484
|
+
const contextLines = formatIssueContext(issueContext);
|
|
3485
|
+
if (contextLines.length > 0) {
|
|
3486
|
+
lines.push("", "Vector context:", ...contextLines);
|
|
3487
|
+
}
|
|
3488
|
+
if (initialPrompt?.trim()) {
|
|
3489
|
+
lines.push("", "User instruction:", initialPrompt.trim());
|
|
3490
|
+
}
|
|
3491
|
+
lines.push(
|
|
3492
|
+
"",
|
|
3493
|
+
`The repository is at ${workspacePath}.`,
|
|
3494
|
+
"Do exactly and only what the issue describes \u2014 nothing more, nothing less.",
|
|
3495
|
+
"If anything is unclear or ambiguous, ask clarifying questions before making changes.",
|
|
3496
|
+
'Do not refactor, clean up, or "improve" code that is not part of the issue scope.'
|
|
3497
|
+
);
|
|
3498
|
+
return lines.join("\n");
|
|
3499
|
+
}
|
|
3500
|
+
function buildFollowUpPrompt(userMessage, issueContext) {
|
|
3501
|
+
const contextLines = formatIssueContext(issueContext);
|
|
3502
|
+
if (contextLines.length === 0) {
|
|
3503
|
+
return userMessage;
|
|
3504
|
+
}
|
|
3505
|
+
return [
|
|
3506
|
+
"Vector context for the current issue:",
|
|
3507
|
+
...contextLines,
|
|
3508
|
+
"",
|
|
3509
|
+
"User message:",
|
|
3510
|
+
userMessage
|
|
3511
|
+
].join("\n");
|
|
3512
|
+
}
|
|
3513
|
+
function formatIssueContext(issueContext) {
|
|
3514
|
+
const lines = [];
|
|
3515
|
+
const organization = readPayloadValue(issueContext, "organization");
|
|
3516
|
+
const organizationName = readPayloadString(organization, "name");
|
|
3517
|
+
const organizationSlug = readPayloadString(organization, "slug");
|
|
3518
|
+
if (organizationName || organizationSlug) {
|
|
3519
|
+
lines.push(
|
|
3520
|
+
`- Organization: ${[organizationName, organizationSlug ? `(${organizationSlug})` : void 0].filter(Boolean).join(" ")}`
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
const team = readPayloadValue(issueContext, "team");
|
|
3524
|
+
const teamName = readPayloadString(team, "name");
|
|
3525
|
+
const teamKey = readPayloadString(team, "key");
|
|
3526
|
+
if (teamName || teamKey) {
|
|
3527
|
+
lines.push(
|
|
3528
|
+
`- Team: ${[teamName, teamKey ? `(${teamKey})` : void 0].filter(Boolean).join(" ")}`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
const project = readPayloadValue(issueContext, "project");
|
|
3532
|
+
const projectName = readPayloadString(project, "name");
|
|
3533
|
+
const projectKey = readPayloadString(project, "key");
|
|
3534
|
+
const projectDescription = readPayloadString(project, "description");
|
|
3535
|
+
if (projectName || projectKey) {
|
|
3536
|
+
lines.push(
|
|
3537
|
+
`- Project: ${[projectName, projectKey ? `(${projectKey})` : void 0].filter(Boolean).join(" ")}`
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
if (projectDescription) {
|
|
3541
|
+
lines.push(`- Project description: ${projectDescription}`);
|
|
3542
|
+
}
|
|
3543
|
+
const state = readPayloadValue(issueContext, "state");
|
|
3544
|
+
const stateName = readPayloadString(state, "name");
|
|
3545
|
+
const stateType = readPayloadString(state, "type");
|
|
3546
|
+
if (stateName || stateType) {
|
|
3547
|
+
lines.push(
|
|
3548
|
+
`- State: ${[stateName, stateType ? `(${stateType})` : void 0].filter(Boolean).join(" ")}`
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
const priority = readPayloadString(issueContext, "priority");
|
|
3552
|
+
if (priority) lines.push(`- Priority: ${priority}`);
|
|
3553
|
+
const reporter = readPayloadString(issueContext, "reporter");
|
|
3554
|
+
if (reporter) lines.push(`- Reporter: ${reporter}`);
|
|
3555
|
+
const assignees = readPayloadStringArray(issueContext, "assignees");
|
|
3556
|
+
if (assignees.length > 0) {
|
|
3557
|
+
lines.push(`- Assignees: ${assignees.join(", ")}`);
|
|
3558
|
+
}
|
|
3559
|
+
const labels = readPayloadStringArray(issueContext, "labels");
|
|
3560
|
+
if (labels.length > 0) {
|
|
3561
|
+
lines.push(`- Labels: ${labels.join(", ")}`);
|
|
3562
|
+
}
|
|
3563
|
+
const dates = readPayloadValue(issueContext, "dates");
|
|
3564
|
+
const startDate = readPayloadString(dates, "startDate");
|
|
3565
|
+
const dueDate = readPayloadString(dates, "dueDate");
|
|
3566
|
+
if (startDate || dueDate) {
|
|
3567
|
+
lines.push(
|
|
3568
|
+
`- Dates: ${[startDate ? `start ${startDate}` : void 0, dueDate ? `due ${dueDate}` : void 0].filter(Boolean).join(", ")}`
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
const recentComments = readPayloadArray(issueContext, "recentComments");
|
|
3572
|
+
if (recentComments.length > 0) {
|
|
3573
|
+
lines.push("- Recent comments:");
|
|
3574
|
+
for (const comment of recentComments) {
|
|
3575
|
+
const author = readPayloadString(comment, "authorName") ?? "Unknown";
|
|
3576
|
+
const body = readPayloadString(comment, "body");
|
|
3577
|
+
if (body) {
|
|
3578
|
+
lines.push(` - ${author}: ${body}`);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return lines;
|
|
3583
|
+
}
|
|
3584
|
+
function summarizeMessage(message) {
|
|
3585
|
+
if (!message) {
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
return message.length > 120 ? `${message.slice(0, 117).trimEnd()}...` : message;
|
|
3589
|
+
}
|
|
3590
|
+
function truncateForLog(message) {
|
|
3591
|
+
return message.length > 80 ? `${message.slice(0, 77).trimEnd()}...` : message;
|
|
3592
|
+
}
|
|
3593
|
+
function listObservedSessionsForWorkspace(provider, workspacePath) {
|
|
3594
|
+
return discoverAttachableSessions().filter(
|
|
3595
|
+
(session) => session.provider === provider && matchesWorkspacePath(session, workspacePath)
|
|
3596
|
+
).sort(compareLocalSessionRecency);
|
|
3597
|
+
}
|
|
3598
|
+
function findObservedSessionInProcessTree(sessions, paneProcessId) {
|
|
3599
|
+
const descendantIds = listDescendantProcessIds(paneProcessId);
|
|
3600
|
+
if (descendantIds.size === 0) {
|
|
3601
|
+
return void 0;
|
|
3602
|
+
}
|
|
3603
|
+
return sessions.find(
|
|
3604
|
+
(session) => session.localProcessId ? descendantIds.has(session.localProcessId) : false
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
function listDescendantProcessIds(rootPid) {
|
|
3608
|
+
const descendants = /* @__PURE__ */ new Set([rootPid]);
|
|
3609
|
+
try {
|
|
3610
|
+
const output = execSync2("ps -axo pid=,ppid=", {
|
|
3611
|
+
encoding: "utf-8",
|
|
3612
|
+
timeout: 3e3
|
|
3613
|
+
});
|
|
3614
|
+
const parentToChildren = /* @__PURE__ */ new Map();
|
|
3615
|
+
for (const line of output.split("\n").map((value) => value.trim()).filter(Boolean)) {
|
|
3616
|
+
const [pid, ppid] = line.split(/\s+/, 2);
|
|
3617
|
+
if (!pid || !ppid) {
|
|
3618
|
+
continue;
|
|
3619
|
+
}
|
|
3620
|
+
const children = parentToChildren.get(ppid) ?? [];
|
|
3621
|
+
children.push(pid);
|
|
3622
|
+
parentToChildren.set(ppid, children);
|
|
3623
|
+
}
|
|
3624
|
+
const queue = [rootPid];
|
|
3625
|
+
while (queue.length > 0) {
|
|
3626
|
+
const currentPid = queue.shift();
|
|
3627
|
+
if (!currentPid) {
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
for (const childPid of parentToChildren.get(currentPid) ?? []) {
|
|
3631
|
+
if (descendants.has(childPid)) {
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
descendants.add(childPid);
|
|
3635
|
+
queue.push(childPid);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
} catch {
|
|
3639
|
+
return descendants;
|
|
3640
|
+
}
|
|
3641
|
+
return descendants;
|
|
3642
|
+
}
|
|
3643
|
+
function matchesWorkspacePath(session, workspacePath) {
|
|
3644
|
+
const normalizedWorkspace = normalizePath(workspacePath);
|
|
3645
|
+
const candidatePaths = [session.cwd, session.repoRoot].filter((value) => Boolean(value)).map(normalizePath);
|
|
3646
|
+
return candidatePaths.some((path3) => path3 === normalizedWorkspace);
|
|
3647
|
+
}
|
|
3648
|
+
function normalizePath(value) {
|
|
3649
|
+
return value.replace(/\/+$/, "");
|
|
3650
|
+
}
|
|
3651
|
+
function sessionIdentityKey(session) {
|
|
3652
|
+
return [
|
|
3653
|
+
session.provider,
|
|
3654
|
+
session.sessionKey,
|
|
3655
|
+
session.localProcessId,
|
|
3656
|
+
session.cwd
|
|
3657
|
+
].filter(Boolean).join("::");
|
|
3658
|
+
}
|
|
3659
|
+
function compareLocalSessionRecency(a, b) {
|
|
3660
|
+
return Number(b.localProcessId ?? 0) - Number(a.localProcessId ?? 0);
|
|
3661
|
+
}
|
|
3662
|
+
function sleep(ms) {
|
|
3663
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3664
|
+
}
|
|
3665
|
+
function readPayloadValue(payload, key) {
|
|
3666
|
+
return payload !== null && typeof payload === "object" ? Reflect.get(payload, key) : void 0;
|
|
3667
|
+
}
|
|
3668
|
+
function readPayloadString(payload, key) {
|
|
3669
|
+
const value = readPayloadValue(payload, key);
|
|
3670
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
3671
|
+
}
|
|
3672
|
+
function readPayloadNumber(payload, key) {
|
|
3673
|
+
const value = readPayloadValue(payload, key);
|
|
3674
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3675
|
+
}
|
|
3676
|
+
function readPayloadBoolean(payload, key) {
|
|
3677
|
+
const value = readPayloadValue(payload, key);
|
|
3678
|
+
return typeof value === "boolean" ? value : void 0;
|
|
3679
|
+
}
|
|
3680
|
+
function readPayloadPermissionMode(payload, key) {
|
|
3681
|
+
const value = readPayloadValue(payload, key);
|
|
3682
|
+
return value === "plan" || value === "ask" || value === "bypass" ? value : void 0;
|
|
3683
|
+
}
|
|
3684
|
+
function readPayloadThinkingLevel(payload, key) {
|
|
3685
|
+
const value = readPayloadValue(payload, key);
|
|
3686
|
+
return value === "off" || value === "low" || value === "medium" || value === "high" || value === "max" || value === "xhigh" ? value : void 0;
|
|
3687
|
+
}
|
|
3688
|
+
function readPayloadContextLength(payload, key) {
|
|
3689
|
+
const value = readPayloadValue(payload, key);
|
|
3690
|
+
return value === "default" || value === "extended" ? value : void 0;
|
|
3691
|
+
}
|
|
3692
|
+
function readPayloadArray(payload, key) {
|
|
3693
|
+
const value = readPayloadValue(payload, key);
|
|
3694
|
+
return Array.isArray(value) ? value : [];
|
|
3695
|
+
}
|
|
3696
|
+
function readPayloadStringArray(payload, key) {
|
|
3697
|
+
return readPayloadArray(payload, key).filter(
|
|
3698
|
+
(value) => typeof value === "string" && value.trim() !== ""
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
function readPayloadId(payload, key) {
|
|
3702
|
+
const value = readPayloadString(payload, key);
|
|
3703
|
+
return value;
|
|
3704
|
+
}
|
|
3705
|
+
function readPayloadAgentProvider(payload, key) {
|
|
3706
|
+
const value = readPayloadValue(payload, key);
|
|
3707
|
+
return value === "codex" || value === "claude_code" || value === "cursor" || value === "copilot" || value === "opencode" || value === "pi" || value === "vector_cli" ? value : void 0;
|
|
3708
|
+
}
|
|
3709
|
+
function isBridgeProvider(provider) {
|
|
3710
|
+
return provider === "codex" || provider === "claude_code" || provider === "cursor" || provider === "copilot" || provider === "opencode" || provider === "pi";
|
|
3711
|
+
}
|
|
3712
|
+
function providerLabel2(provider) {
|
|
3713
|
+
if (provider === "codex") {
|
|
3714
|
+
return "Codex";
|
|
3715
|
+
}
|
|
3716
|
+
if (provider === "claude_code") {
|
|
3717
|
+
return "Claude";
|
|
3718
|
+
}
|
|
3719
|
+
if (provider === "cursor") {
|
|
3720
|
+
return "Cursor";
|
|
3721
|
+
}
|
|
3722
|
+
if (provider === "copilot") {
|
|
3723
|
+
return "GitHub Copilot";
|
|
3724
|
+
}
|
|
3725
|
+
if (provider === "opencode") {
|
|
3726
|
+
return "OpenCode";
|
|
3727
|
+
}
|
|
3728
|
+
if (provider === "pi") {
|
|
3729
|
+
return "Pi";
|
|
3730
|
+
}
|
|
3731
|
+
return "Vector CLI";
|
|
3732
|
+
}
|
|
3733
|
+
function installLaunchAgent(vcliPath) {
|
|
3734
|
+
if (platform() !== "darwin") {
|
|
3735
|
+
console.error("LaunchAgent is macOS only. Use systemd on Linux.");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
const programArguments = getLaunchAgentProgramArguments(vcliPath);
|
|
3739
|
+
const launchPath = buildLaunchAgentPath();
|
|
3740
|
+
const environmentVariables = [
|
|
3741
|
+
" <key>PATH</key>",
|
|
3742
|
+
` <string>${escapePlistString(launchPath)}</string>`,
|
|
3743
|
+
...process.env.VECTOR_HOME?.trim() ? [
|
|
3744
|
+
" <key>VECTOR_HOME</key>",
|
|
3745
|
+
` <string>${escapePlistString(process.env.VECTOR_HOME.trim())}</string>`
|
|
3746
|
+
] : []
|
|
3747
|
+
].join("\n");
|
|
3748
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3749
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3750
|
+
<plist version="1.0">
|
|
3751
|
+
<dict>
|
|
3752
|
+
<key>Label</key>
|
|
3753
|
+
<string>${LAUNCHAGENT_LABEL}</string>
|
|
3754
|
+
<key>ProgramArguments</key>
|
|
3755
|
+
${programArguments}
|
|
3756
|
+
<key>RunAtLoad</key>
|
|
3757
|
+
<true/>
|
|
3758
|
+
<key>KeepAlive</key>
|
|
3759
|
+
<true/>
|
|
3760
|
+
<key>StandardOutPath</key>
|
|
3761
|
+
<string>${CONFIG_DIR}/bridge.log</string>
|
|
3762
|
+
<key>StandardErrorPath</key>
|
|
3763
|
+
<string>${CONFIG_DIR}/bridge.err.log</string>
|
|
3764
|
+
<key>EnvironmentVariables</key>
|
|
3765
|
+
<dict>
|
|
3766
|
+
${environmentVariables}
|
|
3767
|
+
</dict>
|
|
3768
|
+
</dict>
|
|
3769
|
+
</plist>`;
|
|
3770
|
+
if (!existsSync3(LAUNCHAGENT_DIR)) {
|
|
3771
|
+
mkdirSync(LAUNCHAGENT_DIR, { recursive: true });
|
|
3772
|
+
}
|
|
3773
|
+
removeLegacyMenuBarLaunchAgent();
|
|
3774
|
+
writeFileSync(LAUNCHAGENT_PLIST, plist);
|
|
3775
|
+
console.log(`Installed LaunchAgent: ${LAUNCHAGENT_PLIST}`);
|
|
3776
|
+
}
|
|
3777
|
+
function getLaunchAgentProgramArguments(vcliPath) {
|
|
3778
|
+
const args = resolveCliInvocation(vcliPath);
|
|
3779
|
+
return [
|
|
3780
|
+
"<array>",
|
|
3781
|
+
...args.map((arg) => ` <string>${escapePlistString(arg)}</string>`),
|
|
3782
|
+
" <string>service</string>",
|
|
3783
|
+
" <string>run</string>",
|
|
3784
|
+
" </array>"
|
|
3785
|
+
].join("\n");
|
|
3786
|
+
}
|
|
3787
|
+
function resolveCliInvocation(vcliPath) {
|
|
3788
|
+
const resolvedPath = resolveExecutablePath(vcliPath);
|
|
3789
|
+
if (resolvedPath.endsWith(".js")) {
|
|
3790
|
+
return [process.execPath, resolvedPath];
|
|
3791
|
+
}
|
|
3792
|
+
if (resolvedPath.endsWith(".ts")) {
|
|
3793
|
+
const tsxPath = join2(
|
|
3794
|
+
import.meta.dirname ?? process.cwd(),
|
|
3795
|
+
"..",
|
|
3796
|
+
"..",
|
|
3797
|
+
"..",
|
|
3798
|
+
"node_modules",
|
|
3799
|
+
".bin",
|
|
3800
|
+
"tsx"
|
|
3801
|
+
);
|
|
3802
|
+
if (existsSync3(tsxPath)) {
|
|
3803
|
+
return [tsxPath, resolvedPath];
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
return [resolvedPath];
|
|
3807
|
+
}
|
|
3808
|
+
function resolveExecutablePath(executablePath) {
|
|
3809
|
+
try {
|
|
3810
|
+
return realpathSync(executablePath);
|
|
3811
|
+
} catch {
|
|
3812
|
+
return executablePath;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
function buildLaunchAgentPath() {
|
|
3816
|
+
const entries = [
|
|
3817
|
+
dirname(process.execPath),
|
|
3818
|
+
join2(homedir3(), ".volta", "bin"),
|
|
3819
|
+
"/opt/homebrew/bin",
|
|
3820
|
+
"/usr/local/bin",
|
|
3821
|
+
"/usr/bin",
|
|
3822
|
+
"/bin",
|
|
3823
|
+
"/usr/sbin",
|
|
3824
|
+
"/sbin"
|
|
3825
|
+
];
|
|
3826
|
+
return [...new Set(entries.filter(Boolean))].join(":");
|
|
3827
|
+
}
|
|
3828
|
+
function escapePlistString(value) {
|
|
3829
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3830
|
+
}
|
|
3831
|
+
function loadLaunchAgent() {
|
|
3832
|
+
if (runLaunchctl(["bootstrap", launchctlGuiDomain(), LAUNCHAGENT_PLIST])) {
|
|
3833
|
+
runLaunchctl([
|
|
3834
|
+
"kickstart",
|
|
3835
|
+
"-k",
|
|
3836
|
+
`${launchctlGuiDomain()}/${LAUNCHAGENT_LABEL}`
|
|
3837
|
+
]);
|
|
3838
|
+
console.log(
|
|
3839
|
+
"LaunchAgent loaded. Bridge will start automatically on login."
|
|
3840
|
+
);
|
|
3841
|
+
return;
|
|
3842
|
+
}
|
|
3843
|
+
if (runLaunchctl([
|
|
3844
|
+
"kickstart",
|
|
3845
|
+
"-k",
|
|
3846
|
+
`${launchctlGuiDomain()}/${LAUNCHAGENT_LABEL}`
|
|
3847
|
+
]) || runLaunchctl(["load", LAUNCHAGENT_PLIST])) {
|
|
3848
|
+
console.log(
|
|
3849
|
+
"LaunchAgent loaded. Bridge will start automatically on login."
|
|
3850
|
+
);
|
|
3851
|
+
return;
|
|
3852
|
+
}
|
|
3853
|
+
console.error("Failed to load LaunchAgent");
|
|
3854
|
+
}
|
|
3855
|
+
function unloadLaunchAgent() {
|
|
3856
|
+
if (runLaunchctl(["bootout", `${launchctlGuiDomain()}/${LAUNCHAGENT_LABEL}`]) || runLaunchctl(["bootout", launchctlGuiDomain(), LAUNCHAGENT_PLIST]) || runLaunchctl(["unload", LAUNCHAGENT_PLIST])) {
|
|
3857
|
+
console.log("LaunchAgent unloaded.");
|
|
3858
|
+
return true;
|
|
3859
|
+
}
|
|
3860
|
+
console.error("Failed to unload LaunchAgent (may not be loaded)");
|
|
3861
|
+
return false;
|
|
3862
|
+
}
|
|
3863
|
+
function uninstallLaunchAgent() {
|
|
3864
|
+
unloadLaunchAgent();
|
|
3865
|
+
removeLegacyMenuBarLaunchAgent();
|
|
3866
|
+
try {
|
|
3867
|
+
unlinkSync(LAUNCHAGENT_PLIST);
|
|
3868
|
+
console.log("LaunchAgent removed.");
|
|
3869
|
+
} catch {
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
var MENUBAR_PID_FILE = join2(CONFIG_DIR, "menubar.pid");
|
|
3873
|
+
function removeLegacyMenuBarLaunchAgent() {
|
|
3874
|
+
if (platform() !== "darwin" || !existsSync3(LEGACY_MENUBAR_LAUNCHAGENT_PLIST)) {
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
try {
|
|
3878
|
+
execSync2(`launchctl unload ${LEGACY_MENUBAR_LAUNCHAGENT_PLIST}`, {
|
|
3879
|
+
stdio: "pipe"
|
|
3880
|
+
});
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
try {
|
|
3884
|
+
unlinkSync(LEGACY_MENUBAR_LAUNCHAGENT_PLIST);
|
|
3885
|
+
} catch {
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function launchctlGuiDomain() {
|
|
3889
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : typeof process.geteuid === "function" ? process.geteuid() : 0;
|
|
3890
|
+
return `gui/${uid}`;
|
|
3891
|
+
}
|
|
3892
|
+
function runLaunchctl(args) {
|
|
3893
|
+
try {
|
|
3894
|
+
execFileSync2("launchctl", args, {
|
|
3895
|
+
stdio: "ignore"
|
|
3896
|
+
});
|
|
3897
|
+
return true;
|
|
3898
|
+
} catch {
|
|
3899
|
+
return false;
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
function findCliEntrypoint() {
|
|
3903
|
+
const candidates = [
|
|
3904
|
+
join2(import.meta.dirname ?? "", "index.js"),
|
|
3905
|
+
join2(import.meta.dirname ?? "", "index.ts"),
|
|
3906
|
+
join2(import.meta.dirname ?? "", "..", "dist", "index.js")
|
|
3907
|
+
];
|
|
3908
|
+
for (const p of candidates) {
|
|
3909
|
+
if (existsSync3(p)) return p;
|
|
3910
|
+
}
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
function getCurrentCliInvocation() {
|
|
3914
|
+
const entrypoint = findCliEntrypoint();
|
|
3915
|
+
if (!entrypoint) {
|
|
3916
|
+
return null;
|
|
3917
|
+
}
|
|
3918
|
+
return resolveCliInvocation(entrypoint);
|
|
3919
|
+
}
|
|
3920
|
+
function findMenuBarExecutable() {
|
|
3921
|
+
const candidates = [
|
|
3922
|
+
join2(
|
|
3923
|
+
import.meta.dirname ?? "",
|
|
3924
|
+
"..",
|
|
3925
|
+
"native",
|
|
3926
|
+
"VectorMenuBar.app",
|
|
3927
|
+
"Contents",
|
|
3928
|
+
"MacOS",
|
|
3929
|
+
"VectorMenuBar"
|
|
3930
|
+
),
|
|
3931
|
+
join2(
|
|
3932
|
+
import.meta.dirname ?? "",
|
|
3933
|
+
"native",
|
|
3934
|
+
"VectorMenuBar.app",
|
|
3935
|
+
"Contents",
|
|
3936
|
+
"MacOS",
|
|
3937
|
+
"VectorMenuBar"
|
|
3938
|
+
)
|
|
3939
|
+
];
|
|
3940
|
+
for (const p of candidates) {
|
|
3941
|
+
if (existsSync3(p)) {
|
|
3942
|
+
return p;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return null;
|
|
3946
|
+
}
|
|
3947
|
+
function isKnownMenuBarProcess(pid) {
|
|
3948
|
+
try {
|
|
3949
|
+
const command = execSync2(`ps -p ${pid} -o args=`, {
|
|
3950
|
+
encoding: "utf-8",
|
|
3951
|
+
timeout: 3e3
|
|
3952
|
+
});
|
|
3953
|
+
return command.includes("menubar.js") || command.includes("menubar.ts") || command.includes("VectorMenuBar");
|
|
3954
|
+
} catch {
|
|
3955
|
+
return false;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
function killExistingMenuBar() {
|
|
3959
|
+
if (existsSync3(MENUBAR_PID_FILE)) {
|
|
3960
|
+
try {
|
|
3961
|
+
const pid = Number(readFileSync2(MENUBAR_PID_FILE, "utf-8").trim());
|
|
3962
|
+
if (Number.isFinite(pid) && pid > 0 && isKnownMenuBarProcess(pid)) {
|
|
3963
|
+
process.kill(pid, "SIGTERM");
|
|
3964
|
+
}
|
|
3965
|
+
} catch {
|
|
3966
|
+
}
|
|
3967
|
+
try {
|
|
3968
|
+
unlinkSync(MENUBAR_PID_FILE);
|
|
3969
|
+
} catch {
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
function getRunningMenuBarPid() {
|
|
3974
|
+
if (!existsSync3(MENUBAR_PID_FILE)) {
|
|
3975
|
+
return null;
|
|
3976
|
+
}
|
|
3977
|
+
try {
|
|
3978
|
+
const pid = Number(readFileSync2(MENUBAR_PID_FILE, "utf-8").trim());
|
|
3979
|
+
if (Number.isFinite(pid) && pid > 0 && isKnownMenuBarProcess(pid)) {
|
|
3980
|
+
process.kill(pid, 0);
|
|
3981
|
+
return pid;
|
|
3982
|
+
}
|
|
3983
|
+
} catch {
|
|
3984
|
+
}
|
|
3985
|
+
try {
|
|
3986
|
+
unlinkSync(MENUBAR_PID_FILE);
|
|
3987
|
+
} catch {
|
|
3988
|
+
}
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
async function launchMenuBar() {
|
|
3992
|
+
if (platform() !== "darwin") return;
|
|
3993
|
+
removeLegacyMenuBarLaunchAgent();
|
|
3994
|
+
const executable = findMenuBarExecutable();
|
|
3995
|
+
const cliInvocation = getCurrentCliInvocation();
|
|
3996
|
+
if (!executable || !cliInvocation) return;
|
|
3997
|
+
if (getRunningMenuBarPid()) {
|
|
3998
|
+
return;
|
|
3999
|
+
}
|
|
4000
|
+
try {
|
|
4001
|
+
const { spawn: spawnChild } = await import("child_process");
|
|
4002
|
+
const child = spawnChild(executable, [], {
|
|
4003
|
+
detached: true,
|
|
4004
|
+
stdio: "ignore",
|
|
4005
|
+
env: {
|
|
4006
|
+
...process.env,
|
|
4007
|
+
VECTOR_CLI_COMMAND: cliInvocation[0],
|
|
4008
|
+
VECTOR_CLI_ARGS_JSON: JSON.stringify(cliInvocation.slice(1))
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
child.unref();
|
|
4012
|
+
if (child.pid) {
|
|
4013
|
+
writeFileSync(MENUBAR_PID_FILE, String(child.pid));
|
|
4014
|
+
}
|
|
4015
|
+
} catch {
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
function stopMenuBar() {
|
|
4019
|
+
killExistingMenuBar();
|
|
4020
|
+
}
|
|
4021
|
+
function getBridgeStatus() {
|
|
4022
|
+
const config = loadBridgeConfig();
|
|
4023
|
+
if (!config) return { configured: false, running: false, starting: false };
|
|
4024
|
+
let running = false;
|
|
4025
|
+
let starting = false;
|
|
4026
|
+
let pid;
|
|
4027
|
+
if (existsSync3(PID_FILE)) {
|
|
4028
|
+
const pidStr = readFileSync2(PID_FILE, "utf-8").trim();
|
|
4029
|
+
pid = Number(pidStr);
|
|
4030
|
+
try {
|
|
4031
|
+
process.kill(pid, 0);
|
|
4032
|
+
running = true;
|
|
4033
|
+
} catch {
|
|
4034
|
+
running = false;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
if (!running && platform() === "darwin") {
|
|
4038
|
+
starting = runLaunchctl(["print", `${launchctlGuiDomain()}/${LAUNCHAGENT_LABEL}`]) || runLaunchctl(["list", LAUNCHAGENT_LABEL]);
|
|
4039
|
+
}
|
|
4040
|
+
return { configured: true, running, starting, pid, config };
|
|
4041
|
+
}
|
|
4042
|
+
function stopBridge(options) {
|
|
4043
|
+
if (options?.includeMenuBar) {
|
|
4044
|
+
killExistingMenuBar();
|
|
4045
|
+
}
|
|
4046
|
+
try {
|
|
4047
|
+
writeLiveActivitiesCache([]);
|
|
4048
|
+
} catch {
|
|
4049
|
+
}
|
|
4050
|
+
if (!existsSync3(PID_FILE)) return false;
|
|
4051
|
+
const pid = Number(readFileSync2(PID_FILE, "utf-8").trim());
|
|
4052
|
+
try {
|
|
4053
|
+
process.kill(pid, "SIGTERM");
|
|
4054
|
+
return true;
|
|
4055
|
+
} catch {
|
|
4056
|
+
return false;
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
function ts2() {
|
|
4060
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
296
4061
|
}
|
|
297
4062
|
|
|
298
|
-
//
|
|
4063
|
+
// src/index.ts
|
|
4064
|
+
import { platform as osPlatform } from "os";
|
|
299
4065
|
loadEnv({ path: ".env.local", override: false });
|
|
300
4066
|
loadEnv({ path: ".env", override: false });
|
|
301
4067
|
var cliApi = {
|
|
@@ -404,7 +4170,103 @@ function buildPaginationOptions(limit, cursor) {
|
|
|
404
4170
|
function normalizeMatch(value) {
|
|
405
4171
|
return value?.trim().toLowerCase();
|
|
406
4172
|
}
|
|
407
|
-
|
|
4173
|
+
function parseDate(value) {
|
|
4174
|
+
const ms = Date.parse(value);
|
|
4175
|
+
if (!Number.isFinite(ms)) {
|
|
4176
|
+
throw new Error(`Invalid date: ${value}`);
|
|
4177
|
+
}
|
|
4178
|
+
return ms;
|
|
4179
|
+
}
|
|
4180
|
+
function applyListFilters(items, options) {
|
|
4181
|
+
let result = [...items];
|
|
4182
|
+
if (options.createdAfter) {
|
|
4183
|
+
const threshold = parseDate(options.createdAfter);
|
|
4184
|
+
result = result.filter(
|
|
4185
|
+
(item) => typeof item.createdAt === "number" && item.createdAt >= threshold
|
|
4186
|
+
);
|
|
4187
|
+
}
|
|
4188
|
+
if (options.createdBefore) {
|
|
4189
|
+
const threshold = parseDate(options.createdBefore);
|
|
4190
|
+
result = result.filter(
|
|
4191
|
+
(item) => typeof item.createdAt === "number" && item.createdAt <= threshold
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
if (options.updatedAfter) {
|
|
4195
|
+
const threshold = parseDate(options.updatedAfter);
|
|
4196
|
+
result = result.filter(
|
|
4197
|
+
(item) => typeof item.lastEditedAt === "number" && item.lastEditedAt >= threshold
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
if (options.updatedBefore) {
|
|
4201
|
+
const threshold = parseDate(options.updatedBefore);
|
|
4202
|
+
result = result.filter(
|
|
4203
|
+
(item) => typeof item.lastEditedAt === "number" && item.lastEditedAt <= threshold
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
if (options.sort) {
|
|
4207
|
+
const field = options.sort;
|
|
4208
|
+
const desc = options.order?.toLowerCase() === "desc";
|
|
4209
|
+
result.sort((a, b) => {
|
|
4210
|
+
const aVal = a[field];
|
|
4211
|
+
const bVal = b[field];
|
|
4212
|
+
if (aVal == null && bVal == null) return 0;
|
|
4213
|
+
if (aVal == null) return 1;
|
|
4214
|
+
if (bVal == null) return -1;
|
|
4215
|
+
if (typeof aVal === "number" && typeof bVal === "number")
|
|
4216
|
+
return desc ? bVal - aVal : aVal - bVal;
|
|
4217
|
+
return desc ? String(bVal).localeCompare(String(aVal)) : String(aVal).localeCompare(String(bVal));
|
|
4218
|
+
});
|
|
4219
|
+
}
|
|
4220
|
+
if (options.limit) {
|
|
4221
|
+
const limit = Number(options.limit);
|
|
4222
|
+
if (Number.isFinite(limit) && limit > 0) {
|
|
4223
|
+
result = result.slice(0, limit);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
return result;
|
|
4227
|
+
}
|
|
4228
|
+
function addEntityUrls(items, appUrl, orgSlug, entityType) {
|
|
4229
|
+
return items.map((item) => {
|
|
4230
|
+
let path3;
|
|
4231
|
+
switch (entityType) {
|
|
4232
|
+
case "issues":
|
|
4233
|
+
path3 = `/${orgSlug}/issues/${item.key}`;
|
|
4234
|
+
break;
|
|
4235
|
+
case "projects":
|
|
4236
|
+
path3 = `/${orgSlug}/projects/${item.key}`;
|
|
4237
|
+
break;
|
|
4238
|
+
case "teams":
|
|
4239
|
+
path3 = `/${orgSlug}/teams/${item.key}`;
|
|
4240
|
+
break;
|
|
4241
|
+
case "documents":
|
|
4242
|
+
path3 = `/${orgSlug}/documents/${item.id}`;
|
|
4243
|
+
break;
|
|
4244
|
+
case "folders":
|
|
4245
|
+
path3 = `/${orgSlug}/documents/folders/${item.id}`;
|
|
4246
|
+
break;
|
|
4247
|
+
}
|
|
4248
|
+
return { ...item, url: `${appUrl}${path3}` };
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
function normalizeAppUrl(raw) {
|
|
4252
|
+
let url = raw.trim();
|
|
4253
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
4254
|
+
const isLocal = /^localhost(:\d+)?/i.test(url) || /^127\.0\.0\.1(:\d+)?/.test(url);
|
|
4255
|
+
url = isLocal ? `http://${url}` : `https://${url}`;
|
|
4256
|
+
}
|
|
4257
|
+
return url.replace(/\/+$/, "");
|
|
4258
|
+
}
|
|
4259
|
+
async function resolveAppUrl(raw) {
|
|
4260
|
+
const url = normalizeAppUrl(raw);
|
|
4261
|
+
try {
|
|
4262
|
+
const response = await fetch(url, { method: "HEAD", redirect: "follow" });
|
|
4263
|
+
const resolved = new URL(response.url).origin;
|
|
4264
|
+
return resolved;
|
|
4265
|
+
} catch {
|
|
4266
|
+
return url;
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
async function fetchAppConfig(appUrl) {
|
|
408
4270
|
try {
|
|
409
4271
|
const url = new URL("/api/config", appUrl).toString();
|
|
410
4272
|
const response = await fetch(url);
|
|
@@ -413,21 +4275,29 @@ async function fetchConvexUrl(appUrl) {
|
|
|
413
4275
|
}
|
|
414
4276
|
const data = await response.json();
|
|
415
4277
|
if (data.convexUrl) {
|
|
416
|
-
return
|
|
4278
|
+
return {
|
|
4279
|
+
convexUrl: data.convexUrl,
|
|
4280
|
+
tunnelHost: data.tunnelHost || void 0
|
|
4281
|
+
};
|
|
417
4282
|
}
|
|
418
4283
|
} catch {
|
|
419
4284
|
}
|
|
420
|
-
return "http://127.0.0.1:3210";
|
|
4285
|
+
return { convexUrl: "http://127.0.0.1:3210" };
|
|
4286
|
+
}
|
|
4287
|
+
async function fetchConvexUrl(appUrl) {
|
|
4288
|
+
const config = await fetchAppConfig(appUrl);
|
|
4289
|
+
return config.convexUrl;
|
|
421
4290
|
}
|
|
422
4291
|
async function getRuntime(command) {
|
|
423
4292
|
const options = command.optsWithGlobals();
|
|
424
|
-
const profile = options.profile ??
|
|
4293
|
+
const profile = options.profile ?? await readDefaultProfile();
|
|
425
4294
|
const session = await readSession(profile);
|
|
426
4295
|
const appUrlSource = options.appUrl ?? session?.appUrl ?? process.env.NEXT_PUBLIC_APP_URL;
|
|
427
|
-
const appUrl = requiredString(appUrlSource, "app URL");
|
|
428
|
-
let convexUrl = options.convexUrl ?? session?.convexUrl
|
|
4296
|
+
const appUrl = await resolveAppUrl(requiredString(appUrlSource, "app URL"));
|
|
4297
|
+
let convexUrl = options.convexUrl ?? session?.convexUrl;
|
|
429
4298
|
if (!convexUrl) {
|
|
430
|
-
|
|
4299
|
+
const fetchedUrl = await fetchConvexUrl(appUrl);
|
|
4300
|
+
convexUrl = fetchedUrl !== "http://127.0.0.1:3210" ? fetchedUrl : process.env.NEXT_PUBLIC_CONVEX_URL ?? process.env.CONVEX_URL ?? fetchedUrl;
|
|
431
4301
|
}
|
|
432
4302
|
return {
|
|
433
4303
|
appUrl,
|
|
@@ -439,7 +4309,7 @@ async function getRuntime(command) {
|
|
|
439
4309
|
};
|
|
440
4310
|
}
|
|
441
4311
|
function requireSession(runtime) {
|
|
442
|
-
if (!runtime.session || Object.keys(runtime.session.cookies).length === 0) {
|
|
4312
|
+
if (!runtime.session || Object.keys(runtime.session.cookies).length === 0 && !runtime.session.bearerToken) {
|
|
443
4313
|
throw new Error("Not logged in. Run `vcli auth login` first.");
|
|
444
4314
|
}
|
|
445
4315
|
return runtime.session;
|
|
@@ -453,6 +4323,80 @@ function requireOrg(runtime, explicit) {
|
|
|
453
4323
|
}
|
|
454
4324
|
return orgSlug;
|
|
455
4325
|
}
|
|
4326
|
+
function hostForAppUrl(appUrl) {
|
|
4327
|
+
if (!appUrl) {
|
|
4328
|
+
return void 0;
|
|
4329
|
+
}
|
|
4330
|
+
try {
|
|
4331
|
+
const url = new URL(appUrl);
|
|
4332
|
+
return url.port ? `${url.hostname}:${url.port}` : url.hostname;
|
|
4333
|
+
} catch {
|
|
4334
|
+
return void 0;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
function decodeSessionClaims(session) {
|
|
4338
|
+
const jwt = session?.cookies?.["__Secure-better-auth.convex_jwt"];
|
|
4339
|
+
if (!jwt) {
|
|
4340
|
+
return {};
|
|
4341
|
+
}
|
|
4342
|
+
const parts = jwt.split(".");
|
|
4343
|
+
if (parts.length < 2) {
|
|
4344
|
+
return {};
|
|
4345
|
+
}
|
|
4346
|
+
let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
4347
|
+
while (payload.length % 4 !== 0) {
|
|
4348
|
+
payload += "=";
|
|
4349
|
+
}
|
|
4350
|
+
try {
|
|
4351
|
+
const decoded = JSON.parse(
|
|
4352
|
+
Buffer.from(payload, "base64").toString("utf8")
|
|
4353
|
+
);
|
|
4354
|
+
return {
|
|
4355
|
+
email: decoded.email,
|
|
4356
|
+
userId: decoded.sub ?? decoded.userId
|
|
4357
|
+
};
|
|
4358
|
+
} catch {
|
|
4359
|
+
return {};
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
function buildMenuSessionInfo(session) {
|
|
4363
|
+
const claims = decodeSessionClaims(session);
|
|
4364
|
+
return {
|
|
4365
|
+
orgSlug: session?.activeOrgSlug ?? "oss-lab",
|
|
4366
|
+
appUrl: session?.appUrl,
|
|
4367
|
+
appDomain: hostForAppUrl(session?.appUrl),
|
|
4368
|
+
email: claims.email,
|
|
4369
|
+
userId: claims.userId
|
|
4370
|
+
};
|
|
4371
|
+
}
|
|
4372
|
+
function parseAgentProvider(value) {
|
|
4373
|
+
if (value === "codex" || value === "claude_code" || value === "vector_cli") {
|
|
4374
|
+
return value;
|
|
4375
|
+
}
|
|
4376
|
+
throw new Error("provider must be one of: codex, claude_code, vector_cli");
|
|
4377
|
+
}
|
|
4378
|
+
function isBridgeDeviceAuthError(error) {
|
|
4379
|
+
if (!error || typeof error !== "object") {
|
|
4380
|
+
return false;
|
|
4381
|
+
}
|
|
4382
|
+
const maybeData = error.data;
|
|
4383
|
+
return maybeData === "INVALID_DEVICE_SECRET" || maybeData === "DEVICE_NOT_FOUND";
|
|
4384
|
+
}
|
|
4385
|
+
async function validateStoredBridgeConfig(config) {
|
|
4386
|
+
const client = new ConvexHttpClient3(config.convexUrl);
|
|
4387
|
+
try {
|
|
4388
|
+
await client.mutation(api.agentBridge.bridgePublic.heartbeat, {
|
|
4389
|
+
deviceId: config.deviceId,
|
|
4390
|
+
deviceSecret: config.deviceSecret
|
|
4391
|
+
});
|
|
4392
|
+
return true;
|
|
4393
|
+
} catch (error) {
|
|
4394
|
+
if (isBridgeDeviceAuthError(error)) {
|
|
4395
|
+
return false;
|
|
4396
|
+
}
|
|
4397
|
+
throw error;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
456
4400
|
async function getClient(command) {
|
|
457
4401
|
const runtime = await getRuntime(command);
|
|
458
4402
|
const session = requireSession(runtime);
|
|
@@ -463,6 +4407,43 @@ async function getClient(command) {
|
|
|
463
4407
|
);
|
|
464
4408
|
return { client, runtime, session };
|
|
465
4409
|
}
|
|
4410
|
+
async function ensureBridgeConfig(command) {
|
|
4411
|
+
let config = loadBridgeConfig();
|
|
4412
|
+
try {
|
|
4413
|
+
const runtime = await getRuntime(command);
|
|
4414
|
+
const session = requireSession(runtime);
|
|
4415
|
+
const client = await createConvexClient(
|
|
4416
|
+
session,
|
|
4417
|
+
runtime.appUrl,
|
|
4418
|
+
runtime.convexUrl
|
|
4419
|
+
);
|
|
4420
|
+
const user = await runQuery(client, api.users.currentUser);
|
|
4421
|
+
if (!user) {
|
|
4422
|
+
throw new Error("Not logged in. Run `vcli auth login` first.");
|
|
4423
|
+
}
|
|
4424
|
+
const backendDevice = config ? await runQuery(client, api.agentBridge.queries.getDevice, {
|
|
4425
|
+
deviceId: config.deviceId
|
|
4426
|
+
}) : null;
|
|
4427
|
+
const needsRegistration = !config || config.userId !== user._id || config.convexUrl !== runtime.convexUrl || !backendDevice || !await validateStoredBridgeConfig(config);
|
|
4428
|
+
if (needsRegistration) {
|
|
4429
|
+
config = await setupBridgeDevice(client, runtime.convexUrl);
|
|
4430
|
+
}
|
|
4431
|
+
if (!config) {
|
|
4432
|
+
throw new Error("Bridge device is not configured.");
|
|
4433
|
+
}
|
|
4434
|
+
const appConfig = await fetchAppConfig(runtime.appUrl);
|
|
4435
|
+
if (appConfig.tunnelHost) {
|
|
4436
|
+
config.tunnelHost = appConfig.tunnelHost;
|
|
4437
|
+
}
|
|
4438
|
+
saveBridgeConfig(config);
|
|
4439
|
+
return config;
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
if (config) {
|
|
4442
|
+
return config;
|
|
4443
|
+
}
|
|
4444
|
+
throw error;
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
466
4447
|
async function resolveMemberId(client, orgSlug, ref) {
|
|
467
4448
|
const members = await runQuery(
|
|
468
4449
|
client,
|
|
@@ -638,10 +4619,19 @@ async function parseEstimatedTimes(client, orgSlug, value) {
|
|
|
638
4619
|
return estimatedTimes;
|
|
639
4620
|
}
|
|
640
4621
|
var program = new Command();
|
|
641
|
-
|
|
4622
|
+
function readPackageVersionSync() {
|
|
4623
|
+
try {
|
|
4624
|
+
const dir = import.meta.dirname ?? dirname2(fileURLToPath2(import.meta.url));
|
|
4625
|
+
const raw = readFileSync3(join3(dir, "..", "package.json"), "utf8");
|
|
4626
|
+
return JSON.parse(raw).version ?? "unknown";
|
|
4627
|
+
} catch {
|
|
4628
|
+
return "unknown";
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
program.name("vcli").description("Vector CLI").version(readPackageVersionSync(), "-v, --version").showHelpAfterError().option(
|
|
642
4632
|
"--app-url <url>",
|
|
643
4633
|
"Vector app URL. Required unless saved in the profile or NEXT_PUBLIC_APP_URL is set."
|
|
644
|
-
).option("--convex-url <url>", "Convex deployment URL").option("--org <slug>", "Organization slug override").option("--profile <name>", "CLI profile name"
|
|
4634
|
+
).option("--convex-url <url>", "Convex deployment URL").option("--org <slug>", "Organization slug override").option("--profile <name>", "CLI profile name").option("--json", "Output JSON");
|
|
645
4635
|
var authCommand = program.command("auth").description("Authentication");
|
|
646
4636
|
authCommand.command("signup").option("--email <email>", "Email address").option("--username <username>", "Username").option("--password <password>", "Password").action(async (options, command) => {
|
|
647
4637
|
const runtime = await getRuntime(command);
|
|
@@ -682,19 +4672,48 @@ authCommand.command("signup").option("--email <email>", "Email address").option(
|
|
|
682
4672
|
runtime.json
|
|
683
4673
|
);
|
|
684
4674
|
});
|
|
685
|
-
authCommand.command("login [identifier]").option("--password <password>", "Password").action(async (identifier, options, command) => {
|
|
4675
|
+
authCommand.command("login [identifier]").option("--password <password>", "Password (uses device flow if omitted)").action(async (identifier, options, command) => {
|
|
686
4676
|
const runtime = await getRuntime(command);
|
|
687
|
-
const loginId = identifier?.trim() || await prompt("Email or username: ");
|
|
688
|
-
const password = options.password?.trim() || await promptSecret("Password: ");
|
|
689
4677
|
let session = createEmptySession();
|
|
690
4678
|
session.appUrl = runtime.appUrl;
|
|
691
4679
|
session.convexUrl = runtime.convexUrl;
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
4680
|
+
const usePassword = Boolean(identifier || options.password);
|
|
4681
|
+
if (usePassword) {
|
|
4682
|
+
const loginId = identifier?.trim() || await prompt("Email or username: ");
|
|
4683
|
+
const password = options.password?.trim() || await promptSecret("Password: ");
|
|
4684
|
+
session = await loginWithPassword(
|
|
4685
|
+
session,
|
|
4686
|
+
runtime.appUrl,
|
|
4687
|
+
loginId,
|
|
4688
|
+
password
|
|
4689
|
+
);
|
|
4690
|
+
} else {
|
|
4691
|
+
const deviceResp = await requestDeviceCode(runtime.appUrl, "vcli");
|
|
4692
|
+
const verifyUrl = `${runtime.appUrl}/device?user_code=${deviceResp.user_code}`;
|
|
4693
|
+
console.log();
|
|
4694
|
+
console.log(` Open this URL in your browser to log in:`);
|
|
4695
|
+
console.log();
|
|
4696
|
+
console.log(` ${verifyUrl}`);
|
|
4697
|
+
console.log();
|
|
4698
|
+
console.log(` Or go to ${runtime.appUrl}/device and enter code:`);
|
|
4699
|
+
console.log();
|
|
4700
|
+
console.log(` ${deviceResp.user_code}`);
|
|
4701
|
+
console.log();
|
|
4702
|
+
const open2 = await Promise.resolve().then(() => (init_open(), open_exports)).then((m) => m.default).catch(() => null);
|
|
4703
|
+
if (open2) {
|
|
4704
|
+
await open2(verifyUrl).catch(() => {
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4707
|
+
console.log(" Waiting for authorization...");
|
|
4708
|
+
session = await pollDeviceToken(
|
|
4709
|
+
session,
|
|
4710
|
+
runtime.appUrl,
|
|
4711
|
+
deviceResp.device_code,
|
|
4712
|
+
"vcli",
|
|
4713
|
+
deviceResp.interval,
|
|
4714
|
+
deviceResp.expires_in
|
|
4715
|
+
);
|
|
4716
|
+
}
|
|
698
4717
|
const authState = await fetchAuthSession(session, runtime.appUrl);
|
|
699
4718
|
session = authState.session;
|
|
700
4719
|
const client = await createConvexClient(
|
|
@@ -735,6 +4754,38 @@ authCommand.command("whoami").action(async (_options, command) => {
|
|
|
735
4754
|
runtime.json
|
|
736
4755
|
);
|
|
737
4756
|
});
|
|
4757
|
+
authCommand.command("profiles").action(async (_options, command) => {
|
|
4758
|
+
const options = command.optsWithGlobals();
|
|
4759
|
+
const explicitProfile = options.profile?.trim();
|
|
4760
|
+
const defaultProfile = await readDefaultProfile();
|
|
4761
|
+
const profiles = await listProfiles();
|
|
4762
|
+
const activeProfile = explicitProfile || defaultProfile;
|
|
4763
|
+
printOutput(
|
|
4764
|
+
{
|
|
4765
|
+
activeProfile,
|
|
4766
|
+
defaultProfile,
|
|
4767
|
+
profiles
|
|
4768
|
+
},
|
|
4769
|
+
Boolean(options.json)
|
|
4770
|
+
);
|
|
4771
|
+
});
|
|
4772
|
+
authCommand.command("use-profile <name>").action(async (name, _options, command) => {
|
|
4773
|
+
const options = command.optsWithGlobals();
|
|
4774
|
+
const profile = name.trim();
|
|
4775
|
+
if (!profile) {
|
|
4776
|
+
throw new Error("Profile name is required.");
|
|
4777
|
+
}
|
|
4778
|
+
await writeDefaultProfile(profile);
|
|
4779
|
+
const session = await readSession(profile);
|
|
4780
|
+
printOutput(
|
|
4781
|
+
{
|
|
4782
|
+
ok: true,
|
|
4783
|
+
defaultProfile: profile,
|
|
4784
|
+
hasSession: session !== null
|
|
4785
|
+
},
|
|
4786
|
+
Boolean(options.json)
|
|
4787
|
+
);
|
|
4788
|
+
});
|
|
738
4789
|
var orgCommand = program.command("org").description("Organizations");
|
|
739
4790
|
orgCommand.command("list").action(async (_options, command) => {
|
|
740
4791
|
const { client, runtime } = await getClient(command);
|
|
@@ -1065,6 +5116,75 @@ permissionCommand.command("check-many <permissions>").option("--team <teamKey>")
|
|
|
1065
5116
|
printOutput(result, runtime.json);
|
|
1066
5117
|
});
|
|
1067
5118
|
var activityCommand = program.command("activity").description("Activity feed");
|
|
5119
|
+
activityCommand.command("list").description(
|
|
5120
|
+
"List org-wide activity with optional filters by entity type, event type, and time range"
|
|
5121
|
+
).option(
|
|
5122
|
+
"--entity-type <type>",
|
|
5123
|
+
"Filter by entity type: issue, project, team, document"
|
|
5124
|
+
).option(
|
|
5125
|
+
"--event-type <type>",
|
|
5126
|
+
"Filter by event type (e.g. issue_created, issue_priority_changed)"
|
|
5127
|
+
).option(
|
|
5128
|
+
"--since <datetime>",
|
|
5129
|
+
"Start of time range (ISO date or shorthand: today, yesterday, 7d, 30d)"
|
|
5130
|
+
).option(
|
|
5131
|
+
"--until <datetime>",
|
|
5132
|
+
"End of time range (ISO date or shorthand: today, now)"
|
|
5133
|
+
).option("--limit <n>").option("--cursor <cursor>").action(async (options, command) => {
|
|
5134
|
+
const { client, runtime } = await getClient(command);
|
|
5135
|
+
const orgSlug = requireOrg(runtime);
|
|
5136
|
+
function parseTimeArg(value, bound) {
|
|
5137
|
+
if (!value) return void 0;
|
|
5138
|
+
const now = /* @__PURE__ */ new Date();
|
|
5139
|
+
const startOfToday = new Date(
|
|
5140
|
+
now.getFullYear(),
|
|
5141
|
+
now.getMonth(),
|
|
5142
|
+
now.getDate()
|
|
5143
|
+
);
|
|
5144
|
+
const endOfToday = new Date(
|
|
5145
|
+
now.getFullYear(),
|
|
5146
|
+
now.getMonth(),
|
|
5147
|
+
now.getDate(),
|
|
5148
|
+
23,
|
|
5149
|
+
59,
|
|
5150
|
+
59,
|
|
5151
|
+
999
|
|
5152
|
+
);
|
|
5153
|
+
switch (value) {
|
|
5154
|
+
case "now":
|
|
5155
|
+
return now.getTime();
|
|
5156
|
+
case "today":
|
|
5157
|
+
return bound === "start" ? startOfToday.getTime() : endOfToday.getTime();
|
|
5158
|
+
case "yesterday":
|
|
5159
|
+
return bound === "start" ? startOfToday.getTime() - 864e5 : startOfToday.getTime() - 1;
|
|
5160
|
+
default: {
|
|
5161
|
+
const daysMatch = value.match(/^(\d+)d$/);
|
|
5162
|
+
if (daysMatch) {
|
|
5163
|
+
return now.getTime() - Number(daysMatch[1]) * 864e5;
|
|
5164
|
+
}
|
|
5165
|
+
const parsed = new Date(value).getTime();
|
|
5166
|
+
if (Number.isNaN(parsed)) {
|
|
5167
|
+
throw new Error(`Invalid time value: ${value}`);
|
|
5168
|
+
}
|
|
5169
|
+
return parsed;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
const result = await runQuery(
|
|
5174
|
+
client,
|
|
5175
|
+
api.activities.queries.listOrgActivity,
|
|
5176
|
+
{
|
|
5177
|
+
orgSlug,
|
|
5178
|
+
entityType: options.entityType ?? void 0,
|
|
5179
|
+
eventType: options.eventType ?? void 0,
|
|
5180
|
+
since: parseTimeArg(options.since, "start"),
|
|
5181
|
+
until: parseTimeArg(options.until, "end"),
|
|
5182
|
+
limit: optionalNumber(options.limit, "limit") ?? void 0,
|
|
5183
|
+
cursor: options.cursor ?? void 0
|
|
5184
|
+
}
|
|
5185
|
+
);
|
|
5186
|
+
printOutput(result, runtime.json);
|
|
5187
|
+
});
|
|
1068
5188
|
activityCommand.command("project <projectKey>").option("--limit <n>").option("--cursor <cursor>").action(async (projectKey, options, command) => {
|
|
1069
5189
|
const { client, runtime } = await getClient(command);
|
|
1070
5190
|
const orgSlug = requireOrg(runtime);
|
|
@@ -1449,6 +5569,71 @@ statusCommand.command("reset [slug]").action(async (slug, _options, command) =>
|
|
|
1449
5569
|
);
|
|
1450
5570
|
printOutput(result ?? { success: true }, runtime.json);
|
|
1451
5571
|
});
|
|
5572
|
+
var presenceCommand = program.command("presence").description("User presence and custom status (Discord-like)");
|
|
5573
|
+
presenceCommand.command("get").action(async (_options, command) => {
|
|
5574
|
+
const { client, runtime } = await getClient(command);
|
|
5575
|
+
const result = await runQuery(client, api.status.getCurrentUserStatus, {});
|
|
5576
|
+
printOutput(result ?? { presence: "online" }, runtime.json);
|
|
5577
|
+
});
|
|
5578
|
+
presenceCommand.command("set <presence>").description("Set presence: online, idle, dnd, invisible").action(async (presence, _options, command) => {
|
|
5579
|
+
const valid = ["online", "idle", "dnd", "invisible"];
|
|
5580
|
+
if (!valid.includes(presence)) {
|
|
5581
|
+
throw new Error(
|
|
5582
|
+
`Invalid presence "${presence}". Must be one of: ${valid.join(", ")}`
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
const { client, runtime } = await getClient(command);
|
|
5586
|
+
await runMutation(client, api.status.setPresence, {
|
|
5587
|
+
presence
|
|
5588
|
+
});
|
|
5589
|
+
printOutput({ ok: true, presence }, runtime.json);
|
|
5590
|
+
});
|
|
5591
|
+
presenceCommand.command("custom").description("Set a custom status with emoji and text").option("--emoji <emoji>", "Status emoji").option("--text <text>", "Status text").option(
|
|
5592
|
+
"--clear-after <duration>",
|
|
5593
|
+
"Auto-clear: 30m, 1h, 4h, today, or never (default)"
|
|
5594
|
+
).action(async (options, command) => {
|
|
5595
|
+
const emoji = options.emoji?.trim();
|
|
5596
|
+
const text2 = options.text?.trim();
|
|
5597
|
+
const clearAfterRaw = options.clearAfter;
|
|
5598
|
+
if (!emoji && !text2) {
|
|
5599
|
+
throw new Error("At least --emoji or --text is required.");
|
|
5600
|
+
}
|
|
5601
|
+
let clearsAt;
|
|
5602
|
+
if (clearAfterRaw) {
|
|
5603
|
+
const durations = {
|
|
5604
|
+
"30m": 30 * 60 * 1e3,
|
|
5605
|
+
"1h": 60 * 60 * 1e3,
|
|
5606
|
+
"4h": 4 * 60 * 60 * 1e3,
|
|
5607
|
+
today: "today",
|
|
5608
|
+
never: 0
|
|
5609
|
+
};
|
|
5610
|
+
const dur = durations[clearAfterRaw];
|
|
5611
|
+
if (dur === void 0) {
|
|
5612
|
+
throw new Error(
|
|
5613
|
+
`Invalid --clear-after "${clearAfterRaw}". Must be one of: 30m, 1h, 4h, today, never`
|
|
5614
|
+
);
|
|
5615
|
+
}
|
|
5616
|
+
if (dur === "today") {
|
|
5617
|
+
const end = /* @__PURE__ */ new Date();
|
|
5618
|
+
end.setHours(23, 59, 59, 999);
|
|
5619
|
+
clearsAt = end.getTime();
|
|
5620
|
+
} else if (dur > 0) {
|
|
5621
|
+
clearsAt = Date.now() + dur;
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
const { client, runtime } = await getClient(command);
|
|
5625
|
+
await runMutation(client, api.status.setCustomStatus, {
|
|
5626
|
+
customEmoji: emoji,
|
|
5627
|
+
customText: text2,
|
|
5628
|
+
clearsAt
|
|
5629
|
+
});
|
|
5630
|
+
printOutput({ ok: true, emoji, text: text2, clearsAt }, runtime.json);
|
|
5631
|
+
});
|
|
5632
|
+
presenceCommand.command("clear").description("Clear custom status").action(async (_options, command) => {
|
|
5633
|
+
const { client, runtime } = await getClient(command);
|
|
5634
|
+
await runMutation(client, api.status.clearCustomStatus, {});
|
|
5635
|
+
printOutput({ ok: true }, runtime.json);
|
|
5636
|
+
});
|
|
1452
5637
|
var adminCommand = program.command("admin").description("Platform admin");
|
|
1453
5638
|
adminCommand.command("branding").action(async (_options, command) => {
|
|
1454
5639
|
const { client, runtime } = await getClient(command);
|
|
@@ -1521,13 +5706,14 @@ adminCommand.command("sync-disposable-domains").action(async (_options, command)
|
|
|
1521
5706
|
printOutput(result, runtime.json);
|
|
1522
5707
|
});
|
|
1523
5708
|
var teamCommand = program.command("team").description("Teams");
|
|
1524
|
-
teamCommand.command("list [slug]").option("--limit <n>").action(async (slug, options, command) => {
|
|
5709
|
+
teamCommand.command("list [slug]").option("--limit <n>").option("--created-after <date>", "Filter: created on or after date (ISO)").option("--created-before <date>", "Filter: created on or before date (ISO)").option("--sort <field>", "Sort by field (e.g. createdAt, name, key)").option("--order <direction>", "Sort order: asc or desc (default: asc)").action(async (slug, options, command) => {
|
|
1525
5710
|
const { client, runtime } = await getClient(command);
|
|
1526
5711
|
const orgSlug = requireOrg(runtime, slug);
|
|
1527
|
-
const
|
|
1528
|
-
orgSlug
|
|
1529
|
-
limit: options.limit ? Number(options.limit) : void 0
|
|
5712
|
+
const raw = await runAction(client, cliApi.listTeams, {
|
|
5713
|
+
orgSlug
|
|
1530
5714
|
});
|
|
5715
|
+
const filtered = applyListFilters(raw, options);
|
|
5716
|
+
const result = addEntityUrls(filtered, runtime.appUrl, orgSlug, "teams");
|
|
1531
5717
|
printOutput(result, runtime.json);
|
|
1532
5718
|
});
|
|
1533
5719
|
teamCommand.command("get <teamKey>").action(async (teamKey, _options, command) => {
|
|
@@ -1621,14 +5807,15 @@ teamCommand.command("set-lead <teamKey> <member>").action(async (teamKey, member
|
|
|
1621
5807
|
printOutput(result, runtime.json);
|
|
1622
5808
|
});
|
|
1623
5809
|
var projectCommand = program.command("project").description("Projects");
|
|
1624
|
-
projectCommand.command("list [slug]").option("--team <teamKey>").option("--limit <n>").action(async (slug, options, command) => {
|
|
5810
|
+
projectCommand.command("list [slug]").option("--team <teamKey>").option("--limit <n>").option("--created-after <date>", "Filter: created on or after date (ISO)").option("--created-before <date>", "Filter: created on or before date (ISO)").option("--sort <field>", "Sort by field (e.g. createdAt, name, key)").option("--order <direction>", "Sort order: asc or desc (default: asc)").action(async (slug, options, command) => {
|
|
1625
5811
|
const { client, runtime } = await getClient(command);
|
|
1626
5812
|
const orgSlug = requireOrg(runtime, slug);
|
|
1627
|
-
const
|
|
5813
|
+
const raw = await runAction(client, cliApi.listProjects, {
|
|
1628
5814
|
orgSlug,
|
|
1629
|
-
teamKey: options.team
|
|
1630
|
-
limit: options.limit ? Number(options.limit) : void 0
|
|
5815
|
+
teamKey: options.team
|
|
1631
5816
|
});
|
|
5817
|
+
const filtered = applyListFilters(raw, options);
|
|
5818
|
+
const result = addEntityUrls(filtered, runtime.appUrl, orgSlug, "projects");
|
|
1632
5819
|
printOutput(result, runtime.json);
|
|
1633
5820
|
});
|
|
1634
5821
|
projectCommand.command("get <projectKey>").action(async (projectKey, _options, command) => {
|
|
@@ -1728,15 +5915,17 @@ projectCommand.command("set-lead <projectKey> <member>").action(async (projectKe
|
|
|
1728
5915
|
printOutput(result, runtime.json);
|
|
1729
5916
|
});
|
|
1730
5917
|
var issueCommand = program.command("issue").description("Issues");
|
|
1731
|
-
issueCommand.command("list [slug]").option("--project <projectKey>").option("--team <teamKey>").option("--limit <n>").action(async (slug, options, command) => {
|
|
5918
|
+
issueCommand.command("list [slug]").option("--project <projectKey>").option("--team <teamKey>").option("--assignee <name>", "Filter by assignee name or email").option("--limit <n>").option("--created-after <date>", "Filter: created on or after date (ISO)").option("--created-before <date>", "Filter: created on or before date (ISO)").option("--sort <field>", "Sort by field (e.g. createdAt, title, key)").option("--order <direction>", "Sort order: asc or desc (default: asc)").action(async (slug, options, command) => {
|
|
1732
5919
|
const { client, runtime } = await getClient(command);
|
|
1733
5920
|
const orgSlug = requireOrg(runtime, slug);
|
|
1734
|
-
const
|
|
5921
|
+
const raw = await runAction(client, cliApi.listIssues, {
|
|
1735
5922
|
orgSlug,
|
|
1736
5923
|
projectKey: options.project,
|
|
1737
5924
|
teamKey: options.team,
|
|
1738
|
-
|
|
5925
|
+
assigneeName: options.assignee
|
|
1739
5926
|
});
|
|
5927
|
+
const filtered = applyListFilters(raw, options);
|
|
5928
|
+
const result = addEntityUrls(filtered, runtime.appUrl, orgSlug, "issues");
|
|
1740
5929
|
printOutput(result, runtime.json);
|
|
1741
5930
|
});
|
|
1742
5931
|
issueCommand.command("get <issueKey>").action(async (issueKey, _options, command) => {
|
|
@@ -1933,15 +6122,40 @@ issueCommand.command("comment <issueKey>").requiredOption("--body <body>").actio
|
|
|
1933
6122
|
});
|
|
1934
6123
|
printOutput(result, runtime.json);
|
|
1935
6124
|
});
|
|
6125
|
+
issueCommand.command("link-github <issueKey> <url>").description("Link a GitHub pull request, issue, or commit URL to an issue").action(async (issueKey, url, _options, command) => {
|
|
6126
|
+
const { client, runtime } = await getClient(command);
|
|
6127
|
+
const orgSlug = requireOrg(runtime);
|
|
6128
|
+
await runAction(client, api.github.actions.linkArtifactByUrl, {
|
|
6129
|
+
orgSlug,
|
|
6130
|
+
issueKey,
|
|
6131
|
+
url
|
|
6132
|
+
});
|
|
6133
|
+
printOutput({ success: true, issueKey, url }, runtime.json);
|
|
6134
|
+
});
|
|
1936
6135
|
var documentCommand = program.command("document").description("Documents");
|
|
1937
|
-
documentCommand.command("list [slug]").option("--folder-id <id>").option("--limit <n>").
|
|
6136
|
+
documentCommand.command("list [slug]").option("--folder-id <id>").option("--limit <n>").option("--created-after <date>", "Filter: created on or after date (ISO)").option("--created-before <date>", "Filter: created on or before date (ISO)").option(
|
|
6137
|
+
"--updated-after <date>",
|
|
6138
|
+
"Filter: last edited on or after date (ISO)"
|
|
6139
|
+
).option(
|
|
6140
|
+
"--updated-before <date>",
|
|
6141
|
+
"Filter: last edited on or before date (ISO)"
|
|
6142
|
+
).option(
|
|
6143
|
+
"--sort <field>",
|
|
6144
|
+
"Sort by field (e.g. createdAt, title, lastEditedAt)"
|
|
6145
|
+
).option("--order <direction>", "Sort order: asc or desc (default: asc)").action(async (slug, options, command) => {
|
|
1938
6146
|
const { client, runtime } = await getClient(command);
|
|
1939
6147
|
const orgSlug = requireOrg(runtime, slug);
|
|
1940
|
-
const
|
|
6148
|
+
const raw = await runAction(client, cliApi.listDocuments, {
|
|
1941
6149
|
orgSlug,
|
|
1942
|
-
folderId: options.folderId
|
|
1943
|
-
limit: options.limit ? Number(options.limit) : void 0
|
|
6150
|
+
folderId: options.folderId
|
|
1944
6151
|
});
|
|
6152
|
+
const filtered = applyListFilters(raw, options);
|
|
6153
|
+
const result = addEntityUrls(
|
|
6154
|
+
filtered,
|
|
6155
|
+
runtime.appUrl,
|
|
6156
|
+
orgSlug,
|
|
6157
|
+
"documents"
|
|
6158
|
+
);
|
|
1945
6159
|
printOutput(result, runtime.json);
|
|
1946
6160
|
});
|
|
1947
6161
|
documentCommand.command("get <documentId>").action(async (documentId, _options, command) => {
|
|
@@ -2007,10 +6221,12 @@ documentCommand.command("delete <documentId>").action(async (documentId, _option
|
|
|
2007
6221
|
printOutput(result, runtime.json);
|
|
2008
6222
|
});
|
|
2009
6223
|
var folderCommand = program.command("folder").description("Document folders");
|
|
2010
|
-
folderCommand.command("list [slug]").action(async (slug,
|
|
6224
|
+
folderCommand.command("list [slug]").option("--limit <n>").option("--created-after <date>", "Filter: created on or after date (ISO)").option("--created-before <date>", "Filter: created on or before date (ISO)").option("--sort <field>", "Sort by field (e.g. createdAt, name)").option("--order <direction>", "Sort order: asc or desc (default: asc)").action(async (slug, options, command) => {
|
|
2011
6225
|
const { client, runtime } = await getClient(command);
|
|
2012
6226
|
const orgSlug = requireOrg(runtime, slug);
|
|
2013
|
-
const
|
|
6227
|
+
const raw = await runAction(client, cliApi.listFolders, { orgSlug });
|
|
6228
|
+
const filtered = applyListFilters(raw, options);
|
|
6229
|
+
const result = addEntityUrls(filtered, runtime.appUrl, orgSlug, "folders");
|
|
2014
6230
|
printOutput(result, runtime.json);
|
|
2015
6231
|
});
|
|
2016
6232
|
folderCommand.command("create").requiredOption("--name <name>").option("--description <description>").option("--icon <icon>").option("--color <color>").action(async (options, command) => {
|
|
@@ -2050,6 +6266,371 @@ folderCommand.command("delete <folderId>").action(async (folderId, _options, com
|
|
|
2050
6266
|
});
|
|
2051
6267
|
printOutput(result, runtime.json);
|
|
2052
6268
|
});
|
|
6269
|
+
var VECTOR_HOME = process.env.VECTOR_HOME?.trim() || `${process.env.HOME ?? "~"}/.vector`;
|
|
6270
|
+
var serviceCommand = program.command("service").description("Manage the local bridge service");
|
|
6271
|
+
serviceCommand.command("start").description("Start the bridge service via LaunchAgent (macOS) or foreground").action(async (_options, command) => {
|
|
6272
|
+
const existing = getBridgeStatus();
|
|
6273
|
+
if (existing.running) {
|
|
6274
|
+
console.log(`Bridge is already running (PID ${existing.pid}).`);
|
|
6275
|
+
return;
|
|
6276
|
+
}
|
|
6277
|
+
const { spinner } = await import("@clack/prompts");
|
|
6278
|
+
const s = spinner();
|
|
6279
|
+
s.start("Ensuring device registration...");
|
|
6280
|
+
const config = await ensureBridgeConfig(command);
|
|
6281
|
+
s.stop(`Device ready: ${config.displayName}`);
|
|
6282
|
+
if (osPlatform() === "darwin") {
|
|
6283
|
+
s.start("Starting bridge service...");
|
|
6284
|
+
const vcliPath = process.argv[1] ?? "vcli";
|
|
6285
|
+
installLaunchAgent(vcliPath);
|
|
6286
|
+
loadLaunchAgent();
|
|
6287
|
+
s.stop("Bridge service started.");
|
|
6288
|
+
} else {
|
|
6289
|
+
console.log(
|
|
6290
|
+
"Starting bridge in foreground (use systemd for background)..."
|
|
6291
|
+
);
|
|
6292
|
+
const bridge = new BridgeService(config);
|
|
6293
|
+
await bridge.run();
|
|
6294
|
+
}
|
|
6295
|
+
});
|
|
6296
|
+
serviceCommand.command("run").description("Run the bridge service in the foreground (used by LaunchAgent)").action(async (_options, command) => {
|
|
6297
|
+
const config = await ensureBridgeConfig(command);
|
|
6298
|
+
if (osPlatform() === "darwin") {
|
|
6299
|
+
await launchMenuBar();
|
|
6300
|
+
}
|
|
6301
|
+
const bridge = new BridgeService(config);
|
|
6302
|
+
await bridge.run();
|
|
6303
|
+
});
|
|
6304
|
+
serviceCommand.command("stop").description("Stop the bridge service").action(() => {
|
|
6305
|
+
let unloaded = false;
|
|
6306
|
+
if (osPlatform() === "darwin") {
|
|
6307
|
+
unloaded = unloadLaunchAgent();
|
|
6308
|
+
}
|
|
6309
|
+
if (stopBridge() || unloaded) {
|
|
6310
|
+
console.log("Bridge stopped.");
|
|
6311
|
+
} else if (osPlatform() !== "darwin") {
|
|
6312
|
+
console.log("Bridge is not running.");
|
|
6313
|
+
} else {
|
|
6314
|
+
console.log("Bridge is not running.");
|
|
6315
|
+
}
|
|
6316
|
+
});
|
|
6317
|
+
serviceCommand.command("status").description("Show bridge service status").action(() => {
|
|
6318
|
+
const status = getBridgeStatus();
|
|
6319
|
+
if (!status.configured) {
|
|
6320
|
+
console.log("Bridge not configured. Run: vcli service start");
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
console.log("Vector Bridge");
|
|
6324
|
+
console.log(
|
|
6325
|
+
` Device: ${status.config.displayName} (${status.config.deviceId})`
|
|
6326
|
+
);
|
|
6327
|
+
console.log(` User: ${status.config.userId}`);
|
|
6328
|
+
const statusLabel = status.running ? `Running (PID ${status.pid})` : status.starting ? "Starting..." : "Not running";
|
|
6329
|
+
console.log(` Status: ${statusLabel}`);
|
|
6330
|
+
console.log(` Config: ${VECTOR_HOME}/bridge.json`);
|
|
6331
|
+
});
|
|
6332
|
+
serviceCommand.command("menu-state").description("Return JSON state for the macOS tray").action(async (_options, command) => {
|
|
6333
|
+
const status = getBridgeStatus();
|
|
6334
|
+
const globalOptions = command.optsWithGlobals();
|
|
6335
|
+
const profile = globalOptions.profile ?? await readDefaultProfile();
|
|
6336
|
+
const session = await readSession(profile);
|
|
6337
|
+
const profiles = await listProfiles();
|
|
6338
|
+
const defaultProfile = await readDefaultProfile();
|
|
6339
|
+
let workspaces = [];
|
|
6340
|
+
let workSessions = [];
|
|
6341
|
+
let detectedSessions = [];
|
|
6342
|
+
try {
|
|
6343
|
+
const runtime = await getRuntime(command);
|
|
6344
|
+
if (runtime.session && status.config?.deviceId) {
|
|
6345
|
+
const client = await createConvexClient(
|
|
6346
|
+
runtime.session,
|
|
6347
|
+
runtime.appUrl,
|
|
6348
|
+
runtime.convexUrl
|
|
6349
|
+
);
|
|
6350
|
+
workspaces = await runQuery(
|
|
6351
|
+
client,
|
|
6352
|
+
api.agentBridge.queries.listDeviceWorkspaces,
|
|
6353
|
+
{
|
|
6354
|
+
deviceId: status.config.deviceId
|
|
6355
|
+
}
|
|
6356
|
+
);
|
|
6357
|
+
workSessions = await runQuery(
|
|
6358
|
+
client,
|
|
6359
|
+
api.agentBridge.queries.listDeviceWorkSessions,
|
|
6360
|
+
{
|
|
6361
|
+
deviceId: status.config.deviceId
|
|
6362
|
+
}
|
|
6363
|
+
);
|
|
6364
|
+
const devices = await runQuery(
|
|
6365
|
+
client,
|
|
6366
|
+
api.agentBridge.queries.listProcessesForAttach,
|
|
6367
|
+
{}
|
|
6368
|
+
);
|
|
6369
|
+
const currentDevice = devices.find(
|
|
6370
|
+
(entry) => entry.device._id === status.config?.deviceId
|
|
6371
|
+
);
|
|
6372
|
+
detectedSessions = currentDevice?.processes ?? [];
|
|
6373
|
+
}
|
|
6374
|
+
} catch {
|
|
6375
|
+
workspaces = [];
|
|
6376
|
+
workSessions = [];
|
|
6377
|
+
detectedSessions = [];
|
|
6378
|
+
}
|
|
6379
|
+
printOutput(
|
|
6380
|
+
{
|
|
6381
|
+
configured: status.configured,
|
|
6382
|
+
running: status.running,
|
|
6383
|
+
starting: status.starting,
|
|
6384
|
+
pid: status.pid,
|
|
6385
|
+
config: status.config,
|
|
6386
|
+
sessionInfo: buildMenuSessionInfo(session),
|
|
6387
|
+
activeProfile: profile,
|
|
6388
|
+
defaultProfile,
|
|
6389
|
+
profiles,
|
|
6390
|
+
workspaces,
|
|
6391
|
+
workSessions,
|
|
6392
|
+
detectedSessions
|
|
6393
|
+
},
|
|
6394
|
+
Boolean(globalOptions.json)
|
|
6395
|
+
);
|
|
6396
|
+
});
|
|
6397
|
+
serviceCommand.command("set-default-workspace").description("Set the default workspace for this device").requiredOption("--workspace-id <id>").action(async (options, command) => {
|
|
6398
|
+
const { client, runtime } = await getClient(command);
|
|
6399
|
+
const workspaceId = await runMutation(
|
|
6400
|
+
client,
|
|
6401
|
+
api.agentBridge.mutations.setDefaultWorkspace,
|
|
6402
|
+
{
|
|
6403
|
+
workspaceId: options.workspaceId
|
|
6404
|
+
}
|
|
6405
|
+
);
|
|
6406
|
+
printOutput({ ok: true, workspaceId }, runtime.json);
|
|
6407
|
+
});
|
|
6408
|
+
serviceCommand.command("search-issues <query>").description("Search issues for tray attach actions").option("--limit <n>").action(async (query, options, command) => {
|
|
6409
|
+
const { client, runtime } = await getClient(command);
|
|
6410
|
+
const orgSlug = requireOrg(runtime);
|
|
6411
|
+
const result = await runQuery(client, api.search.queries.searchEntities, {
|
|
6412
|
+
orgSlug,
|
|
6413
|
+
query,
|
|
6414
|
+
limit: optionalNumber(options.limit, "limit") ?? 8
|
|
6415
|
+
});
|
|
6416
|
+
printOutput(result.issues ?? [], runtime.json);
|
|
6417
|
+
});
|
|
6418
|
+
serviceCommand.command("attach-process").description("Attach a detected local process to an issue").requiredOption("--issue-id <id>").requiredOption("--device-id <id>").requiredOption("--process-id <id>").requiredOption("--provider <provider>").option("--title <title>").action(async (options, command) => {
|
|
6419
|
+
const { client, runtime } = await getClient(command);
|
|
6420
|
+
const liveActivityId = await runMutation(
|
|
6421
|
+
client,
|
|
6422
|
+
api.agentBridge.mutations.attachLiveActivity,
|
|
6423
|
+
{
|
|
6424
|
+
issueId: options.issueId,
|
|
6425
|
+
deviceId: options.deviceId,
|
|
6426
|
+
processId: options.processId,
|
|
6427
|
+
provider: parseAgentProvider(options.provider),
|
|
6428
|
+
title: options.title?.trim() || void 0
|
|
6429
|
+
}
|
|
6430
|
+
);
|
|
6431
|
+
printOutput({ ok: true, liveActivityId }, runtime.json);
|
|
6432
|
+
});
|
|
6433
|
+
serviceCommand.command("install").description("Install the bridge as a system service (macOS LaunchAgent)").action(async (_options, command) => {
|
|
6434
|
+
if (osPlatform() !== "darwin") {
|
|
6435
|
+
console.error("Service install is currently macOS only (LaunchAgent).");
|
|
6436
|
+
console.error("On Linux, use systemd --user manually for now.");
|
|
6437
|
+
return;
|
|
6438
|
+
}
|
|
6439
|
+
const { spinner } = await import("@clack/prompts");
|
|
6440
|
+
const s = spinner();
|
|
6441
|
+
s.start("Ensuring device registration...");
|
|
6442
|
+
const config = await ensureBridgeConfig(command);
|
|
6443
|
+
s.stop(`Device ready: ${config.displayName}`);
|
|
6444
|
+
s.start("Installing LaunchAgent...");
|
|
6445
|
+
const vcliPath = process.argv[1] ?? "vcli";
|
|
6446
|
+
installLaunchAgent(vcliPath);
|
|
6447
|
+
s.stop("LaunchAgent installed");
|
|
6448
|
+
s.start("Starting bridge service...");
|
|
6449
|
+
loadLaunchAgent();
|
|
6450
|
+
s.stop("Bridge service started");
|
|
6451
|
+
console.log("");
|
|
6452
|
+
console.log(
|
|
6453
|
+
"Bridge installed and running. Will start automatically on login."
|
|
6454
|
+
);
|
|
6455
|
+
});
|
|
6456
|
+
serviceCommand.command("uninstall").description("Stop the bridge and uninstall the system service").action(() => {
|
|
6457
|
+
stopBridge({ includeMenuBar: true });
|
|
6458
|
+
uninstallLaunchAgent();
|
|
6459
|
+
stopMenuBar();
|
|
6460
|
+
console.log("Bridge stopped and service uninstalled.");
|
|
6461
|
+
});
|
|
6462
|
+
serviceCommand.command("logs").description("Show bridge service logs").action(async () => {
|
|
6463
|
+
const fs7 = await import("fs");
|
|
6464
|
+
const p = await import("path");
|
|
6465
|
+
const logPath = p.join(VECTOR_HOME, "bridge.log");
|
|
6466
|
+
if (fs7.existsSync(logPath)) {
|
|
6467
|
+
const content = fs7.readFileSync(logPath, "utf-8");
|
|
6468
|
+
const lines = content.split("\n");
|
|
6469
|
+
console.log(lines.slice(-50).join("\n"));
|
|
6470
|
+
} else {
|
|
6471
|
+
console.log(`No log file found at ${logPath}`);
|
|
6472
|
+
}
|
|
6473
|
+
});
|
|
6474
|
+
serviceCommand.command("enable").description("Enable bridge to start at login (macOS LaunchAgent)").action(async () => {
|
|
6475
|
+
if (osPlatform() !== "darwin") {
|
|
6476
|
+
console.error("Login item is macOS only.");
|
|
6477
|
+
return;
|
|
6478
|
+
}
|
|
6479
|
+
const vcliPath = process.argv[1] ?? "vcli";
|
|
6480
|
+
installLaunchAgent(vcliPath);
|
|
6481
|
+
loadLaunchAgent();
|
|
6482
|
+
console.log("Bridge will start automatically on login.");
|
|
6483
|
+
});
|
|
6484
|
+
serviceCommand.command("disable").description("Disable bridge from starting at login").action(() => {
|
|
6485
|
+
uninstallLaunchAgent();
|
|
6486
|
+
stopMenuBar();
|
|
6487
|
+
console.log("Bridge will no longer start at login.");
|
|
6488
|
+
});
|
|
6489
|
+
var bridgeCommand = program.command("bridge").description("Start/stop the local agent bridge");
|
|
6490
|
+
bridgeCommand.command("start").description("Register device, install service, and start the bridge").action(async (_options, command) => {
|
|
6491
|
+
const existingConfig = loadBridgeConfig();
|
|
6492
|
+
const config = await ensureBridgeConfig(command);
|
|
6493
|
+
if (!existingConfig || existingConfig.deviceId !== config.deviceId || existingConfig.userId !== config.userId) {
|
|
6494
|
+
console.log(
|
|
6495
|
+
`Device registered: ${config.displayName} (${config.deviceId})`
|
|
6496
|
+
);
|
|
6497
|
+
}
|
|
6498
|
+
if (osPlatform() === "darwin") {
|
|
6499
|
+
const vcliPath = process.argv[1] ?? "vcli";
|
|
6500
|
+
installLaunchAgent(vcliPath);
|
|
6501
|
+
loadLaunchAgent();
|
|
6502
|
+
console.log("\nBridge installed and started as LaunchAgent.");
|
|
6503
|
+
console.log("It will restart automatically on login.");
|
|
6504
|
+
console.log("Run `vcli service status` to check.");
|
|
6505
|
+
} else {
|
|
6506
|
+
console.log("Starting bridge in foreground...");
|
|
6507
|
+
const bridge = new BridgeService(config);
|
|
6508
|
+
await bridge.run();
|
|
6509
|
+
}
|
|
6510
|
+
});
|
|
6511
|
+
bridgeCommand.command("stop").description("Stop the bridge service").action(() => {
|
|
6512
|
+
let unloaded = false;
|
|
6513
|
+
if (osPlatform() === "darwin") {
|
|
6514
|
+
uninstallLaunchAgent();
|
|
6515
|
+
unloaded = true;
|
|
6516
|
+
}
|
|
6517
|
+
if (stopBridge() || unloaded) {
|
|
6518
|
+
console.log("Bridge stopped.");
|
|
6519
|
+
} else {
|
|
6520
|
+
console.log("Bridge is not running.");
|
|
6521
|
+
}
|
|
6522
|
+
});
|
|
6523
|
+
bridgeCommand.command("status").description("Show bridge status").action(() => {
|
|
6524
|
+
const s = getBridgeStatus();
|
|
6525
|
+
if (!s.configured) {
|
|
6526
|
+
console.log("Bridge not configured. Run: vcli bridge start");
|
|
6527
|
+
return;
|
|
6528
|
+
}
|
|
6529
|
+
console.log("Vector Bridge");
|
|
6530
|
+
console.log(` Device: ${s.config.displayName} (${s.config.deviceId})`);
|
|
6531
|
+
console.log(
|
|
6532
|
+
` Status: ${s.running ? `Running (PID ${s.pid})` : "Not running"}`
|
|
6533
|
+
);
|
|
6534
|
+
});
|
|
6535
|
+
function detectInstallMethod(version) {
|
|
6536
|
+
const execPath = process.argv[1] ?? "";
|
|
6537
|
+
const pkg = `@rehpic/vcli@${version}`;
|
|
6538
|
+
if (execPath.includes(".volta")) {
|
|
6539
|
+
return {
|
|
6540
|
+
method: "volta",
|
|
6541
|
+
command: ["volta", "install", pkg]
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
if (execPath.includes("pnpm")) {
|
|
6545
|
+
return {
|
|
6546
|
+
method: "pnpm",
|
|
6547
|
+
command: ["pnpm", "add", "-g", pkg]
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
if (execPath.includes("yarn")) {
|
|
6551
|
+
return {
|
|
6552
|
+
method: "yarn",
|
|
6553
|
+
command: ["yarn", "global", "add", pkg]
|
|
6554
|
+
};
|
|
6555
|
+
}
|
|
6556
|
+
return {
|
|
6557
|
+
method: "npm",
|
|
6558
|
+
command: ["npm", "install", "-g", pkg]
|
|
6559
|
+
};
|
|
6560
|
+
}
|
|
6561
|
+
async function checkForUpdate() {
|
|
6562
|
+
try {
|
|
6563
|
+
const { execSync: exec } = await import("child_process");
|
|
6564
|
+
const tagsRaw = exec("npm view @rehpic/vcli dist-tags --json", {
|
|
6565
|
+
encoding: "utf-8",
|
|
6566
|
+
timeout: 1e4
|
|
6567
|
+
}).trim();
|
|
6568
|
+
const tags = JSON.parse(tagsRaw);
|
|
6569
|
+
const latest = tags.beta ?? tags.latest ?? "";
|
|
6570
|
+
const current = readPackageVersionSync();
|
|
6571
|
+
return {
|
|
6572
|
+
current,
|
|
6573
|
+
latest,
|
|
6574
|
+
hasUpdate: Boolean(latest) && latest !== current
|
|
6575
|
+
};
|
|
6576
|
+
} catch {
|
|
6577
|
+
return null;
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
program.command("update").description("Update the CLI to the latest version").action(async () => {
|
|
6581
|
+
const { spinner, log } = await import("@clack/prompts");
|
|
6582
|
+
const s = spinner();
|
|
6583
|
+
s.start("Checking for updates...");
|
|
6584
|
+
const updateInfo = await checkForUpdate();
|
|
6585
|
+
if (!updateInfo) {
|
|
6586
|
+
s.stop("Could not check for updates.");
|
|
6587
|
+
return;
|
|
6588
|
+
}
|
|
6589
|
+
if (!updateInfo.hasUpdate) {
|
|
6590
|
+
s.stop(`Already on the latest version (${updateInfo.current}).`);
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6593
|
+
s.stop(`Update available: ${updateInfo.current} \u2192 ${updateInfo.latest}`);
|
|
6594
|
+
const install = detectInstallMethod(updateInfo.latest);
|
|
6595
|
+
log.info(`Install method: ${install.method}`);
|
|
6596
|
+
s.start("Stopping bridge service...");
|
|
6597
|
+
const wasRunning = getBridgeStatus().running;
|
|
6598
|
+
if (wasRunning) {
|
|
6599
|
+
stopBridge({ includeMenuBar: true });
|
|
6600
|
+
if (osPlatform() === "darwin") {
|
|
6601
|
+
unloadLaunchAgent();
|
|
6602
|
+
}
|
|
6603
|
+
stopMenuBar();
|
|
6604
|
+
}
|
|
6605
|
+
s.stop(wasRunning ? "Bridge stopped." : "Bridge was not running.");
|
|
6606
|
+
s.start(`Updating via ${install.method}...`);
|
|
6607
|
+
try {
|
|
6608
|
+
const { execFileSync: exec } = await import("child_process");
|
|
6609
|
+
exec(install.command[0], install.command.slice(1), {
|
|
6610
|
+
stdio: "inherit",
|
|
6611
|
+
timeout: 12e4
|
|
6612
|
+
});
|
|
6613
|
+
s.stop("CLI updated successfully.");
|
|
6614
|
+
} catch {
|
|
6615
|
+
s.stop("Update failed.");
|
|
6616
|
+
log.error(`Run manually: ${install.command.join(" ")}`);
|
|
6617
|
+
return;
|
|
6618
|
+
}
|
|
6619
|
+
if (wasRunning) {
|
|
6620
|
+
s.start("Restarting bridge service...");
|
|
6621
|
+
try {
|
|
6622
|
+
const { execFileSync: exec } = await import("child_process");
|
|
6623
|
+
exec("vcli", ["service", "start"], {
|
|
6624
|
+
stdio: "inherit",
|
|
6625
|
+
timeout: 3e4
|
|
6626
|
+
});
|
|
6627
|
+
s.stop("Bridge restarted.");
|
|
6628
|
+
} catch {
|
|
6629
|
+
s.stop("Could not auto-restart. Run: vcli service start");
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
log.success(`Updated to v${updateInfo.latest}`);
|
|
6633
|
+
});
|
|
2053
6634
|
async function main() {
|
|
2054
6635
|
await program.parseAsync(process.argv);
|
|
2055
6636
|
}
|