conductor-remote 1.80.0 → 1.82.0
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 +22 -9
- package/dist/assets/index-CJakNa4V.css +1 -0
- package/dist/assets/index-CZWaZeNN.js +47 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/dev-server.js +184 -62
- package/dist-node/src/mcp-tools.js +21 -1
- package/dist-node/src/plan-usage.js +430 -0
- package/dist-node/src/preview-urls.js +283 -0
- package/dist-node/src/reads.js +19 -12
- package/dist-node/src/routes.js +2 -0
- package/dist-node/src/server.js +22 -6
- package/package.json +1 -1
- package/dist/assets/index-DaafT1eA.css +0 -1
- package/dist/assets/index-ILQ9q-V7.js +0 -47
package/dist/index.html
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<title>Conductor Remote</title>
|
|
14
14
|
<!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
|
|
15
15
|
<script src="/self-heal.js"></script>
|
|
16
|
-
<script type="module" crossorigin src="/assets/index-
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-CZWaZeNN.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CJakNa4V.css">
|
|
18
18
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const
|
|
1
|
+
if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e7ef44deca46c0539e6ff7bba5eb815e"},{url:"index.html",revision:"92fe37786069a3859a13db5d5d63f6e3"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CZWaZeNN.js",revision:null},{url:"assets/index-CJakNa4V.css",revision:null},{url:"apple-touch-icon.png",revision:"1127bb396b4648add53dce3f22c92aee"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a9b0d962686287452216492cd2247499"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
* The relay never spawns a repository command itself. Starting and stopping go
|
|
6
6
|
* through Conductor's real Run button (`writes.ts`), which preserves its task
|
|
7
7
|
* selection, environment, run-mode rules and process-group cleanup. This module
|
|
8
|
-
* discovers that workspace's allocated `CONDUCTOR_PORT`,
|
|
9
|
-
*
|
|
8
|
+
* discovers that workspace's allocated `CONDUCTOR_PORT`, expands Conductor's
|
|
9
|
+
* configured `preview_urls`, and gives each local preview a tailnet HTTPS origin
|
|
10
|
+
* on the Mac's MagicDNS name. Conductor's detected Open-button ports remain the
|
|
11
|
+
* fallback for repositories that do not declare previews.
|
|
10
12
|
*
|
|
11
13
|
* Tailscale's reverse proxy preserves the public Host header. Dev servers such
|
|
12
14
|
* as Vite reject that by default, so a tiny loopback bridge rewrites Host/Origin
|
|
@@ -22,12 +24,18 @@ import net from 'node:net';
|
|
|
22
24
|
import path from 'node:path';
|
|
23
25
|
import { promisify } from 'node:util';
|
|
24
26
|
import { stateDir } from "./config.js";
|
|
27
|
+
import { previewUrlSettings, resolvePreviewTargets } from "./preview-urls.js";
|
|
25
28
|
import { magicDnsName, tailscaleBin } from "./tailscale.js";
|
|
26
29
|
import { setRunTask } from "./writes.js";
|
|
27
30
|
const exec = promisify(execFile);
|
|
31
|
+
// Keep the receipt additive and version-1-readable: an installed relay and a
|
|
32
|
+
// source `yarn dev` relay can briefly share this file during an update.
|
|
28
33
|
const STORE_VERSION = 1;
|
|
29
34
|
const PORT_WAIT_MS = 15_000;
|
|
30
35
|
const PORT_SNAPSHOT_TTL_MS = 5000;
|
|
36
|
+
function recordKey(workspaceId, targetPort) {
|
|
37
|
+
return `${workspaceId}:${targetPort}`;
|
|
38
|
+
}
|
|
31
39
|
function validPort(value) {
|
|
32
40
|
return Number.isInteger(value) && value > 0 && value <= 65535;
|
|
33
41
|
}
|
|
@@ -287,21 +295,21 @@ function servePorts(status) {
|
|
|
287
295
|
.map(Number)
|
|
288
296
|
.filter(validPort));
|
|
289
297
|
}
|
|
290
|
-
function chooseServePort(status, targetPort) {
|
|
298
|
+
function chooseServePort(status, targetPort, reserved = new Set()) {
|
|
291
299
|
const used = servePorts(status);
|
|
292
300
|
for (let offset = 0; offset < 10; offset++) {
|
|
293
301
|
const candidate = targetPort + offset;
|
|
294
|
-
if (validPort(candidate) && !used.has(candidate))
|
|
302
|
+
if (validPort(candidate) && !used.has(candidate) && !reserved.has(candidate))
|
|
295
303
|
return candidate;
|
|
296
304
|
}
|
|
297
305
|
for (let candidate = 49152; candidate <= 65535; candidate++) {
|
|
298
|
-
if (!used.has(candidate))
|
|
306
|
+
if (!used.has(candidate) && !reserved.has(candidate))
|
|
299
307
|
return candidate;
|
|
300
308
|
}
|
|
301
309
|
return null;
|
|
302
310
|
}
|
|
303
|
-
function forwardUrl(record) {
|
|
304
|
-
return `https://${record.host}${record.servePort === 443 ? '' : `:${record.servePort}`}
|
|
311
|
+
function forwardUrl(record, previewPath = record.path || '/') {
|
|
312
|
+
return `https://${record.host}${record.servePort === 443 ? '' : `:${record.servePort}`}${previewPath}`;
|
|
305
313
|
}
|
|
306
314
|
export class DevServerController {
|
|
307
315
|
storeFile;
|
|
@@ -329,12 +337,15 @@ export class DevServerController {
|
|
|
329
337
|
return;
|
|
330
338
|
for (const record of parsed.forwards) {
|
|
331
339
|
if (record.workspaceId &&
|
|
340
|
+
(record.basePort === undefined || record.basePort === null || validPort(record.basePort)) &&
|
|
332
341
|
validPort(record.targetPort) &&
|
|
333
342
|
validPort(record.servePort) &&
|
|
334
343
|
validPort(record.bridgePort) &&
|
|
335
344
|
typeof record.host === 'string' &&
|
|
336
345
|
record.host.length > 0) {
|
|
337
|
-
|
|
346
|
+
if (record.basePort === undefined)
|
|
347
|
+
record.basePort = record.targetPort;
|
|
348
|
+
this.records.set(recordKey(record.workspaceId, record.targetPort), record);
|
|
338
349
|
}
|
|
339
350
|
}
|
|
340
351
|
}
|
|
@@ -360,7 +371,31 @@ export class DevServerController {
|
|
|
360
371
|
const discovered = await workspacePort(workspaceId, PORT_SNAPSHOT_TTL_MS);
|
|
361
372
|
if (discovered)
|
|
362
373
|
this.ports.set(workspaceId, discovered);
|
|
363
|
-
|
|
374
|
+
if (discovered)
|
|
375
|
+
return discovered;
|
|
376
|
+
for (const record of this.records.values()) {
|
|
377
|
+
if (record.workspaceId === workspaceId && record.basePort)
|
|
378
|
+
return record.basePort;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
targetsFor(workspace, basePort, detectedPorts = []) {
|
|
383
|
+
const configured = resolvePreviewTargets(previewUrlSettings(workspace), basePort);
|
|
384
|
+
if (configured.length)
|
|
385
|
+
return configured;
|
|
386
|
+
const ports = [...new Set(detectedPorts.filter(validPort))];
|
|
387
|
+
if (!ports.length && basePort)
|
|
388
|
+
ports.push(basePort);
|
|
389
|
+
if (!ports.length) {
|
|
390
|
+
for (const record of this.records.values()) {
|
|
391
|
+
if (record.workspaceId === workspace.id)
|
|
392
|
+
ports.push(record.targetPort);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return ports.slice(0, 10).map(port => {
|
|
396
|
+
const record = this.records.get(recordKey(workspace.id, port));
|
|
397
|
+
return { name: record?.name || `Port ${port}`, port, path: record?.path || '/' };
|
|
398
|
+
});
|
|
364
399
|
}
|
|
365
400
|
async serveStatus() {
|
|
366
401
|
if (!this.bin)
|
|
@@ -399,9 +434,9 @@ export class DevServerController {
|
|
|
399
434
|
throw new Error(`could not remove the Tailscale Serve mapping on :${record.servePort}`);
|
|
400
435
|
}
|
|
401
436
|
}
|
|
402
|
-
async
|
|
403
|
-
const record = this.records.get(
|
|
404
|
-
const proxy = this.proxies.get(
|
|
437
|
+
async releaseRecord(key) {
|
|
438
|
+
const record = this.records.get(key);
|
|
439
|
+
const proxy = this.proxies.get(key);
|
|
405
440
|
let cleanupError;
|
|
406
441
|
if (record) {
|
|
407
442
|
try {
|
|
@@ -415,30 +450,51 @@ export class DevServerController {
|
|
|
415
450
|
// its stale HTTPS mapping cannot reach a later process that reuses the port.
|
|
416
451
|
if (proxy)
|
|
417
452
|
await proxy.close();
|
|
418
|
-
this.proxies.delete(
|
|
453
|
+
this.proxies.delete(key);
|
|
419
454
|
if (!cleanupError)
|
|
420
|
-
this.records.delete(
|
|
455
|
+
this.records.delete(key);
|
|
421
456
|
this.save();
|
|
422
457
|
if (cleanupError)
|
|
423
458
|
throw cleanupError;
|
|
424
459
|
}
|
|
425
|
-
async
|
|
426
|
-
|
|
427
|
-
|
|
460
|
+
async release(workspaceId) {
|
|
461
|
+
let firstError;
|
|
462
|
+
for (const [key, record] of [...this.records]) {
|
|
463
|
+
if (record.workspaceId !== workspaceId)
|
|
464
|
+
continue;
|
|
465
|
+
try {
|
|
466
|
+
await this.releaseRecord(key);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
firstError ??= err;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (firstError)
|
|
473
|
+
throw firstError;
|
|
474
|
+
}
|
|
475
|
+
async forward(workspaceId, basePort, target, reservedPorts) {
|
|
476
|
+
const key = recordKey(workspaceId, target.port);
|
|
477
|
+
const existing = this.records.get(key);
|
|
478
|
+
if (existing) {
|
|
428
479
|
const status = await this.serveStatus();
|
|
429
480
|
const expected = `http://127.0.0.1:${existing.bridgePort}`;
|
|
430
|
-
if (serveProxyAt(status, existing.servePort) === expected && (await bridgeMatches(existing)))
|
|
481
|
+
if (serveProxyAt(status, existing.servePort) === expected && (await bridgeMatches(existing))) {
|
|
482
|
+
existing.basePort = basePort ?? existing.basePort;
|
|
483
|
+
existing.name = target.name;
|
|
484
|
+
existing.path = target.path;
|
|
485
|
+
this.save();
|
|
431
486
|
return existing;
|
|
487
|
+
}
|
|
432
488
|
}
|
|
433
489
|
if (existing)
|
|
434
|
-
await this.
|
|
490
|
+
await this.releaseRecord(key);
|
|
435
491
|
if (!this.bin || !this.host)
|
|
436
492
|
throw new Error('Tailscale is not connected on this Mac');
|
|
437
493
|
const status = await this.serveStatus();
|
|
438
|
-
const servePort = chooseServePort(status,
|
|
494
|
+
const servePort = chooseServePort(status, target.port, reservedPorts);
|
|
439
495
|
if (!servePort)
|
|
440
496
|
throw new Error('no free Tailscale Serve port is available');
|
|
441
|
-
const proxy = await createDevProxy(
|
|
497
|
+
const proxy = await createDevProxy(target.port);
|
|
442
498
|
try {
|
|
443
499
|
await this.setServe(servePort, proxy.port);
|
|
444
500
|
}
|
|
@@ -463,18 +519,37 @@ export class DevServerController {
|
|
|
463
519
|
}
|
|
464
520
|
const record = {
|
|
465
521
|
workspaceId,
|
|
466
|
-
|
|
522
|
+
basePort,
|
|
523
|
+
targetPort: target.port,
|
|
467
524
|
servePort,
|
|
468
525
|
bridgePort: proxy.port,
|
|
469
526
|
host: this.host,
|
|
527
|
+
name: target.name,
|
|
528
|
+
path: target.path,
|
|
470
529
|
ownerPid: process.pid,
|
|
471
530
|
bridgeToken: proxy.token
|
|
472
531
|
};
|
|
473
|
-
this.proxies.set(
|
|
474
|
-
this.records.set(
|
|
532
|
+
this.proxies.set(key, proxy);
|
|
533
|
+
this.records.set(key, record);
|
|
475
534
|
this.save();
|
|
476
535
|
return record;
|
|
477
536
|
}
|
|
537
|
+
async forwardAll(workspaceId, basePort, targets) {
|
|
538
|
+
const byPort = new Map();
|
|
539
|
+
for (const target of targets)
|
|
540
|
+
if (!byPort.has(target.port))
|
|
541
|
+
byPort.set(target.port, target);
|
|
542
|
+
const wanted = new Set([...byPort].map(([port]) => recordKey(workspaceId, port)));
|
|
543
|
+
for (const [key, record] of [...this.records]) {
|
|
544
|
+
if (record.workspaceId === workspaceId && !wanted.has(key))
|
|
545
|
+
await this.releaseRecord(key);
|
|
546
|
+
}
|
|
547
|
+
const naturalPorts = new Set(byPort.keys());
|
|
548
|
+
for (const target of byPort.values()) {
|
|
549
|
+
const reserved = new Set([...naturalPorts].filter(port => port !== target.port));
|
|
550
|
+
await this.forward(workspaceId, basePort, target, reserved);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
478
553
|
/** Rebuild loopback bridges after launchd or the self-updater restarts the relay. */
|
|
479
554
|
async restore() {
|
|
480
555
|
this.refreshTailscale();
|
|
@@ -482,9 +557,10 @@ export class DevServerController {
|
|
|
482
557
|
// that case; a later tap can safely replace or remove their exact mappings.
|
|
483
558
|
if (!this.bin || !this.host)
|
|
484
559
|
return;
|
|
485
|
-
for (const record of [...this.records
|
|
560
|
+
for (const [key, record] of [...this.records]) {
|
|
486
561
|
try {
|
|
487
|
-
|
|
562
|
+
if (record.basePort)
|
|
563
|
+
this.ports.set(record.workspaceId, record.basePort);
|
|
488
564
|
// `yarn dev` runs another relay beside the installed LaunchAgent. A
|
|
489
565
|
// node --watch restart must not steal the installed relay's live bridge
|
|
490
566
|
// merely because both instances share the same state directory.
|
|
@@ -493,11 +569,11 @@ export class DevServerController {
|
|
|
493
569
|
const status = await this.serveStatus();
|
|
494
570
|
const owned = serveProxyAt(status, record.servePort) === `http://127.0.0.1:${record.bridgePort}`;
|
|
495
571
|
if (!owned) {
|
|
496
|
-
this.records.delete(
|
|
572
|
+
this.records.delete(key);
|
|
497
573
|
continue;
|
|
498
574
|
}
|
|
499
575
|
if (!(await tcpOpen(record.targetPort))) {
|
|
500
|
-
await this.
|
|
576
|
+
await this.releaseRecord(key);
|
|
501
577
|
continue;
|
|
502
578
|
}
|
|
503
579
|
const proxy = await createDevProxy(record.targetPort);
|
|
@@ -505,8 +581,8 @@ export class DevServerController {
|
|
|
505
581
|
record.bridgePort = proxy.port;
|
|
506
582
|
record.ownerPid = process.pid;
|
|
507
583
|
record.bridgeToken = proxy.token;
|
|
508
|
-
this.proxies.set(
|
|
509
|
-
this.records.set(
|
|
584
|
+
this.proxies.set(key, proxy);
|
|
585
|
+
this.records.set(key, record);
|
|
510
586
|
}
|
|
511
587
|
catch (err) {
|
|
512
588
|
console.warn(`⚠ could not restore dev-server forward for ${record.workspaceId} (${err instanceof Error ? err.message : err})`);
|
|
@@ -516,32 +592,67 @@ export class DevServerController {
|
|
|
516
592
|
}
|
|
517
593
|
async state(workspace) {
|
|
518
594
|
this.refreshTailscale();
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
595
|
+
const basePort = await this.portFor(workspace.id);
|
|
596
|
+
if (basePort)
|
|
597
|
+
this.ports.set(workspace.id, basePort);
|
|
598
|
+
const targets = this.targetsFor(workspace, basePort);
|
|
599
|
+
const targetPorts = new Set(targets.map(target => target.port));
|
|
600
|
+
// Keep a configured URL list authoritative, but don't hide a still-owned
|
|
601
|
+
// forward after the file changes; it must remain visible until Start
|
|
602
|
+
// reconciles it or Stop removes it.
|
|
603
|
+
for (const record of this.records.values()) {
|
|
604
|
+
if (record.workspaceId !== workspace.id || targetPorts.has(record.targetPort))
|
|
605
|
+
continue;
|
|
606
|
+
targets.push({
|
|
607
|
+
name: record.name || `Port ${record.targetPort}`,
|
|
608
|
+
port: record.targetPort,
|
|
609
|
+
path: record.path || '/'
|
|
610
|
+
});
|
|
611
|
+
targetPorts.add(record.targetPort);
|
|
612
|
+
}
|
|
613
|
+
let status = null;
|
|
614
|
+
try {
|
|
615
|
+
if (targets.some(target => this.records.has(recordKey(workspace.id, target.port))))
|
|
616
|
+
status = await this.serveStatus();
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
// A disconnected CLI makes every URL unverified, never optimistically live.
|
|
620
|
+
}
|
|
621
|
+
const openByPort = new Map();
|
|
622
|
+
const ownedByPort = new Map();
|
|
623
|
+
const forwards = await Promise.all(targets.map(async (target) => {
|
|
624
|
+
let open = openByPort.get(target.port);
|
|
625
|
+
if (!open) {
|
|
626
|
+
open = tcpOpen(target.port);
|
|
627
|
+
openByPort.set(target.port, open);
|
|
531
628
|
}
|
|
532
|
-
|
|
533
|
-
|
|
629
|
+
const record = this.records.get(recordKey(workspace.id, target.port));
|
|
630
|
+
let owned = ownedByPort.get(target.port);
|
|
631
|
+
if (!owned) {
|
|
632
|
+
owned = Promise.resolve(Boolean(status && record && serveProxyAt(status, record.servePort) === `http://127.0.0.1:${record.bridgePort}`)).then(matches => (matches && record ? bridgeMatches(record) : false));
|
|
633
|
+
ownedByPort.set(target.port, owned);
|
|
534
634
|
}
|
|
535
|
-
|
|
635
|
+
const [targetRunning, targetForwarded] = await Promise.all([open, owned]);
|
|
636
|
+
return {
|
|
637
|
+
name: target.name,
|
|
638
|
+
port: target.port,
|
|
639
|
+
running: targetRunning,
|
|
640
|
+
forwarded: targetForwarded,
|
|
641
|
+
url: targetForwarded && record ? forwardUrl(record, target.path) : null
|
|
642
|
+
};
|
|
643
|
+
}));
|
|
644
|
+
const primary = forwards[0];
|
|
645
|
+
const firstForwarded = forwards.find(forward => forward.forwarded && forward.url);
|
|
536
646
|
let error;
|
|
537
647
|
if (!this.bin || !this.host)
|
|
538
648
|
error = 'Tailscale is not connected on this Mac';
|
|
539
649
|
return {
|
|
540
650
|
available: !!this.bin && !!this.host,
|
|
541
|
-
running,
|
|
542
|
-
forwarded,
|
|
543
|
-
port,
|
|
544
|
-
url:
|
|
651
|
+
running: primary?.running ?? false,
|
|
652
|
+
forwarded: Boolean(firstForwarded),
|
|
653
|
+
port: primary?.port ?? basePort,
|
|
654
|
+
url: firstForwarded?.url ?? null,
|
|
655
|
+
forwards,
|
|
545
656
|
error
|
|
546
657
|
};
|
|
547
658
|
}
|
|
@@ -577,15 +688,17 @@ export class DevServerController {
|
|
|
577
688
|
error: 'Tailscale Serve is not available on this Mac'
|
|
578
689
|
};
|
|
579
690
|
}
|
|
580
|
-
let
|
|
691
|
+
let basePort = await this.portFor(workspace.id);
|
|
692
|
+
let targets = this.targetsFor(workspace, basePort);
|
|
693
|
+
let primaryPort = targets[0]?.port ?? null;
|
|
581
694
|
// A task already listening needs no button. Forwarding it touches only
|
|
582
695
|
// Tailscale and this relay, so it costs about a second, steals no focus
|
|
583
696
|
// from the Mac, and stays inside the tap activation a phone can open a
|
|
584
697
|
// tab with. Pressing Run here would also assert a pane for a press it
|
|
585
698
|
// then decides not to make.
|
|
586
|
-
if (
|
|
699
|
+
if (primaryPort && (await tcpOpen(primaryPort))) {
|
|
587
700
|
try {
|
|
588
|
-
await this.
|
|
701
|
+
await this.forwardAll(workspace.id, basePort, targets);
|
|
589
702
|
return { ok: true, ...(await this.state(workspace)), changed: false };
|
|
590
703
|
}
|
|
591
704
|
catch (err) {
|
|
@@ -600,12 +713,14 @@ export class DevServerController {
|
|
|
600
713
|
const run = await setRunTask(workspace, true);
|
|
601
714
|
if (!run.ok)
|
|
602
715
|
return { ok: false, ...(await this.state(workspace)), error: run.error };
|
|
603
|
-
|
|
604
|
-
if (
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
716
|
+
basePort ??= await workspacePort(workspace.id);
|
|
717
|
+
if (basePort)
|
|
718
|
+
this.ports.set(workspace.id, basePort);
|
|
719
|
+
// Configured previews win. Conductor's expanded Open buttons are the
|
|
720
|
+
// compatibility path for repositories that have not declared them yet.
|
|
721
|
+
targets = this.targetsFor(workspace, basePort, run.ports);
|
|
722
|
+
primaryPort = targets[0]?.port ?? null;
|
|
723
|
+
if (!primaryPort) {
|
|
609
724
|
const rollback = run.changed ? await setRunTask(workspace, false) : null;
|
|
610
725
|
const suffix = rollback
|
|
611
726
|
? rollback.ok
|
|
@@ -617,10 +732,10 @@ export class DevServerController {
|
|
|
617
732
|
...(await this.state(workspace)),
|
|
618
733
|
task: run.task,
|
|
619
734
|
changed: run.changed,
|
|
620
|
-
error: `Conductor started the task, but
|
|
735
|
+
error: `Conductor started the task, but no configured or detected preview port was visible${suffix}`
|
|
621
736
|
};
|
|
622
737
|
}
|
|
623
|
-
if (!(await waitForPort(
|
|
738
|
+
if (!(await waitForPort(primaryPort, true, PORT_WAIT_MS))) {
|
|
624
739
|
const rollback = run.changed ? await setRunTask(workspace, false) : null;
|
|
625
740
|
const suffix = rollback
|
|
626
741
|
? rollback.ok
|
|
@@ -632,14 +747,21 @@ export class DevServerController {
|
|
|
632
747
|
...(await this.state(workspace)),
|
|
633
748
|
task: run.task,
|
|
634
749
|
changed: run.changed,
|
|
635
|
-
error: `${run.task ?? 'Run task'} started, but nothing listened on :${
|
|
750
|
+
error: `${run.task ?? 'Run task'} started, but nothing listened on :${primaryPort}${suffix}`
|
|
636
751
|
};
|
|
637
752
|
}
|
|
638
753
|
try {
|
|
639
|
-
await this.
|
|
754
|
+
await this.forwardAll(workspace.id, basePort, targets);
|
|
640
755
|
return { ok: true, ...(await this.state(workspace)), task: run.task, changed: run.changed };
|
|
641
756
|
}
|
|
642
757
|
catch (err) {
|
|
758
|
+
try {
|
|
759
|
+
await this.release(workspace.id);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// The original forwarding failure remains the actionable error. Any
|
|
763
|
+
// receipt cleanup failure stays persisted for the next relay start.
|
|
764
|
+
}
|
|
643
765
|
const rollback = run.changed ? await setRunTask(workspace, false) : null;
|
|
644
766
|
const suffix = rollback
|
|
645
767
|
? rollback.ok
|
|
@@ -31,7 +31,7 @@ import { HIT_CLOSE, HIT_OPEN, isToolResult, timestampMs, workspaceTitle } from "
|
|
|
31
31
|
/** Versions we know how to speak. The client's choice wins when we know it. */
|
|
32
32
|
export const PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
33
33
|
export const SERVER_INFO = { name: 'conductor-remote', version: '1' };
|
|
34
|
-
export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. dismiss_prompt, keep_awake and relay_logs touch no UI. send_prompt, split_chat, stop_turn, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
|
|
34
|
+
export const INSTRUCTIONS = 'Drives local Conductor agents through the conductor-remote relay. search_chats and read_chat reach archived workspaces, which is where most finished work lives. create_workspace opens its workspace link without direct UI control; requested agent settings and the first prompt are applied later through Conductor’s UI. plan_usage, dismiss_prompt, keep_awake and relay_logs touch no UI. send_prompt, split_chat, stop_turn, set_agent_options, set_default_model, a live list_models call, set_workspace_status and archive_workspace drive the real Mac UI and steal focus for a few seconds — confirm with the user before using them on a chat they did not name. archive_workspace also deletes the worktree and stops whatever is running there.';
|
|
35
35
|
/** Reads are quick; a UI write is measured in tens of seconds (writes.ts ▸ SEND_ATTEMPT_MS). */
|
|
36
36
|
export const READ_TIMEOUT_MS = 10_000;
|
|
37
37
|
export const WRITE_TIMEOUT_MS = 75_000;
|
|
@@ -359,6 +359,26 @@ export function createTools(call) {
|
|
|
359
359
|
return data.repos.map(r => `${r.name} (${r.default_branch ?? '?'}) ${r.root_path ?? ''}`).join('\n');
|
|
360
360
|
}
|
|
361
361
|
},
|
|
362
|
+
{
|
|
363
|
+
name: 'plan_usage',
|
|
364
|
+
description: 'Read rolling Claude Code and Codex subscription allowances from their local CLIs: percentage used and reset time. Sends no model prompt and touches no Conductor UI. Cursor Agent and OpenCode explain why no comparable plan quota is available.',
|
|
365
|
+
inputSchema: { type: 'object', properties: {} },
|
|
366
|
+
run: async () => {
|
|
367
|
+
const data = await call(routes.planUsage.path(), { timeoutMs: 15_000 });
|
|
368
|
+
const sections = data.providers.map(provider => {
|
|
369
|
+
const head = `## ${provider.label}${provider.plan ? ` · ${provider.plan}` : ''}`;
|
|
370
|
+
if (provider.status !== 'available')
|
|
371
|
+
return `${head}\n${provider.message ?? 'unavailable'}`;
|
|
372
|
+
const lines = provider.buckets.flatMap(bucket => bucket.windows.map(window => {
|
|
373
|
+
const bucketName = provider.buckets.length > 1 ? `${bucket.label} — ` : '';
|
|
374
|
+
const reset = window.resetsAt === null ? 'reset unavailable' : `resets ${new Date(window.resetsAt).toISOString()}`;
|
|
375
|
+
return `- ${bucketName}${window.label}: ${Math.round(window.usedPercent)}% used · ${reset}`;
|
|
376
|
+
}));
|
|
377
|
+
return `${head}\n${lines.join('\n')}`;
|
|
378
|
+
});
|
|
379
|
+
return `${sections.join('\n\n')}\n\nupdated ${new Date(data.fetchedAt).toISOString()}`;
|
|
380
|
+
}
|
|
381
|
+
},
|
|
362
382
|
{
|
|
363
383
|
name: 'create_workspace',
|
|
364
384
|
description: 'Start a new Conductor workspace in a repo, optionally with a first prompt and agent settings. The workspace starts from a Conductor deep link, so creation itself needs no Accessibility. The relay applies requested model, effort, plan, and fast settings through Conductor’s UI after it creates the first chat and before it delivers the prompt. Returns as soon as the workspace row exists (~2s), before the worktree is built.',
|