perplexity-user-mcp 0.8.36
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 +192 -0
- package/dist/attachments.d.ts +20 -0
- package/dist/attachments.mjs +43 -0
- package/dist/checks/browser.d.ts +100 -0
- package/dist/checks/browser.mjs +89 -0
- package/dist/checks/config.d.ts +91 -0
- package/dist/checks/config.mjs +88 -0
- package/dist/checks/ide.d.ts +89 -0
- package/dist/checks/ide.mjs +80 -0
- package/dist/checks/mcp.d.ts +61 -0
- package/dist/checks/mcp.mjs +56 -0
- package/dist/checks/native-deps.d.ts +131 -0
- package/dist/checks/native-deps.mjs +115 -0
- package/dist/checks/network.d.ts +71 -0
- package/dist/checks/network.mjs +70 -0
- package/dist/checks/probe.d.ts +93 -0
- package/dist/checks/probe.mjs +82 -0
- package/dist/checks/profiles.d.ts +99 -0
- package/dist/checks/profiles.mjs +90 -0
- package/dist/checks/runtime.d.ts +89 -0
- package/dist/checks/runtime.mjs +90 -0
- package/dist/checks/vault.d.ts +101 -0
- package/dist/checks/vault.mjs +90 -0
- package/dist/chunk-3B276PGG.mjs +115 -0
- package/dist/chunk-4UEJOM6W.mjs +9 -0
- package/dist/chunk-6EP2BLTV.mjs +205 -0
- package/dist/chunk-6YMQVLFX.mjs +146 -0
- package/dist/chunk-7JL36EBH.mjs +118 -0
- package/dist/chunk-DPGMKSSA.mjs +57 -0
- package/dist/chunk-H4BUAPPO.mjs +1950 -0
- package/dist/chunk-HMKLWVXB.mjs +109 -0
- package/dist/chunk-HTUAQRKH.mjs +125 -0
- package/dist/chunk-HU5B4FXS.mjs +139 -0
- package/dist/chunk-KCXM2M4B.mjs +1006 -0
- package/dist/chunk-LKJMLGFP.mjs +237 -0
- package/dist/chunk-LZPLNZ5U.mjs +67 -0
- package/dist/chunk-MTDFKNXX.mjs +19 -0
- package/dist/chunk-OF4DMAPJ.mjs +511 -0
- package/dist/chunk-PE23RMXY.mjs +43 -0
- package/dist/chunk-Q2VY4R5F.mjs +175 -0
- package/dist/chunk-S5VD7WTU.mjs +2540 -0
- package/dist/chunk-SVPRB62V.mjs +106 -0
- package/dist/chunk-TQLCLE4L.mjs +345 -0
- package/dist/chunk-U3DGFLXZ.mjs +43 -0
- package/dist/chunk-X45O6YD3.mjs +688 -0
- package/dist/chunk-XKSWCEGI.mjs +168 -0
- package/dist/chunk-Z7DAACGZ.mjs +534 -0
- package/dist/chunk-ZQFUZPLO.mjs +257 -0
- package/dist/cli.d.ts +952 -0
- package/dist/cli.mjs +827 -0
- package/dist/client.d.ts +355 -0
- package/dist/client.mjs +27 -0
- package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
- package/dist/cloud-sync.d.ts +42 -0
- package/dist/cloud-sync.mjs +17 -0
- package/dist/config.d.ts +186 -0
- package/dist/config.mjs +54 -0
- package/dist/daemon/attach.d.ts +36 -0
- package/dist/daemon/attach.mjs +25 -0
- package/dist/daemon/audit.d.ts +23 -0
- package/dist/daemon/audit.mjs +12 -0
- package/dist/daemon/client-http.d.ts +42 -0
- package/dist/daemon/client-http.mjs +29 -0
- package/dist/daemon/index.d.ts +14 -0
- package/dist/daemon/index.mjs +110 -0
- package/dist/daemon/install-tunnel.d.ts +46 -0
- package/dist/daemon/install-tunnel.mjs +14 -0
- package/dist/daemon/launcher.d.ts +163 -0
- package/dist/daemon/launcher.mjs +50 -0
- package/dist/daemon/lockfile.d.ts +29 -0
- package/dist/daemon/lockfile.mjs +18 -0
- package/dist/daemon/server.d.ts +159 -0
- package/dist/daemon/server.mjs +20 -0
- package/dist/daemon/token.d.ts +17 -0
- package/dist/daemon/token.mjs +17 -0
- package/dist/daemon/tunnel-providers/index.d.ts +330 -0
- package/dist/daemon/tunnel-providers/index.mjs +57 -0
- package/dist/daemon/tunnel.d.ts +23 -0
- package/dist/daemon/tunnel.mjs +9 -0
- package/dist/doctor-report.d.ts +24 -0
- package/dist/doctor-report.mjs +14 -0
- package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.mjs +16 -0
- package/dist/export.d.ts +19 -0
- package/dist/export.mjs +15 -0
- package/dist/health-check.d.ts +108 -0
- package/dist/health-check.mjs +92 -0
- package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
- package/dist/history-store.d.ts +65 -0
- package/dist/history-store.mjs +48 -0
- package/dist/impit-login-runner.d.ts +469 -0
- package/dist/impit-login-runner.mjs +685 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.mjs +236 -0
- package/dist/login-runner.d.ts +333 -0
- package/dist/login-runner.mjs +320 -0
- package/dist/logout.d.ts +28 -0
- package/dist/logout.mjs +45 -0
- package/dist/manual-login-runner.d.ts +150 -0
- package/dist/manual-login-runner.mjs +146 -0
- package/dist/native-deps-BNThFHxa.d.ts +175 -0
- package/dist/native-deps-YNKXITRY.mjs +139 -0
- package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
- package/dist/profiles.d.ts +41 -0
- package/dist/profiles.mjs +33 -0
- package/dist/redact.d.ts +159 -0
- package/dist/redact.mjs +11 -0
- package/dist/refresh.d.ts +118 -0
- package/dist/refresh.mjs +21 -0
- package/dist/reinit-watcher.d.ts +15 -0
- package/dist/reinit-watcher.mjs +8 -0
- package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
- package/dist/tty-prompt.d.ts +44 -0
- package/dist/tty-prompt.mjs +39 -0
- package/dist/vault.d-BtRSLZiM.d.ts +8 -0
- package/dist/vault.d.ts +37 -0
- package/dist/vault.mjs +21 -0
- package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
- package/dist/viewer-detect.d.ts +4 -0
- package/dist/viewer-detect.mjs +37 -0
- package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
- package/dist/viewers.d.ts +18 -0
- package/dist/viewers.mjs +122 -0
- package/package.json +152 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTunnelProvider,
|
|
3
|
+
readTunnelSettings
|
|
4
|
+
} from "./chunk-KCXM2M4B.mjs";
|
|
5
|
+
import {
|
|
6
|
+
acquire,
|
|
7
|
+
getLockfilePath,
|
|
8
|
+
isStale,
|
|
9
|
+
read,
|
|
10
|
+
release,
|
|
11
|
+
replace
|
|
12
|
+
} from "./chunk-6EP2BLTV.mjs";
|
|
13
|
+
import {
|
|
14
|
+
getPackageVersion,
|
|
15
|
+
startDaemonServer
|
|
16
|
+
} from "./chunk-S5VD7WTU.mjs";
|
|
17
|
+
import {
|
|
18
|
+
ensureToken,
|
|
19
|
+
getTokenPath,
|
|
20
|
+
readToken
|
|
21
|
+
} from "./chunk-HTUAQRKH.mjs";
|
|
22
|
+
import {
|
|
23
|
+
watchReinit
|
|
24
|
+
} from "./chunk-U3DGFLXZ.mjs";
|
|
25
|
+
import {
|
|
26
|
+
PerplexityClient
|
|
27
|
+
} from "./chunk-H4BUAPPO.mjs";
|
|
28
|
+
import {
|
|
29
|
+
getActiveName,
|
|
30
|
+
getConfigDir
|
|
31
|
+
} from "./chunk-XKSWCEGI.mjs";
|
|
32
|
+
|
|
33
|
+
// src/daemon/launcher.ts
|
|
34
|
+
import { spawn } from "child_process";
|
|
35
|
+
import { randomUUID } from "crypto";
|
|
36
|
+
import { existsSync } from "fs";
|
|
37
|
+
import { fileURLToPath } from "url";
|
|
38
|
+
import { setTimeout as delay } from "timers/promises";
|
|
39
|
+
async function getDaemonStatus(options = {}) {
|
|
40
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
41
|
+
const lockPath = getLockfilePath(configDir);
|
|
42
|
+
const tokenPath = getTokenPath(configDir);
|
|
43
|
+
const record = read({ lockPath });
|
|
44
|
+
if (!record) {
|
|
45
|
+
return {
|
|
46
|
+
running: false,
|
|
47
|
+
healthy: false,
|
|
48
|
+
stale: false,
|
|
49
|
+
configDir,
|
|
50
|
+
lockPath,
|
|
51
|
+
tokenPath,
|
|
52
|
+
record: null,
|
|
53
|
+
health: null
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (options.treatSelfAsZombie && record.pid === process.pid) {
|
|
57
|
+
if (options.reclaimStale) {
|
|
58
|
+
release({ lockPath, expectedUuid: record.uuid });
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
running: false,
|
|
62
|
+
healthy: false,
|
|
63
|
+
stale: true,
|
|
64
|
+
configDir,
|
|
65
|
+
lockPath,
|
|
66
|
+
tokenPath,
|
|
67
|
+
record,
|
|
68
|
+
health: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
let health = await probeHealth(record, { timeoutMs: options.healthTimeoutMs });
|
|
72
|
+
if (!health) {
|
|
73
|
+
try {
|
|
74
|
+
const tokenRecord = readToken({ tokenPath });
|
|
75
|
+
if (tokenRecord && tokenRecord.bearerToken !== record.bearerToken) {
|
|
76
|
+
health = await probeHealth(
|
|
77
|
+
{ ...record, bearerToken: tokenRecord.bearerToken },
|
|
78
|
+
{ timeoutMs: options.healthTimeoutMs }
|
|
79
|
+
);
|
|
80
|
+
if (health && options.reclaimStale) {
|
|
81
|
+
try {
|
|
82
|
+
replace(
|
|
83
|
+
{ ...record, bearerToken: tokenRecord.bearerToken },
|
|
84
|
+
{ lockPath, expectedUuid: record.uuid }
|
|
85
|
+
);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const healthy = Boolean(health?.ok && health.uuid === record.uuid);
|
|
94
|
+
const stale = !healthy && isStale(record, { echoedUuid: health?.uuid ?? null });
|
|
95
|
+
if (stale && options.reclaimStale) {
|
|
96
|
+
release({ lockPath, expectedUuid: record.uuid });
|
|
97
|
+
return {
|
|
98
|
+
running: false,
|
|
99
|
+
healthy: false,
|
|
100
|
+
stale: true,
|
|
101
|
+
configDir,
|
|
102
|
+
lockPath,
|
|
103
|
+
tokenPath,
|
|
104
|
+
record,
|
|
105
|
+
health
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
running: !stale,
|
|
110
|
+
healthy,
|
|
111
|
+
stale,
|
|
112
|
+
configDir,
|
|
113
|
+
lockPath,
|
|
114
|
+
tokenPath,
|
|
115
|
+
record,
|
|
116
|
+
health
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function ensureDaemon(options = {}) {
|
|
120
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
121
|
+
const deadline = Date.now() + (options.startTimeoutMs ?? 15e3);
|
|
122
|
+
let launched = false;
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
const status = await getDaemonStatus({
|
|
125
|
+
configDir,
|
|
126
|
+
reclaimStale: true,
|
|
127
|
+
healthTimeoutMs: options.healthTimeoutMs,
|
|
128
|
+
treatSelfAsZombie: options.treatSelfAsZombie
|
|
129
|
+
});
|
|
130
|
+
if (status.running && status.healthy && status.record && status.health) {
|
|
131
|
+
return toConnectionInfo(status.record, status.health);
|
|
132
|
+
}
|
|
133
|
+
if (!status.running && !launched) {
|
|
134
|
+
await (options.spawnDaemon ?? spawnDetachedDaemon)({
|
|
135
|
+
configDir,
|
|
136
|
+
host: options.host,
|
|
137
|
+
port: options.port,
|
|
138
|
+
tunnel: options.tunnel
|
|
139
|
+
});
|
|
140
|
+
launched = true;
|
|
141
|
+
}
|
|
142
|
+
await delay(options.pollIntervalMs ?? 200);
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Timed out waiting for daemon startup in ${configDir}.`);
|
|
145
|
+
}
|
|
146
|
+
async function startDaemon(options = {}) {
|
|
147
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
148
|
+
const lockPath = getLockfilePath(configDir);
|
|
149
|
+
const tokenPath = getTokenPath(configDir);
|
|
150
|
+
const retries = options.retries ?? 3;
|
|
151
|
+
const retryDelayMs = options.retryDelayMs ?? 200;
|
|
152
|
+
const version = options.version ?? getPackageVersion();
|
|
153
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
154
|
+
const status = await getDaemonStatus({
|
|
155
|
+
configDir,
|
|
156
|
+
reclaimStale: true,
|
|
157
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
158
|
+
});
|
|
159
|
+
if (status.running && status.healthy && status.record && status.health) {
|
|
160
|
+
return {
|
|
161
|
+
attached: true,
|
|
162
|
+
...toConnectionInfo(status.record, status.health),
|
|
163
|
+
close: async () => void 0,
|
|
164
|
+
closed: Promise.resolve()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (status.running) {
|
|
168
|
+
await delay(retryDelayMs);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const uuid = randomUUID();
|
|
172
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
173
|
+
const token = ensureToken({ tokenPath });
|
|
174
|
+
const provisional = {
|
|
175
|
+
pid: process.pid,
|
|
176
|
+
uuid,
|
|
177
|
+
port: typeof options.port === "number" ? options.port : 0,
|
|
178
|
+
bearerToken: token.bearerToken,
|
|
179
|
+
version,
|
|
180
|
+
startedAt,
|
|
181
|
+
cloudflaredPid: null,
|
|
182
|
+
tunnelUrl: null
|
|
183
|
+
};
|
|
184
|
+
if (!acquire(provisional, { lockPath })) {
|
|
185
|
+
await delay(retryDelayMs);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let watcher;
|
|
189
|
+
let server;
|
|
190
|
+
let finalizePromise = null;
|
|
191
|
+
let finalizeResolve;
|
|
192
|
+
const closed = new Promise((resolve) => {
|
|
193
|
+
finalizeResolve = resolve;
|
|
194
|
+
});
|
|
195
|
+
const profile = process.env.PERPLEXITY_PROFILE || getActiveName() || "default";
|
|
196
|
+
const client = options.createClient ? options.createClient() : new PerplexityClient();
|
|
197
|
+
let tunnelState = {
|
|
198
|
+
status: "disabled",
|
|
199
|
+
url: null,
|
|
200
|
+
pid: null,
|
|
201
|
+
error: null
|
|
202
|
+
};
|
|
203
|
+
let tunnelController = null;
|
|
204
|
+
let tunnelStartPromise = null;
|
|
205
|
+
const buildRecord = (bearerToken = server?.bearerToken ?? token.bearerToken) => ({
|
|
206
|
+
pid: process.pid,
|
|
207
|
+
uuid,
|
|
208
|
+
port: server?.port ?? provisional.port,
|
|
209
|
+
bearerToken,
|
|
210
|
+
version,
|
|
211
|
+
startedAt,
|
|
212
|
+
cloudflaredPid: tunnelState.pid ?? null,
|
|
213
|
+
tunnelUrl: tunnelState.url ?? null
|
|
214
|
+
});
|
|
215
|
+
const syncLockfile = (bearerToken = server?.bearerToken ?? token.bearerToken) => {
|
|
216
|
+
replace(buildRecord(bearerToken), { lockPath, expectedUuid: uuid });
|
|
217
|
+
};
|
|
218
|
+
const publishTunnelState = () => {
|
|
219
|
+
if (!server) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
syncLockfile(server.bearerToken);
|
|
223
|
+
server.publishEvent("daemon:tunnel-url", {
|
|
224
|
+
status: tunnelState.status,
|
|
225
|
+
url: tunnelState.url,
|
|
226
|
+
pid: tunnelState.pid,
|
|
227
|
+
error: tunnelState.error ?? null
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const enableTunnelRuntime = async () => {
|
|
231
|
+
if (!server) {
|
|
232
|
+
throw new Error("Daemon server is not ready yet.");
|
|
233
|
+
}
|
|
234
|
+
if (tunnelState.status === "enabled") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (tunnelStartPromise) {
|
|
238
|
+
await tunnelStartPromise;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const settings = readTunnelSettings(configDir);
|
|
242
|
+
const provider = getTunnelProvider(settings.activeProvider);
|
|
243
|
+
const setup = await provider.isSetupComplete(configDir);
|
|
244
|
+
if (!setup.ready) {
|
|
245
|
+
throw new Error(setup.reason ?? `${provider.displayName} setup incomplete.`);
|
|
246
|
+
}
|
|
247
|
+
tunnelController = await provider.start({
|
|
248
|
+
port: server.port,
|
|
249
|
+
configDir,
|
|
250
|
+
onStateChange: (nextState) => {
|
|
251
|
+
tunnelState = nextState;
|
|
252
|
+
if (nextState.status === "crashed" || nextState.status === "disabled") {
|
|
253
|
+
tunnelController = null;
|
|
254
|
+
}
|
|
255
|
+
publishTunnelState();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
tunnelStartPromise = tunnelController.waitUntilReady.then(() => void 0).finally(() => {
|
|
259
|
+
tunnelStartPromise = null;
|
|
260
|
+
});
|
|
261
|
+
await tunnelStartPromise;
|
|
262
|
+
};
|
|
263
|
+
const disableTunnelRuntime = async () => {
|
|
264
|
+
const controller = tunnelController;
|
|
265
|
+
tunnelController = null;
|
|
266
|
+
if (!controller) {
|
|
267
|
+
if (tunnelState.status !== "disabled") {
|
|
268
|
+
tunnelState = {
|
|
269
|
+
status: "disabled",
|
|
270
|
+
url: null,
|
|
271
|
+
pid: null,
|
|
272
|
+
error: null
|
|
273
|
+
};
|
|
274
|
+
publishTunnelState();
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
await controller.stop();
|
|
279
|
+
tunnelState = {
|
|
280
|
+
status: "disabled",
|
|
281
|
+
url: null,
|
|
282
|
+
pid: null,
|
|
283
|
+
error: null
|
|
284
|
+
};
|
|
285
|
+
publishTunnelState();
|
|
286
|
+
};
|
|
287
|
+
const finalize = async () => {
|
|
288
|
+
if (!finalizePromise) {
|
|
289
|
+
finalizePromise = (async () => {
|
|
290
|
+
await disableTunnelRuntime().catch(() => void 0);
|
|
291
|
+
watcher?.dispose();
|
|
292
|
+
if (options.signal && abortHandler) {
|
|
293
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
294
|
+
}
|
|
295
|
+
process.off("SIGINT", signalHandler);
|
|
296
|
+
process.off("SIGTERM", signalHandler);
|
|
297
|
+
release({ lockPath, expectedUuid: uuid });
|
|
298
|
+
finalizeResolve?.();
|
|
299
|
+
})();
|
|
300
|
+
}
|
|
301
|
+
await finalizePromise;
|
|
302
|
+
};
|
|
303
|
+
const signalHandler = () => {
|
|
304
|
+
void close();
|
|
305
|
+
};
|
|
306
|
+
const abortHandler = () => {
|
|
307
|
+
void close();
|
|
308
|
+
};
|
|
309
|
+
const close = async () => {
|
|
310
|
+
if (server) {
|
|
311
|
+
await server.close().catch(() => void 0);
|
|
312
|
+
}
|
|
313
|
+
await finalize();
|
|
314
|
+
};
|
|
315
|
+
try {
|
|
316
|
+
watcher = watchReinit(profile, async () => {
|
|
317
|
+
await client.reinit();
|
|
318
|
+
});
|
|
319
|
+
server = await startDaemonServer({
|
|
320
|
+
host: options.host,
|
|
321
|
+
port: options.port,
|
|
322
|
+
uuid,
|
|
323
|
+
version,
|
|
324
|
+
configDir,
|
|
325
|
+
bearerToken: token.bearerToken,
|
|
326
|
+
createClient: () => client,
|
|
327
|
+
onShutdown: finalize,
|
|
328
|
+
getTunnelState: () => tunnelState,
|
|
329
|
+
onEnableTunnel: enableTunnelRuntime,
|
|
330
|
+
onDisableTunnel: disableTunnelRuntime,
|
|
331
|
+
onTunnelAutoDisable: async (info) => {
|
|
332
|
+
await disableTunnelRuntime().catch(() => void 0);
|
|
333
|
+
tunnelState = {
|
|
334
|
+
status: "crashed",
|
|
335
|
+
url: null,
|
|
336
|
+
pid: null,
|
|
337
|
+
error: `Auto-disabled: ${info.failures} auth failures within ${Math.round(info.windowMs / 1e3)}s.`
|
|
338
|
+
};
|
|
339
|
+
publishTunnelState();
|
|
340
|
+
},
|
|
341
|
+
onTokenRotated: async (nextToken) => {
|
|
342
|
+
syncLockfile(nextToken.bearerToken);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
syncLockfile(server.bearerToken);
|
|
346
|
+
process.on("SIGINT", signalHandler);
|
|
347
|
+
process.on("SIGTERM", signalHandler);
|
|
348
|
+
options.signal?.addEventListener("abort", abortHandler);
|
|
349
|
+
if (options.tunnel) {
|
|
350
|
+
await enableTunnelRuntime();
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
attached: false,
|
|
354
|
+
pid: process.pid,
|
|
355
|
+
uuid,
|
|
356
|
+
port: server.port,
|
|
357
|
+
url: server.url,
|
|
358
|
+
bearerToken: server.bearerToken,
|
|
359
|
+
version,
|
|
360
|
+
startedAt,
|
|
361
|
+
tunnelUrl: tunnelState.url,
|
|
362
|
+
close,
|
|
363
|
+
closed
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
watcher?.dispose();
|
|
367
|
+
await server?.close?.().catch(() => void 0);
|
|
368
|
+
release({ lockPath, expectedUuid: uuid });
|
|
369
|
+
if (isAddressInUseError(error)) {
|
|
370
|
+
if (typeof options.port === "number" && options.port > 0) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Port ${options.port} is in use; daemon cannot start. Another perplexity daemon instance or unrelated process holds it.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
await delay(retryDelayMs);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
throw new Error(`Unable to start or attach to daemon after ${retries} attempts.`);
|
|
382
|
+
}
|
|
383
|
+
function isAddressInUseError(error) {
|
|
384
|
+
if (!error || typeof error !== "object") return false;
|
|
385
|
+
const code = error.code;
|
|
386
|
+
return code === "EADDRINUSE";
|
|
387
|
+
}
|
|
388
|
+
async function stopDaemon(options = {}) {
|
|
389
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
390
|
+
const status = await getDaemonStatus({
|
|
391
|
+
configDir,
|
|
392
|
+
reclaimStale: true,
|
|
393
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
394
|
+
});
|
|
395
|
+
if (!status.running || !status.record) {
|
|
396
|
+
if (options.force && status.record) {
|
|
397
|
+
try {
|
|
398
|
+
release({ lockPath: getLockfilePath(configDir), expectedUuid: status.record.uuid });
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return { stopped: false, forced: false, pid: status.record?.pid ?? null };
|
|
403
|
+
}
|
|
404
|
+
const recordForShutdown = status.record;
|
|
405
|
+
if (status.healthy) {
|
|
406
|
+
try {
|
|
407
|
+
await adminRequest(recordForShutdown, "/daemon/shutdown", { method: "POST" });
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (!options.force) throw err;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const deadline = Date.now() + (options.waitTimeoutMs ?? 1e4);
|
|
413
|
+
while (Date.now() < deadline) {
|
|
414
|
+
const nextStatus = await getDaemonStatus({
|
|
415
|
+
configDir,
|
|
416
|
+
reclaimStale: true,
|
|
417
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
418
|
+
});
|
|
419
|
+
if (!nextStatus.running) {
|
|
420
|
+
return { stopped: true, forced: false, pid: recordForShutdown.pid };
|
|
421
|
+
}
|
|
422
|
+
await delay(options.pollIntervalMs ?? 200);
|
|
423
|
+
}
|
|
424
|
+
if (!options.force) {
|
|
425
|
+
throw new Error("Timed out waiting for daemon shutdown.");
|
|
426
|
+
}
|
|
427
|
+
const pid = recordForShutdown.pid;
|
|
428
|
+
let signalled = false;
|
|
429
|
+
try {
|
|
430
|
+
process.kill(pid, "SIGTERM");
|
|
431
|
+
signalled = true;
|
|
432
|
+
await delay(1e3);
|
|
433
|
+
try {
|
|
434
|
+
process.kill(pid, 0);
|
|
435
|
+
process.kill(pid, "SIGKILL");
|
|
436
|
+
await delay(500);
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
release({ lockPath: getLockfilePath(configDir), expectedUuid: recordForShutdown.uuid });
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return { stopped: signalled, forced: true, pid };
|
|
446
|
+
}
|
|
447
|
+
async function restartDaemon(options = {}) {
|
|
448
|
+
let stopped = false;
|
|
449
|
+
try {
|
|
450
|
+
const result = await stopDaemon({
|
|
451
|
+
configDir: options.configDir,
|
|
452
|
+
waitTimeoutMs: options.waitTimeoutMs,
|
|
453
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
454
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
455
|
+
});
|
|
456
|
+
stopped = result.stopped;
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
const connection = await ensureDaemon({
|
|
460
|
+
configDir: options.configDir,
|
|
461
|
+
healthTimeoutMs: options.healthTimeoutMs,
|
|
462
|
+
startTimeoutMs: options.startTimeoutMs,
|
|
463
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
464
|
+
spawnDaemon: options.spawnDaemon,
|
|
465
|
+
treatSelfAsZombie: options.treatSelfAsZombie
|
|
466
|
+
});
|
|
467
|
+
return { stopped, reSpawned: true, connection };
|
|
468
|
+
}
|
|
469
|
+
async function rotateDaemonToken(options = {}) {
|
|
470
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
471
|
+
const status = await getDaemonStatus({
|
|
472
|
+
configDir,
|
|
473
|
+
reclaimStale: true,
|
|
474
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
475
|
+
});
|
|
476
|
+
if (!status.running || !status.healthy || !status.record) {
|
|
477
|
+
throw new Error("Daemon is not running.");
|
|
478
|
+
}
|
|
479
|
+
await adminRequest(status.record, "/daemon/rotate-token", { method: "POST" });
|
|
480
|
+
await delay(100);
|
|
481
|
+
const updated = await getDaemonStatus({
|
|
482
|
+
configDir,
|
|
483
|
+
reclaimStale: false,
|
|
484
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
485
|
+
});
|
|
486
|
+
if (!updated.running || !updated.healthy || !updated.record || !updated.health) {
|
|
487
|
+
throw new Error("Daemon token rotation completed, but the daemon is not healthy.");
|
|
488
|
+
}
|
|
489
|
+
return toConnectionInfo(updated.record, updated.health);
|
|
490
|
+
}
|
|
491
|
+
async function enableDaemonTunnel(options = {}) {
|
|
492
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
493
|
+
const status = await getDaemonStatus({
|
|
494
|
+
configDir,
|
|
495
|
+
reclaimStale: true,
|
|
496
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
497
|
+
});
|
|
498
|
+
if (!status.running || !status.healthy || !status.record) {
|
|
499
|
+
throw new Error("Daemon is not running.");
|
|
500
|
+
}
|
|
501
|
+
await adminRequest(status.record, "/daemon/enable-tunnel", { method: "POST" });
|
|
502
|
+
await delay(100);
|
|
503
|
+
return await getDaemonStatus({
|
|
504
|
+
configDir,
|
|
505
|
+
reclaimStale: false,
|
|
506
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async function disableDaemonTunnel(options = {}) {
|
|
510
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
511
|
+
const status = await getDaemonStatus({
|
|
512
|
+
configDir,
|
|
513
|
+
reclaimStale: true,
|
|
514
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
515
|
+
});
|
|
516
|
+
if (!status.running || !status.healthy || !status.record) {
|
|
517
|
+
throw new Error("Daemon is not running.");
|
|
518
|
+
}
|
|
519
|
+
await adminRequest(status.record, "/daemon/disable-tunnel", { method: "POST" });
|
|
520
|
+
await delay(100);
|
|
521
|
+
return await getDaemonStatus({
|
|
522
|
+
configDir,
|
|
523
|
+
reclaimStale: false,
|
|
524
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
async function requireRunningRecord(options) {
|
|
528
|
+
const configDir = options.configDir ?? getConfigDir();
|
|
529
|
+
const status = await getDaemonStatus({
|
|
530
|
+
configDir,
|
|
531
|
+
reclaimStale: true,
|
|
532
|
+
healthTimeoutMs: options.healthTimeoutMs
|
|
533
|
+
});
|
|
534
|
+
if (!status.running || !status.healthy || !status.record) {
|
|
535
|
+
throw new Error("Daemon is not running.");
|
|
536
|
+
}
|
|
537
|
+
return status.record;
|
|
538
|
+
}
|
|
539
|
+
async function listOAuthConsents(options = {}) {
|
|
540
|
+
const record = await requireRunningRecord(options);
|
|
541
|
+
const body = await adminRequest(record, "/daemon/oauth-consents", { method: "GET" });
|
|
542
|
+
const consents = body?.consents;
|
|
543
|
+
return Array.isArray(consents) ? consents : [];
|
|
544
|
+
}
|
|
545
|
+
async function revokeOAuthConsent(clientId, redirectUri, options = {}) {
|
|
546
|
+
const record = await requireRunningRecord(options);
|
|
547
|
+
const body = await adminRequest(record, "/daemon/oauth-consents", {
|
|
548
|
+
method: "DELETE",
|
|
549
|
+
body: redirectUri ? { clientId, redirectUri } : { clientId }
|
|
550
|
+
});
|
|
551
|
+
const removed = body?.removed ?? 0;
|
|
552
|
+
return Number(removed) || 0;
|
|
553
|
+
}
|
|
554
|
+
async function revokeAllOAuthConsents(options = {}) {
|
|
555
|
+
const record = await requireRunningRecord(options);
|
|
556
|
+
const body = await adminRequest(record, "/daemon/oauth-consents", { method: "DELETE" });
|
|
557
|
+
const removed = body?.removed ?? 0;
|
|
558
|
+
return Number(removed) || 0;
|
|
559
|
+
}
|
|
560
|
+
async function listOAuthClients(options = {}) {
|
|
561
|
+
const record = await requireRunningRecord(options);
|
|
562
|
+
const body = await adminRequest(record, "/daemon/oauth-clients", { method: "GET" });
|
|
563
|
+
const clients = body?.clients;
|
|
564
|
+
return Array.isArray(clients) ? clients : [];
|
|
565
|
+
}
|
|
566
|
+
async function revokeOAuthClient(clientId, options = {}) {
|
|
567
|
+
const record = await requireRunningRecord(options);
|
|
568
|
+
const body = await adminRequest(record, "/daemon/oauth-clients", {
|
|
569
|
+
method: "DELETE",
|
|
570
|
+
body: { clientId }
|
|
571
|
+
});
|
|
572
|
+
const ok = body?.ok;
|
|
573
|
+
return Boolean(ok);
|
|
574
|
+
}
|
|
575
|
+
async function revokeAllOAuthClients(options = {}) {
|
|
576
|
+
const record = await requireRunningRecord(options);
|
|
577
|
+
const body = await adminRequest(record, "/daemon/oauth-clients", { method: "DELETE" });
|
|
578
|
+
const removed = body?.removed ?? 0;
|
|
579
|
+
return Number(removed) || 0;
|
|
580
|
+
}
|
|
581
|
+
async function probeHealth(record, options = {}) {
|
|
582
|
+
if (!record.port || record.port <= 0) {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const controller = new AbortController();
|
|
586
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 2e3);
|
|
587
|
+
try {
|
|
588
|
+
const response = await fetch(`http://127.0.0.1:${record.port}/daemon/health`, {
|
|
589
|
+
method: "GET",
|
|
590
|
+
headers: {
|
|
591
|
+
Authorization: `Bearer ${record.bearerToken}`
|
|
592
|
+
},
|
|
593
|
+
signal: controller.signal
|
|
594
|
+
});
|
|
595
|
+
if (!response.ok) {
|
|
596
|
+
if (process.env.PERPLEXITY_DEBUG === "1") {
|
|
597
|
+
console.error(`[trace] probeHealth non-ok status=${response.status} port=${record.port}`);
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
return await response.json();
|
|
602
|
+
} catch (err) {
|
|
603
|
+
if (process.env.PERPLEXITY_DEBUG === "1") {
|
|
604
|
+
const stack = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
605
|
+
console.error(`[trace] probeHealth threw port=${record.port}: ${stack}`);
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
} finally {
|
|
609
|
+
clearTimeout(timeout);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function adminRequest(record, path, options) {
|
|
613
|
+
const response = await fetch(`http://127.0.0.1:${record.port}${path}`, {
|
|
614
|
+
method: options.method,
|
|
615
|
+
headers: {
|
|
616
|
+
Authorization: `Bearer ${record.bearerToken}`,
|
|
617
|
+
...options.body ? { "Content-Type": "application/json" } : {}
|
|
618
|
+
},
|
|
619
|
+
...options.body ? { body: JSON.stringify(options.body) } : {}
|
|
620
|
+
});
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
const detail = await response.text().catch(() => "");
|
|
623
|
+
throw new Error(`Daemon admin request failed (${response.status}): ${detail || response.statusText}`);
|
|
624
|
+
}
|
|
625
|
+
if (response.status === 204) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
629
|
+
if (contentType.includes("application/json")) {
|
|
630
|
+
return await response.json();
|
|
631
|
+
}
|
|
632
|
+
return await response.text().catch(() => null);
|
|
633
|
+
}
|
|
634
|
+
function toConnectionInfo(record, health) {
|
|
635
|
+
return {
|
|
636
|
+
pid: record.pid,
|
|
637
|
+
uuid: record.uuid,
|
|
638
|
+
port: record.port,
|
|
639
|
+
url: `http://127.0.0.1:${record.port}`,
|
|
640
|
+
bearerToken: record.bearerToken,
|
|
641
|
+
version: record.version,
|
|
642
|
+
startedAt: record.startedAt,
|
|
643
|
+
tunnelUrl: health.tunnel?.url ?? record.tunnelUrl ?? null
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function spawnDetachedDaemon(options) {
|
|
647
|
+
const cliEntry = resolveCliEntry();
|
|
648
|
+
const args = [cliEntry, "daemon", "start"];
|
|
649
|
+
if (typeof options.port === "number") {
|
|
650
|
+
args.push("--port", String(options.port));
|
|
651
|
+
}
|
|
652
|
+
if (options.tunnel) {
|
|
653
|
+
args.push("--tunnel");
|
|
654
|
+
}
|
|
655
|
+
const child = spawn(process.execPath, args, {
|
|
656
|
+
detached: true,
|
|
657
|
+
stdio: "ignore",
|
|
658
|
+
env: {
|
|
659
|
+
...process.env,
|
|
660
|
+
PERPLEXITY_CONFIG_DIR: options.configDir
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
child.unref();
|
|
664
|
+
}
|
|
665
|
+
function resolveCliEntry() {
|
|
666
|
+
const mjsPath = fileURLToPath(new URL("../cli.mjs", import.meta.url));
|
|
667
|
+
if (existsSync(mjsPath)) {
|
|
668
|
+
return mjsPath;
|
|
669
|
+
}
|
|
670
|
+
return fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export {
|
|
674
|
+
getDaemonStatus,
|
|
675
|
+
ensureDaemon,
|
|
676
|
+
startDaemon,
|
|
677
|
+
stopDaemon,
|
|
678
|
+
restartDaemon,
|
|
679
|
+
rotateDaemonToken,
|
|
680
|
+
enableDaemonTunnel,
|
|
681
|
+
disableDaemonTunnel,
|
|
682
|
+
listOAuthConsents,
|
|
683
|
+
revokeOAuthConsent,
|
|
684
|
+
revokeAllOAuthConsents,
|
|
685
|
+
listOAuthClients,
|
|
686
|
+
revokeOAuthClient,
|
|
687
|
+
revokeAllOAuthClients
|
|
688
|
+
};
|