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.
Files changed (125) hide show
  1. package/README.md +192 -0
  2. package/dist/attachments.d.ts +20 -0
  3. package/dist/attachments.mjs +43 -0
  4. package/dist/checks/browser.d.ts +100 -0
  5. package/dist/checks/browser.mjs +89 -0
  6. package/dist/checks/config.d.ts +91 -0
  7. package/dist/checks/config.mjs +88 -0
  8. package/dist/checks/ide.d.ts +89 -0
  9. package/dist/checks/ide.mjs +80 -0
  10. package/dist/checks/mcp.d.ts +61 -0
  11. package/dist/checks/mcp.mjs +56 -0
  12. package/dist/checks/native-deps.d.ts +131 -0
  13. package/dist/checks/native-deps.mjs +115 -0
  14. package/dist/checks/network.d.ts +71 -0
  15. package/dist/checks/network.mjs +70 -0
  16. package/dist/checks/probe.d.ts +93 -0
  17. package/dist/checks/probe.mjs +82 -0
  18. package/dist/checks/profiles.d.ts +99 -0
  19. package/dist/checks/profiles.mjs +90 -0
  20. package/dist/checks/runtime.d.ts +89 -0
  21. package/dist/checks/runtime.mjs +90 -0
  22. package/dist/checks/vault.d.ts +101 -0
  23. package/dist/checks/vault.mjs +90 -0
  24. package/dist/chunk-3B276PGG.mjs +115 -0
  25. package/dist/chunk-4UEJOM6W.mjs +9 -0
  26. package/dist/chunk-6EP2BLTV.mjs +205 -0
  27. package/dist/chunk-6YMQVLFX.mjs +146 -0
  28. package/dist/chunk-7JL36EBH.mjs +118 -0
  29. package/dist/chunk-DPGMKSSA.mjs +57 -0
  30. package/dist/chunk-H4BUAPPO.mjs +1950 -0
  31. package/dist/chunk-HMKLWVXB.mjs +109 -0
  32. package/dist/chunk-HTUAQRKH.mjs +125 -0
  33. package/dist/chunk-HU5B4FXS.mjs +139 -0
  34. package/dist/chunk-KCXM2M4B.mjs +1006 -0
  35. package/dist/chunk-LKJMLGFP.mjs +237 -0
  36. package/dist/chunk-LZPLNZ5U.mjs +67 -0
  37. package/dist/chunk-MTDFKNXX.mjs +19 -0
  38. package/dist/chunk-OF4DMAPJ.mjs +511 -0
  39. package/dist/chunk-PE23RMXY.mjs +43 -0
  40. package/dist/chunk-Q2VY4R5F.mjs +175 -0
  41. package/dist/chunk-S5VD7WTU.mjs +2540 -0
  42. package/dist/chunk-SVPRB62V.mjs +106 -0
  43. package/dist/chunk-TQLCLE4L.mjs +345 -0
  44. package/dist/chunk-U3DGFLXZ.mjs +43 -0
  45. package/dist/chunk-X45O6YD3.mjs +688 -0
  46. package/dist/chunk-XKSWCEGI.mjs +168 -0
  47. package/dist/chunk-Z7DAACGZ.mjs +534 -0
  48. package/dist/chunk-ZQFUZPLO.mjs +257 -0
  49. package/dist/cli.d.ts +952 -0
  50. package/dist/cli.mjs +827 -0
  51. package/dist/client.d.ts +355 -0
  52. package/dist/client.mjs +27 -0
  53. package/dist/cloud-sync.d-Cqt6y18U.d.ts +42 -0
  54. package/dist/cloud-sync.d.ts +42 -0
  55. package/dist/cloud-sync.mjs +17 -0
  56. package/dist/config.d.ts +186 -0
  57. package/dist/config.mjs +54 -0
  58. package/dist/daemon/attach.d.ts +36 -0
  59. package/dist/daemon/attach.mjs +25 -0
  60. package/dist/daemon/audit.d.ts +23 -0
  61. package/dist/daemon/audit.mjs +12 -0
  62. package/dist/daemon/client-http.d.ts +42 -0
  63. package/dist/daemon/client-http.mjs +29 -0
  64. package/dist/daemon/index.d.ts +14 -0
  65. package/dist/daemon/index.mjs +110 -0
  66. package/dist/daemon/install-tunnel.d.ts +46 -0
  67. package/dist/daemon/install-tunnel.mjs +14 -0
  68. package/dist/daemon/launcher.d.ts +163 -0
  69. package/dist/daemon/launcher.mjs +50 -0
  70. package/dist/daemon/lockfile.d.ts +29 -0
  71. package/dist/daemon/lockfile.mjs +18 -0
  72. package/dist/daemon/server.d.ts +159 -0
  73. package/dist/daemon/server.mjs +20 -0
  74. package/dist/daemon/token.d.ts +17 -0
  75. package/dist/daemon/token.mjs +17 -0
  76. package/dist/daemon/tunnel-providers/index.d.ts +330 -0
  77. package/dist/daemon/tunnel-providers/index.mjs +57 -0
  78. package/dist/daemon/tunnel.d.ts +23 -0
  79. package/dist/daemon/tunnel.mjs +9 -0
  80. package/dist/doctor-report.d.ts +24 -0
  81. package/dist/doctor-report.mjs +14 -0
  82. package/dist/doctor.d-CXmUqOXX.d.ts +43 -0
  83. package/dist/doctor.d.ts +44 -0
  84. package/dist/doctor.mjs +16 -0
  85. package/dist/export.d.ts +19 -0
  86. package/dist/export.mjs +15 -0
  87. package/dist/health-check.d.ts +108 -0
  88. package/dist/health-check.mjs +92 -0
  89. package/dist/history-store.d-BzjBF2m3.d.ts +65 -0
  90. package/dist/history-store.d.ts +65 -0
  91. package/dist/history-store.mjs +48 -0
  92. package/dist/impit-login-runner.d.ts +469 -0
  93. package/dist/impit-login-runner.mjs +685 -0
  94. package/dist/index.d.ts +159 -0
  95. package/dist/index.mjs +236 -0
  96. package/dist/login-runner.d.ts +333 -0
  97. package/dist/login-runner.mjs +320 -0
  98. package/dist/logout.d.ts +28 -0
  99. package/dist/logout.mjs +45 -0
  100. package/dist/manual-login-runner.d.ts +150 -0
  101. package/dist/manual-login-runner.mjs +146 -0
  102. package/dist/native-deps-BNThFHxa.d.ts +175 -0
  103. package/dist/native-deps-YNKXITRY.mjs +139 -0
  104. package/dist/profiles.d-DqS1oZWr.d.ts +41 -0
  105. package/dist/profiles.d.ts +41 -0
  106. package/dist/profiles.mjs +33 -0
  107. package/dist/redact.d.ts +159 -0
  108. package/dist/redact.mjs +11 -0
  109. package/dist/refresh.d.ts +118 -0
  110. package/dist/refresh.mjs +21 -0
  111. package/dist/reinit-watcher.d.ts +15 -0
  112. package/dist/reinit-watcher.mjs +8 -0
  113. package/dist/session-metadata-B9aV_n5g.d.ts +148 -0
  114. package/dist/tty-prompt.d.ts +44 -0
  115. package/dist/tty-prompt.mjs +39 -0
  116. package/dist/vault.d-BtRSLZiM.d.ts +8 -0
  117. package/dist/vault.d.ts +37 -0
  118. package/dist/vault.mjs +21 -0
  119. package/dist/viewer-detect.d-HWGnyFAA.d.ts +4 -0
  120. package/dist/viewer-detect.d.ts +4 -0
  121. package/dist/viewer-detect.mjs +37 -0
  122. package/dist/viewers.d-BGCK6sw6.d.ts +10 -0
  123. package/dist/viewers.d.ts +18 -0
  124. package/dist/viewers.mjs +122 -0
  125. 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
+ };