conductor-remote 1.75.0 → 1.76.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 +6 -0
- package/dist/assets/{index-CF1hSnUi.js → index-B7l0pOim.js} +28 -28
- package/dist/assets/index-BnacVtpu.css +1 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/service.js +151 -55
- package/dist-node/src/funnel-watchdog.js +37 -1
- package/dist-node/src/mcp-tools.js +14 -3
- package/dist-node/src/reads.js +24 -3
- package/dist-node/src/search.js +24 -4
- package/dist-node/src/server.js +10 -3
- package/dist-node/src/tailscale.js +69 -0
- package/package.json +1 -1
- package/dist/assets/index-CXd-zI4t.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#0a0b0e;--color-surface:#14161b;--color-surface-2:#1b1e26;--color-border:#262a33;--color-border-soft:#1e222a;--color-text:#e7e9ee;--color-muted:#8b909d;--color-faint:#5c616d;--color-accent:#8b7dff;--color-accent-soft:#2a2650;--color-working:#f5a623;--color-idle:#3ecf8e;--color-done:#5b9dff;--color-pr-merged:#ac47ff;--color-pr-draft:#a4a3a2;--color-pr-attention:#facc15;--color-pr-mergeable:#49de80;--color-add:#3ecf8e;--color-del:#ff6b6b}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{-webkit-tap-highlight-color:transparent}html,body,#root{height:var(--app-height,100dvh)}html,body{overflow:hidden}@media(display-mode:standalone){html,body,#root{height:var(--app-height,100vh)}}html[data-standalone],html[data-standalone] body,html[data-standalone] #root{height:var(--app-height,100vh)}.app-height{height:var(--app-height,100dvh)}@media(display-mode:standalone){.app-height{height:var(--app-height,100vh)}}html[data-standalone] .app-height{height:var(--app-height,100vh)}@media(min-width:48rem){.app-height,html[data-standalone] .app-height{height:auto}}html{background:var(--color-bg)}body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;overscroll-behavior:none;margin:0}button{font:inherit;color:inherit;cursor:pointer}::-webkit-scrollbar{width:0;height:0}}@layer components{.card{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.card:active{background-color:var(--color-surface-2);scale:.985}.pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.pill-active{background-color:var(--color-surface-2);color:var(--color-text)}.ctl{padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:13px;font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.ctl-on{background-color:var(--color-surface-2);color:var(--color-text)}.ctl-staged{background-color:var(--color-accent-soft);color:var(--color-accent)}.dot{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);background-color:var(--color-faint);border-radius:3.40282e38px;flex-shrink:0}.dot-spinner{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);animation:var(--animate-spin);border-style:var(--tw-border-style);--spin-color:var(--color-working);border-width:2px;border-color:var(--spin-color);border-radius:3.40282e38px;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.dot-spinner{border-color:color-mix(in srgb,var(--spin-color) 30%,transparent)}}.dot-spinner{border-top-color:var(--spin-color);border-right-color:var(--spin-color)}.dot-idle{background:var(--color-idle)}.dot-done{background:var(--color-done)}.typing-dot{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5);background-color:var(--color-muted);border-radius:3.40282e38px;animation:1.2s ease-in-out infinite typing}.typing-dot:nth-child(2){animation-delay:.15s}.typing-dot:nth-child(3){animation-delay:.3s}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:.5em 0}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.9em 0 .4em;font-weight:600;line-height:1.3}.md h1{font-size:1.6em}.md h2{font-size:1.4em}.md h3{font-size:1.2em}.md h4{font-size:1.1em}.md ul,.md ol{margin:.5em 0;padding-left:1.4em}.md ul{list-style:outside}.md ol{list-style:decimal}.md li{margin:.3em 0}.md li>ul,.md li>ol{margin:.2em 0}.md code{background:#1b1e26cc;border-radius:.3rem}@supports (color:color-mix(in lab,red,red)){.md code{background:color-mix(in srgb,var(--color-surface-2) 80%,transparent)}}.md code{font-family:var(--font-mono);padding:.1em .35em;font-size:.85em}.md pre{border:1px solid var(--color-border-soft);background:var(--color-bg);border-radius:.6rem;margin:.5em 0;padding:.6em .75em;overflow-x:auto}.md pre code{background:0 0;padding:0;font-size:12px;line-height:1.5}.md a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.md blockquote{border-left:2px solid var(--color-border);color:var(--color-muted);margin:.5em 0;padding-left:.75em}.md hr{border:0;border-top:1px solid var(--color-border-soft);margin:1.25em 0}.md img{border-radius:.6rem;max-width:100%;height:auto;margin:.5em 0;display:block}.md table{border-collapse:collapse;max-width:100%;margin:.5em 0;font-size:.9em;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--color-border);text-align:left;padding:.3em .6em}.md th{background:var(--color-surface-2);font-weight:600}.md strong{font-weight:600}.md input[type=checkbox]{margin-right:.4em}.hljs-comment,.hljs-quote{color:var(--color-faint);font-style:italic}.hljs-keyword,.hljs-literal,.hljs-type,.hljs-built_in,.hljs-selector-tag,.hljs-doctag,.hljs-meta .hljs-keyword{color:var(--color-accent)}.hljs-string,.hljs-regexp,.hljs-char.escape_,.hljs-addition,.hljs-meta .hljs-string{color:var(--color-add)}.hljs-number,.hljs-symbol,.hljs-bullet,.hljs-link,.hljs-meta,.hljs-template-variable{color:var(--color-working)}.hljs-title,.hljs-title.class_,.hljs-title.function_,.hljs-name,.hljs-section,.hljs-selector-id,.hljs-selector-class{color:var(--color-done)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-variable,.hljs-params,.hljs-subst,.hljs-template-tag{color:var(--color-text)}.hljs-deletion{color:var(--color-del)}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:600}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-1{inset:var(--spacing)}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-1\/2{top:50%}.top-full{top:100%}.-right-0\.5{right:calc(var(--spacing) * -.5)}.right-1{right:var(--spacing)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.right-2{right:calc(var(--spacing) * 2)}.right-3{right:calc(var(--spacing) * 3)}.-bottom-0\.5{bottom:calc(var(--spacing) * -.5)}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:calc(var(--spacing) * .5)}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.col-span-3{grid-column:span 3/span 3}.m-0{margin:0}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-auto{margin-inline:auto}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.mr-1{margin-right:var(--spacing)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-1{margin-left:var(--spacing)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-20{width:calc(var(--spacing) * 20);height:calc(var(--spacing) * 20)}.size-60{width:calc(var(--spacing) * 60);height:calc(var(--spacing) * 60)}.size-\[15px\]{width:15px;height:15px}.size-\[18px\]{width:18px;height:18px}.size-full{width:100%;height:100%}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-12{height:calc(var(--spacing) * 12)}.h-\[15px\]{height:15px}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-\[70\%\]{max-height:70%}.max-h-\[85dvh\]{max-height:85dvh}.min-h-0{min-height:0}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1{width:var(--spacing)}.w-5{width:calc(var(--spacing) * 5)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-56{width:calc(var(--spacing) * 56)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-\[85\%\]{width:85%}.w-full{width:100%}.max-w-36{max-width:calc(var(--spacing) * 36)}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-72{max-width:calc(var(--spacing) * 72)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[55vw\]{max-width:55vw}.max-w-\[70vw\]{max-width:70vw}.max-w-\[85\%\]{max-width:85%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[92\%\]{max-width:92%}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-max{min-width:max-content}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-rotate-90{rotate:-90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.scrollbar-thin{scrollbar-width:thin}.list-none{list-style-type:none}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.grid-cols-\[minmax\(0\,1fr\)_auto_auto\]{grid-template-columns:minmax(0,1fr) auto auto}.grid-cols-subgrid{grid-template-columns:subgrid}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-\[1\.5px\]{gap:1.5px}:where(.space-y-1\.25>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.25) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.25) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-y-1{row-gap:var(--spacing)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-accent{border-color:var(--color-accent)}.border-accent\/50{border-color:#8b7dff80}@supports (color:color-mix(in lab,red,red)){.border-accent\/50{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.border-border{border-color:var(--color-border)}.border-border-soft{border-color:var(--color-border-soft)}.border-del\/30{border-color:#ff6b6b4d}@supports (color:color-mix(in lab,red,red)){.border-del\/30{border-color:color-mix(in oklab,var(--color-del) 30%,transparent)}}.border-del\/40{border-color:#ff6b6b66}@supports (color:color-mix(in lab,red,red)){.border-del\/40{border-color:color-mix(in oklab,var(--color-del) 40%,transparent)}}.border-faint{border-color:var(--color-faint)}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab,red,red)){.border-white\/80{border-color:color-mix(in oklab,var(--color-white) 80%,transparent)}}.border-t-accent{border-top-color:var(--color-accent)}.border-t-text{border-top-color:var(--color-text)}.border-l-accent{border-left-color:var(--color-accent)}.bg-accent{background-color:var(--color-accent)}.bg-accent-soft{background-color:var(--color-accent-soft)}.bg-accent-soft\/90{background-color:#2a2650e6}@supports (color:color-mix(in lab,red,red)){.bg-accent-soft\/90{background-color:color-mix(in oklab,var(--color-accent-soft) 90%,transparent)}}.bg-accent\/10{background-color:#8b7dff1a}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.bg-accent\/25{background-color:#8b7dff40}@supports (color:color-mix(in lab,red,red)){.bg-accent\/25{background-color:color-mix(in oklab,var(--color-accent) 25%,transparent)}}.bg-add{background-color:var(--color-add)}.bg-add\/10{background-color:#3ecf8e1a}@supports (color:color-mix(in lab,red,red)){.bg-add\/10{background-color:color-mix(in oklab,var(--color-add) 10%,transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-bg\/80{background-color:#0a0b0ecc}@supports (color:color-mix(in lab,red,red)){.bg-bg\/80{background-color:color-mix(in oklab,var(--color-bg) 80%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-black\/75{background-color:#000000bf}@supports (color:color-mix(in lab,red,red)){.bg-black\/75{background-color:color-mix(in oklab,var(--color-black) 75%,transparent)}}.bg-current{background-color:currentColor}.bg-del{background-color:var(--color-del)}.bg-del\/5{background-color:#ff6b6b0d}@supports (color:color-mix(in lab,red,red)){.bg-del\/5{background-color:color-mix(in oklab,var(--color-del) 5%,transparent)}}.bg-del\/10{background-color:#ff6b6b1a}@supports (color:color-mix(in lab,red,red)){.bg-del\/10{background-color:color-mix(in oklab,var(--color-del) 10%,transparent)}}.bg-del\/15{background-color:#ff6b6b26}@supports (color:color-mix(in lab,red,red)){.bg-del\/15{background-color:color-mix(in oklab,var(--color-del) 15%,transparent)}}.bg-pr-merged\/10{background-color:#ac47ff1a}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/10{background-color:color-mix(in oklab,var(--color-pr-merged) 10%,transparent)}}.bg-pr-merged\/15{background-color:#ac47ff26}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/15{background-color:color-mix(in oklab,var(--color-pr-merged) 15%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-2{background-color:var(--color-surface-2)}.bg-surface-2\/60{background-color:#1b1e2699}@supports (color:color-mix(in lab,red,red)){.bg-surface-2\/60{background-color:color-mix(in oklab,var(--color-surface-2) 60%,transparent)}}.bg-surface\/40{background-color:#14161b66}@supports (color:color-mix(in lab,red,red)){.bg-surface\/40{background-color:color-mix(in oklab,var(--color-surface) 40%,transparent)}}.bg-surface\/60{background-color:#14161b99}@supports (color:color-mix(in lab,red,red)){.bg-surface\/60{background-color:color-mix(in oklab,var(--color-surface) 60%,transparent)}}.bg-surface\/70{background-color:#14161bb3}@supports (color:color-mix(in lab,red,red)){.bg-surface\/70{background-color:color-mix(in oklab,var(--color-surface) 70%,transparent)}}.bg-surface\/95{background-color:#14161bf2}@supports (color:color-mix(in lab,red,red)){.bg-surface\/95{background-color:color-mix(in oklab,var(--color-surface) 95%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-working{background-color:var(--color-working)}.bg-working\/10{background-color:#f5a6231a}@supports (color:color-mix(in lab,red,red)){.bg-working\/10{background-color:color-mix(in oklab,var(--color-working) 10%,transparent)}}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:0}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-px{padding-block:1px}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-2\.5{padding-top:calc(var(--spacing) * 2.5)}.pr-1{padding-right:var(--spacing)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-2\.5{padding-bottom:calc(var(--spacing) * 2.5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.9em\]{font-size:.9em}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-add{color:var(--color-add)}.text-bg{color:var(--color-bg)}.text-black{color:var(--color-black)}.text-del{color:var(--color-del)}.text-del\/80{color:#ff6b6bcc}@supports (color:color-mix(in lab,red,red)){.text-del\/80{color:color-mix(in oklab,var(--color-del) 80%,transparent)}}.text-faint{color:var(--color-faint)}.text-muted{color:var(--color-muted)}.text-pr-merged{color:var(--color-pr-merged)}.text-text{color:var(--color-text)}.text-white{color:var(--color-white)}.text-working{color:var(--color-working)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.underline-offset-2{text-underline-offset:2px}.\[color-scheme\:dark\]{color-scheme:dark}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_100vmax_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 0 0 100vmax var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color:#0006}@supports (color:color-mix(in lab,red,red)){.shadow-black\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-black\/60{--tw-shadow-color:#0009}@supports (color:color-mix(in lab,red,red)){.shadow-black\/60{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 60%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-surface{--tw-ring-color:var(--color-surface)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\/steps\:invisible:is(:where(.group\/steps):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/steps\:rotate-90:is(:where(.group\/steps):is([open],:popover-open,:open) *),.group-open\/think\:rotate-90:is(:where(.group\/think):is([open],:popover-open,:open) *){rotate:90deg}.group-open\/tool\:invisible:is(:where(.group\/tool):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/tool\:rotate-90:is(:where(.group\/tool):is([open],:popover-open,:open) *){rotate:90deg}.peer-focus-visible\:outline-2:is(:where(.peer):focus-visible~*){outline-style:var(--tw-outline-style);outline-width:2px}.peer-focus-visible\:outline-offset-2:is(:where(.peer):focus-visible~*){outline-offset:2px}.peer-focus-visible\:outline-accent:is(:where(.peer):focus-visible~*){outline-color:var(--color-accent)}.placeholder\:font-sans::placeholder{font-family:var(--font-sans)}.placeholder\:text-faint::placeholder{color:var(--color-faint)}@media(hover:hover){.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent\/50:focus{border-color:#8b7dff80}@supports (color:color-mix(in lab,red,red)){.focus\:border-accent\/50:focus{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:-outline-offset-2:focus-visible{outline-offset:-2px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--color-accent)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.985\]:active{scale:.985}.active\:bg-black\/70:active{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.active\:bg-black\/70:active{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.active\:bg-surface:active{background-color:var(--color-surface)}.active\:bg-surface-2:active{background-color:var(--color-surface-2)}.active\:text-text:active{color:var(--color-text)}.active\:opacity-80:active{opacity:.8}.disabled\:bg-surface-2:disabled{background-color:var(--color-surface-2)}.disabled\:text-faint:disabled{color:var(--color-faint)}.disabled\:text-faint\/40:disabled{color:#5c616d66}@supports (color:color-mix(in lab,red,red)){.disabled\:text-faint\/40:disabled{color:color-mix(in oklab,var(--color-faint) 40%,transparent)}}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:#8b7dff99}@supports (color:color-mix(in lab,red,red)){.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:color-mix(in oklab,var(--color-accent) 60%,transparent)}}@media(prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:340px){.max-\[340px\]\:hidden{display:none}}@media(min-width:400px){.min-\[400px\]\:block{display:block}}@media(min-width:48rem){.md\:static{position:static}.md\:inset-0{inset:0}.md\:inset-6{inset:calc(var(--spacing) * 6)}.md\:inset-x-auto{inset-inline:auto}.md\:top-16{top:calc(var(--spacing) * 16)}.md\:left-1\/2{left:50%}.md\:z-auto{z-index:auto}.md\:m-auto{margin:auto}.md\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.md\:block{display:block}.md\:hidden{display:none}.md\:h-fit{height:fit-content}.md\:max-h-\[70vh\]{max-height:70vh}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:w-\[36rem\]{width:36rem}.md\:max-w-\[92vw\]{max-width:92vw}.md\:max-w-none{max-width:none}.md\:shrink-0{flex-shrink:0}.md\:-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:rounded-2xl{border-radius:var(--radius-2xl)}.md\:rounded-3xl{border-radius:var(--radius-3xl)}.md\:border{border-style:var(--tw-border-style);border-width:1px}.md\:border-border{border-color:var(--color-border)}.md\:border-border-soft{border-color:var(--color-border-soft)}.md\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:transition-none{transition-property:none}}@media(min-width:64rem){.lg\:static{position:static}.lg\:z-auto{z-index:auto}.lg\:w-80{width:calc(var(--spacing) * 80)}.lg\:w-\[380px\]{width:380px}.lg\:shrink-0{flex-shrink:0}.lg\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.lg\:border-border-soft{border-color:var(--color-border-soft)}}@media(min-width:80rem){.xl\:w-\[460px\]{width:460px}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@keyframes typing{0%,60%,to{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-3px)}}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.fade-in{animation:.18s ease-out fade-in}@media(prefers-reduced-motion:reduce){.fade-in{opacity:1;animation:none}.typing-dot{opacity:.6;animation:none}}.pt-safe{padding-top:max(env(safe-area-inset-top),.5rem)}.pb-safe{padding-bottom:max(env(safe-area-inset-bottom),.5rem)}html[data-keyboard] .pb-safe{padding-bottom:.5rem}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}
|
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-B7l0pOim.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BnacVtpu.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 c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(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:"b2d7edff3447d77e3f268d0e40abfcc1"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-BnacVtpu.css",revision:null},{url:"assets/index-B7l0pOim.js",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\//]}))});
|
|
@@ -14,7 +14,7 @@ import os from 'node:os';
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { installedServiceEnvironment, serviceEnvironmentWithSetting } from "../src/config.js";
|
|
16
16
|
import { packageRoot } from "../src/pkg-root.js";
|
|
17
|
-
import { driftWarningLines, exposeStorePath, magicDnsName, normalizeExposeMode, relayPort, tailscaleBin, writeUrlHost } from "../src/tailscale.js";
|
|
17
|
+
import { driftWarningLines, exposeStorePath, FUNNEL_PORTS, freeServePort, magicDnsName, normalizeExposeMode, parseServeStatus, relayPort, relayServeState, serveUrl, tailscaleBin, writeUrlHost } from "../src/tailscale.js";
|
|
18
18
|
import { qrLines } from "./qr.js";
|
|
19
19
|
const LABEL = 'no.adluna.conductor-remote';
|
|
20
20
|
const projectDir = packageRoot(import.meta.dirname);
|
|
@@ -264,35 +264,95 @@ function resolveExposeMode() {
|
|
|
264
264
|
}
|
|
265
265
|
return 'public';
|
|
266
266
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Live serve/funnel state for this node as it concerns the relay: the HTTPS port fronting it (any port, not
|
|
269
|
+
* only :443), whether that port is public, and which ports carry mounts that are not the relay's. A status
|
|
270
|
+
* that cannot be read counts as nothing fronting — the same answer the old reader gave, and one that only
|
|
271
|
+
* ever leads to a mount on a port nobody holds.
|
|
272
|
+
*/
|
|
273
|
+
function tailscaleState(bin, port = RELAY_PORT) {
|
|
271
274
|
try {
|
|
272
275
|
const out = execFileSync(bin, ['serve', 'status', '--json'], { encoding: 'utf8', stdio: 'pipe' });
|
|
273
|
-
|
|
274
|
-
const key = `${dns}:443`;
|
|
275
|
-
const proxyOk = cfg?.Web?.[key]?.Handlers?.['/']?.Proxy === `http://127.0.0.1:${RELAY_PORT}`;
|
|
276
|
-
return { proxyOk, funnelOn: Boolean(cfg?.AllowFunnel?.[key]) };
|
|
276
|
+
return relayServeState(parseServeStatus(out), port);
|
|
277
277
|
}
|
|
278
278
|
catch {
|
|
279
|
-
return {
|
|
279
|
+
return relayServeState({}, port);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
/**
|
|
283
|
+
* The one Tailscale write here: `tailscale <serve|funnel> --bg --https=<port> <relay port>`. Always scoped
|
|
284
|
+
* to a port the relay owns outright or nobody holds — the callers check, this does not. Serving a port
|
|
285
|
+
* Funnel currently exposes drops that port's Funnel flag and touches no other port, which is what made
|
|
286
|
+
* the old `funnel reset` (every mount on the node, gone) unnecessary. Returns the CLI's error, or null.
|
|
287
|
+
*/
|
|
288
|
+
function mountRelay(bin, kind, port) {
|
|
288
289
|
try {
|
|
289
|
-
execFileSync(bin, ['
|
|
290
|
-
|
|
290
|
+
execFileSync(bin, [kind, '--bg', '--yes', `--https=${port}`, RELAY_PORT], { stdio: 'pipe' });
|
|
291
|
+
return null;
|
|
291
292
|
}
|
|
292
293
|
catch (err) {
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
return err instanceof Error ? err.message.trim() : String(err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function byHand(kind, port) {
|
|
298
|
+
return `tailscale ${kind} --bg --yes --https=${port} ${RELAY_PORT}`;
|
|
299
|
+
}
|
|
300
|
+
function describeTaken(state, port) {
|
|
301
|
+
return (state.taken.get(port) ?? []).join(', ');
|
|
302
|
+
}
|
|
303
|
+
/** A port the relay shares is left exactly as found: Funnel is per port, so a change would drag the rest along. */
|
|
304
|
+
function leaveShared(url, state, want) {
|
|
305
|
+
console.info(`\n ⚠ ${url} fronts the relay, but :${state.port} also carries ${state.shared.join(', ')}.`);
|
|
306
|
+
console.info(` Funnel is per port, so making the relay ${want} would take those mounts along. Left as is.`);
|
|
307
|
+
console.info(` To change it, move the relay to a port of its own: ${byHand(want === 'public' ? 'funnel' : 'serve', '<port>')}`);
|
|
308
|
+
}
|
|
309
|
+
function refuseNoPort(state, candidates) {
|
|
310
|
+
const held = candidates.map(p => `:${p} → ${describeTaken(state, p)}`).join('; ');
|
|
311
|
+
console.info(`\n ⚠ every port the relay could take already carries another service (${held}). Not touching them.`);
|
|
312
|
+
console.info(` Free one, then run \`yarn deploy\` again — or mount by hand: ${byHand('serve', '<port>')}`);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Assert a tailnet-only `serve` proxy in front of the relay — tailnet mode, and the fallback when Funnel is
|
|
316
|
+
* refused. Non-destructive by construction: a mapping is kept wherever it already lives (:443 or not), a
|
|
317
|
+
* shared port is reported rather than rewritten, and a fresh mount steps around any port someone else holds.
|
|
318
|
+
* The order of candidates is 443 (the default, unchanged), then the relay's own port number — the memorable
|
|
319
|
+
* choice, and it leaves 8443/10000 for services that need Funnel — then the remaining Funnel ports.
|
|
320
|
+
*/
|
|
321
|
+
function ensureServeOnly(bin, dns, state) {
|
|
322
|
+
const host = dns ?? '<node>';
|
|
323
|
+
if (state.port !== null) {
|
|
324
|
+
const url = serveUrl(host, state.port);
|
|
325
|
+
if (!state.funnelOn) {
|
|
326
|
+
console.info(`✓ tailscale serve fronts ${url} → 127.0.0.1:${RELAY_PORT} (tailnet-only)`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (state.shared.length) {
|
|
330
|
+
leaveShared(url, state, 'tailnet-only');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const failed = mountRelay(bin, 'serve', state.port);
|
|
334
|
+
if (failed) {
|
|
335
|
+
console.info(`\n ⚠ could not take Funnel off ${url} (${failed}). Run by hand:\n ${byHand('serve', state.port)}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
console.info(`✓ tailscale serve → ${url} is tailnet-only again → 127.0.0.1:${RELAY_PORT}`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const candidates = [443, Number(RELAY_PORT), ...FUNNEL_PORTS.filter(p => p !== 443)];
|
|
342
|
+
const port = freeServePort(state, candidates);
|
|
343
|
+
if (port === null) {
|
|
344
|
+
refuseNoPort(state, candidates);
|
|
345
|
+
return;
|
|
295
346
|
}
|
|
347
|
+
if (port !== 443)
|
|
348
|
+
console.info(` :443 carries ${describeTaken(state, 443)}, so the relay goes on :${port} instead.`);
|
|
349
|
+
const url = serveUrl(host, port);
|
|
350
|
+
const failed = mountRelay(bin, 'serve', port);
|
|
351
|
+
if (failed) {
|
|
352
|
+
console.info(`\n ⚠ could not configure tailscale serve (${failed}). Run by hand:\n ${byHand('serve', port)}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
console.info(`✓ tailscale serve → ${url} proxies 127.0.0.1:${RELAY_PORT} (tailnet-only)`);
|
|
296
356
|
}
|
|
297
357
|
/**
|
|
298
358
|
* Persist a stable Tailscale device name so this node's MagicDNS URL — the origin the installed PWA is
|
|
@@ -351,39 +411,58 @@ function ensureTailscale() {
|
|
|
351
411
|
const dns = magicDnsName(bin);
|
|
352
412
|
if (dns)
|
|
353
413
|
writeUrlHost(dns); // baseline for drift detection (server startup + `service status` compare against this)
|
|
354
|
-
const
|
|
414
|
+
const host = dns ?? '<node>';
|
|
355
415
|
const mode = resolveExposeMode();
|
|
356
|
-
const state = tailscaleState(bin
|
|
416
|
+
const state = tailscaleState(bin);
|
|
357
417
|
if (mode === 'tailnet') {
|
|
418
|
+
ensureServeOnly(bin, dns, state);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// public (Funnel)
|
|
422
|
+
if (state.port !== null) {
|
|
423
|
+
const url = serveUrl(host, state.port);
|
|
358
424
|
if (state.funnelOn) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
// best-effort; ensureServeOnly re-asserts the proxy below
|
|
364
|
-
}
|
|
365
|
-
ensureServeOnly(bin, url, { proxyOk: false, funnelOn: false });
|
|
425
|
+
console.info(`✓ tailscale funnel already exposes ${url} → 127.0.0.1:${RELAY_PORT} (public, token-gated)`);
|
|
426
|
+
return;
|
|
366
427
|
}
|
|
367
|
-
|
|
368
|
-
|
|
428
|
+
if (state.shared.length) {
|
|
429
|
+
leaveShared(url, state, 'public');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!FUNNEL_PORTS.includes(state.port)) {
|
|
433
|
+
console.info(`\n ⚠ the relay is fronted tailnet-only at ${url}, and Funnel cannot listen on :${state.port}`);
|
|
434
|
+
console.info(` (only ${FUNNEL_PORTS.join(', ')}). Left as is — \`conductor-remote config\` will show the mismatch.`);
|
|
435
|
+
console.info(` To go public, move it to a Funnel port by hand: ${byHand('funnel', '<443|8443|10000>')}`);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const failed = mountRelay(bin, 'funnel', state.port);
|
|
439
|
+
if (failed) {
|
|
440
|
+
funnelRefused(failed);
|
|
441
|
+
ensureServeOnly(bin, dns, state);
|
|
442
|
+
return;
|
|
369
443
|
}
|
|
444
|
+
console.info(`✓ tailscale funnel → ${url} now public over the internet (token-gated) → 127.0.0.1:${RELAY_PORT}`);
|
|
370
445
|
return;
|
|
371
446
|
}
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
447
|
+
const port = freeServePort(state, FUNNEL_PORTS);
|
|
448
|
+
if (port === null) {
|
|
449
|
+
refuseNoPort(state, FUNNEL_PORTS);
|
|
375
450
|
return;
|
|
376
451
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
console.info(' "funnel" nodeAttr in Admin console ▸ Access controls. Falling back to tailnet-only for now.');
|
|
385
|
-
ensureServeOnly(bin, url, state);
|
|
452
|
+
if (port !== 443)
|
|
453
|
+
console.info(` :443 carries ${describeTaken(state, 443)}, so the relay goes on :${port} instead.`);
|
|
454
|
+
const failed = mountRelay(bin, 'funnel', port);
|
|
455
|
+
if (failed) {
|
|
456
|
+
funnelRefused(failed);
|
|
457
|
+
ensureServeOnly(bin, dns, state);
|
|
458
|
+
return;
|
|
386
459
|
}
|
|
460
|
+
console.info(`✓ tailscale funnel → ${serveUrl(host, port)} now public over the internet (token-gated) → 127.0.0.1:${RELAY_PORT}`);
|
|
461
|
+
}
|
|
462
|
+
function funnelRefused(error) {
|
|
463
|
+
console.info(`\n ⚠ could not enable Funnel (${error}).`);
|
|
464
|
+
console.info(' Funnel must be enabled for this tailnet: open the URL Tailscale printed above, or add the');
|
|
465
|
+
console.info(' "funnel" nodeAttr in Admin console ▸ Access controls. Falling back to tailnet-only for now.');
|
|
387
466
|
}
|
|
388
467
|
/** Print a scannable QR of `url` (theme-independent black-on-white). Never fatal — QR is a convenience. */
|
|
389
468
|
function printQr(url) {
|
|
@@ -404,10 +483,10 @@ function printUrl() {
|
|
|
404
483
|
console.info(`\n${drift.join('\n')}`);
|
|
405
484
|
}
|
|
406
485
|
const dns = bin ? magicDnsName(bin) : null;
|
|
407
|
-
const state = bin ? tailscaleState(bin
|
|
408
|
-
if (dns && state.
|
|
486
|
+
const state = bin ? tailscaleState(bin) : relayServeState({}, RELAY_PORT);
|
|
487
|
+
if (dns && state.port !== null) {
|
|
409
488
|
const scope = state.funnelOn ? 'public — any browser, token-gated' : 'same Tailnet only';
|
|
410
|
-
const url =
|
|
489
|
+
const url = `${serveUrl(dns, state.port)}${frag}`;
|
|
411
490
|
console.info(`\n Phone URL (HTTPS, ${scope}):\n ${url}`);
|
|
412
491
|
if (token) {
|
|
413
492
|
console.info('\n Scan to open on your phone:');
|
|
@@ -417,7 +496,7 @@ function printUrl() {
|
|
|
417
496
|
}
|
|
418
497
|
// Nothing fronting yet — the relay is only on loopback.
|
|
419
498
|
console.info(`\n Local URL:\n http://127.0.0.1:${RELAY_PORT}/${frag}`);
|
|
420
|
-
console.info(`\n ⚠ Not reachable from your phone yet. Run \`tailscale funnel --bg ${RELAY_PORT}\` (public)
|
|
499
|
+
console.info(`\n ⚠ Not reachable from your phone yet. Run \`yarn deploy\` (it picks a free port), or by hand \`tailscale funnel --bg ${RELAY_PORT}\` (public) / \`tailscale serve --bg ${RELAY_PORT}\` (tailnet)${dns ? ` → https://${dns}/` : ''}, then \`yarn service status\`.`);
|
|
421
500
|
}
|
|
422
501
|
/** npx unpacks into a throwaway cache that gets purged; a LaunchAgent baked against it would rot. */
|
|
423
502
|
function isEphemeralInstall(dir) {
|
|
@@ -607,6 +686,17 @@ function config() {
|
|
|
607
686
|
// no token file yet
|
|
608
687
|
}
|
|
609
688
|
rows.push({ name: 'token', value: token, source: 'token file' });
|
|
689
|
+
// The HTTPS port is a live Tailscale fact, not a plist knob: :443 by default, elsewhere once another
|
|
690
|
+
// service holds :443 (see ensureServeOnly), and the phone URL carries whichever it is. Read against the
|
|
691
|
+
// daemon's own port, since this shell's RELAY_PORT is not the one the relay listens on.
|
|
692
|
+
const bin = tailscaleBin();
|
|
693
|
+
const livePort = env.RELAY_PORT ?? '8787';
|
|
694
|
+
const live = bin ? tailscaleState(bin, livePort) : relayServeState({}, livePort);
|
|
695
|
+
rows.push({
|
|
696
|
+
name: 'https-port',
|
|
697
|
+
value: live.port === null ? '(not fronted)' : String(live.port),
|
|
698
|
+
source: 'tailscale serve'
|
|
699
|
+
});
|
|
610
700
|
const width = Math.max(...rows.map(r => r.name.length));
|
|
611
701
|
const valueWidth = Math.max(...rows.map(r => r.value.length));
|
|
612
702
|
console.info('');
|
|
@@ -618,25 +708,31 @@ function config() {
|
|
|
618
708
|
// The cross-check worth having. A posture the relay believes and a Tailscale that is doing something
|
|
619
709
|
// else is invisible in every other command, and it is self-healing in the wrong direction: the funnel
|
|
620
710
|
// watchdog only runs for `public`, so a relay that thinks it is public will re-register Funnel.
|
|
621
|
-
const bin = tailscaleBin();
|
|
622
711
|
const configured = rows.find(r => r.name === 'expose')?.value;
|
|
623
712
|
if (!bin || !configured)
|
|
624
713
|
return;
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
const
|
|
714
|
+
const liveMode = live.funnelOn ? 'public' : live.port !== null ? 'tailnet' : null;
|
|
715
|
+
const where = live.port !== null && live.port !== 443 ? ` on :${live.port}` : '';
|
|
716
|
+
const funnelable = live.port === null || FUNNEL_PORTS.includes(live.port);
|
|
628
717
|
console.info('');
|
|
629
718
|
if (liveMode === null) {
|
|
630
|
-
console.info(` \u26a0 tailscale is not fronting 127.0.0.1:${
|
|
719
|
+
console.info(` \u26a0 tailscale is not fronting 127.0.0.1:${livePort} at all — the phone URL is not wired.`);
|
|
631
720
|
console.info(' Fix with: conductor-remote service install');
|
|
632
721
|
}
|
|
633
722
|
else if (liveMode === configured) {
|
|
634
|
-
console.info(` \u2713 tailscale agrees: ${liveMode === 'public' ? 'Funnel on (internet-facing)' : 'serve only (tailnet)'}`);
|
|
723
|
+
console.info(` \u2713 tailscale agrees: ${liveMode === 'public' ? 'Funnel on (internet-facing)' : 'serve only (tailnet)'}${where}`);
|
|
635
724
|
}
|
|
636
725
|
else {
|
|
637
|
-
console.info(` \u26a0 tailscale says ${liveMode}, the daemon is configured ${configured}. They must match:`);
|
|
726
|
+
console.info(` \u26a0 tailscale says ${liveMode}${where}, the daemon is configured ${configured}. They must match:`);
|
|
638
727
|
console.info(` the funnel watchdog only runs for \`public\`, so the two can heal each other apart.`);
|
|
639
|
-
|
|
728
|
+
if (configured === 'public' && !funnelable) {
|
|
729
|
+
console.info(` Funnel cannot listen on :${live.port} (only ${FUNNEL_PORTS.join(', ')}), so either move the relay by hand`);
|
|
730
|
+
console.info(` (tailscale funnel --bg --yes --https=<443|8443|10000> ${livePort}) or match the daemon to it:`);
|
|
731
|
+
console.info(' conductor-remote service install --expose tailnet');
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
console.info(` Fix with: conductor-remote service install --expose ${configured}`);
|
|
735
|
+
}
|
|
640
736
|
}
|
|
641
737
|
}
|
|
642
738
|
function status() {
|
|
@@ -29,7 +29,7 @@ import https from 'node:https';
|
|
|
29
29
|
import net from 'node:net';
|
|
30
30
|
import { promisify } from 'node:util';
|
|
31
31
|
import { readSettings } from "./settings.js";
|
|
32
|
-
import { magicDnsName, readExposeMode, relayPort, tailscaleBin } from "./tailscale.js";
|
|
32
|
+
import { magicDnsName, parseServeStatus, readExposeMode, relayPort, relayServeState, tailscaleBin } from "./tailscale.js";
|
|
33
33
|
import { hasDefaultRoute, joinNetwork, preferredNetworks } from "./wifi.js";
|
|
34
34
|
import { joinInstantHotspot } from "./writes.js";
|
|
35
35
|
const execFileP = promisify(execFile);
|
|
@@ -121,7 +121,33 @@ function tcpOpen(ip, port, timeoutMs) {
|
|
|
121
121
|
socket.once('error', () => finish(false));
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The heal is `funnel reset` + `funnel --bg <port>`, which mounts the relay on :443. That is refused when
|
|
126
|
+
* :443 carries a mount that is not the relay's, or the relay is served on another port — the arrangement
|
|
127
|
+
* once another service needs :443 (OpenAI's cloud dials nothing else, measured 2026-09-02, so the voice
|
|
128
|
+
* listener holds :443 and the relay sits tailnet-only on :8787). The probe above was never reaching the
|
|
129
|
+
* relay in that case, and the heal would delete the mount it did reach. Fails closed: a status that cannot
|
|
130
|
+
* be read blocks the heal, never allows it.
|
|
131
|
+
*/
|
|
132
|
+
async function reRegisterBlocked(bin, port) {
|
|
133
|
+
try {
|
|
134
|
+
const { stdout } = await execFileP(bin, ['serve', 'status', '--json'], { encoding: 'utf8', timeout: 10_000 });
|
|
135
|
+
const state = relayServeState(parseServeStatus(stdout), port);
|
|
136
|
+
const foreign = state.taken.get(443);
|
|
137
|
+
if (foreign?.length)
|
|
138
|
+
return `:443 carries ${foreign.join(', ')}`;
|
|
139
|
+
if (state.port !== null && state.port !== 443)
|
|
140
|
+
return `the relay is served on :${state.port}, not :443`;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return `could not read tailscale serve status (${err instanceof Error ? err.message.trim() : String(err)})`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
124
147
|
async function reRegisterFunnel(bin, port) {
|
|
148
|
+
const blocked = await reRegisterBlocked(bin, port);
|
|
149
|
+
if (blocked)
|
|
150
|
+
throw new Error(`not re-registering, ${blocked}`);
|
|
125
151
|
await execFileP(bin, ['funnel', 'reset'], { timeout: 15_000 });
|
|
126
152
|
await execFileP(bin, ['funnel', '--bg', '--yes', port], { timeout: 20_000 });
|
|
127
153
|
}
|
|
@@ -156,6 +182,7 @@ let fails = 0;
|
|
|
156
182
|
let dnsFails = 0;
|
|
157
183
|
let lastHealAt = 0;
|
|
158
184
|
let lastRejoinAt = 0;
|
|
185
|
+
let blockedLogged = null;
|
|
159
186
|
const REJOIN_COOLDOWN_MS = 5 * 60 * 1000; // a network switch is disruptive; never churn on one
|
|
160
187
|
const REJOIN_SETTLE_MS = 12 * 1000; // DHCP + tailscaled noticing the new endpoint
|
|
161
188
|
// An Instant Hotspot press has the phone's Bluetooth wake + hotspot spin-up in front of the
|
|
@@ -289,6 +316,15 @@ async function tick(host, bin, port, intervalMs) {
|
|
|
289
316
|
}
|
|
290
317
|
if (Date.now() - lastHealAt < HEAL_COOLDOWN_MS)
|
|
291
318
|
return again(intervalMs);
|
|
319
|
+
// A block is a standing fact, so it is logged when it changes and not once a minute for as long as it holds.
|
|
320
|
+
const blocked = await reRegisterBlocked(bin, port);
|
|
321
|
+
if (blocked) {
|
|
322
|
+
if (blocked !== blockedLogged)
|
|
323
|
+
log(`ingress probe failing (${fails}), but not re-registering: ${blocked}`);
|
|
324
|
+
blockedLogged = blocked;
|
|
325
|
+
return again(intervalMs);
|
|
326
|
+
}
|
|
327
|
+
blockedLogged = null;
|
|
292
328
|
log(`funnel ingress stale (${fails} failed probes, ingress TCP-reachable) — re-registering: funnel reset && funnel --bg ${port}`);
|
|
293
329
|
try {
|
|
294
330
|
await reRegisterFunnel(bin, port);
|
|
@@ -120,21 +120,32 @@ export function createTools(call) {
|
|
|
120
120
|
type: 'string',
|
|
121
121
|
description: 'Plain words, ranked by relevance; wrap a "phrase in quotes" to require it word for word. Other punctuation and operators are ignored, not parsed.'
|
|
122
122
|
},
|
|
123
|
-
limit: { type: 'number', description: 'Max workspaces to return (default 12, max 50).' }
|
|
123
|
+
limit: { type: 'number', description: 'Max workspaces to return (default 12, max 50).' },
|
|
124
|
+
repo: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Only workspaces in this repo, by the exact name list_repos prints. Ranking happens inside the repo, so a rare mention there is not buried by busier repos.'
|
|
127
|
+
}
|
|
124
128
|
},
|
|
125
129
|
required: ['query']
|
|
126
130
|
},
|
|
127
131
|
run: async (args) => {
|
|
128
132
|
const query = need(args, 'query');
|
|
129
133
|
const limit = num(args.limit);
|
|
130
|
-
const
|
|
134
|
+
const repo = typeof args.repo === 'string' && args.repo.trim() ? args.repo.trim() : null;
|
|
135
|
+
const params = new URLSearchParams({ q: query });
|
|
136
|
+
if (limit)
|
|
137
|
+
params.set('limit', String(limit));
|
|
138
|
+
if (repo)
|
|
139
|
+
params.set('repo', repo);
|
|
140
|
+
const data = await call(`${routes.search.path()}?${params}`);
|
|
131
141
|
const lines = [];
|
|
132
142
|
if (data.index.error)
|
|
133
143
|
lines.push(`! chat index unavailable (${data.index.error}) — names matched only`);
|
|
134
144
|
else if (!data.index.ready)
|
|
135
145
|
lines.push(`! still indexing (${Math.round(data.index.progress * 100)}%) — older chats not searchable yet`);
|
|
146
|
+
const scope = repo ? ` in ${repo}` : '';
|
|
136
147
|
if (!data.results.length)
|
|
137
|
-
return [...lines, `no workspace or chat matches ${JSON.stringify(query)}`].join('\n');
|
|
148
|
+
return [...lines, `no workspace or chat matches ${JSON.stringify(query)}${scope}`].join('\n');
|
|
138
149
|
for (const r of data.results) {
|
|
139
150
|
const w = r.workspace;
|
|
140
151
|
const tags = [w.repo_name, w.branch, w.archived ? 'ARCHIVED' : w.state].filter(Boolean).join(' · ');
|
package/dist-node/src/reads.js
CHANGED
|
@@ -187,22 +187,43 @@ export class Reads {
|
|
|
187
187
|
out.set(r.session_id, { sessionTitle: r.session_title, workspace: toSearchWorkspace(r) });
|
|
188
188
|
return out;
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Every chat in the named repos, archived workspaces included — the repo filter
|
|
192
|
+
* as the search index needs it, since its chunks carry a chat id and nothing else.
|
|
193
|
+
* Names, not ids, because names are what the phone's picker and `list_repos` hold.
|
|
194
|
+
*/
|
|
195
|
+
sessionIdsInRepos(names) {
|
|
196
|
+
if (!names.length)
|
|
197
|
+
return [];
|
|
198
|
+
const holes = names.map(() => '?').join(',');
|
|
199
|
+
return this.db
|
|
200
|
+
.query(`SELECT s.id FROM sessions s
|
|
201
|
+
JOIN workspaces w ON w.id = s.workspace_id
|
|
202
|
+
JOIN repos r ON r.id = w.repository_id
|
|
203
|
+
WHERE r.name IN (${holes})`, names)
|
|
204
|
+
.map(r => r.id);
|
|
205
|
+
}
|
|
190
206
|
/**
|
|
191
207
|
* Workspaces whose own identity matches every token — name, PR title, branch,
|
|
192
208
|
* worktree codename or repo. Archived included, same reason as `searchTargets`.
|
|
209
|
+
* `repos` narrows to those repos by name; an empty list matches nothing.
|
|
193
210
|
*
|
|
194
211
|
* This is the half of search the transcript index cannot do. A workspace named for
|
|
195
212
|
* the thing you are looking for may never have said those words in its chat, and
|
|
196
213
|
* one whose chat is empty has no chunks at all.
|
|
197
214
|
*/
|
|
198
|
-
findWorkspacesByName(tokens, limit = 20) {
|
|
215
|
+
findWorkspacesByName(tokens, limit = 20, repos) {
|
|
199
216
|
if (!tokens.length)
|
|
200
217
|
return [];
|
|
218
|
+
if (repos && !repos.length)
|
|
219
|
+
return [];
|
|
201
220
|
const fields = ['w.workspace_name', 'w.pr_title', 'w.branch', 'w.directory_name', 'r.name'];
|
|
202
221
|
// AND across tokens, OR across fields: "auk lamp" should find the lamp workspace in
|
|
203
222
|
// the auk repo, where no single column holds both words.
|
|
204
|
-
const
|
|
205
|
-
const
|
|
223
|
+
const byToken = tokens.map(() => `(${fields.map(f => `${f} LIKE ? ESCAPE '\\'`).join(' OR ')})`);
|
|
224
|
+
const scope = repos ? [`r.name IN (${repos.map(() => '?').join(',')})`] : [];
|
|
225
|
+
const where = [...byToken, ...scope].join(' AND ');
|
|
226
|
+
const params = [...tokens.flatMap(t => fields.map(() => `%${escapeLike(t)}%`)), ...(repos ?? [])];
|
|
206
227
|
const rows = this.db.query(`SELECT w.id, w.workspace_name, w.pr_title, w.branch, w.directory_name, w.state, w.updated_at,
|
|
207
228
|
r.name AS repo_name, r.icon AS repo_icon, r.root_path AS repo_root, r.remote_url AS remote_url
|
|
208
229
|
FROM workspaces w
|
package/dist-node/src/search.js
CHANGED
|
@@ -291,21 +291,41 @@ export class SearchIndex {
|
|
|
291
291
|
const progress = this.sourceMax ? Math.min(1, this.cursor / this.sourceMax) : 0;
|
|
292
292
|
return { chunks, ready: false, progress };
|
|
293
293
|
}
|
|
294
|
-
/**
|
|
295
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Top matching chunks, best first. Empty when the query has no searchable tokens.
|
|
296
|
+
*
|
|
297
|
+
* `sessionIds` scopes the ranking, not just the result: it is the repo filter,
|
|
298
|
+
* resolved to chat ids by the caller because this index knows nothing about
|
|
299
|
+
* workspaces. It has to sit inside the query rather than be applied to the
|
|
300
|
+
* chunks it returns, because the `limit` is spent before any post-filter runs
|
|
301
|
+
* — a common word fills all 300 slots from the busiest repo and a smaller one
|
|
302
|
+
* folds up to nothing. Measured on this Mac's 205k chunks with the largest repo's
|
|
303
|
+
* 1,005 chats as the list: +0.3ms on a rare word, +60ms on the worst common-word
|
|
304
|
+
* query (165ms → 220ms), still under the phone's 250ms debounce. The list rides
|
|
305
|
+
* in as one JSON parameter through `json_each`, so its length never meets
|
|
306
|
+
* SQLite's bound-variable limit. An empty list matches nothing, which is the
|
|
307
|
+
* right answer for a repo with no chats and never "everything".
|
|
308
|
+
*/
|
|
309
|
+
search(raw, { limit = CHUNK_LIMIT, sessionIds } = {}) {
|
|
296
310
|
const db = this.db;
|
|
297
311
|
if (!db)
|
|
298
312
|
return [];
|
|
299
313
|
const match = matchQuery(raw);
|
|
300
314
|
if (!match)
|
|
301
315
|
return [];
|
|
316
|
+
if (sessionIds && !sessionIds.length)
|
|
317
|
+
return [];
|
|
318
|
+
const scope = sessionIds ? 'AND session_id IN (SELECT value FROM json_each(?))' : '';
|
|
319
|
+
const params = sessionIds
|
|
320
|
+
? [HIT_OPEN, HIT_CLOSE, match, JSON.stringify(sessionIds), limit]
|
|
321
|
+
: [HIT_OPEN, HIT_CLOSE, match, limit];
|
|
302
322
|
let rows;
|
|
303
323
|
try {
|
|
304
324
|
rows = db
|
|
305
325
|
.prepare(`SELECT session_id, src_rowid, role, at, -bm25(chunks) AS score,
|
|
306
326
|
snippet(chunks, 0, ?, ?, '…', 24) AS snippet
|
|
307
|
-
FROM chunks WHERE chunks MATCH ? ORDER BY bm25(chunks) LIMIT ?`)
|
|
308
|
-
.all(
|
|
327
|
+
FROM chunks WHERE chunks MATCH ? ${scope} ORDER BY bm25(chunks) LIMIT ?`)
|
|
328
|
+
.all(...params);
|
|
309
329
|
}
|
|
310
330
|
catch (err) {
|
|
311
331
|
// A MATCH that still fails to parse is a bug in matchQuery, not user error —
|