@@ -465,6 +465,13 @@ app.get("/setup", requireSetupAuth, (_req, res) => {
465465 <button id="reset" style="background:#444; margin-left:0.5rem">Reset setup</button>
466466 <pre id="log" style="white-space:pre-wrap"></pre>
467467 <p class="muted">Reset deletes the OpenClaw config file so you can rerun onboarding. Pairing approval lets you grant DM access when dmPolicy=pairing.</p>
468+
469+ <details style="margin-top: 0.75rem">
470+ <summary><strong>Pairing helper</strong> (for “disconnected (1008): pairing required”)</summary>
471+ <p class="muted">This lists pending device requests and lets you approve them without SSH.</p>
472+ <button id="devicesRefresh" style="background:#0f172a">Refresh pending devices</button>
473+ <div id="devicesList" class="muted" style="margin-top:0.5rem"></div>
474+ </details>
468475 </div>
469476
470477 <script src="/setup/app.js"></script>
@@ -824,6 +831,16 @@ function redactSecrets(text) {
824831 . replace ( / ( A A [ A - Z a - z 0 - 9 _ - ] { 10 , } : \S { 10 , } ) / g, "[REDACTED]" ) ;
825832}
826833
834+ function extractDeviceRequestIds ( text ) {
835+ const s = String ( text || "" ) ;
836+ const out = new Set ( ) ;
837+
838+ for ( const m of s . matchAll ( / r e q u e s t I d \s * (?: = | : ) \s * ( [ A - Z a - z 0 - 9 _ - ] { 6 , } ) / g) ) out . add ( m [ 1 ] ) ;
839+ for ( const m of s . matchAll ( / " r e q u e s t I d " \s * : \s * " ( [ A - Z a - z 0 - 9 _ - ] { 6 , } ) " / g) ) out . add ( m [ 1 ] ) ;
840+
841+ return Array . from ( out ) ;
842+ }
843+
827844const ALLOWED_CONSOLE_COMMANDS = new Set ( [
828845 // Wrapper-managed lifecycle
829846 "gateway.restart" ,
@@ -969,12 +986,31 @@ app.post("/setup/api/pairing/approve", requireSetupAuth, async (req, res) => {
969986 return res . status ( r . code === 0 ? 200 : 500 ) . json ( { ok : r . code === 0 , output : r . output } ) ;
970987} ) ;
971988
989+ // Device pairing helper (list + approve) to avoid needing SSH.
990+ app . get ( "/setup/api/devices/pending" , requireSetupAuth , async ( _req , res ) => {
991+ const r = await runCmd ( OPENCLAW_NODE , clawArgs ( [ "devices" , "list" ] ) ) ;
992+ const output = redactSecrets ( r . output ) ;
993+ const requestIds = extractDeviceRequestIds ( output ) ;
994+ return res . status ( r . code === 0 ? 200 : 500 ) . json ( { ok : r . code === 0 , requestIds, output } ) ;
995+ } ) ;
996+
997+ app . post ( "/setup/api/devices/approve" , requireSetupAuth , async ( req , res ) => {
998+ const requestId = String ( ( req . body && req . body . requestId ) || "" ) . trim ( ) ;
999+ if ( ! requestId ) return res . status ( 400 ) . json ( { ok : false , error : "Missing device request ID" } ) ;
1000+ if ( ! / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( requestId ) ) return res . status ( 400 ) . json ( { ok : false , error : "Invalid device request ID" } ) ;
1001+ const r = await runCmd ( OPENCLAW_NODE , clawArgs ( [ "devices" , "approve" , requestId ] ) ) ;
1002+ return res . status ( r . code === 0 ? 200 : 500 ) . json ( { ok : r . code === 0 , output : redactSecrets ( r . output ) } ) ;
1003+ } ) ;
1004+
9721005app . post ( "/setup/api/reset" , requireSetupAuth , async ( _req , res ) => {
9731006 // Minimal reset: delete the config file so /setup can rerun.
9741007 // Keep credentials/sessions/workspace by default.
9751008 try {
976- fs . rmSync ( configPath ( ) , { force : true } ) ;
977- res . type ( "text/plain" ) . send ( "OK - deleted config file. You can rerun setup now." ) ;
1009+ const candidates = typeof resolveConfigCandidates === "function" ? resolveConfigCandidates ( ) : [ configPath ( ) ] ;
1010+ for ( const p of candidates ) {
1011+ try { fs . rmSync ( p , { force : true } ) ; } catch { }
1012+ }
1013+ res . type ( "text/plain" ) . send ( "OK - deleted config file(s). You can rerun setup now." ) ;
9781014 } catch ( err ) {
9791015 res . status ( 500 ) . type ( "text/plain" ) . send ( String ( err ) ) ;
9801016 }
0 commit comments